| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950 |
- import tempfile
- import unittest
- from datetime import datetime
- from pathlib import Path
- from types import SimpleNamespace
- import pandas as pd
- from openpyxl import load_workbook
- from roi_control.data_source import (
- SourceDataNotReadyError,
- build_daily_sql,
- build_source_readiness_sql,
- date_window,
- resolve_end_date,
- )
- from roi_control.fission_multiplier import (
- DISPLAY_MULTIPLIER_COLUMN,
- load_fission_multiplier_parameters,
- )
- from roi_control.metrics import (
- ENTITY_GZH,
- ENTITY_SELF,
- ENTITY_SELF_AD,
- GZH_CHANNEL,
- SELF_CHANNEL,
- prepare_daily_metrics,
- )
- from roi_control.reporting import (
- AGENCY_SUMMARY_SHEETS,
- DAILY_SHEETS,
- SUMMARY_SHEETS,
- _agency_summary_frame,
- _daily_frame,
- _summary_frame,
- _visible_columns,
- write_agency_workbooks,
- write_workbook,
- )
- from roi_control.rules import evaluate_rules as _evaluate_rules
- from roi_control.service import _annotate_current_creative_status, _run_identity
- from tencent_client import ACTIVE_STATUS, SUSPEND_STATUS
- DATES = ["20260720", "20260721", "20260722"]
- FISSION_PARAMETERS = load_fission_multiplier_parameters()
- def evaluate_rules(*args, **kwargs):
- kwargs["fission_parameters"] = FISSION_PARAMETERS
- return _evaluate_rules(*args, **kwargs)
- def row(entity_type, entity_id, dt, roi, *, uv=600, cost=200.0):
- common = {
- "dt": dt,
- "entity_type": entity_type,
- "channel": SELF_CHANNEL if entity_type != ENTITY_GZH else GZH_CHANNEL,
- "代理名称": "",
- "账号id": "",
- "账号名称": "",
- "广告id": "",
- "广告名称": "",
- "包名": "",
- "广告优化目标": "",
- "创意id": "",
- "合作方名": "",
- "公众号名": "",
- "首层UV": uv,
- "T0裂变数": uv * 0.2,
- "成本": cost,
- "效率收入": roi * cost,
- "裂变效率收入": 0,
- }
- if entity_type in (ENTITY_SELF, ENTITY_SELF_AD):
- common.update(
- {
- "代理名称": "代理",
- "账号id": "84502354",
- "账号名称": "账户",
- "广告id": entity_id,
- "广告名称": f"广告{entity_id}",
- "包名": "泛人群",
- "广告优化目标": "关键页面访问次数",
- "创意id": f"creative-{entity_id}" if entity_type == ENTITY_SELF else "",
- }
- )
- else:
- common.update({"合作方名": "合作方", "公众号名": entity_id})
- return common
- class RoiThreeDayRulesTest(unittest.TestCase):
- @staticmethod
- def source_client(latest_dt, *, row_count=100, self_rows=60, gzh_rows=40):
- partition = SimpleNamespace(partition_spec={"dt": latest_dt})
- table = SimpleNamespace(get_max_partition=lambda: partition)
- odps = SimpleNamespace(get_table=lambda _name: table)
- return SimpleNamespace(
- odps=odps,
- execute_sql=lambda _sql: pd.DataFrame(
- [
- {
- "row_count": row_count,
- "self_rows": self_rows,
- "gzh_rows": gzh_rows,
- }
- ]
- ),
- )
- def test_source_readiness_requires_exact_t_minus_one(self):
- client = self.source_client("20260803")
- end_date = resolve_end_date(
- client,
- now=datetime(2026, 8, 4, 9, 0),
- )
- self.assertEqual(end_date, "20260803")
- sql = build_source_readiness_sql(end_date)
- self.assertIn("dt = '20260803'", sql)
- self.assertIn(SELF_CHANNEL, sql)
- self.assertIn(GZH_CHANNEL, sql)
- def test_source_readiness_never_falls_back_to_older_partition(self):
- client = self.source_client("20260802")
- with self.assertRaisesRegex(SourceDataNotReadyError, "required_dt=20260803"):
- resolve_end_date(client, now=datetime(2026, 8, 4, 9, 0))
- def test_source_readiness_rejects_missing_report_channel(self):
- client = self.source_client("20260803", gzh_rows=0)
- with self.assertRaisesRegex(SourceDataNotReadyError, "gzh_rows"):
- resolve_end_date(client, now=datetime(2026, 8, 4, 9, 0))
- def build_daily(self):
- rows = []
- for dt in DATES:
- for index, roi in enumerate([0.1, 1.0, 3.0, 4.0]):
- rows.append(row(ENTITY_SELF, f"creative-ad-{index}", dt, roi))
- for index, roi in enumerate([0.5, 2.0, 5.0, 6.0]):
- rows.append(row(ENTITY_GZH, f"公众号-{index}", dt, roi, uv=300))
- for index, roi in enumerate([0.2, 3.5]):
- rows.append(row(ENTITY_SELF_AD, f"ad-{index}", dt, roi))
- return pd.DataFrame(rows)
- def ages(self):
- return pd.DataFrame(
- {
- "广告id": [f"creative-ad-{i}" for i in range(4)]
- + [f"ad-{i}" for i in range(2)],
- "广告age": [10] * 6,
- }
- )
- def test_versions_and_three_day_window(self):
- self.assertEqual(date_window("20260722"), ("20260720", "20260722"))
- run_id, _ = _run_identity("20260722", FISSION_PARAMETERS)
- self.assertIn("m8", run_id)
- self.assertIn("p15", run_id)
- self.assertIn("r41", run_id)
- def test_current_creative_status_only_reads_stop_decisions(self):
- rows = pd.DataFrame(
- [
- {"entity_type": ENTITY_SELF, "动作": "关停", "账号id": "1", "创意id": "11"},
- {"entity_type": ENTITY_SELF, "动作": "扩量", "账号id": "1", "创意id": "12"},
- {"entity_type": ENTITY_SELF, "动作": "关停", "账号id": "1", "创意id": "13"},
- {"entity_type": ENTITY_GZH, "动作": "关停", "账号id": "", "创意id": ""},
- ]
- )
- class FakeTencent:
- def __init__(self):
- self.calls = []
- def get_dynamic_creative(self, account_id, creative_id):
- self.calls.append((account_id, creative_id))
- return {
- "configured_status": (
- ACTIVE_STATUS if creative_id == 11 else SUSPEND_STATUS
- )
- }
- client = FakeTencent()
- result = _annotate_current_creative_status(rows, client)
- self.assertEqual(client.calls, [(1, 11), (1, 13)])
- self.assertEqual(
- result["当前创意状态"].tolist(),
- ["正常", "", "已停止", ""],
- )
- def test_daily_sql_has_direct_ad_grain_and_excludes_qiwei(self):
- sql = build_daily_sql(DATES[0], DATES[-1])
- self.assertEqual(sql.count("SUM(NVL(t0_fission_uv_root, 0))"), 3)
- self.assertIn("'self_ad' AS entity_type", sql)
- self.assertNotIn("'qiwei' AS entity_type", sql)
- self.assertIn("COUNT(DISTINCT mid) AS 首层UV", sql)
- self.assertIn("usersharedepth <= 1", sql)
- self.assertNotIn("usersharedepth = '0'", sql)
- def test_channel_independent_stop_samples_and_creative_only_p80_scale_actions(self):
- candidates, thresholds, summary = evaluate_rules(
- self.build_daily(), DATES, self.ages()
- )
- self.assertEqual(len(thresholds), 2)
- by_type = thresholds.set_index("entity_type")
- self.assertTrue(pd.isna(by_type.loc[ENTITY_SELF, "t_stop"]))
- self.assertAlmostEqual(float(by_type.loc[ENTITY_GZH, "t_stop"]), 1.4)
- self.assertAlmostEqual(float(by_type.loc[ENTITY_SELF, "t_up"]), 3.4)
- self.assertEqual(int(by_type.loc[ENTITY_SELF, "阈值样本数"]), 4)
- self.assertEqual(int(by_type.loc[ENTITY_GZH, "阈值样本数"]), 4)
- self.assertEqual(int(by_type.loc[ENTITY_SELF, "扩量样本数"]), 4)
- self.assertEqual(
- by_type.loc[ENTITY_SELF, "关停线口径"],
- "T-1实际成本5%软预算动态线",
- )
- self.assertEqual(
- by_type.loc[ENTITY_SELF, "扩量线口径"],
- "合格小程序创意实体等权P80",
- )
- scale_rows = candidates[candidates["动作"].eq("扩量")]
- self.assertEqual(len(scale_rows), 1)
- self.assertEqual(scale_rows.iloc[0]["entity_type"], ENTITY_SELF)
- self.assertEqual(scale_rows.iloc[0]["广告id"], "creative-ad-3")
- self.assertTrue(scale_rows.iloc[0]["是否位于创意三日ROI前20%"])
- self.assertTrue((summary["覆盖天数"] == 3).all())
- pool = summary[summary["entity_type"].isin([ENTITY_SELF, ENTITY_GZH])]
- self.assertTrue(
- pool["阈值样本状态"].eq("进入三日渠道独立阈值样本池").all()
- )
- def test_self_stop_budget_uses_latest_cost_with_six_four_soft_allocation(self):
- rows = []
- formal = [
- ("formal-1", 0.10, 30.0),
- ("formal-2", 0.20, 30.0),
- ("formal-3", 1.00, 500.0),
- ("formal-4", 2.00, 500.0),
- ]
- for entity_id, roi, latest_cost in formal:
- for dt in DATES:
- rows.append(
- row(
- ENTITY_SELF,
- entity_id,
- dt,
- roi,
- cost=latest_cost if dt == DATES[-1] else 100.0,
- )
- )
- rows.extend(
- [
- row(ENTITY_SELF, "hard-1", DATES[-1], 0.05, uv=300, cost=20.0),
- row(ENTITY_SELF, "hard-2", DATES[-1], 0.10, uv=300, cost=200.0),
- row(ENTITY_SELF, "rank-1", DATES[-1], 0.05, uv=600, cost=20.0),
- row(ENTITY_SELF, "rank-2", DATES[-1], 0.40, uv=600, cost=700.0),
- ]
- )
- ages = pd.DataFrame(
- {
- "广告id": [item[0] for item in formal]
- + ["hard-1", "hard-2", "rank-1", "rank-2"],
- "广告age": [10] * 8,
- }
- )
- _, thresholds, summary = evaluate_rules(pd.DataFrame(rows), DATES, ages)
- self_threshold = thresholds.set_index("entity_type").loc[ENTITY_SELF]
- selected = summary[
- summary["entity_type"].eq(ENTITY_SELF) & summary["动作"].eq("关停")
- ]
- selected_ids = set(selected["广告id"])
- latest_cost = selected[f"成本_{DATES[-1]}"].sum()
- self.assertEqual(
- selected_ids,
- {"formal-1", "formal-2", "hard-1", "rank-1"},
- )
- self.assertAlmostEqual(float(latest_cost), 100.0)
- self.assertAlmostEqual(float(self_threshold["小程序昨日总成本"]), 2000.0)
- self.assertAlmostEqual(float(self_threshold["目标关停成本"]), 100.0)
- self.assertAlmostEqual(float(self_threshold["实际关停成本"]), 100.0)
- self.assertAlmostEqual(float(self_threshold["实际关停成本占比"]), 0.05)
- self.assertEqual(self_threshold["关停成本预算状态"], "正常范围")
- self.assertAlmostEqual(float(self_threshold["三日基础预算成本"]), 60.0)
- self.assertAlmostEqual(float(self_threshold["单日基础预算成本"]), 40.0)
- self.assertAlmostEqual(float(self_threshold["三日关停线"]), 0.20)
- self.assertAlmostEqual(float(self_threshold["三日关停线分位点"]), 0.50)
- self.assertAlmostEqual(float(self_threshold["单日P10线"]), 0.05)
- self.assertAlmostEqual(float(self_threshold["单日合并资格线"]), 0.05)
- self.assertAlmostEqual(float(self_threshold["单日实际关停线"]), 0.05)
- self.assertEqual(
- selected.groupby("关停规则")[f"成本_{DATES[-1]}"].sum().to_dict(),
- {
- "三日持续低ROI": 60.0,
- "单日绝对低ROI且P10": 40.0,
- },
- )
- def test_self_and_official_account_stop_lines_are_independent(self):
- daily = self.build_daily()
- latest_costs = {
- "creative-ad-0": 10.0,
- "creative-ad-1": 330.0,
- "creative-ad-2": 330.0,
- "creative-ad-3": 330.0,
- }
- for entity_id, cost in latest_costs.items():
- mask = (
- daily["entity_type"].eq(ENTITY_SELF)
- & daily["广告id"].eq(entity_id)
- & daily["dt"].eq(DATES[-1])
- )
- roi = float(daily.loc[mask, "效率收入"].iloc[0]) / float(
- daily.loc[mask, "成本"].iloc[0]
- )
- daily.loc[mask, "成本"] = cost
- daily.loc[mask, "效率收入"] = roi * cost
- _, thresholds, summary = evaluate_rules(daily, DATES, self.ages())
- by_type = thresholds.set_index("entity_type")
- self.assertEqual(set(by_type.index), {ENTITY_SELF, ENTITY_GZH})
- self.assertAlmostEqual(float(by_type.loc[ENTITY_SELF, "t_stop"]), 0.10)
- self.assertAlmostEqual(
- float(by_type.loc[ENTITY_SELF, "关停线分位点"]),
- 0.25,
- )
- self.assertAlmostEqual(float(by_type.loc[ENTITY_GZH, "t_stop"]), 1.40)
- self.assertAlmostEqual(
- float(by_type.loc[ENTITY_GZH, "关停线分位点"]),
- 0.20,
- )
- self_rows = summary[summary["entity_type"].eq(ENTITY_SELF)]
- gzh_rows = summary[summary["entity_type"].eq(ENTITY_GZH)]
- self.assertTrue(self_rows["t_stop"].eq(0.10).all())
- self.assertTrue(
- gzh_rows["t_stop"].apply(
- lambda value: abs(float(value) - 1.40) < 1e-9
- ).all()
- )
- def test_unused_one_day_budget_rolls_to_three_day_candidates(self):
- rows = []
- formal = [
- ("formal-roll-1", 0.10, 60.0),
- ("formal-roll-2", 0.20, 20.0),
- ("formal-roll-3", 1.00, 900.0),
- ]
- for entity_id, roi, latest_cost in formal:
- for dt in DATES:
- rows.append(
- row(
- ENTITY_SELF,
- entity_id,
- dt,
- roi,
- cost=latest_cost if dt == DATES[-1] else 100.0,
- )
- )
- rows.extend(
- [
- row(ENTITY_SELF, "rank-roll-1", DATES[-1], 0.05, uv=600, cost=20),
- row(ENTITY_SELF, "rank-roll-2", DATES[-1], 0.40, uv=600, cost=1000),
- ]
- )
- ages = pd.DataFrame(
- {
- "广告id": [item[0] for item in formal]
- + ["rank-roll-1", "rank-roll-2"],
- "广告age": [10] * 5,
- }
- )
- _, thresholds, summary = evaluate_rules(pd.DataFrame(rows), DATES, ages)
- selected = summary[
- summary["entity_type"].eq(ENTITY_SELF) & summary["动作"].eq("关停")
- ]
- self_threshold = thresholds.set_index("entity_type").loc[ENTITY_SELF]
- self.assertEqual(
- set(selected["广告id"]),
- {"formal-roll-1", "formal-roll-2", "rank-roll-1"},
- )
- self.assertAlmostEqual(float(self_threshold["实际关停成本"]), 100.0)
- self.assertAlmostEqual(float(self_threshold["三日基础预算成本"]), 60.0)
- self.assertAlmostEqual(float(self_threshold["三日实际关停成本"]), 80.0)
- self.assertAlmostEqual(float(self_threshold["单日实际关停成本"]), 20.0)
- self.assertEqual(self_threshold["关停成本预算状态"], "正常范围")
- def test_creative_p80_scale_requires_ad_age_at_least_three_days(self):
- ages = self.ages()
- ages.loc[ages["广告id"].eq("creative-ad-3"), "广告age"] = 2
- candidates, _, _ = evaluate_rules(self.build_daily(), DATES, ages)
- target = candidates[candidates["广告id"].eq("creative-ad-3")].iloc[0]
- self.assertEqual(target["动作"], "观察")
- self.assertIn("广告age<3天", target["动作原因"])
- def test_ad_level_reuses_threshold_without_entering_sample_and_can_stop(self):
- daily = self.build_daily()
- low_creative = daily["entity_type"].eq(ENTITY_SELF) & daily["广告id"].eq(
- "creative-ad-0"
- )
- latest_low = low_creative & daily["dt"].eq(DATES[-1])
- daily.loc[latest_low, ["成本", "效率收入"]] = [10.0, 1.0]
- low_ad = daily["entity_type"].eq(ENTITY_SELF_AD) & daily["广告id"].eq("ad-0")
- daily.loc[low_ad, "效率收入"] = daily.loc[low_ad, "成本"] * 0.05
- _, thresholds, summary = evaluate_rules(daily, DATES, self.ages())
- ad_rows = summary[summary["entity_type"].eq(ENTITY_SELF_AD)]
- self.assertTrue(
- ad_rows["阈值样本状态"].eq("广告级三日合格_不进入阈值样本池").all()
- )
- actions = dict(zip(ad_rows["广告id"], ad_rows["动作"]))
- self.assertEqual(actions["ad-0"], "关停")
- self.assertEqual(actions["ad-1"], "")
- self_line = float(
- thresholds.set_index("entity_type").loc[ENTITY_SELF, "t_stop"]
- )
- self.assertTrue(ad_rows["t_stop"].eq(self_line).all())
- self.assertTrue(ad_rows["调控参与状态"].str.contains("审批后可暂停广告").all())
- def test_latest_day_uv_over_200_is_appended_with_fixed_three_day_average(self):
- daily = self.build_daily()
- latest_only = row(ENTITY_SELF, "latest-only", DATES[-1], 1.5, uv=300)
- daily = pd.concat([daily, pd.DataFrame([latest_only])], ignore_index=True)
- candidates, _, summary = evaluate_rules(daily, DATES, self.ages())
- target = summary[summary["广告id"].eq("latest-only")].iloc[0]
- self.assertEqual(target["覆盖天数"], 1)
- self.assertEqual(target["阈值样本状态"], "单日补充决策_昨日UV>200")
- self.assertEqual(target["动作"], "观察")
- self.assertEqual(target["日均首层UV"], 100)
- self.assertTrue(candidates["广告id"].eq("latest-only").any())
- def test_one_day_rule_requires_absolute_low_roi_and_p10_and_age(self):
- daily = self.build_daily()
- supplemental = [
- row(ENTITY_SELF, "one-day-both", DATES[-1], 0.05, uv=300, cost=10),
- row(ENTITY_SELF, "one-day-not-p10", DATES[-1], 0.15, uv=300, cost=10),
- row(ENTITY_SELF, "one-day-young", DATES[-1], 0.01, uv=300, cost=10),
- row(ENTITY_SELF, "one-day-mid", DATES[-1], 1.0, uv=300, cost=10),
- row(ENTITY_SELF, "one-day-high", DATES[-1], 2.0, uv=300, cost=900),
- ]
- daily = pd.concat([daily, pd.DataFrame(supplemental)], ignore_index=True)
- ages = pd.concat(
- [
- self.ages(),
- pd.DataFrame(
- {
- "广告id": [
- "one-day-both",
- "one-day-not-p10",
- "one-day-young",
- "one-day-mid",
- "one-day-high",
- ],
- "广告age": [4, 4, 3, 4, 4],
- }
- ),
- ],
- ignore_index=True,
- )
- _, thresholds, summary = evaluate_rules(daily, DATES, ages)
- self_threshold = thresholds.set_index("entity_type").loc[ENTITY_SELF]
- self.assertEqual(int(self_threshold["单日候选池样本数"]), 4)
- self.assertAlmostEqual(float(self_threshold["单日P10线"]), 0.08)
- self.assertAlmostEqual(float(self_threshold["单日合并资格线"]), 0.08)
- targets = summary.set_index("广告id")
- self.assertEqual(targets.loc["one-day-both", "动作"], "关停")
- self.assertEqual(
- targets.loc["one-day-both", "关停规则"],
- "单日绝对低ROI且P10",
- )
- self.assertIn("同时满足", targets.loc["one-day-both", "动作原因"])
- self.assertIn("ROI≤0.20", targets.loc["one-day-both", "动作原因"])
- self.assertIn("P10", targets.loc["one-day-both", "动作原因"])
- self.assertEqual(targets.loc["one-day-not-p10", "动作"], "观察")
- self.assertIn("未进入后10%", targets.loc["one-day-not-p10", "动作原因"])
- self.assertEqual(targets.loc["one-day-young", "动作"], "观察")
- self.assertIn("广告age=3≤3天", targets.loc["one-day-young", "动作原因"])
- creative_frame = _summary_frame(summary, ENTITY_SELF)
- creative_stops = creative_frame[
- creative_frame["建议动作"].eq("关停创意")
- ]
- self.assertIn("one-day-both", creative_stops["广告id"].tolist())
- self.assertNotIn("one-day-not-p10", creative_stops["广告id"].tolist())
- self.assertTrue(creative_stops["动作"].eq("关停").all())
- self.assertTrue(
- summary[
- summary["entity_type"].isin([ENTITY_SELF, ENTITY_SELF_AD])
- & summary["动作"].eq("关停")
- ]["广告age"].ge(4).all()
- )
- def test_one_day_p10_without_absolute_low_roi_is_observe(self):
- daily = self.build_daily()
- supplemental = [
- row(ENTITY_SELF, f"one-day-high-{index}", DATES[-1], roi, uv=300, cost=10)
- for index, roi in enumerate([0.30, 0.40, 0.50, 0.60, 0.70])
- ]
- daily = pd.concat([daily, pd.DataFrame(supplemental)], ignore_index=True)
- ages = pd.concat(
- [
- self.ages(),
- pd.DataFrame(
- {
- "广告id": [f"one-day-high-{index}" for index in range(5)],
- "广告age": [10] * 5,
- }
- ),
- ],
- ignore_index=True,
- )
- _, thresholds, summary = evaluate_rules(daily, DATES, ages)
- self_threshold = thresholds.set_index("entity_type").loc[ENTITY_SELF]
- target = summary[summary["广告id"].eq("one-day-high-0")].iloc[0]
- self.assertGreater(float(self_threshold["单日P10线"]), 0.20)
- self.assertAlmostEqual(float(self_threshold["单日合并资格线"]), 0.20)
- self.assertEqual(target["动作"], "观察")
- self.assertIn("高于绝对线0.20", target["动作原因"])
- def test_three_day_formal_entity_never_reenters_one_day_rule(self):
- daily = self.build_daily()
- ages = self.ages()
- _, _, summary = evaluate_rules(daily, DATES, ages)
- target = summary[summary["广告id"].eq("creative-ad-0")].iloc[0]
- self.assertEqual(target["阈值样本状态"], "进入三日渠道独立阈值样本池")
- self.assertNotEqual(target["关停规则"], "单日绝对低ROI且P10")
- self.assertNotIn("单日", target["动作原因"])
- def test_daily_prediction_uses_same_day_t0_fission_revenue_once(self):
- raw = pd.DataFrame([row(ENTITY_SELF, "formula", DATES[-1], 0)])
- raw.loc[0, ["成本", "效率收入", "裂变效率收入"]] = [100, 50, 30]
- result = prepare_daily_metrics(raw, FISSION_PARAMETERS).iloc[0]
- multiplier = result[DISPLAY_MULTIPLIER_COLUMN]
- self.assertEqual(result["T0实际裂变收入"], 30)
- self.assertAlmostEqual(result["预测全链路效率收入"], 50 + 30 * multiplier)
- self.assertAlmostEqual(result["ROI"], (50 + 30 * multiplier) / 100)
- def test_agency_workbooks_filter_and_physically_remove_sensitive_columns(self):
- _, _, summary = evaluate_rules(self.build_daily(), DATES, self.ages())
- agency_rows = summary.copy()
- creative_indexes = agency_rows[
- agency_rows["entity_type"].eq(ENTITY_SELF)
- ].index.tolist()
- ad_indexes = agency_rows[
- agency_rows["entity_type"].eq(ENTITY_SELF_AD)
- ].index.tolist()
- agency_rows.loc[creative_indexes, "代理名称"] = "代理B"
- agency_rows.loc[ad_indexes, "代理名称"] = "代理B"
- agency_rows.loc[creative_indexes[0], "代理名称"] = "小程序-代投-贝湉"
- agency_rows.loc[creative_indexes[1], "代理名称"] = "小程序 -代投-贝湉"
- agency_rows.loc[ad_indexes[0], "代理名称"] = "小程序-代投-贝湉"
- original = agency_rows.copy(deep=True)
- with tempfile.TemporaryDirectory() as directory:
- outputs = write_agency_workbooks(
- agency_rows,
- Path(directory),
- "20260803",
- )
- self.assertEqual(
- [row["agency_name"] for row in outputs],
- ["代理B", "小程序-代投-贝湉"],
- )
- pd.testing.assert_frame_equal(agency_rows, original)
- bay = next(
- row for row in outputs if row["agency_name"] == "小程序-代投-贝湉"
- )
- self.assertEqual(bay["creative_rows"], 2)
- self.assertEqual(bay["ad_rows"], 1)
- self.assertEqual(
- Path(bay["report"]).name,
- "20260803_小程序-代投-贝湉_调控建议.xlsx",
- )
- filtered = write_agency_workbooks(
- agency_rows,
- Path(directory) / "filtered",
- "20260803",
- agency_names={"代理B"},
- )
- self.assertEqual(
- [row["agency_name"] for row in filtered],
- ["代理B"],
- )
- forbidden_fragments = (
- "ROI",
- "收入",
- "关停线",
- "扩量线",
- "排名",
- "是否位于",
- "t_stop",
- "t_up",
- "审批",
- "执行",
- "幂等键",
- )
- for output in outputs:
- workbook = load_workbook(output["report"], read_only=False)
- expected_sheets = {
- AGENCY_SUMMARY_SHEETS[ENTITY_SELF],
- AGENCY_SUMMARY_SHEETS[ENTITY_SELF_AD],
- }
- self.assertEqual(set(workbook.sheetnames), expected_sheets)
- self.assertEqual(
- workbook[AGENCY_SUMMARY_SHEETS[ENTITY_SELF_AD]].sheet_state,
- "hidden",
- )
- for sheet_name in expected_sheets:
- sheet = workbook[sheet_name]
- headers = [cell.value for cell in sheet[1]]
- for removed in (
- "包名",
- "广告age",
- "日均首层UV",
- "建议说明",
- "裂变系数-总裂变UV/T0裂变UV",
- "裂变系数-总裂变UV/首层UV",
- "日均T0裂变人数",
- "日均T0裂变率",
- ):
- self.assertNotIn(removed, headers)
- self.assertFalse(
- any(
- fragment in str(header)
- for header in headers
- for fragment in forbidden_fragments
- )
- )
- self.assertTrue(
- all(
- not sheet.column_dimensions[cell.column_letter].hidden
- for cell in sheet[1]
- )
- )
- creative_headers = [
- cell.value
- for cell in workbook[AGENCY_SUMMARY_SHEETS[ENTITY_SELF]][1]
- ]
- self.assertEqual(
- creative_headers,
- [
- "渠道",
- "代理名称",
- "账号id",
- "账号名称",
- "广告id",
- "广告名称",
- "广告优化目标",
- "创意id",
- "日均成本",
- "评分",
- "建议动作",
- "当前创意状态",
- ],
- )
- creative = workbook[AGENCY_SUMMARY_SHEETS[ENTITY_SELF]]
- score_column = creative_headers.index("评分") + 1
- self.assertEqual(
- creative.cell(2, score_column).number_format,
- "0.00",
- )
- self.assertNotIn("当日效率ROI", creative_headers)
- def test_summary_and_daily_report_frames(self):
- daily = self.build_daily()
- daily = pd.concat(
- [
- daily,
- pd.DataFrame(
- [
- row(ENTITY_SELF, "latest-only-low", DATES[-1], 0.1, uv=220),
- row(ENTITY_SELF, "latest-only-high", DATES[-1], 9.0, uv=280),
- ]
- ),
- ],
- ignore_index=True,
- )
- _, thresholds, summary = evaluate_rules(daily, DATES, self.ages())
- frame = _summary_frame(summary, ENTITY_SELF)
- detail = _daily_frame(summary, ENTITY_SELF, DATES)
- self.assertEqual(frame.iloc[-1]["阈值样本状态"], "单日补充决策_昨日UV>200")
- observations = frame[
- frame["阈值样本状态"].eq("单日补充决策_昨日UV>200")
- ]
- self.assertEqual(observations.iloc[0]["广告id"], "latest-only-low")
- formal_actions = frame[
- ~frame["阈值样本状态"].eq("单日补充决策_昨日UV>200")
- ]["建议动作"].tolist()
- action_rank = {"关停创意": 0, "扩量": 1, "观察": 2}
- self.assertEqual(
- [action_rank[action] for action in formal_actions],
- sorted(action_rank[action] for action in formal_actions),
- )
- self.assertIn("日均总预估效率收入", frame.columns)
- neutral = frame[frame["动作"].eq("")]
- self.assertTrue(neutral["建议动作"].eq("观察").all())
- self.assertTrue(
- neutral["建议说明"].str.contains("当前无需关停或扩量").all()
- )
- self.assertIn("日均T0裂变率", frame.columns)
- self.assertNotIn("三日加权平均T0裂变率", _visible_columns(SUMMARY_SHEETS[ENTITY_SELF]))
- self.assertIn("三日均值ROI", frame.columns)
- self.assertIn("当日效率ROI", frame.columns)
- self.assertIn("预测总效率ROI", frame.columns)
- visible = _visible_columns(SUMMARY_SHEETS[ENTITY_SELF])
- self.assertNotIn("整体三日ROI排名百分位", visible)
- self.assertNotIn("是否位于三日ROI后20%", visible)
- self.assertNotIn("最新日首层UV", visible)
- self.assertNotIn("覆盖天数", visible)
- self.assertNotIn("审批选择", visible)
- self.assertEqual(visible[-1], "当前创意状态")
- for removed in ("动作", "动作原因", "阈值样本状态", "执行状态", "执行结果"):
- self.assertNotIn(removed, visible)
- self.assertEqual(
- visible[visible.index("三日均值ROI") : visible.index("建议说明") + 1],
- [
- "三日均值ROI",
- "当日效率ROI",
- "预测总效率ROI",
- "关停线",
- "扩量线(P80)",
- "建议动作",
- "建议说明",
- ],
- )
- self.assertIn("关停线", frame.columns)
- self.assertNotIn("关停线分位点", frame.columns)
- self.assertNotIn("关停线分位点", visible)
- self_threshold = thresholds.set_index("entity_type").loc[ENTITY_SELF]
- self.assertTrue(
- frame["扩量线(P80)"].eq(float(self_threshold["t_up"])).all()
- )
- gzh_frame = _summary_frame(summary, ENTITY_GZH)
- self.assertTrue(gzh_frame["扩量线(P80)"].isna().all())
- self.assertIn("是否低于三日关停线", frame.columns)
- self.assertNotIn("关停线(P25)", frame.columns)
- self.assertNotIn("整体实体等权P25关停线", frame.columns)
- self.assertEqual(set(detail["dt"]), set(DATES))
- self.assertTrue((detail.groupby("广告id").size() == 3).all())
- self.assertEqual(detail.columns[0], "dt")
- self.assertTrue(detail["dt"].astype(str).is_monotonic_decreasing)
- self.assertIn("当日效率ROI", detail.columns)
- self.assertIn("预测总效率ROI", detail.columns)
- with tempfile.TemporaryDirectory() as directory:
- output = Path(directory) / "roi.xlsx"
- write_workbook(
- summary,
- thresholds,
- DATES,
- output,
- {},
- pd.DataFrame(
- [
- {
- "实体类型": "self",
- "匹配层级": "miniapp_package_goal_exact",
- "实体数": 1,
- "渠道实体数": 1,
- "匹配率": 1.0,
- "参数版本": "test",
- }
- ]
- ),
- )
- workbook = load_workbook(output, read_only=False)
- expected = set(SUMMARY_SHEETS.values()) | set(DAILY_SHEETS.values())
- self.assertTrue(expected.issubset(workbook.sheetnames))
- self.assertNotIn("企微群合作", workbook.sheetnames)
- self.assertIn("传播裂变系数匹配", workbook.sheetnames)
- self.assertEqual(
- workbook["传播裂变系数匹配"].sheet_state,
- "hidden",
- )
- for sheet_name in expected:
- self.assertEqual(workbook[sheet_name].freeze_panes, "H2")
- expected_rules = 3 if sheet_name in SUMMARY_SHEETS.values() else 2
- self.assertEqual(
- len(workbook[sheet_name].conditional_formatting),
- expected_rules,
- )
- self.assertEqual(
- workbook[SUMMARY_SHEETS[ENTITY_SELF_AD]].sheet_state, "hidden"
- )
- self.assertEqual(
- workbook[DAILY_SHEETS[ENTITY_SELF_AD]].sheet_state, "hidden"
- )
- creative_sheet = workbook[SUMMARY_SHEETS[ENTITY_SELF]]
- color_rules = [
- rule
- for rules in creative_sheet.conditional_formatting._cf_rules.values()
- for rule in rules
- ]
- self.assertEqual(len(color_rules), 3)
- for rule in color_rules:
- self.assertEqual(rule.type, "colorScale")
- colors = [color.rgb[-6:] for color in rule.colorScale.color]
- self.assertEqual(colors, ["C00000", "FFEB84", "00B050"])
- creative_headers = {
- cell.value: cell.column for cell in creative_sheet[1]
- }
- first_scale_row = next(
- row_number
- for row_number in range(2, creative_sheet.max_row + 1)
- if creative_sheet.cell(
- row_number, creative_headers["建议动作"]
- ).value
- == "扩量"
- )
- self.assertEqual(
- creative_sheet.cell(first_scale_row, 1).border.top.style,
- "medium",
- )
- self.assertEqual(
- creative_sheet.cell(
- first_scale_row, creative_headers["建议动作"]
- ).border.top.style,
- "medium",
- )
- first_observe_row = next(
- row_number
- for row_number in range(first_scale_row + 1, creative_sheet.max_row + 1)
- if creative_sheet.cell(
- row_number, creative_headers["建议动作"]
- ).value
- == "观察"
- )
- self.assertEqual(
- creative_sheet.cell(first_observe_row, 1).border.top.style,
- "medium",
- )
- self.assertEqual(
- creative_sheet.cell(
- first_observe_row, creative_headers["建议动作"]
- ).border.top.style,
- "medium",
- )
- self.assertEqual(
- creative_sheet.cell(2, creative_headers["日均首层UV"]).number_format,
- "0",
- )
- self.assertEqual(
- creative_sheet.cell(
- 2, creative_headers["日均T0裂变人数"]
- ).number_format,
- "0",
- )
- self.assertEqual(
- creative_sheet.cell(2, creative_headers["广告age"]).number_format,
- "0",
- )
- self.assertEqual(
- creative_sheet.cell(
- 2, creative_headers["三日均值ROI"]
- ).number_format,
- "0.00",
- )
- self.assertEqual(
- creative_sheet.cell(
- 2, creative_headers["当日效率ROI"]
- ).number_format,
- "0.00",
- )
- self.assertEqual(
- creative_sheet.cell(
- 2, creative_headers["预测总效率ROI"]
- ).number_format,
- "0.00",
- )
- for header in (
- "裂变系数-总裂变UV/T0裂变UV",
- "裂变系数-总裂变UV/首层UV",
- ):
- self.assertEqual(
- creative_sheet.cell(
- 2, creative_headers[header]
- ).number_format,
- "0.00",
- )
- self.assertEqual(
- creative_sheet.cell(
- 2, creative_headers["扩量线(P80)"]
- ).number_format,
- "0.00",
- )
- run_summary = workbook["运行摘要"]
- summary_rows = {
- run_summary.cell(row_number, 1).value: row_number
- for row_number in range(1, run_summary.max_row + 1)
- }
- self.assertEqual(
- run_summary.cell(summary_rows["创意扩量线(P80)"], 2).number_format,
- "0.00",
- )
- self.assertEqual(
- run_summary.cell(summary_rows["扩量样本数"], 2).number_format,
- "0",
- )
- self.assertEqual(
- run_summary.cell(
- summary_rows["小程序单日实际关停线"], 2
- ).number_format,
- "0.00",
- )
- self.assertEqual(
- run_summary.cell(
- summary_rows["小程序实际关停成本占比"], 2
- ).number_format,
- "0.00%",
- )
- ad_sheet = workbook[SUMMARY_SHEETS[ENTITY_SELF_AD]]
- ad_headers = [cell.value for cell in ad_sheet[1]]
- self.assertIn("审批选择", ad_headers)
- approval_column = ad_headers.index("审批选择") + 1
- self.assertTrue(
- ad_sheet.column_dimensions[
- ad_sheet.cell(1, approval_column).column_letter
- ].hidden
- )
- self.assertGreater(len(ad_sheet.data_validations.dataValidation), 0)
- def test_summary_latest_actual_roi_and_agency_score_use_latest_day(self):
- rows = pd.DataFrame(
- [
- row(ENTITY_SELF, "score-window", dt, roi)
- for dt, roi in zip(DATES, [1.0, 2.0, 3.0])
- ]
- )
- rows.loc[rows["dt"].eq(DATES[-1]), "裂变效率收入"] = 100.0
- ages = pd.DataFrame({"广告id": ["score-window"], "广告age": [10]})
- _, _, summary = evaluate_rules(rows, DATES, ages)
- internal = _summary_frame(summary, ENTITY_SELF).iloc[0]
- agency = _agency_summary_frame(summary, ENTITY_SELF).iloc[0]
- snapshot = summary.iloc[0]
- latest_actual = float(snapshot[f"实际ROI_{DATES[-1]}"])
- latest_predicted = float(snapshot[f"ROI_{DATES[-1]}"])
- self.assertNotAlmostEqual(latest_actual, latest_predicted)
- self.assertAlmostEqual(float(internal["三日均值ROI"]), float(snapshot["实际ROI"]))
- self.assertAlmostEqual(float(internal["当日效率ROI"]), latest_actual)
- self.assertAlmostEqual(float(internal["预测总效率ROI"]), float(snapshot["ROI"]))
- self.assertAlmostEqual(float(agency["评分"]), latest_actual)
- if __name__ == "__main__":
- unittest.main()
|