널 불변 측정 문서의 검증 코드다. if __name__ == "__main__": 아래가 자체 테스트다.

"""널 불변 측정 검증.

문서: 18.널 불변 측정 (예시, 정의, 카드)
표의 열: BC(둘 다), ¬BC(C만), B¬C(B만), ¬B¬C(둘 다 없음, 널 거래).
주장:
  1. 우유(B)·커피(C) 예: lift(B, C) = 8.44, 카이제곱 = 670, 기대 빈도 11.85, lift(B, ¬C) = 0.92.
  2. D1~D6의 카이제곱, lift, Kulc, IR이 문서의 표와 같다.
  3. D1과 D2는 널 거래 수만 다르다(100,000 vs 100). Kulc 0.91, IR 0은 같고 lift와 카이제곱은 크게 다르다.
  4. 널 거래를 아무리 늘려도 Kulc와 IR은 그대로다.
"""


def measures(bc, nbc, bnc, nbnc):
    n = bc + nbc + bnc + nbnc
    sb, sc = bc + bnc, bc + nbc
    lift = (bc / n) / ((sb / n) * (sc / n))
    t = [[bc, nbc], [bnc, nbnc]]
    rows = [sum(r) for r in t]; cols = [sum(c) for c in zip(*t)]
    chi = sum((t[i][j] - rows[i] * cols[j] / n) ** 2 / (rows[i] * cols[j] / n) for i in range(2) for j in range(2))
    kulc = 0.5 * (bc / sb + bc / sc)
    ir = abs(sb - sc) / (sb + sc - bc)
    return chi, lift, kulc, ir


TABLE = {
    "D1": ((10000, 1000, 1000, 100000), (90557, 9.26, 0.91, 0)),
    "D2": ((10000, 1000, 1000, 100), (0, 1, 0.91, 0)),
    "D3": ((100, 1000, 1000, 100000), (670, 8.44, 0.09, 0)),
    "D4": ((1000, 1000, 1000, 100000), (24740, 25.75, 0.5, 0)),
    "D5": ((1000, 100, 10000, 100000), (8173, 9.18, 0.5, 0.89)),
    "D6": ((1000, 10, 100000, 100000), (965, 1.97, 0.5, 0.99)),
}


def main():
    n = 102100
    assert round((100 / n) / ((1100 / n) ** 2), 2) == 8.44
    assert round(1100 * 1100 / n, 2) == 11.85
    assert round((1000 / n) / ((1100 / n) * (101000 / n)), 2) == 0.92
    chi, lift, _, _ = measures(100, 1000, 1000, 100000)
    assert round(chi) == 670
    print("[OK] 우유·커피 예: lift 8.44, 기대 11.85, 카이제곱 670, lift(B, ¬C) 0.92")

    for name, (counts, (c, l, k, i)) in TABLE.items():
        chi, lift, kulc, ir = measures(*counts)
        assert abs(chi - c) <= max(1, 0.001 * c), (name, chi)
        assert round(lift, 2) == l or abs(lift - l) < 0.01, (name, lift)
        assert round(kulc, 2) == k and round(ir, 2) == i, (name, kulc, ir)
        print(f"     {name}: 카이제곱 {chi:,.0f}, lift {lift:.2f}, Kulc {kulc:.2f}, IR {ir:.2f}")
    print("[OK] 표 D1~D6")

    for extra in (0, 10 ** 3, 10 ** 6, 10 ** 9):
        _, lift, kulc, ir = measures(100, 1000, 1000, extra)
        assert round(kulc, 4) == 0.0909 and ir == 0
    print("[OK] 널 거래를 0~10^9로 늘려도 Kulc 0.0909, IR 0 그대로")

    # 카드 C2: BC 500, ¬BC 500, B¬C 500, 널 거래 x에 따라 lift
    lifts = [measures(500, 500, 500, x)[1] for x in (0, 500, 100000)]
    assert round(lifts[1], 3) == 1.0 and lifts[0] < 1 < lifts[2]
    assert round(measures(500, 500, 500, 0)[2], 2) == 0.5
    print(f"[OK] 카드 C2: 널 거래 0, 500, 100000 -> lift {', '.join(f'{v:.2f}' for v in lifts)}, Kulc 0.5")
    print("ALL CHECKS PASSED")


if __name__ == "__main__":
    main()