o2o-infinith-demo/scripts/merge_qb_results.py
Haewon Kam f8a254126c feat: 뷰성형외과 Question Bank 120문항 2엔진 실측 + 기준표 v1.1 과제 + 점수 정합성 교정(50/C)
- Question Bank 120문항(사전판정 ◯5/△35/✕80) + OpenAI·Perplexity API 실측: 브랜드 언급 97~100%, 논브랜드 GPT 11%/PPLX 3%, 시술 정보 52문항 0건
- 러너 run_question_bank_openai.py(재개 가능)·merge_qb_results.py·export_qb_excel.py, 결과 MD 3종 + xlsx 5시트 + raw JSONL
- AEO_GEO_RUBRIC.md v1.1 과제 5건: 서브도메인 실측, D3 표면 범위 확대, 엔진별 판정, 브랜드/논브랜드 축, 점수 표기 정합성
- 점수 교정: scoreDiscovery 계산값 50/C(44.1/89, 검증 32/36)로 진단 리포트 덱·POC 제안서 일괄 교정(구판 48, 커밋 메시지 49는 착오)
- 진단 리포트 덱 13→14장: Validation 슬라이드 추가, A5·B4·C6 재채점 반영, 연락처 o2oteam@o2o.kr 교정
- POC 제안서 10→14장: 부록 4장(QB 설계·실측 KPI·브랜드vs논브랜드·블로그 인용 증거), 랜딩 스크린샷 50점 화면 재캡처

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0155BJMHJyhqqAPGYjGKYhPS
2026-09-01 11:03:15 +09:00

124 lines
5.3 KiB
Python

#!/usr/bin/env python3
"""OpenAI·Perplexity 실측 JSONL 2개를 합쳐 질문별 통합 비교표 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_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 cell(r):
if not r:
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 = load("openai"), load("perplexity")
lines = ["# 뷰성형외과 Question Bank 실측 결과: ChatGPT(OpenAI) x Perplexity 통합", "",
f"측정일: {time.strftime('%Y-%m-%d')}",
"측정 방식: OpenAI Responses API + web_search(gpt-4o, 서울 고정) / Perplexity API(sonar). "
"답변엔진 소비자 UI 자동 조회는 약관 위반이므로 정식 API로 측정했다.", ""]
# 집계
stats = {}
for eng, data in (("ChatGPT(OpenAI)", oa), ("Perplexity", px)):
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)}%) |")
lines += ["",
"브랜드 질문은 질문 문장에 '뷰성형외과'가 들어간 문항이다. "
"승부처인 논브랜드 질문(추천·비교·시술 정보)에서의 언급률이 AI Discovery의 실제 성적이다.", ""]
# 병원별 빈도 (논브랜드 질문 한정)
for eng, data in (("ChatGPT(OpenAI)", oa), ("Perplexity", px)):
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 컬럼: O(순위) = 뷰성형외과 언급과 답변 내 등장 순번, X = 미언급.", ""]
cur = None
for q in qs:
cat = q["id"].split("-")[0]
if cat != cur:
cur = cat
lines += [f"### {CAT_LABEL.get(cat, cat)}", "",
"| # | 질문 | 사전판정 | GPT | GPT 상위 병원 | PPLX | PPLX 상위 병원 |",
"|---|---|---|---|---|---|---|"]
om, oc = cell(oa.get(q["id"]))
pm, pc = cell(px.get(q["id"]))
lines.append(f"| {q['id']} | {q['question']} | {q['prior']} | {om} | {oc} | {pm} | {pc} |")
open(OUT_MD, "w").write("\n".join(lines) + "\n")
print(f"저장: {OUT_MD} (openai {len(oa)} / perplexity {len(px)}문항)")
if __name__ == "__main__":
main()