|
@@ -19,11 +19,13 @@ from roi_control.metrics import (
|
|
|
prepare_daily_metrics,
|
|
prepare_daily_metrics,
|
|
|
)
|
|
)
|
|
|
from roi_control.reporting import (
|
|
from roi_control.reporting import (
|
|
|
|
|
+ AGENCY_SUMMARY_SHEETS,
|
|
|
DAILY_SHEETS,
|
|
DAILY_SHEETS,
|
|
|
SUMMARY_SHEETS,
|
|
SUMMARY_SHEETS,
|
|
|
_daily_frame,
|
|
_daily_frame,
|
|
|
_summary_frame,
|
|
_summary_frame,
|
|
|
_visible_columns,
|
|
_visible_columns,
|
|
|
|
|
+ write_agency_workbooks,
|
|
|
write_workbook,
|
|
write_workbook,
|
|
|
)
|
|
)
|
|
|
from roi_control.rules import evaluate_rules as _evaluate_rules
|
|
from roi_control.rules import evaluate_rules as _evaluate_rules
|
|
@@ -104,8 +106,8 @@ class RoiThreeDayRulesTest(unittest.TestCase):
|
|
|
self.assertEqual(date_window("20260722"), ("20260720", "20260722"))
|
|
self.assertEqual(date_window("20260722"), ("20260720", "20260722"))
|
|
|
run_id, _ = _run_identity("20260722", FISSION_PARAMETERS)
|
|
run_id, _ = _run_identity("20260722", FISSION_PARAMETERS)
|
|
|
self.assertIn("m8", run_id)
|
|
self.assertIn("m8", run_id)
|
|
|
- self.assertIn("p11", run_id)
|
|
|
|
|
- self.assertIn("r29", run_id)
|
|
|
|
|
|
|
+ self.assertIn("p12", run_id)
|
|
|
|
|
+ self.assertIn("r36", run_id)
|
|
|
|
|
|
|
|
def test_current_creative_status_only_reads_stop_decisions(self):
|
|
def test_current_creative_status_only_reads_stop_decisions(self):
|
|
|
rows = pd.DataFrame(
|
|
rows = pd.DataFrame(
|
|
@@ -193,11 +195,72 @@ class RoiThreeDayRulesTest(unittest.TestCase):
|
|
|
candidates, _, summary = evaluate_rules(daily, DATES, self.ages())
|
|
candidates, _, summary = evaluate_rules(daily, DATES, self.ages())
|
|
|
target = summary[summary["广告id"].eq("latest-only")].iloc[0]
|
|
target = summary[summary["广告id"].eq("latest-only")].iloc[0]
|
|
|
self.assertEqual(target["覆盖天数"], 1)
|
|
self.assertEqual(target["覆盖天数"], 1)
|
|
|
- self.assertEqual(target["阈值样本状态"], "补充观察_昨日UV>200")
|
|
|
|
|
|
|
+ self.assertEqual(target["阈值样本状态"], "单日补充决策_昨日UV>200")
|
|
|
self.assertEqual(target["动作"], "观察")
|
|
self.assertEqual(target["动作"], "观察")
|
|
|
self.assertEqual(target["日均首层UV"], 100)
|
|
self.assertEqual(target["日均首层UV"], 100)
|
|
|
self.assertTrue(candidates["广告id"].eq("latest-only").any())
|
|
self.assertTrue(candidates["广告id"].eq("latest-only").any())
|
|
|
|
|
|
|
|
|
|
+ def test_one_day_supplement_requires_ad_age_over_three_days(self):
|
|
|
|
|
+ daily = self.build_daily()
|
|
|
|
|
+ supplemental = [
|
|
|
|
|
+ row(ENTITY_SELF, "one-day-hard", DATES[-1], 0.2, uv=300),
|
|
|
|
|
+ row(ENTITY_SELF, "one-day-p30", DATES[-1], 0.5, uv=600),
|
|
|
|
|
+ row(ENTITY_SELF, "one-day-young", DATES[-1], 0.6, uv=600),
|
|
|
|
|
+ row(ENTITY_SELF, "one-day-mid", DATES[-1], 8.0, uv=600),
|
|
|
|
|
+ row(ENTITY_SELF, "one-day-high", DATES[-1], 9.0, uv=600),
|
|
|
|
|
+ ]
|
|
|
|
|
+ daily = pd.concat([daily, pd.DataFrame(supplemental)], ignore_index=True)
|
|
|
|
|
+ ages = pd.concat(
|
|
|
|
|
+ [
|
|
|
|
|
+ self.ages(),
|
|
|
|
|
+ pd.DataFrame(
|
|
|
|
|
+ {
|
|
|
|
|
+ "广告id": [
|
|
|
|
|
+ "one-day-hard",
|
|
|
|
|
+ "one-day-p30",
|
|
|
|
|
+ "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.assertEqual(int(thresholds.iloc[0]["单日P30样本数"]), 8)
|
|
|
|
|
+ self.assertAlmostEqual(float(thresholds.iloc[0]["t_one_day_stop"]), 0.64)
|
|
|
|
|
+ targets = summary.set_index("广告id")
|
|
|
|
|
+ self.assertEqual(targets.loc["one-day-hard", "动作"], "关停")
|
|
|
|
|
+ self.assertIn("单日硬关停线", targets.loc["one-day-hard", "动作原因"])
|
|
|
|
|
+ self.assertIn("启用单日补充规则", targets.loc["one-day-hard", "动作原因"])
|
|
|
|
|
+ self.assertIn("最新日预测总效率ROI", targets.loc["one-day-hard", "动作原因"])
|
|
|
|
|
+ self.assertIn("三日预测总效率ROI", targets.loc["one-day-hard", "动作原因"])
|
|
|
|
|
+ self.assertIn("不参与本次单日判断", targets.loc["one-day-hard", "动作原因"])
|
|
|
|
|
+ self.assertIn("广告age=4>3天", targets.loc["one-day-hard", "动作原因"])
|
|
|
|
|
+ self.assertEqual(targets.loc["one-day-p30", "动作"], "关停")
|
|
|
|
|
+ self.assertIn("单日实体等权P30", targets.loc["one-day-p30", "动作原因"])
|
|
|
|
|
+ self.assertEqual(targets.loc["one-day-young", "动作"], "观察")
|
|
|
|
|
+ self.assertIn("广告age=3≤3天", targets.loc["one-day-young", "动作原因"])
|
|
|
|
|
+ self.assertEqual(targets.loc["one-day-mid", "动作"], "观察")
|
|
|
|
|
+ self.assertIn("未命中单日关停规则", targets.loc["one-day-mid", "动作原因"])
|
|
|
|
|
+ creative_frame = _summary_frame(summary, ENTITY_SELF)
|
|
|
|
|
+ creative_stops = creative_frame[
|
|
|
|
|
+ creative_frame["建议动作"].eq("关停创意")
|
|
|
|
|
+ ]
|
|
|
|
|
+ self.assertEqual(
|
|
|
|
|
+ creative_stops["广告id"].tolist(),
|
|
|
|
|
+ ["creative-ad-0", "one-day-hard", "one-day-p30"],
|
|
|
|
|
+ )
|
|
|
|
|
+ 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_daily_prediction_uses_same_day_t0_fission_revenue_once(self):
|
|
def test_daily_prediction_uses_same_day_t0_fission_revenue_once(self):
|
|
|
raw = pd.DataFrame([row(ENTITY_SELF, "formula", DATES[-1], 0)])
|
|
raw = pd.DataFrame([row(ENTITY_SELF, "formula", DATES[-1], 0)])
|
|
|
raw.loc[0, ["成本", "效率收入", "裂变效率收入"]] = [100, 50, 30]
|
|
raw.loc[0, ["成本", "效率收入", "裂变效率收入"]] = [100, 50, 30]
|
|
@@ -207,6 +270,123 @@ class RoiThreeDayRulesTest(unittest.TestCase):
|
|
|
self.assertAlmostEqual(result["预测全链路效率收入"], 50 + 30 * multiplier)
|
|
self.assertAlmostEqual(result["预测全链路效率收入"], 50 + 30 * multiplier)
|
|
|
self.assertAlmostEqual(result["ROI"], (50 + 30 * multiplier) / 100)
|
|
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",
|
|
|
|
|
+ )
|
|
|
|
|
+
|
|
|
|
|
+ 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):
|
|
def test_summary_and_daily_report_frames(self):
|
|
|
daily = self.build_daily()
|
|
daily = self.build_daily()
|
|
|
daily = pd.concat(
|
|
daily = pd.concat(
|
|
@@ -224,18 +404,25 @@ class RoiThreeDayRulesTest(unittest.TestCase):
|
|
|
_, thresholds, summary = evaluate_rules(daily, DATES, self.ages())
|
|
_, thresholds, summary = evaluate_rules(daily, DATES, self.ages())
|
|
|
frame = _summary_frame(summary, ENTITY_SELF)
|
|
frame = _summary_frame(summary, ENTITY_SELF)
|
|
|
detail = _daily_frame(summary, ENTITY_SELF, DATES)
|
|
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-high")
|
|
|
|
|
|
|
+ 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[
|
|
formal_actions = frame[
|
|
|
- ~frame["阈值样本状态"].eq("补充观察_昨日UV>200")
|
|
|
|
|
|
|
+ ~frame["阈值样本状态"].eq("单日补充决策_昨日UV>200")
|
|
|
]["建议动作"].tolist()
|
|
]["建议动作"].tolist()
|
|
|
- action_rank = {"关停": 0, "扩量": 1, "": 2, "观察": 3}
|
|
|
|
|
|
|
+ action_rank = {"关停创意": 0, "扩量": 1, "观察": 2}
|
|
|
self.assertEqual(
|
|
self.assertEqual(
|
|
|
[action_rank[action] for action in formal_actions],
|
|
[action_rank[action] for action in formal_actions],
|
|
|
sorted(action_rank[action] for action in formal_actions),
|
|
sorted(action_rank[action] for action in formal_actions),
|
|
|
)
|
|
)
|
|
|
self.assertIn("日均总预估效率收入", frame.columns)
|
|
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.assertIn("日均T0裂变率", frame.columns)
|
|
|
self.assertNotIn("三日加权平均T0裂变率", _visible_columns(SUMMARY_SHEETS[ENTITY_SELF]))
|
|
self.assertNotIn("三日加权平均T0裂变率", _visible_columns(SUMMARY_SHEETS[ENTITY_SELF]))
|
|
|
self.assertIn("当日效率ROI", frame.columns)
|
|
self.assertIn("当日效率ROI", frame.columns)
|
|
@@ -250,13 +437,14 @@ class RoiThreeDayRulesTest(unittest.TestCase):
|
|
|
for removed in ("动作", "动作原因", "阈值样本状态", "执行状态", "执行结果"):
|
|
for removed in ("动作", "动作原因", "阈值样本状态", "执行状态", "执行结果"):
|
|
|
self.assertNotIn(removed, visible)
|
|
self.assertNotIn(removed, visible)
|
|
|
self.assertEqual(
|
|
self.assertEqual(
|
|
|
- visible[visible.index("当日效率ROI") : visible.index("建议动作") + 1],
|
|
|
|
|
|
|
+ visible[visible.index("当日效率ROI") : visible.index("建议说明") + 1],
|
|
|
[
|
|
[
|
|
|
"当日效率ROI",
|
|
"当日效率ROI",
|
|
|
"预测总效率ROI",
|
|
"预测总效率ROI",
|
|
|
"关停线(P20)",
|
|
"关停线(P20)",
|
|
|
"扩量线(P80)",
|
|
"扩量线(P80)",
|
|
|
"建议动作",
|
|
"建议动作",
|
|
|
|
|
+ "建议说明",
|
|
|
],
|
|
],
|
|
|
)
|
|
)
|
|
|
self.assertIn("关停线(P20)", frame.columns)
|
|
self.assertIn("关停线(P20)", frame.columns)
|
|
@@ -344,20 +532,21 @@ class RoiThreeDayRulesTest(unittest.TestCase):
|
|
|
).border.top.style,
|
|
).border.top.style,
|
|
|
"medium",
|
|
"medium",
|
|
|
)
|
|
)
|
|
|
- first_no_action_row = next(
|
|
|
|
|
|
|
+ first_observe_row = next(
|
|
|
row_number
|
|
row_number
|
|
|
for row_number in range(first_scale_row + 1, creative_sheet.max_row + 1)
|
|
for row_number in range(first_scale_row + 1, creative_sheet.max_row + 1)
|
|
|
- if not creative_sheet.cell(
|
|
|
|
|
|
|
+ if creative_sheet.cell(
|
|
|
row_number, creative_headers["建议动作"]
|
|
row_number, creative_headers["建议动作"]
|
|
|
).value
|
|
).value
|
|
|
|
|
+ == "观察"
|
|
|
)
|
|
)
|
|
|
self.assertEqual(
|
|
self.assertEqual(
|
|
|
- creative_sheet.cell(first_no_action_row, 1).border.top.style,
|
|
|
|
|
|
|
+ creative_sheet.cell(first_observe_row, 1).border.top.style,
|
|
|
"medium",
|
|
"medium",
|
|
|
)
|
|
)
|
|
|
self.assertEqual(
|
|
self.assertEqual(
|
|
|
creative_sheet.cell(
|
|
creative_sheet.cell(
|
|
|
- first_no_action_row, creative_headers["建议动作"]
|
|
|
|
|
|
|
+ first_observe_row, creative_headers["建议动作"]
|
|
|
).border.top.style,
|
|
).border.top.style,
|
|
|
"medium",
|
|
"medium",
|
|
|
)
|
|
)
|
|
@@ -416,6 +605,14 @@ class RoiThreeDayRulesTest(unittest.TestCase):
|
|
|
run_summary.cell(summary_rows["扩量样本数"], 2).number_format,
|
|
run_summary.cell(summary_rows["扩量样本数"], 2).number_format,
|
|
|
"0",
|
|
"0",
|
|
|
)
|
|
)
|
|
|
|
|
+ self.assertEqual(
|
|
|
|
|
+ run_summary.cell(summary_rows["单日关停线(P30)"], 2).number_format,
|
|
|
|
|
+ "0.00",
|
|
|
|
|
+ )
|
|
|
|
|
+ self.assertEqual(
|
|
|
|
|
+ run_summary.cell(summary_rows["单日P30样本数"], 2).number_format,
|
|
|
|
|
+ "0",
|
|
|
|
|
+ )
|
|
|
ad_sheet = workbook[SUMMARY_SHEETS[ENTITY_SELF_AD]]
|
|
ad_sheet = workbook[SUMMARY_SHEETS[ENTITY_SELF_AD]]
|
|
|
ad_headers = [cell.value for cell in ad_sheet[1]]
|
|
ad_headers = [cell.value for cell in ad_sheet[1]]
|
|
|
self.assertIn("审批选择", ad_headers)
|
|
self.assertIn("审批选择", ad_headers)
|