1ca6d8b522
Phase 4 Global Digest 의 클러스터링 핵심 알고리즘 (time-decay weight, adaptive threshold, greedy cosine assign + EMA centroid, importance normalize) 을 `app/services/clustering_common.py` 로 추출. country 축은 caller 책임 — Phase 4 cluster_country 는 그대로 country 별 호출, 신규 morning briefing 모듈이 country 없이 cluster_global 로 호출 예정. selection.py 의 중복 _normalize 도 공통 util 로 통일. 동작 변경 0: - LAMBDA / threshold / EMA alpha / MIN_ARTICLES 모두 Phase 4 기본값 유지 - docs.sort (in-place) → sorted (copy) 변경했으나 caller 가 정렬된 docs 를 재사용하지 않으므로 무관 (dict element 의 weight 부여는 reference 라 그대로 반영) 다음 commit 에서 Phase 4 회귀 검증 (digest regenerate diff 0).
53 lines
1.7 KiB
Python
53 lines
1.7 KiB
Python
"""Phase 4 Global Digest — country 내 topic cluster (time-decay + EMA + adaptive threshold).
|
|
|
|
알고리즘 코어는 `app/services/clustering_common.py` 로 추출되어 briefing 모듈과 공유.
|
|
본 파일은 Phase 4 고유 파라미터 (LAMBDA = ln(2)/3 일, MIN 3, MAX 10) 와 country 축 호출만 담당.
|
|
"""
|
|
|
|
import math
|
|
|
|
from core.utils import setup_logger
|
|
from services.clustering_common import (
|
|
adaptive_threshold_by_density,
|
|
greedy_assign_cluster,
|
|
)
|
|
|
|
logger = setup_logger("digest_clustering")
|
|
|
|
LAMBDA = math.log(2) / 3 # 3일 반감기 — 사용자 확정값
|
|
CENTROID_ALPHA = 0.7 # EMA: 기존 중심 70% 유지, 새 멤버 30% 반영
|
|
MIN_ARTICLES_PER_TOPIC = 3
|
|
MAX_TOPICS_PER_COUNTRY = 10
|
|
|
|
|
|
def adaptive_threshold(n_docs: int) -> float:
|
|
"""Phase 4 임계 (0.75 / 0.78 / 0.80). 외부 import 호환용 alias."""
|
|
return adaptive_threshold_by_density(n_docs)
|
|
|
|
|
|
def cluster_country(country: str, docs: list[dict]) -> list[dict]:
|
|
"""단일 country 의 docs 를 cluster 로 묶어 정렬 + normalize 후 반환.
|
|
|
|
공통 util `greedy_assign_cluster` 위에 country 라벨 로깅만 추가.
|
|
"""
|
|
if not docs:
|
|
logger.info(f"[{country}] docs=0 → skip")
|
|
return []
|
|
|
|
threshold = adaptive_threshold(len(docs))
|
|
clusters, raw_count = greedy_assign_cluster(
|
|
docs,
|
|
threshold=threshold,
|
|
centroid_alpha=CENTROID_ALPHA,
|
|
min_articles=MIN_ARTICLES_PER_TOPIC,
|
|
max_topics=MAX_TOPICS_PER_COUNTRY,
|
|
lambda_val=LAMBDA,
|
|
)
|
|
dropped = raw_count - len(clusters)
|
|
|
|
logger.info(
|
|
f"[{country}] docs={len(docs)} threshold={threshold} "
|
|
f"raw_clusters={raw_count} dropped={dropped} kept={len(clusters)}"
|
|
)
|
|
return clusters
|