hpce-dev / scripts /test_eval_formula.py
์ด๋™ํ˜„
[DOCS] ์ „ ๋ชจ๋“ˆ docstringยทtype hint ์ „์ˆ˜ ๋ณด๊ฐ•
4819352
Raw
History Blame Contribute Delete
11.7 kB
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 # noqa: E402
from core.engines.formula import eval_formula # noqa: E402
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}")
# โ”€โ”€ 1. bundle: Bundle Opportunity Index (affine ํ•ฉ + clamp) โ”€โ”€โ”€โ”€โ”€โ”€
# ์›์‹(bundle.py:59): clamp( (fl-1)/3*50 + (1-scr)*30 + hc/2*20 )
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]},
],
}
# โ”€โ”€ 2. bundle: Benefit Optimization Index (min/clip + ์กฐ๊ฑด) โ”€โ”€โ”€โ”€โ”€โ”€
# ์›์‹(bundle.py:64): clamp( min(gap,3)/3*40 + (2-bu)*35 + (25 if dissat in (ํ†ต์‹ ๋น„,ํ˜œํƒ) else 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},
],
}
# โ”€โ”€ 3. cs: _rule_1110 (๋‹ค๋ถ„๊ธฐ threshold) โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
# ์›์‹(cs.py:478): rate>=.85โ†’.65 / >=.65โ†’.45 / >=.40โ†’.25 / else .10
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,
}
# โ”€โ”€ 4. cs: _pattern_boost (boost, mult=1.5) โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
# ์›์‹(cs.py:452): min(v*scale*1.5, cap*1.5)
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}}
# โ”€โ”€ 5. bundle: ๋ฃฐ INT-B1110 (affine + clamp01 + ์กฐ๊ฑด) โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
# ์›์‹(bundle.py:199): clamp01(0.20 + BOI/100*0.55 + (0.1 if fl>=2 else 0))
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},
],
}
# โ”€โ”€ 6. py escape hatch โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
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"}
# โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• ํ™•์žฅ ๋…ธ๋“œ: ๋ณตํ•ฉ ์กฐ๊ฑด / ๋ถ„๊ธฐ ์‹ (cs ๋ฃฐ 6์ข…) โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
PBM = 1.5 # PATTERN_BOOST_MULTIPLIER
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)
# 7. _rule_1340: AND ์กฐ๊ฑด (cs.py:537)
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,
}
# 8. _rule_3110: AND + bool not (cs.py:645)
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,
}
# 9. _rule_5140: AND + then์ด ์‹(boost+clamp) (cs.py:710)
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,
}
# 10. _rule_6130: then์ด affine ์‹ (cs.py:?)
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,
}
# 11. _rule_2240: ์ค‘์ฒฉ ๋ถ„๊ธฐ โ†’ switch (cs.py:?)
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,
}
# 12. _rule_2120: OR ์กฐ๊ฑด + ๋ถ„๊ธฐ๋งˆ๋‹ค ๋‹ค๋ฅธ ์‹ (cs.py:563)
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) # affine ๋ณ€ํ™˜ โ†’ tol
check("benefit_opt", ref_benefit_opt(f), SPEC_BENEFIT_OPT, f) # min/clip+์กฐ๊ฑด โ†’ tol
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) # then์— affine ์—†์Œ โ†’ ์‚ฌ์‹ค์ƒ exact์ง€๋งŒ boost*1.5 ์•ˆ์ „ tol
check("rule_6130", ref_6130(f), SPEC_6130, f) # affine ๋ณ€ํ™˜ โ†’ tol
check("rule_2240", ref_2240(f), SPEC_2240, f, exact=True)
check("rule_2120", ref_2120(f), SPEC_2120, f) # affine(upsell*0.7) ๋ณ€ํ™˜ โ†’ tol
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()