"""Raw 후보 키워드 → 정제된 카테고리 분류 분류 카테고리: - matchable : 도메인/내용어 (Phase 23~25 keyword 축에 사용) - structure : 구조·레이아웃 서술어 (구조 축 전용, keyword 축에서 제외) - weak_row : row label / 일반어 / 범용 polarity (keyword 축에서 제외) 규칙: 1) STRUCTURE_PATTERNS regex (3열, 2열비교, 4사분면, X섹션, X카드, X강조…) 2) STRUCTURE_EXACT 정확 매칭 (표, 카드, 다이어그램, split-panel, 순환도, 관계도 등) 3) WEAK_ROW_EXACT (정의, 특징, 개요, 구성, 결론, 장점, 단점, 고객, 활용, 관리 등) 4) 나머지 = matchable 목적: keyword 축에 구조어가 새어 들어가 Phase 23~25 비교가 오염되는 문제 해결. """ import re import sys from pathlib import Path sys.path.insert(0, str(Path(__file__).parent)) from phase_common import load_32_frames, load_frame_index HERE = Path(__file__).parent # ─── 구조어 패턴 (regex) ─────────────────────────────── STRUCTURE_PATTERNS = [ r'^\d+열\w*$', # 2열, 3열, 3열비교, 3열카드, 3열표 r'^\d+사분면$', r'^\d+섹션$', r'^\d+강조$', r'^\d+카테고리$', r'^\d+카드$', r'^\d+col-.*$', # 3col-parallel 등 r'^cards-\d+.*$', # cards-3-horizontal ] # ─── 구조어 정확 매칭 ───────────────────────────────── STRUCTURE_EXACT = { # 기본 구조 명사 '표', '카드', '다이어그램', '맵', # 레이아웃 영문/하이픈 'split-panel', 'paired', 'central-split', 'paired-rows', # 한글 레이아웃 서술어 '순환도', '교차다이어그램', '관계도', '4각관계도', '상단배너', '풀페이지시각화', '위치기반시각화', # 주체관계 구조어 (데이터 관계 표현 자체) '주체별관계', # 차트/시각화 서술어 'Rome', '인용구', # BIM 이슈 4사분면 중심 인용 # Phase 17 layouts.yaml 영향 서술어 '3요소', '5요소', # cardinality + 구조 맥락 (매칭 오염 위험) } # ─── 약한 row label / 일반어 ────────────────────────── WEAK_ROW_EXACT = { # 표 컬럼 헤더 전형 '정의', '특징', '개요', '구성', '결론', '장점', '단점', '고객', # 일반 동사형 범용 '관리', '활용', '지원', '운영', '구분', # polarity / 애매어 '적극', '구체', '소극', '의존', # 위치/범주 범용 '해외', '국내', # 과다 등장 수식어 '과다', '기반', } def classify_keyword(kw): """Return one of: 'matchable', 'structure', 'weak_row'.""" # 1) 구조 패턴 for pat in STRUCTURE_PATTERNS: if re.match(pat, kw): return 'structure' # 2) 구조 exact if kw in STRUCTURE_EXACT: return 'structure' # 3) weak row exact if kw in WEAK_ROW_EXACT: return 'weak_row' # 4) default matchable return 'matchable' def classify_frame_keywords(keywords): """Returns dict of lists: matchable / structure / weak_row.""" result = {'matchable': [], 'structure': [], 'weak_row': []} for kw in keywords: cat = classify_keyword(kw) result[cat].append(kw) return result def refined_frame_keywords(keywords): """Phase 23~25 keyword axis 용: matchable 만 반환.""" return [kw for kw in keywords if classify_keyword(kw) == 'matchable'] # ═══ 검수 리포트 생성 ═══ def main(): frames = load_32_frames() idx_data, frame_to_short = load_frame_index() # 전체 키워드 set all_kws = set().union(*[set(v["keywords"]) for v in frames.values()]) # 분류 by_cat = {'matchable': set(), 'structure': set(), 'weak_row': set()} for kw in all_kws: by_cat[classify_keyword(kw)].add(kw) lines = [] lines.append("# 후보 키워드 정제 분류 리포트") lines.append("") lines.append("**문제**: analysis.md 의 '후보 키워드' 필드에 도메인 내용어·구조어·row label 이 혼재.") lines.append("") lines.append("**해결**: 3개 카테고리로 분류해 Phase 23~25 의 keyword 축에는 `matchable` 만 투입.") lines.append("") lines.append("| 카테고리 | 정의 | Phase 23~25 keyword 축 사용 |") lines.append("|----------|------|--------------------------|") lines.append("| **matchable** | 도메인/내용어 (BIM, DX, 발주자, 기술, 품질향상 등) | ✓ |") lines.append("| **structure** | 구조·레이아웃 서술어 (3열비교, 표, 카드, 다이어그램, split-panel 등) | ✗ (구조 축에서 처리) |") lines.append("| **weak_row** | row label / 범용 polarity (정의, 특징, 장점, 적극, 국내 등) | ✗ (변별력 낮음) |") lines.append("") # 전체 통계 lines.append("## 1. 전체 분류 결과") lines.append("") total = len(all_kws) lines.append(f"- 총 고유 키워드: **{total}**") lines.append(f"- matchable: **{len(by_cat['matchable'])}** ({len(by_cat['matchable'])/total*100:.1f}%)") lines.append(f"- structure: **{len(by_cat['structure'])}** ({len(by_cat['structure'])/total*100:.1f}%)") lines.append(f"- weak_row: **{len(by_cat['weak_row'])}** ({len(by_cat['weak_row'])/total*100:.1f}%)") lines.append("") # 카테고리별 전체 리스트 lines.append("## 2. structure 카테고리 전체 ") lines.append("") lines.append("이 어휘들은 '디자인이 어떻게 생겼나'를 표현하는 구조어. " "MDX 본문에 거의 등장하지 않거나 등장해도 매칭 신호로 부적합.") lines.append("") for kw in sorted(by_cat['structure']): lines.append(f"- `{kw}`") lines.append("") lines.append("## 3. weak_row 카테고리 전체") lines.append("") lines.append("row label 또는 일반 polarity 어휘. 여러 MDX/Frame 에 쉽게 등장해 변별력 낮음.") lines.append("") for kw in sorted(by_cat['weak_row']): lines.append(f"- `{kw}`") lines.append("") # 프레임별 정제 전후 비교 lines.append("## 4. 프레임별 정제 전후 비교") lines.append("") lines.append("| # | Frame | raw (수) | matchable (수) | 제거된 structure | 제거된 weak_row |") lines.append("|---|-------|---------|---------------|----------------|---------------|") ordered = sorted(frames.items(), key=lambda kv: frame_to_short.get(kv[0], "99")) for fid, v in ordered: sid = frame_to_short.get(fid, "?") cat = classify_frame_keywords(v["keywords"]) removed_structure = ", ".join(cat['structure']) or "-" removed_weak = ", ".join(cat['weak_row']) or "-" lines.append( f"| {sid} | {fid} | {len(v['keywords'])} | {len(cat['matchable'])} | " f"{removed_structure} | {removed_weak} |" ) lines.append("") # 프레임별 matchable 리스트 lines.append("## 5. 프레임별 정제 후 matchable 키워드") lines.append("") lines.append("| # | Frame | matchable 키워드 |") lines.append("|---|-------|------------------|") for fid, v in ordered: sid = frame_to_short.get(fid, "?") cat = classify_frame_keywords(v["keywords"]) matched = ", ".join(cat['matchable']) or "(비어있음)" lines.append(f"| {sid} | {fid} | {matched} |") lines.append("") # 정책 lines.append("## 6. 적용 정책") lines.append("") lines.append("- **Phase 23~25**: `matchable` 만 keyword 축 vocab 으로 투입.") lines.append("- **Phase 25** 구조 축: `structural_match_v3` (ontology schema) 사용 — 키워드 축과 독립.") lines.append("- **Phase 26** (template-fit-v1): `anchor_sets` 는 이미 구조화된 명명 set 이므로 별도 정제 없음.") lines.append("") lines.append("이 정책으로 'Phase 25 결과 개선 = 구조 축 효과 vs 키워드 안에 섞인 구조어 효과' 구분이 명확해짐.") lines.append("") out = HERE / "KEYWORD_REFINEMENT_REPORT.md" out.write_text("\n".join(lines), encoding="utf-8") print(f"완료: {out}") print() print(f"분류 결과 요약:") print(f" - matchable: {len(by_cat['matchable'])}") print(f" - structure: {len(by_cat['structure'])}") print(f" - weak_row: {len(by_cat['weak_row'])}") if __name__ == "__main__": main()