BurningTimesAi/공유/기획/FirstRising/BM/tools/build_review_table.py

40 lines
4.0 KiB
Python

# CSV(steam_reviews.py 결과) → 보고서 §3-2 마크다운 표로 치환 (BT26-Plan 2026-09-25)
import csv, os, re
D = os.path.dirname(os.path.abspath(__file__))
CSV = os.path.join(D, "..", "data", "steam_reviews_2026-09-25.csv")
MD = os.path.join(D, "..", "2026-09-25_FR_BM_시장조사_v1.md")
BM = { # PM 분류(공개 정보 기준, 논쟁 여지 있는 것은 '논쟁' 표기)
"Dota 2": "코스메틱·트레저(확률 공개)·구독", "Counter-Strike 2": "케이스·키(유료 랜덤)+장터", "Team Fortress 2": "크레이트·키+장터", "Warframe": "코스메틱·편의·플래티넘 유저 거래",
"Path of Exile": "코스메틱·창고 탭", "Path of Exile 2": "유료 EA + 코스메틱·창고 탭", "Apex Legends": "코스메틱·팩(랜덤)·배틀패스", "Destiny 2": "확장팩·시즌·코스메틱",
"Lost Ark": "강화·스킨·재화(P2W 논쟁)", "Marvel Rivals": "코스메틱·만료 없는 패스", "The Finals": "코스메틱·패스", "Once Human": "코스메틱 크레이트·패스",
"War Thunder": "프리미엄 차량·계정(파워성)", "World of Tanks": "프리미엄 차량·계정(파워성)", "PUBG: BATTLEGROUNDS": "코스메틱·크레이트·패스+장터", "Halo Infinite": "코스메틱·패스",
"Overwatch 2": "패스·상점(소급 유료화)", "Brawlhalla": "코스메틱", "Paladins": "코스메틱·챔피언 해금", "Yu-Gi-Oh! Master Duel": "카드팩(랜덤)·젬",
"Black Desert": "편의·펄 상점(P2W 논쟁)", "Throne and Liberty": "패스·코스메틱·재화(P2W 논쟁)", "MapleStory": "큐브 등 확률형·P2W", "Albion Online": "프리미엄 구독·코스메틱",
"EVE Online": "구독(오메가)·PLEX", "Delta Force": "코스메틱·패스", "Rust": "유료 + 코스메틱·장터", "Legends of IdleOn": "젬(편의·일부 랜덤)", "NGU Idle": "편의 결제",
"Clicker Heroes": "루비(편의)", "Idle Slayer": "코스메틱·편의", "Melvor Idle": "유료 + 확장 DLC", "Banana": "장터 드롭 전용", "Tower of Fantasy": "가챠",
"TBH: Task Bar Hero": "장터 드롭·수수료 + DLC 코스튬", "Zenless Zone Zero": "가챠", "Wuthering Waves": "가챠", "GIRLS' FRONTLINE 2: EXILIUM": "가챠", "Reverse: 1999": "가챠",
"Blue Archive": "가챠", "Farmer Against Potatoes Idle": "편의 결제", "Leaf Blower Revolution": "편의·DLC", "Tap Ninja": "편의 결제", "Eternal Return": "코스메틱·패스",
"NARAKA: BLADEPOINT": "코스메틱·외형 가챠", "Idle Wizard": "편의 결제", "Predecessor": "코스메틱", "SMITE 2": "코스메틱·신 해금",
}
rows = list(csv.DictReader(open(CSV, encoding="utf-8-sig")))
def low_month(s):
ms = []
for x in s.split(";"):
if ":" in x:
ym, ud = x.split(":"); u, d = map(int, ud.split("/"))
if u + d >= 200:
ms.append((round(100 * u / (u + d)), ym))
return f"{min(ms)[0]}% ({min(ms)[1]})" if ms else "—"
lines = ["| 게임 | BM 유형(PM 분류) | 무료 | 전체 긍정률(리뷰 수) | Steam 평가 | 최근 30일 | 월별 최저(200건↑) |", "|---|---|---|---|---|---|---|"]
for r in sorted(rows, key=lambda r: -int(r["pct_all"] or 0)):
if r["review_all"] in ("", "No user reviews"):
continue
key = r["name"].split(" -> ")[0]
n_all = int(r["pos_all"] or 0) + int(r["neg_all"] or 0); n30 = int(r["pos_30d"] or 0) + int(r["neg_30d"] or 0)
lines.append(f"| {r['steam_name'] or key} | {BM.get(key, '—')} | {r['free']} | **{r['pct_all']}%** ({n_all:,}) | {r['review_all']} | {r['pct_30d']}% ({n30:,}) | {low_month(r['months_up_down'])} |")
table = "\n".join(lines) + f"\n\n- 총 {len(lines) - 2}종 · '무료'는 Steam 스토어 검색 API 가격 기준 · 최근 30일은 리뷰 히스토그램 일별 합계 · 월별 최저는 월 200건 이상인 달 중 최저 긍정률(급락 시점 = 논란 시점)"
md = open(MD, encoding="utf-8").read()
assert "<!-- REVIEW_TABLE -->" in md
open(MD, "w", encoding="utf-8").write(md.replace("<!-- REVIEW_TABLE -->", table))
print("\n".join(lines[:60]))