내용 기반 추천 문서의 검증 코드다. if __name__ == "__main__": 아래가 자체 테스트다.

"""내용 기반 추천(TF-IDF, 사용자 프로필, 코사인 효용) 검증.

문서: 40.내용 기반 추천 (예시, 정의, 카드)
TF_ij = f_ij / max_z f_zj,  IDF_i = log(N / n_i),  w_ij = TF_ij * IDF_i  (로그는 밑 2)
사용자 프로필 = 읽은 글 벡터의 평균, 효용 u(c, s) = cos(사용자 프로필, 글 프로필)
주장:
  1. 글 4개 × 단어 4개 예에서 TF-IDF 값과 효용의 순서(생명정보학 글이 위).
  2. 모든 글에 나오는 단어는 IDF = 0이라 가중치 0(구별에 쓸모없다).
  3. 새 글은 평점이 없어도 프로필만 있으면 효용을 계산할 수 있다(새 아이템에 강함).
  4. 카드 C2: 단어가 글 8개 중 2개에 나오면 IDF = log2 4 = 2, 그 글에서 최대 빈도가 4이고 이 단어가 2번이면 TF 0.5, 가중치 1.
"""
import math

WORDS = ["genome", "sequencing", "network", "the"]
DOCS = {  # 단어별 등장 횟수
    "p1": [5, 3, 0, 9],   # 생명정보학
    "p2": [4, 4, 1, 8],   # 생명정보학
    "p3": [0, 0, 6, 7],   # 네트워크
    "p4": [0, 1, 5, 9],   # 네트워크
}


def tfidf(docs):
    N = len(docs)
    n = [sum(1 for f in docs.values() if f[i] > 0) for i in range(len(WORDS))]
    idf = [math.log2(N / k) for k in n]
    out = {}
    for d, f in docs.items():
        m = max(f)
        out[d] = [f[i] / m * idf[i] for i in range(len(WORDS))]
    return out, idf


def cos(a, b):
    na = math.sqrt(sum(x * x for x in a)); nb = math.sqrt(sum(x * x for x in b))
    return sum(x * y for x, y in zip(a, b)) / (na * nb) if na and nb else 0.0


def main():
    w, idf = tfidf(DOCS)
    assert idf[3] == 0 and all(w[d][3] == 0 for d in w)
    print("[OK] 모든 글에 나오는 'the'는 IDF 0, 가중치 0")
    assert round(idf[0], 3) == 1.0 and round(idf[2], 3) == round(math.log2(4 / 3), 3)
    print("     IDF:", [round(x, 3) for x in idf])
    print("     p1 가중치:", [round(x, 3) for x in w["p1"]])
    user = [sum(w[d][i] for d in ("p1",)) for i in range(4)]       # 사용자는 p1을 읽었다
    cands = {d: cos(user, w[d]) for d in ("p2", "p3", "p4")}
    print("     효용:", {d: round(v, 3) for d, v in cands.items()})
    assert max(cands, key=cands.get) == "p2" and cands["p3"] < cands["p2"]
    print("[OK] 생명정보학 글 p2의 효용이 가장 높다")
    new = [3, 2, 0, 5]
    m = max(new); wn = [new[i] / m * idf[i] for i in range(4)]
    assert cos(user, wn) > 0.9
    print(f"[OK] 평점 없는 새 글도 효용 {cos(user, wn):.3f} 계산 가능")
    assert math.log2(8 / 2) == 2 and 2 / 4 * 2 == 1
    print("[OK] 카드 C2: IDF 2, TF 0.5, 가중치 1")
    print("ALL CHECKS PASSED")


if __name__ == "__main__":
    main()