군집 수 고르기 문서의 검증 코드다. if __name__ == "__main__": 아래가 자체 테스트다.
"""군집 수 고르기(엘보, 실루엣) 검증.
문서: 28.군집 수 고르기 (예시, 정의, 카드)
식: a(i) = 같은 군집 다른 점까지 평균 거리, b(i) = 다른 군집들 중 평균 거리가 가장 작은 군집까지의 평균 거리,
s(i) = (b - a) / max(a, b), S = 평균 s(i).
주장:
1. a는 작을수록 응집, b는 클수록 분리 (둘 다 평균 거리라서 "a가 높을수록 응집"으로 읽으면 반대다).
2. s(i)는 -1 ~ 1. 다른 군집에 더 가까운 점은 s < 0 (s(i) ≤ 1은 늘 맞으므로 "s(i) < 1"로는 잘못 묶인 점을 가려내지 못한다).
3. 1차원 점 1, 2, 3 / 10, 11, 12 / 20, 21, 22: 엘보가 k = 3에서 꺾이고 실루엣도 k = 3에서 가장 크다.
4. 카드 C2: 군집 A = {0, 1}, B = {5, 6}에서 점 1의 a = 1, b = 4.5, s = 3.5/4.5 ≈ 0.778.
"""
from itertools import product
def sil(X, lab):
ks = sorted(set(lab)); out = []
for i, x in enumerate(X):
same = [abs(x - y) for j, y in enumerate(X) if lab[j] == lab[i] and j != i]
if not same:
out.append(0.0); continue
a = sum(same) / len(same)
b = min(sum(abs(x - y) for j, y in enumerate(X) if lab[j] == c) / lab.count(c) for c in ks if c != lab[i])
out.append((b - a) / max(a, b))
return out
def best(X, k):
bj, bl = None, None
for lab in product(range(k), repeat=len(X)):
if len(set(lab)) < k or lab[0] != 0:
continue
cents = [sum(x for x, l in zip(X, lab) if l == c) / lab.count(c) for c in range(k)]
j = sum((x - cents[l]) ** 2 for x, l in zip(X, lab))
if bj is None or j < bj:
bj, bl = j, list(lab)
return bj, bl
def main():
X = [1, 2, 3, 10, 11, 12, 20, 21, 22]
res = {k: best(X, k) for k in range(1, 6)}
Js = [res[k][0] for k in range(1, 6)]
drops = [Js[i] - Js[i + 1] for i in range(4)]
print(" k별 J:", [round(j, 1) for j in Js], "줄어든 양:", [round(d, 1) for d in drops])
assert drops[1] > 10 * drops[2]
S = {k: sum(sil(X, res[k][1])) / len(X) for k in range(2, 6)}
print(" k별 평균 실루엣:", {k: round(v, 3) for k, v in S.items()})
assert max(S, key=S.get) == 3
print("[OK] 엘보와 실루엣 모두 k = 3")
s = sil([0, 1, 5, 6], [0, 0, 1, 1])
assert abs(s[1] - 3.5 / 4.5) < 1e-12
print("[OK] 카드 C2: s = 3.5/4.5 ≈ 0.778")
bad = sil([0, 1, 5, 6], [0, 1, 1, 1])
assert bad[1] < 0
print(f"[OK] 점 1을 B에 잘못 넣으면 s = {bad[1]:.3f} < 0")
for lab in product(range(2), repeat=6):
if len(set(lab)) == 2:
assert all(-1 <= v <= 1 for v in sil([0, 1, 2, 7, 8, 9], list(lab)))
print("[OK] 모든 2분할에서 -1 <= s <= 1")
print("ALL CHECKS PASSED")
if __name__ == "__main__":
main()