| from __future__ import annotations |
| """ |
| eval_formula ๋ฑ๊ฐ์ฑ ๋จ์ ํ
์คํธ (Step B). |
| |
| ๊ธฐ์กด Python ์์(bundle Index / cs ruleยทboost)์ spec์ผ๋ก ํ๊ธฐํ๊ณ |
| ๋ฌด์์ feature ์
๋ ฅ์์ eval_formula == ์์ ์์ ํ์ ํ๋ค. |
| |
| - affine(์ฐ์ฐ์์ ๋ณํ) : |diff| < 1e-9 |
| - ๋์ผ ์์/์๊ณ/์กฐ๊ฑด/boost : ์์ ์ผ์น (==) |
| |
| ์คํ: python scripts/test_eval_formula.py (์คํจ ์ AssertionError + exit 1) |
| """ |
| import random |
| import sys |
| from pathlib import Path |
|
|
| sys.path.insert(0, str(Path(__file__).parent.parent)) |
|
|
| from core.engines.common import clamp, clamp01 |
| from core.engines.formula import eval_formula |
|
|
| EPS = 1e-9 |
| _fails: list[str] = [] |
|
|
|
|
| def _approx(a: float, b: float, tol: float = EPS) -> bool: |
| """๋ ๊ฐ์ด tol ์ด๋ด๋ก ๊ทผ์ฌํ์ง (affine ์ฐ์ฐ์์ ULP ํ์ฉ์ฉ).""" |
| return abs(a - b) <= tol |
|
|
|
|
| def check(name: str, ref: float, spec, feats: dict, *, exact: bool = False) -> None: |
| """eval_formula(spec) ๊ฒฐ๊ณผ๋ฅผ ๊ธฐ์ค๊ฐ ref์ ๋์กฐ. exact๋ฉด ==, ์๋๋ฉด ๊ทผ์ฌ. ๋ถ์ผ์น๋ _fails์ ๋์ .""" |
| got = eval_formula(spec, feats) |
| ok = (got == ref) if exact else _approx(got, ref) |
| if not ok: |
| _fails.append(f"{name}: ref={ref!r} got={got!r} feats={feats}") |
|
|
|
|
| |
| |
| def ref_bundle_opp(f: dict) -> float: |
| fl = float(f.get("family_line_count", 1)) |
| scr = float(f.get("service_coverage_ratio", 0.0)) |
| hc = float(f.get("household_change", 0)) |
| return clamp(((fl - 1) / 3 * 50) + ((1 - scr) * 30) + (hc / 2 * 20)) |
|
|
| SPEC_BUNDLE_OPP = { |
| "clamp": [0, 100], |
| "terms": [ |
| {"feat": "family_line_count", "default": 1, "linear": [50 / 3, -50 / 3]}, |
| {"feat": "service_coverage_ratio", "linear": [-30, 30]}, |
| {"feat": "household_change", "linear": [10, 0]}, |
| ], |
| } |
|
|
| |
| |
| def ref_benefit_opt(f: dict) -> float: |
| gap = float(f.get("non_mobile_cost_gap", 0)) |
| bu = float(f.get("benefit_utilization", 2)) |
| dissat = str(f.get("dissatisfaction_factor", "์์")) |
| return clamp((min(gap, 3) / 3 * 40) + ((2 - bu) * 35) + (25 if dissat in ("ํต์ ๋น", "ํํ") else 0)) |
|
|
| SPEC_BENEFIT_OPT = { |
| "clamp": [0, 100], |
| "terms": [ |
| {"feat": "non_mobile_cost_gap", "clip": [None, 3], "linear": [40 / 3, 0]}, |
| {"feat": "benefit_utilization", "default": 2, "linear": [-35, 70]}, |
| {"if": {"feat": "dissatisfaction_factor", "in": ["ํต์ ๋น", "ํํ"]}, "then": 25}, |
| ], |
| } |
|
|
| |
| |
| def ref_rule_1110(f: dict) -> float: |
| rate = float(f.get("๋ฐ์ดํฐ ์ฌ์ฉ๋ฅ ", 0)) |
| if rate >= 0.85: return 0.65 |
| if rate >= 0.65: return 0.45 |
| if rate >= 0.40: return 0.25 |
| return 0.10 |
|
|
| SPEC_RULE_1110 = { |
| "feat": "๋ฐ์ดํฐ ์ฌ์ฉ๋ฅ ", |
| "threshold": [[0.85, 0.65], [0.65, 0.45], [0.40, 0.25]], |
| "default": 0.10, |
| } |
|
|
| |
| |
| PBM = 1.5 |
| def ref_boost(f: dict) -> float: |
| v = float(f.get("churn_page_view_count", 0)) |
| return min(v * 0.12 * PBM, 0.30 * PBM) |
|
|
| SPEC_BOOST = {"boost": {"feat": "churn_page_view_count", "scale": 0.12, "cap": 0.30, "mult": PBM}} |
|
|
| |
| |
| def ref_b1110(f: dict) -> float: |
| boi = float(f.get("Bundle Opportunity Index", 0)) |
| fl = float(f.get("family_line_count", 0)) |
| return clamp01(0.20 + boi / 100 * 0.55 + (0.1 if fl >= 2 else 0)) |
|
|
| SPEC_B1110 = { |
| "clamp": [0, 1], |
| "terms": [ |
| 0.20, |
| {"feat": "Bundle Opportunity Index", "linear": [0.55 / 100, 0]}, |
| {"if": {"feat": "family_line_count", "gte": 2}, "then": 0.1}, |
| ], |
| } |
|
|
| |
| def custom_formula(features: dict) -> float: |
| """py escape ๊ฒ์ฆ์ฉ ์ฝ๋ฌ๋ธ (spec {"py": "...:custom_formula"}์์ ํธ์ถ).""" |
| return features.get("x", 0) * 2 + 1 |
| SPEC_PY = {"py": "scripts.test_eval_formula:custom_formula"} |
|
|
|
|
| |
| PBM = 1.5 |
| def _boost(f: dict, key: str, scale: float, cap: float) -> float: |
| """cs _pattern_boost ์ฌํ(รPBM): min(v*scale*PBM, cap*PBM).""" |
| return min(float(f.get(key, 0)) * scale * PBM, cap * PBM) |
|
|
| |
| def ref_1340(f: dict) -> float: |
| return 0.55 if f.get("๊ฒฐํฉ ์ฌ๋ถ", False) and int(f.get("๊ฐ์กฑ ํ์ ์", 1)) >= 2 else 0.20 |
| SPEC_1340 = { |
| "if": {"all": [{"feat": "๊ฒฐํฉ ์ฌ๋ถ", "gte": 1}, {"feat": "๊ฐ์กฑ ํ์ ์", "gte": 2}]}, |
| "then": 0.55, "else": 0.20, |
| } |
|
|
| |
| def ref_3110(f: dict) -> float: |
| if float(f.get("๋ฏธ๋ฉ ๊ธ์ก", 0)) > 0 and not f.get("์๋๋ฉ๋ถ ๋ฑ๋ก ์ฌ๋ถ", True): |
| return 0.70 |
| return 0.0 |
| SPEC_3110 = { |
| "if": {"all": [{"feat": "๋ฏธ๋ฉ ๊ธ์ก", "gt": 0}, {"feat": "์๋๋ฉ๋ถ ๋ฑ๋ก ์ฌ๋ถ", "eq": False}]}, |
| "then": 0.70, "else": 0.0, |
| } |
|
|
| |
| def ref_5140(f: dict) -> float: |
| bundle = str(f.get("๊ฒฐํฉ ํํ", "none")) |
| if bundle in ("home", "full") and float(f.get("ํ์ง ๋ง์กฑ๋", 1)) <= 0.4: |
| return min(0.55 + _boost(f, "quality_action_count", 0.10, 0.25), 0.95) |
| return 0.05 |
| SPEC_5140 = { |
| "if": {"all": [{"feat": "๊ฒฐํฉ ํํ", "in": ["home", "full"]}, {"feat": "ํ์ง ๋ง์กฑ๋", "lte": 0.4}]}, |
| "then": {"clamp": [0, 0.95], "terms": [ |
| 0.55, {"boost": {"feat": "quality_action_count", "scale": 0.10, "cap": 0.25, "mult": PBM}}]}, |
| "else": 0.05, |
| } |
|
|
| |
| def ref_6130(f: dict) -> float: |
| family = int(f.get("๊ฐ์กฑ ํ์ ์", 1)) |
| rem = float(f.get("์์ฌ ๋ฐ์ดํฐ ๋น์จ", 1)) |
| if family >= 2: |
| return 0.30 + (1 - rem) * 0.3 |
| return 0.05 |
| SPEC_6130 = { |
| "if": {"feat": "๊ฐ์กฑ ํ์ ์", "gte": 2}, |
| "then": {"terms": [0.30, {"feat": "์์ฌ ๋ฐ์ดํฐ ๋น์จ", "linear": [-0.3, 0.3]}]}, |
| "else": 0.05, |
| } |
|
|
| |
| def ref_2240(f: dict) -> float: |
| if f.get("๊ฒฐํฉ ์ฌ๋ถ"): |
| return 0.20 |
| return 0.45 if int(f.get("๊ฐ์กฑ ํ์ ์", 1)) >= 2 else 0.20 |
| SPEC_2240 = { |
| "switch": [ |
| {"if": {"feat": "๊ฒฐํฉ ์ฌ๋ถ", "gte": 1}, "then": 0.20}, |
| {"if": {"feat": "๊ฐ์กฑ ํ์ ์", "gte": 2}, "then": 0.45}, |
| ], |
| "else": 0.20, |
| } |
|
|
| |
| def ref_2120(f: dict) -> float: |
| upsell = float(f.get("์
์
์ ํฉ๋ Score", 0)) |
| fee = float(f.get("์๊ธ์ ์์ ์ก", 0)) |
| tier = str(f.get("์๊ธ์ ๊ตฌ๊ฐ", "")) |
| is_5g_like = tier in ("premium", "mid") or fee >= 55000 |
| if is_5g_like: |
| return min(0.20 + _boost(f, "product_explore_count", 0.04, 0.10), 0.40) |
| base = min(upsell * 0.7 + 0.15, 0.70) |
| return min(base + _boost(f, "product_explore_count", 0.08, 0.20), 0.95) |
| SPEC_2120 = { |
| "if": {"any": [{"feat": "์๊ธ์ ๊ตฌ๊ฐ", "in": ["premium", "mid"]}, {"feat": "์๊ธ์ ์์ ์ก", "gte": 55000}]}, |
| "then": {"clamp": [0, 0.40], "terms": [ |
| 0.20, {"boost": {"feat": "product_explore_count", "scale": 0.04, "cap": 0.10, "mult": PBM}}]}, |
| "else": {"clamp": [0, 0.95], "terms": [ |
| {"clamp": [0, 0.70], "value": {"terms": [{"feat": "์
์
์ ํฉ๋ Score", "linear": [0.7, 0]}, 0.15]}}, |
| {"boost": {"feat": "product_explore_count", "scale": 0.08, "cap": 0.20, "mult": PBM}}]}, |
| } |
|
|
|
|
| def _rand_feats(rng: random.Random) -> dict: |
| """๋ฑ๊ฐ์ฑ ๋์กฐ์ฉ ๋ฌด์์ feature dict ์์ฑ.""" |
| return { |
| "family_line_count": rng.choice([1, 2, 3]), |
| "service_coverage_ratio": round(rng.uniform(0, 1), 4), |
| "household_change": rng.choice([0, 1, 2]), |
| "non_mobile_cost_gap": round(rng.uniform(-1, 5), 3), |
| "benefit_utilization": rng.choice([0, 1, 2, 3]), |
| "dissatisfaction_factor": rng.choice(["์์", "ํต์ ๋น", "ํํ", "IPTV ํ์ง"]), |
| "๋ฐ์ดํฐ ์ฌ์ฉ๋ฅ ": round(rng.uniform(0, 1), 4), |
| "churn_page_view_count": rng.choice([0, 1, 2, 3, 5]), |
| "Bundle Opportunity Index": round(rng.uniform(0, 100), 2), |
| "x": rng.randint(0, 10), |
| |
| "๊ฒฐํฉ ์ฌ๋ถ": rng.choice([0, 1]), |
| "๊ฐ์กฑ ํ์ ์": rng.choice([1, 2, 3, 4, 5]), |
| "๋ฏธ๋ฉ ๊ธ์ก": rng.choice([0, 0, 12000, 50000]), |
| "์๋๋ฉ๋ถ ๋ฑ๋ก ์ฌ๋ถ": rng.choice([True, False]), |
| "๊ฒฐํฉ ํํ": rng.choice(["none", "mobile_only", "home", "full"]), |
| "ํ์ง ๋ง์กฑ๋": round(rng.uniform(0, 1), 4), |
| "quality_action_count": rng.choice([0, 1, 2, 3, 5]), |
| "์์ฌ ๋ฐ์ดํฐ ๋น์จ": round(rng.uniform(0, 1), 4), |
| "์
์
์ ํฉ๋ Score": round(rng.uniform(0, 1), 4), |
| "์๊ธ์ ์์ ์ก": rng.choice([35000, 55000, 80000, 100000]), |
| "์๊ธ์ ๊ตฌ๊ฐ": rng.choice(["premium", "mid", "standard", "lite"]), |
| "product_explore_count": rng.choice([0, 1, 2, 4]), |
| } |
|
|
|
|
| def main() -> None: |
| """6+6 ์์ ํจํด์ 2000๊ฐ ๋ฌด์์ ์ผ์ด์ค๋ก eval_formula โก ์์ ๋์กฐ (์คํจ ์ exit 1).""" |
| rng = random.Random(42) |
| for _ in range(2000): |
| f = _rand_feats(rng) |
| check("bundle_opp", ref_bundle_opp(f), SPEC_BUNDLE_OPP, f) |
| check("benefit_opt", ref_benefit_opt(f), SPEC_BENEFIT_OPT, f) |
| check("rule_1110", ref_rule_1110(f), SPEC_RULE_1110, f, exact=True) |
| check("boost", ref_boost(f), SPEC_BOOST, f, exact=True) |
| check("b1110", ref_b1110(f), SPEC_B1110, f) |
| check("py_escape", custom_formula(f), SPEC_PY, f, exact=True) |
| |
| check("rule_1340", ref_1340(f), SPEC_1340, f, exact=True) |
| check("rule_3110", ref_3110(f), SPEC_3110, f, exact=True) |
| check("rule_5140", ref_5140(f), SPEC_5140, f) |
| check("rule_6130", ref_6130(f), SPEC_6130, f) |
| check("rule_2240", ref_2240(f), SPEC_2240, f, exact=True) |
| check("rule_2120", ref_2120(f), SPEC_2120, f) |
|
|
| if _fails: |
| print(f"โ {len(_fails)}๊ฑด ๋ถ์ผ์น (์ต๋ 10๊ฑด):") |
| for e in _fails[:10]: |
| print(" " + e) |
| sys.exit(1) |
| print("โ
eval_formula ๋ฑ๊ฐ์ฑ ํต๊ณผ โ 12๊ฐ ์์ ํจํด(๋ณตํฉ์กฐ๊ฑดยท๋ถ๊ธฐ ํฌํจ) ร 2000 ๋ฌด์์ ์ผ์ด์ค") |
|
|
|
|
| if __name__ == "__main__": |
| main() |
|
|