"""Step 5.1: anchor 후보 랭킹 — 3그룹 + Full candidate.
변경점 (5.0 → 5.1):
- top 15 → top 20 (frequent 그룹만)
- 후보를 3 그룹 + full 로 분리:
1. frequent_candidates — frame_local_count 내림차순 top 20
2. special_candidates — USER_DICT_SL/NNG 중 해당 frame 에 등장한 것 (local desc)
3. unique_to_frame_candidates — frame_df=1 후보 (local desc)
4. full_candidates — 전체 frame 토큰 (local desc)
- score 계산 없음. raw 지표만.
원칙:
- AI 자동 추천 없음. 검수용 후보 목록.
- 3그룹은 서로 겹칠 수 있음 (예: BIM 은 frequent + special 둘 다).
산출:
- anchor_candidates.yaml (4개 리스트 구조)
- anchor_candidates_report.md
- anchor_candidates_report.html (details/summary, TARGET frames open)
"""
import math
import sys
from collections import Counter
from pathlib import Path
import yaml
import markdown
HERE = Path(__file__).parent
sys.path.insert(0, str(HERE))
from pipeline_04_normalize import (
apply_substitutions, build_substitutions, build_kiwi,
extract_tokens, USER_DICT_SL, USER_DICT_NNG,
load_phrase_variants,
)
INPUT_NODES = HERE / "actual_text_nodes.yaml"
INPUT_NORMALIZED = HERE / "normalized_text_tokens.yaml"
OUTPUT_YAML = HERE / "anchor_candidates.yaml"
OUTPUT_MD = HERE / "anchor_candidates_report.md"
OUTPUT_HTML = HERE / "anchor_candidates_report.html"
SPECIAL_TOKENS = set(USER_DICT_SL) | set(USER_DICT_NNG)
FREQUENT_TOP_N = 20
EXAMPLE_COUNT = 3
EXAMPLE_TRUNCATE = 80
TARGET_FRAMES = {13, 14, 18, 29}
TOTAL_FRAMES = 32
def truncate(s, n):
return s if len(s) <= n else s[:n - 1] + '…'
def build_frame_token_counter(text_nodes, subs, kiwi):
dummy_rep = Counter()
dummy_pre = Counter()
counter = Counter()
normalized_texts = []
for t in text_nodes:
after = apply_substitutions(t, subs, dummy_rep, dummy_pre)
normalized_texts.append((t, after))
tokens = extract_tokens(kiwi, after)
for tk in tokens:
counter[tk] += 1
return counter, normalized_texts
def find_examples(token, normalized_texts, limit=EXAMPLE_COUNT):
seen = set()
exs = []
for original, after in normalized_texts:
if token in after and original not in seen:
seen.add(original)
exs.append(original)
if len(exs) >= limit:
break
return exs
def build_row(token, local, corpus_fd, corpus_md_df, normalized_texts):
frame_df = corpus_fd.get(token, 0)
mdx_df = corpus_md_df.get(token, 0)
idf = round(math.log(TOTAL_FRAMES / frame_df), 3) if frame_df > 0 else 0.0
flags = []
if frame_df == 1:
flags.append('unique_to_frame')
return {
'token': token,
'frame_local_count': local,
'frame_df': frame_df,
'mdx_df': mdx_df,
'mdx_hit': mdx_df > 0,
'is_special': token in SPECIAL_TOKENS,
'idf': idf,
'flags': flags,
'examples': find_examples(token, normalized_texts),
}
def rank_groups_for_frame(text_nodes, subs, kiwi, corpus_fd, corpus_md_df):
frame_counter, normalized_texts = build_frame_token_counter(text_nodes, subs, kiwi)
all_rows = [
build_row(tok, local, corpus_fd, corpus_md_df, normalized_texts)
for tok, local in frame_counter.items()
]
# 정렬: local desc, idf desc, token asc
all_rows.sort(key=lambda x: (-x['frame_local_count'], -x['idf'], x['token']))
frequent = all_rows[:FREQUENT_TOP_N]
special = [r for r in all_rows if r['is_special']]
unique_to_frame = [r for r in all_rows if 'unique_to_frame' in r['flags']]
full = all_rows
return {
'total_unique_tokens': len(all_rows),
'frequent_candidates': frequent,
'special_candidates': special,
'unique_to_frame_candidates': unique_to_frame,
'full_candidates': full,
}
# ---------- md / html 렌더 ----------
TABLE_HEADERS = ['token', 'local', 'df/32', 'mdx_df/3', 'mdx_hit',
'special', 'idf', 'flags', 'examples']
def format_examples_md(examples):
if not examples:
return '—'
return '
'.join(truncate(e, EXAMPLE_TRUNCATE) for e in examples)
def rows_to_md_table(rows):
if not rows:
return '_(없음)_'
lines = ['| ' + ' | '.join(TABLE_HEADERS) + ' |',
'|' + '|'.join(['---'] * len(TABLE_HEADERS)) + '|']
for c in rows:
lines.append('| ' + ' | '.join([
c['token'],
str(c['frame_local_count']),
str(c['frame_df']),
str(c['mdx_df']),
'Y' if c['mdx_hit'] else '',
'Y' if c['is_special'] else '',
str(c['idf']),
', '.join(c['flags']),
format_examples_md(c['examples']),
]) + ' |')
return '\n'.join(lines)
def frame_section_md(frame_number, frame_id, groups):
head = f"### Frame {frame_number} / {frame_id} (total_unique {groups['total_unique_tokens']})"
sub1 = f"#### 1. Frequent candidates — top {FREQUENT_TOP_N}\n\n" + rows_to_md_table(groups['frequent_candidates'])
sub2 = f"#### 2. Special / canonical — {len(groups['special_candidates'])}개\n\n" + rows_to_md_table(groups['special_candidates'])
sub3 = f"#### 3. Unique-to-frame (df=1) — {len(groups['unique_to_frame_candidates'])}개\n\n" + rows_to_md_table(groups['unique_to_frame_candidates'])
sub4 = f"#### 4. Full candidates — 전체 {len(groups['full_candidates'])}개\n\n" + rows_to_md_table(groups['full_candidates'])
return '\n\n'.join([head, sub1, sub2, sub3, sub4])
def frame_section_html(frame_number, frame_id, groups, open_by_default):
head_text = f"Frame {frame_number} / {frame_id} (total_unique {groups['total_unique_tokens']})"
def sub(label, rows):
body_md = rows_to_md_table(rows)
body_html = markdown.markdown(body_md, extensions=['tables'])
return f"
score 없음. raw 지표만. AI 자동 추천 없음. 검수용 후보 목록.
TARGET frames 13 / 14 / 18 / 29 는 펼쳐진 상태, 나머지는 접힘.