μ΄λν
[P3] κ²°ν© Q9 μ¬μ€κ³: λͺ¨λ°μΌ μΈ ν΅μ μ§μΆ μ§μ μ§λ¬Έ + λͺ¨λ°μΌλ§ μ‘°κ±΄λΆ λΉνμ±
ffaa3d1 | #!/usr/bin/env python3 | |
| """ | |
| νλ₯΄μλ κΈ°λ° seed_dataset μμ± κ³΅ν΅ νλ μμν¬ (μλλ¦¬μ€ λ¬΄κ΄). | |
| κ° μλλ¦¬μ€ μ€ν¬λ¦½νΈ(build_persona/bundle/worker_dataset.py)λ PERSONASΒ·μμ§Β·νλΌλ―Έν°λ§ | |
| μ£Όμ νκ³ build_samples()λ₯Ό νΈμΆνλ€. λμμ κΈ°μ‘΄ 3κ° μ€ν¬λ¦½νΈμ byte-identical. | |
| ν΅μ¬: | |
| - build_samples(): persona μνλ§ β λ΅λ³ β batch β νλ μνμ€ β pattern/event β λΌλ²¨ (RNG μμ 보쑴) | |
| - tree_action_resolver(): behavior_id 2λ¨ νΈλ¦¬ β actions (CS/bundle) | |
| - app_action_resolver(): μ± entity μνμ€ β actions (worker λ± λ¨μΌμ ν) | |
| """ | |
| from __future__ import annotations | |
| import argparse | |
| import json | |
| import random | |
| import sys | |
| from collections import Counter | |
| from pathlib import Path | |
| from typing import Any, Callable | |
| sys.path.insert(0, str(Path(__file__).parent.parent)) | |
| from core.extractor import get_extractor # noqa: E402 | |
| def parse_args() -> argparse.Namespace: | |
| parser = argparse.ArgumentParser() | |
| parser.add_argument("--n", type=int, default=500) | |
| parser.add_argument("--seed", type=int, default=42) | |
| return parser.parse_args() | |
| # ββ νλ μνμ€ β actions λ³ν ββββββββββββββββββββββββββββββββ | |
| def tree_action_resolver(behaviors_data: dict) -> Callable[[list[str]], list[dict]]: | |
| """behavior_id 2λ¨ νΈλ¦¬(step1 + step2.by_parent + step2.common) β resolver(seq)->actions.""" | |
| by_id = {b["id"]: b for b in behaviors_data["step1"]["behaviors"]} | |
| for items in behaviors_data["step2"]["by_parent"].values(): | |
| for b in items: | |
| by_id[b["id"]] = b | |
| for b in behaviors_data["step2"]["common"]: | |
| by_id[b["id"]] = b | |
| def resolve(seq: list[str]) -> list[dict]: | |
| out = [] | |
| for bid in seq: | |
| b = by_id.get(bid) | |
| if b is None: | |
| continue | |
| out.append({"behavior_id": b["id"], "event_type": b["event_type"], "entity": b["entity"]}) | |
| return out | |
| return resolve | |
| def app_action_resolver(seq: list[str]) -> list[dict]: | |
| """μ± entity μνμ€ β actions (event_type=app_open λ¨μΌν).""" | |
| return [{"behavior_id": f"app-{e}", "event_type": "app_open", "entity": e} for e in seq] | |
| def _scalar(d: dict) -> dict: | |
| """μ€μΉΌλΌ featureλ§(ν€ μ λ ¬). list/dictΒ·wall-clock(*_at, λͺ¨λΈ feature μλ) μ μΈ. | |
| μμ§μ set κΈ°λ° ν€ μμ(μ: bundle event νΈλ¦¬κ±°)κΉμ§ μ λ ¬λ‘ ν‘μ β μμ κ²°μ μ μΆλ ₯.""" | |
| return {k: d[k] for k in sorted(d) | |
| if not isinstance(d[k], (list, dict)) and not k.endswith("_at")} | |
| # ββ μν μμ± λ£¨ν (RNG νΈμΆ μμ κ³ μ ) βββββββββββββββββββββββ | |
| def build_samples( | |
| *, | |
| n: int, | |
| seed: int, | |
| personas: list[dict], | |
| engine: Any, # build_batch_features/pattern_features/event_features 보μ λͺ¨λ | |
| seq_key: str, # "action_seqs" | "app_seqs" | |
| action_resolver: Callable[[list[str]], list[dict]], | |
| entity_intents: dict[str, list[str]], | |
| cust_prefix: str, # "C" | "BC" | "WC" | |
| behavior_labels: bool = True, # Falseλ©΄ λͺ¨λΈ λΌλ²¨μ νλ μ νΈ intent μ μΈ (νλβintentλ ranker λ΄λΉ) | |
| answer_fixup: Callable[[dict], None] | None = None, # μν λ΅λ³ μ ν©μ± 보μ (in-place). μ: μ‘°κ±΄λΆ λ¬Έν | |
| ) -> list[dict]: | |
| rng = random.Random(seed) | |
| ex = get_extractor() | |
| rows = [] | |
| for i in range(n): | |
| persona = rng.choices(personas, weights=[p["weight"] for p in personas], k=1)[0] | |
| answers = {qid: rng.choices(list(d), weights=list(d.values()), k=1)[0] | |
| for qid, d in persona["answer_dist"].items()} | |
| if answer_fixup: | |
| answer_fixup(answers) | |
| batch = engine.build_batch_features(answers) | |
| seq = rng.choice(persona[seq_key]) | |
| actions = action_resolver(seq) | |
| # νλ μνμ€λ₯Ό 곡μ μ μ₯μμ ν΅κ³ΌμμΌ Pattern/Event Feature μ°μΆ(μΆλ‘ μμ κ³Ό λμΌ κ²½λ‘) | |
| sid = f"{cust_prefix}{i + 1:05d}" | |
| ex.reset(sid) | |
| for a in actions: | |
| ex.add_event(sid, a["event_type"], a["entity"]) | |
| pattern = engine.pattern_features(sid) | |
| event = engine.event_features(sid) if actions else {} | |
| ex.reset(sid) | |
| # μμ± λΌλ²¨: persona expected_intents (+ behavior_labels=Trueλ©΄ νλ μ νΈ intentλ) | |
| positives = set(persona["expected_intents"]) | |
| if behavior_labels: | |
| for a in actions: | |
| positives.update(entity_intents.get(a["entity"], [])) | |
| labels = {iid: 1 for iid in sorted(positives)} # μ λ ¬ β κ²°μ μ ν€ μμ | |
| rows.append({ | |
| "cust_id": sid, | |
| "persona_id": persona["id"], | |
| "persona_name": persona["name"], | |
| "survey_answers": answers, | |
| "batch_features": _scalar(batch), | |
| "pattern_features": _scalar(pattern), | |
| "event_features": _scalar(event), | |
| "actions": actions, | |
| "intent_labels": labels, | |
| }) | |
| return rows | |
| def write_dataset(out_path: Path, rows: list[dict], scenario_id: str, n_personas: int, seed: int) -> None: | |
| with open(out_path, "w", encoding="utf-8") as f: | |
| json.dump({ | |
| "scenario_id": scenario_id, | |
| "n_samples": len(rows), | |
| "n_personas": n_personas, | |
| "seed": seed, | |
| "samples": rows, | |
| }, f, ensure_ascii=False, indent=2) | |
| print(f"Wrote {out_path} ({len(rows)} samples)") | |
| def print_label_stats(rows: list[dict], *, total: int | None = None, top_n: int | None = None) -> None: | |
| label_counts = Counter() | |
| for r in rows: | |
| for iid in r["intent_labels"]: | |
| label_counts[iid] += 1 | |
| title = f"μμ± λΌλ²¨ Top {top_n}" if top_n else "μμ± λΌλ²¨" | |
| print(f"\nββ {title} ββ") | |
| items = label_counts.most_common(top_n) if top_n else label_counts.most_common() | |
| for iid, c in items: | |
| print(f" {iid}: {c} ({c / len(rows) * 100:.1f}%)") | |
| suffix = f" / {total}" if total else "" | |
| print(f"\n μ΄ μμ± λΌλ²¨ Intent μ’ λ₯: {len(label_counts)}{suffix}") | |