# ============================================================================= # 감정분석 모듈 (한국어 + 영어) # 방법 1: KNU 한국어 감성사전 (내장) — 오프라인 # 방법 2: HuggingFace transformers — 온라인/GPU 권장 # ============================================================================= import pandas as pd import numpy as np import re # ── KNU 한국어 감성사전 (축약 내장 버전) ────────────────────────────────────── # 원본: https://github.com/park1200656/KnuSentiLex _KNU_DICT = { # 긍정 "좋다":2,"훌륭하다":2,"뛰어나다":2,"만족":2,"감사":2,"행복":2,"기쁘다":2, "즐겁다":2,"편리하다":2,"탁월하다":2,"우수하다":2,"친절":2,"빠르다":1, "깨끗하다":1,"합리적":1,"저렴하다":1,"신뢰":2,"안전하다":1,"편하다":1, "최고":2,"훌륭":2,"사랑":2,"좋아":2,"웃음":1,"밝다":1,"성공":2,"발전":1, "유익":1,"효과적":1,"혁신":1,"창의":1,"희망":2,"긍정":2,"완벽":2,"탁월":2, "추천":1,"칭찬":2,"흡족":2,"감동":2,"유용하다":1,"도움":1,"쉽다":1, # 부정 "나쁘다":-2,"불만":-2,"싫다":-2,"실망":-2,"최악":-2,"불편":-1,"느리다":-1, "비싸다":-1,"불안":-1,"위험":-1,"문제":-1,"불량":-2,"고장":-2,"오류":-2, "짜증":-2,"화나다":-2,"슬프다":-2,"우울":-2,"부족":-1,"어렵다":-1, "힘들다":-1,"복잡하다":-1,"불친절":-2,"무시":-2,"거짓":-2,"사기":-2, "불신":-2,"두렵다":-1,"피곤":-1,"지루하다":-1,"부정":-2,"낭비":-1, "손해":-2,"후회":-2,"걱정":-1,"아쉽다":-1,"부정적":-2,"억울":-2, } # 부정어 패턴 _NEG_PATTERNS = re.compile(r"(안|못|없|않|안됨|불|비|무|미)") def _knu_score(text: str) -> float: if not isinstance(text, str) or not text.strip(): return 0.0 tokens = re.findall(r'\w+', text) score = 0.0 count = 0 for i, tok in enumerate(tokens): for word, val in _KNU_DICT.items(): if word in tok: # 앞 토큰에 부정어가 있으면 극성 반전 neg = (i > 0 and _NEG_PATTERNS.search(tokens[i-1])) score += -val if neg else val count += 1 return round(score / max(count, 1), 4) def _label(score: float, pos_thr: float = 0.3, neg_thr: float = -0.3) -> str: if score >= pos_thr: return "긍정" if score <= neg_thr: return "부정" return "중립" # ── 방법 2: HuggingFace (선택적) ───────────────────────────────────────────── def _hf_sentiment(texts: list, model_name: str = "snunlp/KR-FinBert-SC"): try: from transformers import pipeline pipe = pipeline("text-classification", model=model_name, tokenizer=model_name, truncation=True, max_length=512) results = [] batch = 16 for i in range(0, len(texts), batch): chunk = [str(t)[:512] for t in texts[i:i+batch]] out = pipe(chunk) results.extend(out) return results except Exception as e: return [{"error": str(e)}] * len(texts) # ── 메인 함수 ───────────────────────────────────────────────────────────────── def run_sentiment(df: pd.DataFrame, text_col: str, method: str = "dictionary", # "dictionary" | "transformer" model_name: str = "snunlp/KR-FinBert-SC", pos_threshold: float = 0.3, neg_threshold: float = -0.3): """ Parameters ---------- text_col : 텍스트 컬럼명 method : 'dictionary' (KNU 사전, 빠름) | 'transformer' (딥러닝, 정확) """ texts = df[text_col].fillna("").tolist() if method == "transformer": try: import transformers # noqa except ImportError: return None, "transformers 패키지가 설치되어 있지 않습니다. dictionary 방법을 사용하세요." raw = _hf_sentiment(texts, model_name) if "error" in raw[0]: return None, f"Transformer 오류: {raw[0]['error']}" labels = [r.get("label","").lower() for r in raw] scores = [r.get("score", 0.0) for r in raw] # 레이블 정규화 def norm_label(l): if "pos" in l or "긍정" in l: return "긍정" if "neg" in l or "부정" in l: return "부정" return "중립" df_out = df[[text_col]].copy() df_out["감정레이블"] = [norm_label(l) for l in labels] df_out["신뢰도"] = [round(s, 4) for s in scores] else: scores = [_knu_score(t) for t in texts] labels = [_label(s, pos_threshold, neg_threshold) for s in scores] df_out = df[[text_col]].copy() df_out["감정점수"] = scores df_out["감정레이블"] = labels # 요약 통계 label_counts = df_out["감정레이블"].value_counts() total = len(df_out) summary = pd.DataFrame({ "감정": label_counts.index, "빈도": label_counts.values, "비율(%)": (label_counts.values / total * 100).round(1) }) # 상위 긍정/부정 텍스트 샘플 if "감정점수" in df_out.columns: pos_top = df_out.nlargest(5, "감정점수")[[text_col,"감정점수","감정레이블"]] neg_top = df_out.nsmallest(5, "감정점수")[[text_col,"감정점수","감정레이블"]] else: pos_top = df_out[df_out["감정레이블"]=="긍정"].head(5) neg_top = df_out[df_out["감정레이블"]=="부정"].head(5) return { "전체결과": df_out, "요약": summary, "긍정상위": pos_top.reset_index(drop=True), "부정상위": neg_top.reset_index(drop=True) }, None