hpce-dev / scripts /build_cs_dataset.py
이동현
[FEAT] cs 고도화 3차 — P8(디바이스)·P5(가족) 집중
d660561
Raw
History Blame Contribute Delete
13.9 kB
#!/usr/bin/env python3
"""
페르소나 기반 가상 시드 데이터셋 생성.
흐름:
1. 8개 페르소나 정의 (가중치·답변 분포·선호 행동 시퀀스)
2. 가중치에 따라 N명 샘플링
3. 각 고객의 설문 답변·batch feature·행동 시퀀스 시뮬레이션
4. config.behavior_signals(entity→Intent) 매핑으로 양성 Intent 라벨 산출
5. scenarios/cs-myk-v3/seed_dataset.json 적재
실행:
cd roadshow-server-v3
python scripts/build_cs_dataset.py --n 500 --seed 42
"""
from __future__ import annotations
import sys
from collections import Counter
from pathlib import Path
sys.path.insert(0, str(Path(__file__).parent.parent))
from core.engines import config, get_engine # noqa: E402
from scripts._dataset_common import ( # noqa: E402
parse_args, build_samples, tree_action_resolver, write_dataset, print_label_stats,
)
SCENARIO_ID = "cs-myk-v3"
SCENARIO_DIR = Path(__file__).parent.parent / "scenarios" / SCENARIO_ID
# ─────────────────────────────────────────────────────────────
# 페르소나 정의
# weight : 전체 데이터셋에서 차지하는 비율
# answer_dist : {question_id: {answer_code: weight}}
# action_seqs : 선호 행동 시퀀스 후보들 (각각 [behavior_id, ...])
# expected_intents : 행동 매핑 외에 활성으로 표시할 Intent (도메인 지식)
# ─────────────────────────────────────────────────────────────
PERSONAS = [
{
"id": "P1",
"name": "데이터 헤비 + 안정 (추천 수용형)",
"weight": 0.15,
"answer_dist": {
"Q1": {"A": 0.3, "B": 0.5, "C": 0.2}, # 20~40대
"Q2": {"B": 0.3, "C": 0.4, "D": 0.3}, # 1년~5년+
"Q3": {"A": 0.2, "B": 0.5, "C": 0.3}, # 5~10만원+
"Q4": {"C": 0.4, "D": 0.6}, # 인터넷+IPTV 또는 풀결합
"Q5": {"B": 0.5, "C": 0.5}, # 2~4회선+
"Q6": {"A": 0.7, "C": 0.3}, # 데이터/콘텐츠 헤비
"Q7": {"B": 0.4, "C": 0.6}, # 가끔/매달 부족
"Q8": {"A": 0.7, "B": 0.3}, # 청구 안정
"Q9": {"A": 0.8, "B": 0.2}, # 품질 안정
"Q10": {"B": 0.4, "C": 0.6}, # 멤버십 활용
"Q11": {"A": 0.6, "B": 0.4}, # OTT 매일/주2~4
"Q12": {"B": 0.4, "C": 0.4, "D": 0.2}, # 단말 1~3년
"Q13": {"A": 0.5, "B": 0.4, "C": 0.1},
},
"action_seqs": [
["1-E", "2-E1"], ["1-E", "2-E2"], ["1-A", "2-A2"],
["1-D", "2-D2"], ["1-E", "2-E3"],
],
"expected_intents": ["INT-2130", "INT-2320", "INT-1110", "INT-2310"],
},
{
"id": "P2",
"name": "청구 급증 + 품질 불만 (이탈 고위험)",
"weight": 0.12,
"answer_dist": {
"Q1": {"B": 0.3, "C": 0.4, "D": 0.3},
"Q2": {"C": 0.4, "D": 0.6}, # 장기 사용자
"Q3": {"A": 0.2, "B": 0.5, "C": 0.3},
"Q4": {"A": 0.4, "B": 0.4, "C": 0.2}, # 결합 약함
"Q5": {"A": 0.6, "B": 0.4},
"Q6": {"A": 0.4, "B": 0.3, "D": 0.3},
"Q7": {"B": 0.5, "C": 0.5},
"Q8": {"B": 0.4, "C": 0.6}, # 청구 급증
"Q9": {"B": 0.5, "C": 0.5}, # 품질 CS 많음
"Q10": {"A": 0.7, "B": 0.3},
"Q11": {"B": 0.5, "C": 0.5},
"Q12": {"C": 0.4, "D": 0.6}, # 단말 노후
"Q13": {"A": 0.7, "B": 0.3},
},
"action_seqs": [
["1-B", "2-B1"], ["1-C", "2-C2"], ["1-C", "2-C3"],
["1-B", "2-B1", "BACK", "1-C", "2-C2"],
["1-F", "2-F3"],
],
"expected_intents": ["INT-7110", "INT-7120", "INT-7310", "INT-5410", "INT-3210"],
},
{
"id": "P3",
"name": "시니어 저사용 (음성 헤비)",
"weight": 0.12,
"answer_dist": {
"Q1": {"D": 0.4, "E": 0.6}, # 50~60대+
"Q2": {"C": 0.3, "D": 0.7}, # 장기
"Q3": {"C": 0.3, "D": 0.7}, # 3~6만원
"Q4": {"A": 0.6, "B": 0.4}, # 결합 약함
"Q5": {"A": 0.8, "B": 0.2},
"Q6": {"B": 0.8, "D": 0.2}, # 음성 헤비
"Q7": {"A": 0.9, "B": 0.1},
"Q8": {"A": 0.7, "B": 0.3},
"Q9": {"A": 0.6, "B": 0.4},
"Q10": {"A": 0.9, "B": 0.1}, # 멤버십 안 씀
"Q11": {"D": 0.8, "C": 0.2}, # OTT 안 봄
"Q12": {"D": 0.7, "C": 0.3}, # 단말 오래
"Q13": {"A": 0.95, "B": 0.05},
},
"action_seqs": [
["1-B", "2-B2"], ["1-B", "2-B3"], ["1-F", "2-F3"],
["1-A", "2-A3"], ["1-C", "2-C1"],
],
"expected_intents": ["INT-2150", "INT-2420", "INT-1120", "INT-1320", "INT-5330"],
},
{
"id": "P4",
"name": "출장 잦은 직장인 (로밍 의향)",
"weight": 0.08,
"answer_dist": {
"Q1": {"B": 0.5, "C": 0.5}, # 30~40대
"Q2": {"B": 0.3, "C": 0.4, "D": 0.3},
"Q3": {"A": 0.3, "B": 0.5, "C": 0.2},
"Q4": {"C": 0.3, "D": 0.7},
"Q5": {"B": 0.6, "C": 0.4},
"Q6": {"D": 0.7, "A": 0.3}, # 업무 헤비
"Q7": {"B": 0.5, "C": 0.5},
"Q8": {"A": 0.5, "B": 0.5},
"Q9": {"A": 0.7, "B": 0.3},
"Q10": {"B": 0.5, "C": 0.5},
"Q11": {"A": 0.4, "B": 0.5, "C": 0.1},
"Q12": {"B": 0.4, "C": 0.4, "D": 0.2},
"Q13": {"B": 0.5, "C": 0.5}, # 해외 출국
},
"action_seqs": [
["1-A", "2-A3"], ["1-E", "2-E2"], ["1-E", "2-E3"],
["1-D", "2-D2"],
],
"expected_intents": ["INT-2140", "INT-2540", "INT-1140", "INT-2410"],
},
{
"id": "P5",
"name": "가족 결합 활용 가족",
"weight": 0.13,
"answer_dist": {
"Q1": {"C": 0.6, "D": 0.4}, # 40~50대
"Q2": {"C": 0.3, "D": 0.7},
"Q3": {"B": 0.4, "C": 0.6},
"Q4": {"D": 0.8, "C": 0.2}, # 풀결합 위주
"Q5": {"B": 0.4, "C": 0.6}, # 가족 많음
"Q6": {"A": 0.5, "C": 0.5},
"Q7": {"B": 0.4, "C": 0.6},
"Q8": {"A": 0.4, "B": 0.6},
"Q9": {"A": 0.5, "B": 0.5},
"Q10": {"B": 0.4, "C": 0.6},
"Q11": {"A": 0.5, "B": 0.5},
"Q12": {"B": 0.4, "C": 0.4, "D": 0.2},
"Q13": {"A": 0.6, "B": 0.4},
},
"action_seqs": [
["1-E", "2-E3"], ["1-E", "2-E3", "2-E3"],
["1-D", "2-D2", "BACK", "1-E", "2-E3"],
["1-E", "2-E3", "BACK", "1-C", "2-C1"],
],
"expected_intents": ["INT-6110", "INT-6120", "INT-6210", "INT-6140", "INT-1340"],
},
{
"id": "P6",
"name": "이탈 의향 강 (해지 검토)",
"weight": 0.08,
"answer_dist": {
"Q1": {"B": 0.4, "C": 0.4, "D": 0.2},
"Q2": {"C": 0.4, "D": 0.6}, # 장기 후 이탈
"Q3": {"A": 0.4, "B": 0.4, "C": 0.2},
"Q4": {"A": 0.5, "B": 0.5}, # 결합 약함
"Q5": {"A": 0.7, "B": 0.3},
"Q6": {"A": 0.4, "B": 0.3, "D": 0.3},
"Q7": {"B": 0.4, "C": 0.6},
"Q8": {"B": 0.4, "C": 0.6},
"Q9": {"B": 0.4, "C": 0.6},
"Q10": {"A": 0.8, "B": 0.2}, # 혜택 못 챙김
"Q11": {"C": 0.4, "D": 0.6},
"Q12": {"C": 0.4, "D": 0.6},
"Q13": {"A": 0.6, "B": 0.4},
},
"action_seqs": [
["1-C", "2-C2"], ["1-C", "2-C3"],
["1-C", "2-C2", "BACK", "1-C", "2-C3"],
["1-B", "2-B1", "BACK", "1-C", "2-C2", "EXIT"],
],
"expected_intents": ["INT-7110", "INT-7120", "INT-7130", "INT-7210", "INT-7310"],
},
{
"id": "P7",
"name": "신규 가입 콘텐츠 헤비",
"weight": 0.10,
"answer_dist": {
"Q1": {"A": 0.7, "B": 0.3}, # 20~30대
"Q2": {"A": 0.7, "B": 0.3}, # 1년 미만
"Q3": {"B": 0.3, "C": 0.5, "D": 0.2},
"Q4": {"A": 0.6, "B": 0.4}, # 결합 없음
"Q5": {"A": 0.7, "B": 0.3},
"Q6": {"A": 0.5, "C": 0.5},
"Q7": {"B": 0.3, "C": 0.7},
"Q8": {"A": 0.6, "B": 0.4},
"Q9": {"A": 0.8, "B": 0.2},
"Q10": {"B": 0.5, "C": 0.5},
"Q11": {"A": 0.7, "B": 0.3}, # OTT 매일
"Q12": {"A": 0.4, "B": 0.6}, # 신단말
"Q13": {"A": 0.7, "B": 0.3},
},
"action_seqs": [
["1-D", "2-D2"], ["1-D", "2-D3"], ["1-E", "2-E1"],
["1-A", "2-A1"],
],
"expected_intents": ["INT-2310", "INT-2410", "INT-1410", "INT-2130"],
},
{
"id": "P8",
"name": "단말 교체 임박 (약정 만료)",
"weight": 0.10,
"answer_dist": {
"Q1": {"B": 0.3, "C": 0.5, "D": 0.2},
"Q2": {"C": 0.4, "D": 0.6}, # 약정 후반
"Q3": {"B": 0.4, "C": 0.4, "D": 0.2},
"Q4": {"C": 0.4, "D": 0.6},
"Q5": {"B": 0.5, "C": 0.5},
"Q6": {"A": 0.5, "C": 0.5},
"Q7": {"B": 0.5, "C": 0.5},
"Q8": {"A": 0.5, "B": 0.5},
"Q9": {"A": 0.7, "B": 0.3},
"Q10": {"B": 0.5, "C": 0.5},
"Q11": {"A": 0.4, "B": 0.5, "C": 0.1},
"Q12": {"D": 0.8, "C": 0.2}, # 3년+
"Q13": {"A": 0.6, "B": 0.4},
},
"action_seqs": [
["1-E", "2-E2"], ["1-C", "2-C1"], ["1-E", "2-E2", "BACK", "1-D", "2-D1"],
["1-A", "2-A1"],
],
"expected_intents": ["INT-2410", "INT-2440", "INT-2530", "INT-1330", "INT-2110"],
},
{
"id": "P9",
"name": "비용 민감 절감 추구",
"weight": 0.07,
"answer_dist": {
"Q1": {"B": 0.3, "C": 0.4, "D": 0.3},
"Q2": {"B": 0.3, "C": 0.4, "D": 0.3},
"Q3": {"D": 0.6, "C": 0.3, "E": 0.1}, # 저가
"Q4": {"A": 0.5, "B": 0.5},
"Q5": {"A": 0.7, "B": 0.3},
"Q6": {"B": 0.4, "A": 0.3, "C": 0.3},
"Q7": {"B": 0.4, "C": 0.6}, # 데이터 자주 부족 (비용압박)
"Q8": {"B": 0.4, "C": 0.6}, # 청구 급증 경험 (비용부담↑)
"Q9": {"A": 0.7, "B": 0.3},
"Q10": {"A": 0.5, "B": 0.4, "C": 0.1},
"Q11": {"B": 0.4, "C": 0.4, "D": 0.2},
"Q12": {"C": 0.4, "D": 0.6},
"Q13": {"A": 0.8, "B": 0.2},
},
"action_seqs": [
["1-E", "2-E1"], ["1-D", "2-D1"], ["1-B", "2-B1"],
["1-D", "2-D3"],
],
"expected_intents": ["INT-7310", "INT-2150", "INT-4110", "INT-4120", "INT-7340"],
},
{
"id": "P10",
"name": "혜택 적극 활용 VIP",
"weight": 0.05,
"answer_dist": {
"Q1": {"C": 0.4, "D": 0.4, "B": 0.2},
"Q2": {"D": 0.9, "C": 0.1}, # 장기 우수 고객
"Q3": {"A": 0.5, "B": 0.5}, # 고액 요금제
"Q4": {"D": 0.9, "C": 0.1},
"Q5": {"C": 0.7, "B": 0.3},
"Q6": {"A": 0.4, "C": 0.4, "D": 0.2},
"Q7": {"B": 0.5, "C": 0.5},
"Q8": {"A": 0.7, "B": 0.3},
"Q9": {"A": 0.7, "B": 0.3},
"Q10": {"C": 0.9, "B": 0.1}, # 멤버십 적극
"Q11": {"A": 0.6, "B": 0.4},
"Q12": {"B": 0.4, "C": 0.4, "D": 0.2},
"Q13": {"B": 0.5, "C": 0.5},
},
"action_seqs": [
["1-D", "2-D2"], ["1-D", "2-D1"], ["1-D", "2-D3"],
["1-E", "2-E2"],
],
"expected_intents": ["INT-1440", "INT-4240", "INT-4220", "INT-4230", "INT-4210"],
},
]
# entity → 양성 Intent 매핑은 config.get_behavior_signals("cs-myk-v3")
# (= L2_inference.ranker.behavior_signals)를 단일 소스로 사용 — bundle/worker와 동일.
def main() -> None:
args = parse_args()
behaviors = config.get_behaviors(SCENARIO_ID)
rows = build_samples(
n=args.n, seed=args.seed, personas=PERSONAS, engine=get_engine(SCENARIO_ID),
seq_key="action_seqs", action_resolver=tree_action_resolver(behaviors),
entity_intents=config.get_behavior_signals(SCENARIO_ID), # 단일 소스(bundle/worker와 동일)
cust_prefix="C",
)
write_dataset(SCENARIO_DIR / "seed_dataset.json", rows, SCENARIO_ID, len(PERSONAS), args.seed)
# CS 상세: Persona 분포
persona_counts = Counter(r["persona_id"] for r in rows)
print(f"\n── Persona 분포 ────────────────────────────────────────")
for pid, cnt in sorted(persona_counts.items()):
name = next(p["name"] for p in PERSONAS if p["id"] == pid)
print(f" {pid:4s} {name:32s} : {cnt:4d} ({cnt/len(rows)*100:5.1f}%)")
print_label_stats(rows, top_n=20)
if __name__ == "__main__":
main()