Spaces:
Runtime error
Runtime error
| """B2: 유형 카드 분류 테스트.""" | |
| import json | |
| from pathlib import Path | |
| from matching_engine.cluster import classify, compute_cluster_scores | |
| from matching_engine.models import ClusterType, Profile | |
| DATA_PATH = Path(__file__).parent.parent / "heartlog" / "data" / "seed_profiles_1000.json" | |
| def load_profiles() -> list[Profile]: | |
| with open(DATA_PATH, encoding="utf-8") as f: | |
| return [Profile(**p) for p in json.load(f)] | |
| # --- 단위 테스트 --- | |
| class TestComputeClusterScores: | |
| def test_returns_all_five_types(self): | |
| traits = {k: 0.0 for k in ["cooperation", "leadership", "emotional_depth", | |
| "pace", "humor", "risk", "contact_frequency", | |
| "affection", "jealousy", "planning"]} | |
| big_five = {k: 0.0 for k in ["openness", "conscientiousness", | |
| "extraversion", "agreeableness", "neuroticism"]} | |
| scores = compute_cluster_scores(traits, big_five) | |
| assert set(scores.keys()) == set(ClusterType) | |
| def test_flame_high_for_risk_pace_humor(self): | |
| """risk/pace/humor/extraversion 높으면 불꽃형 점수 높아야 함.""" | |
| traits = {"risk": 2.5, "pace": 2.5, "humor": 2.0, | |
| "cooperation": 0.0, "leadership": 0.0, "emotional_depth": 0.0, | |
| "contact_frequency": 0.0, "affection": 0.0, "jealousy": 0.0, "planning": 0.0} | |
| big_five = {"extraversion": 2.0, "openness": 0.0, "conscientiousness": 0.0, | |
| "agreeableness": 0.0, "neuroticism": 0.0} | |
| scores = compute_cluster_scores(traits, big_five) | |
| assert scores[ClusterType.FLAME] == max(scores.values()) | |
| def test_stable_high_for_cooperation_planning(self): | |
| traits = {"cooperation": 2.5, "planning": 2.5, "contact_frequency": 2.0, | |
| "leadership": 0.0, "emotional_depth": 0.0, "pace": 0.0, | |
| "humor": 0.0, "risk": 0.0, "affection": 0.0, "jealousy": 0.0} | |
| big_five = {"agreeableness": 2.0, "conscientiousness": 2.0, | |
| "openness": 0.0, "extraversion": 0.0, "neuroticism": 0.0} | |
| scores = compute_cluster_scores(traits, big_five) | |
| assert scores[ClusterType.STABLE] == max(scores.values()) | |
| class TestClassify: | |
| def test_returns_tuple(self): | |
| traits = {"risk": 3.0, "pace": 3.0, "humor": 3.0, | |
| "cooperation": 0.0, "leadership": 0.0, "emotional_depth": 0.0, | |
| "contact_frequency": 0.0, "affection": 0.0, "jealousy": 0.0, "planning": 0.0} | |
| big_five = {"extraversion": 3.0, "openness": 0.0, "conscientiousness": 0.0, | |
| "agreeableness": 0.0, "neuroticism": 0.0} | |
| top1, top2 = classify(traits, big_five) | |
| assert isinstance(top1, ClusterType) | |
| def test_top2_none_when_gap_large(self): | |
| """1위와 2위 차이가 크면 top2는 None.""" | |
| traits = {"risk": 3.0, "pace": 3.0, "humor": 3.0, | |
| "cooperation": -3.0, "leadership": -3.0, "emotional_depth": -3.0, | |
| "contact_frequency": -3.0, "affection": -3.0, "jealousy": -3.0, "planning": -3.0} | |
| big_five = {"extraversion": 3.0, "openness": -3.0, "conscientiousness": -3.0, | |
| "agreeableness": -3.0, "neuroticism": -3.0} | |
| _, top2 = classify(traits, big_five) | |
| assert top2 is None | |
| # --- 시드 데이터 통합 테스트 --- | |
| class TestSeedProfileClassification: | |
| """시드 프로필의 아키타입 → 클러스터 매핑 검증 (90% 이상 일치).""" | |
| MIN_RATIO = 0.90 # 노이즈로 인한 경계 이탈 허용 | |
| def setup_method(self): | |
| self.profiles = load_profiles() | |
| def _get_profiles_by_archetype(self, archetype: str) -> list[Profile]: | |
| return [p for p in self.profiles if p.archetype == archetype] | |
| def _assert_majority_maps(self, archetype: str, expected: ClusterType) -> None: | |
| profiles = self._get_profiles_by_archetype(archetype) | |
| clusters = [classify(p.traits, p.big_five)[0] for p in profiles] | |
| match_count = sum(1 for c in clusters if c == expected) | |
| ratio = match_count / len(profiles) | |
| assert ratio >= self.MIN_RATIO, ( | |
| f"{archetype} → {expected.value}: {match_count}/{len(profiles)} " | |
| f"({ratio:.0%}, 최소 {self.MIN_RATIO:.0%} 필요)" | |
| ) | |
| def test_all_profiles_classify(self): | |
| for p in self.profiles: | |
| top1, _ = classify(p.traits, p.big_five) | |
| assert isinstance(top1, ClusterType) | |
| def test_flame_archetype_maps_to_flame(self): | |
| self._assert_majority_maps("불꽃형", ClusterType.FLAME) | |
| def test_stable_archetype_maps_to_stable(self): | |
| self._assert_majority_maps("안정형", ClusterType.STABLE) | |
| def test_independent_archetype_maps_to_free(self): | |
| self._assert_majority_maps("독립형", ClusterType.FREE) | |
| def test_strategy_archetype_maps_to_strategy(self): | |
| self._assert_majority_maps("전략형", ClusterType.STRATEGY) | |
| def test_emotion_archetype_maps_to_emotion(self): | |
| self._assert_majority_maps("감성형", ClusterType.EMOTION) | |
| def test_jealous_archetype_maps_to_emotion(self): | |
| self._assert_majority_maps("질투형", ClusterType.EMOTION) | |
| def test_humor_archetype_maps_to_flame(self): | |
| self._assert_majority_maps("유머형", ClusterType.FLAME) | |
| def test_realistic_archetype_maps_to_strategy(self): | |
| self._assert_majority_maps("현실형", ClusterType.STRATEGY) | |
| def test_devoted_mostly_maps_to_stable(self): | |
| """헌신형은 대부분 안정형으로 매핑 (일부 감정형 허용, 60% 이상).""" | |
| profiles = self._get_profiles_by_archetype("헌신형") | |
| clusters = [classify(p.traits, p.big_five)[0] for p in profiles] | |
| stable_count = sum(1 for c in clusters if c == ClusterType.STABLE) | |
| ratio = stable_count / len(profiles) | |
| assert ratio >= 0.60, f"헌신형 중 안정형 {stable_count}/{len(profiles)} ({ratio:.0%})" | |