- 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
124 lines
5.3 KiB
Python
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()
|