#!/usr/bin/env python3 """OpenAI·Perplexity·네이버 AI 브리핑 실측 JSONL을 합쳐 질문별 통합 비교표 MD를 생성한다. 네이버 결과 파일이 없으면 두 엔진만으로 만든다.""" import json import os import time ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) QUESTIONS = os.path.join(ROOT, "scripts", "question_bank_viewclinic.json") OUT_MD = os.path.join(ROOT, "docs", "reports", "viewclinic", "03_question_bank", "Viewclinic_QB_Results_Combined.md") CAT_LABEL = { "A": "A. 브랜드 직접", "B": "B. 위치·운영", "C": "C. 지역 추천·비교", "D1": "D1. 가슴성형", "D2": "D2. 눈성형", "D3": "D3. 코성형", "D4": "D4. 안면윤곽·양악", "D5": "D5. 지방성형", "D6": "D6. 피부·리프팅", "D7": "D7. 재수술", "E": "E. 신뢰·안전", "F": "F. 가격·이벤트", "G": "G. 후기·평판", } # 질문 문장에 병원명이 들어간 브랜드 질문 (뷰 언급이 당연한 그룹) def is_brand(q): return "뷰성형외과" in q["question"] def canon(name): """빈도 집계용 병원명 정규화: 표기 변형을 하나로 합친다.""" n = name.strip().lower().replace(" ", "") if "view" in n and ("plastic" in n or "성형" in n) or n.startswith("뷰성형"): return "뷰성형외과" for suf in ("성형외과의원", "성형외과", "의원", "클리닉", "clinic", "plasticsurgery"): if n.endswith(suf) and len(n) > len(suf): n = n[: -len(suf)] break return n def load(engine): p = os.path.join(ROOT, "scripts", "out", f"qb_{engine}_results.jsonl") rows = {} if os.path.exists(p): for line in open(p): if line.strip(): r = json.loads(line) rows[r["id"]] = r return rows def measured_on(data): """엔진 결과의 실제 측정일(ts 최소~최대). 생성일로 찍으면 과거 측정이 오늘 측정처럼 보인다.""" days = sorted({(r.get("ts") or r.get("measured_at") or "")[:10] for r in data.values()} - {""}) if not days: return "미상" return days[0] if days[0] == days[-1] else f"{days[0]}~{days[-1]}" def cell(r): if not r: return "미실측", "-" if r.get("engine") == "naver_briefing" and not r.get("briefing"): return "브리핑 없음", "-" clinics = [c for c in r.get("clinics", []) if isinstance(c, str)][:5] rank = r.get("view_rank") mark = f"O ({rank}위)" if r.get("view_mentioned") else "X" return mark, ", ".join(clinics) if clinics else "(병원 언급 없음)" def main(): qs = json.load(open(QUESTIONS)) oa, px, nv = load("openai"), load("perplexity"), load("naver_briefing") engines = [("ChatGPT(OpenAI)", oa), ("Perplexity", px)] + ([("네이버 AI 브리핑", nv)] if nv else []) lines = ["# 뷰성형외과 Question Bank 실측 결과: " + " x ".join(e for e, _ in engines) + " 통합", "", "측정일: " + " · ".join(f"{e} {measured_on(d)}" for e, d in engines), "측정 방식: OpenAI Responses API + web_search(gpt-4o, 서울 고정) / Perplexity API(sonar)" + (" / 네이버 AI 브리핑(SerpApi naver_ai_overview, 제3자 SERP API, 모바일)" if nv else "") + ". " "답변엔진 소비자 UI 자동 조회는 약관 위반이므로 정식 API로 측정했다." + (" 네이버는 공식 API가 없어 제3자 API를 썼으며, 고객 리포트 사용 전 법무 확인이 필요하다." if nv else ""), ""] # 집계 stats = {} for eng, data in engines: b = [q for q in qs if is_brand(q) and q["id"] in data] nb = [q for q in qs if not is_brand(q) and q["id"] in data] bm = sum(1 for q in b if data[q["id"]].get("view_mentioned")) nbm = sum(1 for q in nb if data[q["id"]].get("view_mentioned")) stats[eng] = (len(b), bm, len(nb), nbm) lines += ["## 요약", "", "| 엔진 | 브랜드 질문 뷰 언급 | 논브랜드 질문 뷰 언급 |", "|---|---|---|"] for eng, (b, bm, nb, nbm) in stats.items(): lines.append(f"| {eng} | {bm}/{b} ({bm*100//max(b,1)}%) | {nbm}/{nb} ({nbm*100//max(nb,1)}%) |") if nv: shown = [r for r in nv.values() if r.get("briefing")] lines += ["", f"네이버 AI 브리핑 노출: {len(shown)}/{len(nv)}문항. 브리핑이 뜨지 않은 문항은 언급 없음으로 셌다."] lines += ["", "브랜드 질문은 질문 문장에 '뷰성형외과'가 들어간 문항이다. " "승부처인 논브랜드 질문(추천·비교·시술 정보)에서의 언급률이 AI Discovery의 실제 성적이다.", ""] # 병원별 빈도 (논브랜드 질문 한정) for eng, data in engines: freq, label = {}, {} for q in qs: if is_brand(q) or q["id"] not in data: continue seen = set() for i, c in enumerate([c for c in data[q["id"]].get("clinics", []) if isinstance(c, str)][:5]): key = canon(c) if key in seen: continue seen.add(key) label.setdefault(key, c) freq.setdefault(key, [0, 0]) freq[key][0] += 1 if i == 0: freq[key][1] += 1 freq = {label[k]: v for k, v in freq.items()} lines += [f"### {eng}: 논브랜드 질문에서 언급된 병원 상위 15", "", "| 병원 | 언급 질문 수 | 1순위 등장 수 |", "|---|---|---|"] for name, (cnt, first) in sorted(freq.items(), key=lambda x: -x[1][0])[:15]: lines.append(f"| {name} | {cnt} | {first} |") lines.append("") # 문항별 표 (카테고리 섹션별) lines += ["## 문항별 결과", "", "GPT/PPLX" + ("/NAVER" if nv else "") + " 컬럼: O(순위) = 뷰성형외과 언급과 답변 내 등장 순번, X = 미언급" + (", 브리핑 없음 = 네이버가 AI 브리핑을 띄우지 않음" if nv else "") + ".", ""] cur = None for q in qs: cat = q["id"].split("-")[0] if cat != cur: cur = cat head = "| # | 질문 | 사전판정 | GPT | GPT 상위 병원 | PPLX | PPLX 상위 병원 |" + (" NAVER | NAVER 상위 병원 |" if nv else "") lines += [f"### {CAT_LABEL.get(cat, cat)}", "", head, "|" + "---|" * (head.count("|") - 1)] om, oc = cell(oa.get(q["id"])) pm, pc = cell(px.get(q["id"])) row = f"| {q['id']} | {q['question']} | {q['prior']} | {om} | {oc} | {pm} | {pc} |" if nv: nm, nc = cell(nv.get(q["id"])) row += f" {nm} | {nc} |" lines.append(row) open(OUT_MD, "w").write("\n".join(lines) + "\n") print(f"저장: {OUT_MD} (openai {len(oa)} / perplexity {len(px)} / naver_briefing {len(nv)}문항)") if __name__ == "__main__": main()