|
@@ -3,6 +3,13 @@ from dataclasses import replace
|
|
|
|
|
|
|
|
import pandas as pd
|
|
import pandas as pd
|
|
|
|
|
|
|
|
|
|
+from roi_control.fission_multiplier import (
|
|
|
|
|
+ DISPLAY_TOTAL_TO_FIRST_COLUMN,
|
|
|
|
|
+ MATCH_QIWEI_PARTNER,
|
|
|
|
|
+ QIWEI_REFERENCE_RUN_SUFFIX,
|
|
|
|
|
+ QIWEI_REFERENCE_VERSION,
|
|
|
|
|
+ load_fission_multiplier_parameters,
|
|
|
|
|
+)
|
|
|
from roi_control.metrics import (
|
|
from roi_control.metrics import (
|
|
|
ENTITY_GZH,
|
|
ENTITY_GZH,
|
|
|
ENTITY_QIWEI,
|
|
ENTITY_QIWEI,
|
|
@@ -11,25 +18,19 @@ from roi_control.metrics import (
|
|
|
QIWEI_CHANNEL,
|
|
QIWEI_CHANNEL,
|
|
|
SELF_CHANNEL,
|
|
SELF_CHANNEL,
|
|
|
)
|
|
)
|
|
|
-from roi_control.fission_multiplier import (
|
|
|
|
|
- DISPLAY_TOTAL_TO_FIRST_COLUMN,
|
|
|
|
|
- MATCH_QIWEI_PARTNER,
|
|
|
|
|
- QIWEI_REFERENCE_RUN_SUFFIX,
|
|
|
|
|
- QIWEI_REFERENCE_VERSION,
|
|
|
|
|
- load_fission_multiplier_parameters,
|
|
|
|
|
-)
|
|
|
|
|
from roi_control.reporting import (
|
|
from roi_control.reporting import (
|
|
|
- BASE_COLUMNS,
|
|
|
|
|
FINAL_ROI_COLUMN,
|
|
FINAL_ROI_COLUMN,
|
|
|
T0_FISSION_MULTIPLIER_COLUMN,
|
|
T0_FISSION_MULTIPLIER_COLUMN,
|
|
|
TOTAL_FISSION_TO_FIRST_UV_COLUMN,
|
|
TOTAL_FISSION_TO_FIRST_UV_COLUMN,
|
|
|
_sheet_frame,
|
|
_sheet_frame,
|
|
|
|
|
+ _visible_columns,
|
|
|
)
|
|
)
|
|
|
|
|
+from roi_control.data_source import build_daily_sql
|
|
|
from roi_control.rules import evaluate_rules as _evaluate_rules
|
|
from roi_control.rules import evaluate_rules as _evaluate_rules
|
|
|
from roi_control.service import _run_identity
|
|
from roi_control.service import _run_identity
|
|
|
|
|
|
|
|
|
|
|
|
|
-DATES = ["20260720", "20260721", "20260722"]
|
|
|
|
|
|
|
+DATES = ["20260721", "20260722"]
|
|
|
FISSION_PARAMETERS = load_fission_multiplier_parameters()
|
|
FISSION_PARAMETERS = load_fission_multiplier_parameters()
|
|
|
|
|
|
|
|
|
|
|
|
@@ -38,7 +39,7 @@ def evaluate_rules(*args, **kwargs):
|
|
|
return _evaluate_rules(*args, **kwargs)
|
|
return _evaluate_rules(*args, **kwargs)
|
|
|
|
|
|
|
|
|
|
|
|
|
-def row(entity_type, channel, entity_id, dt, roi, uv=600, age=10, cost=200.0):
|
|
|
|
|
|
|
+def row(entity_type, channel, entity_id, dt, roi, uv=600, cost=200.0):
|
|
|
common = {
|
|
common = {
|
|
|
"dt": dt,
|
|
"dt": dt,
|
|
|
"entity_type": entity_type,
|
|
"entity_type": entity_type,
|
|
@@ -63,7 +64,7 @@ def row(entity_type, channel, entity_id, dt, roi, uv=600, age=10, cost=200.0):
|
|
|
common.update(
|
|
common.update(
|
|
|
{
|
|
{
|
|
|
"代理名称": "代理",
|
|
"代理名称": "代理",
|
|
|
- "账号id": "account",
|
|
|
|
|
|
|
+ "账号id": "84502354",
|
|
|
"账号名称": "账户",
|
|
"账号名称": "账户",
|
|
|
"广告id": entity_id,
|
|
"广告id": entity_id,
|
|
|
"广告名称": f"广告{entity_id}",
|
|
"广告名称": f"广告{entity_id}",
|
|
@@ -80,10 +81,43 @@ def row(entity_type, channel, entity_id, dt, roi, uv=600, age=10, cost=200.0):
|
|
|
|
|
|
|
|
|
|
|
|
|
class RoiRulesTest(unittest.TestCase):
|
|
class RoiRulesTest(unittest.TestCase):
|
|
|
- def test_run_identity_tracks_qiwei_parameter_source(self):
|
|
|
|
|
- temporary_id, temporary_key = _run_identity("20260727", FISSION_PARAMETERS)
|
|
|
|
|
- self.assertIn(QIWEI_REFERENCE_RUN_SUFFIX, temporary_id)
|
|
|
|
|
- self.assertIn(QIWEI_REFERENCE_VERSION, temporary_key)
|
|
|
|
|
|
|
+ def build_daily(self):
|
|
|
|
|
+ rows = []
|
|
|
|
|
+ for dt in DATES:
|
|
|
|
|
+ for index, roi in enumerate(
|
|
|
|
|
+ [0.1, 0.5, 0.8, 1.0, 1.2, 1.5, 2.0, 3.0, 4.0, 5.0]
|
|
|
|
|
+ ):
|
|
|
|
|
+ rows.append(
|
|
|
|
|
+ row(ENTITY_SELF, SELF_CHANNEL, f"ad-{index}", dt, roi)
|
|
|
|
|
+ )
|
|
|
|
|
+ for index, roi in enumerate([0.1, 0.5, 1.0, 2.0, 4.0]):
|
|
|
|
|
+ rows.append(
|
|
|
|
|
+ row(
|
|
|
|
|
+ ENTITY_GZH,
|
|
|
|
|
+ GZH_CHANNEL,
|
|
|
|
|
+ f"公众号-{index}",
|
|
|
|
|
+ dt,
|
|
|
|
|
+ roi,
|
|
|
|
|
+ uv=300,
|
|
|
|
|
+ )
|
|
|
|
|
+ )
|
|
|
|
|
+ rows.append(
|
|
|
|
|
+ row(ENTITY_QIWEI, QIWEI_CHANNEL, "企微合作方", dt, 2.0, uv=300)
|
|
|
|
|
+ )
|
|
|
|
|
+ return pd.DataFrame(rows)
|
|
|
|
|
+
|
|
|
|
|
+ def test_run_identity_tracks_metric_policy_and_qiwei_versions(self):
|
|
|
|
|
+ run_id, run_key = _run_identity("20260727", FISSION_PARAMETERS)
|
|
|
|
|
+ self.assertIn("m7", run_id)
|
|
|
|
|
+ self.assertIn("p6", run_id)
|
|
|
|
|
+ self.assertIn("r15", run_id)
|
|
|
|
|
+ self.assertIn(QIWEI_REFERENCE_RUN_SUFFIX, run_id)
|
|
|
|
|
+ self.assertIn(QIWEI_REFERENCE_VERSION, run_key)
|
|
|
|
|
+
|
|
|
|
|
+ def test_daily_sql_uses_root_deduplicated_t0_fission_uv(self):
|
|
|
|
|
+ sql = build_daily_sql(DATES[0], DATES[1])
|
|
|
|
|
+ self.assertEqual(sql.count("SUM(NVL(t0_fission_uv_root, 0))"), 3)
|
|
|
|
|
+ self.assertNotIn("t0裂变人数", sql)
|
|
|
|
|
|
|
|
formal = replace(
|
|
formal = replace(
|
|
|
FISSION_PARAMETERS,
|
|
FISSION_PARAMETERS,
|
|
@@ -95,240 +129,179 @@ class RoiRulesTest(unittest.TestCase):
|
|
|
qiwei_exact_rows=1,
|
|
qiwei_exact_rows=1,
|
|
|
qiwei_exact_available_rows=1,
|
|
qiwei_exact_available_rows=1,
|
|
|
)
|
|
)
|
|
|
- formal_id, formal_key = _run_identity("20260727", formal)
|
|
|
|
|
|
|
+ formal_id, _ = _run_identity("20260727", formal)
|
|
|
self.assertIn(QIWEI_REFERENCE_RUN_SUFFIX, formal_id)
|
|
self.assertIn(QIWEI_REFERENCE_RUN_SUFFIX, formal_id)
|
|
|
- self.assertIn(QIWEI_REFERENCE_VERSION, formal_key)
|
|
|
|
|
-
|
|
|
|
|
- def test_run_identity_isolates_corrected_source_revision(self):
|
|
|
|
|
- default_id, default_key = _run_identity("20260729", FISSION_PARAMETERS)
|
|
|
|
|
- revised_id, revised_key = _run_identity(
|
|
|
|
|
- "20260729",
|
|
|
|
|
- FISSION_PARAMETERS,
|
|
|
|
|
- "datafix_20260730_1",
|
|
|
|
|
- )
|
|
|
|
|
-
|
|
|
|
|
- self.assertEqual(f"{default_id}_datafix_20260730_1", revised_id)
|
|
|
|
|
- self.assertNotEqual(default_key, revised_key)
|
|
|
|
|
- self.assertNotIn("datafix_20260730_1", revised_key)
|
|
|
|
|
- self.assertLessEqual(len(revised_id), 64)
|
|
|
|
|
- self.assertLessEqual(len(revised_key), 128)
|
|
|
|
|
- self.assertEqual(
|
|
|
|
|
- (revised_id, revised_key),
|
|
|
|
|
- _run_identity(
|
|
|
|
|
- "20260729",
|
|
|
|
|
- FISSION_PARAMETERS,
|
|
|
|
|
- "datafix_20260730_1",
|
|
|
|
|
- ),
|
|
|
|
|
- )
|
|
|
|
|
- with self.assertRaisesRegex(ValueError, "source_revision"):
|
|
|
|
|
- _run_identity("20260729", FISSION_PARAMETERS, "Invalid Revision")
|
|
|
|
|
- with self.assertRaisesRegex(ValueError, "run_id"):
|
|
|
|
|
- _run_identity("20260729", FISSION_PARAMETERS, "a" * 40)
|
|
|
|
|
|
|
|
|
|
- def build_daily(self):
|
|
|
|
|
- rows = []
|
|
|
|
|
- # 每日形成稳定分布,低值实体连续低于后20%,高值实体连续高于前20%。
|
|
|
|
|
- for dt in DATES:
|
|
|
|
|
- for index, roi in enumerate([0.1, 0.5, 0.8, 1.0, 1.2, 1.5, 2.0, 3.0, 4.0, 5.0]):
|
|
|
|
|
- rows.append(row(ENTITY_SELF, SELF_CHANNEL, f"ad-{index}", dt, roi))
|
|
|
|
|
- rows.append(row(ENTITY_GZH, GZH_CHANNEL, "低ROI公众号", dt, 0.05, uv=300))
|
|
|
|
|
- rows.append(row(ENTITY_GZH, GZH_CHANNEL, "高ROI公众号", dt, 6.0, uv=300))
|
|
|
|
|
- rows.append(row(ENTITY_QIWEI, QIWEI_CHANNEL, "低ROI企微", dt, 0.02, uv=300))
|
|
|
|
|
- rows.append(row(ENTITY_QIWEI, QIWEI_CHANNEL, "高ROI企微", dt, 7.0, uv=300))
|
|
|
|
|
- return pd.DataFrame(rows)
|
|
|
|
|
-
|
|
|
|
|
- def test_stop_and_up_actions(self):
|
|
|
|
|
- daily = self.build_daily()
|
|
|
|
|
|
|
+ def test_two_daily_thresholds_drive_consecutive_actions(self):
|
|
|
ages = pd.DataFrame(
|
|
ages = pd.DataFrame(
|
|
|
{
|
|
{
|
|
|
"广告id": [f"ad-{index}" for index in range(10)],
|
|
"广告id": [f"ad-{index}" for index in range(10)],
|
|
|
"广告age": [10] * 10,
|
|
"广告age": [10] * 10,
|
|
|
}
|
|
}
|
|
|
)
|
|
)
|
|
|
- candidates, thresholds, summary = evaluate_rules(daily, DATES, ages)
|
|
|
|
|
|
|
+ candidates, thresholds, summary = evaluate_rules(
|
|
|
|
|
+ self.build_daily(), DATES, ages
|
|
|
|
|
+ )
|
|
|
|
|
|
|
|
- actions = set(candidates["动作"])
|
|
|
|
|
- self.assertIn("关停", actions)
|
|
|
|
|
- self.assertIn("扩量", actions)
|
|
|
|
|
- self.assertEqual(len(thresholds), 1)
|
|
|
|
|
|
|
+ self.assertEqual(len(thresholds), 4)
|
|
|
self.assertEqual(
|
|
self.assertEqual(
|
|
|
- thresholds.iloc[0]["阈值样本数"],
|
|
|
|
|
- int(summary["entity_type"].ne(ENTITY_QIWEI).sum()),
|
|
|
|
|
|
|
+ set(zip(thresholds["统计日期"], thresholds["entity_type"])),
|
|
|
|
|
+ {
|
|
|
|
|
+ (DATES[0], ENTITY_SELF),
|
|
|
|
|
+ (DATES[1], ENTITY_SELF),
|
|
|
|
|
+ (DATES[0], ENTITY_GZH),
|
|
|
|
|
+ (DATES[1], ENTITY_GZH),
|
|
|
|
|
+ },
|
|
|
)
|
|
)
|
|
|
- self.assertTrue((summary["覆盖天数"] == 3).all())
|
|
|
|
|
- self.assertTrue((summary["t_stop"] == thresholds.iloc[0]["t_stop"]).all())
|
|
|
|
|
- self.assertTrue((summary["t_up"] == thresholds.iloc[0]["t_up"]).all())
|
|
|
|
|
- qiwei = summary[summary["entity_type"].eq(ENTITY_QIWEI)]
|
|
|
|
|
- self.assertEqual(len(qiwei), 2)
|
|
|
|
|
|
|
+ self.assertIn("关停", set(candidates["动作"]))
|
|
|
|
|
+ self.assertIn("扩量", set(candidates["动作"]))
|
|
|
|
|
+ self.assertTrue((summary["覆盖天数"] == 2).all())
|
|
|
self.assertTrue(
|
|
self.assertTrue(
|
|
|
- qiwei["调控参与状态"].eq("仅展示_不进入阈值和调控").all()
|
|
|
|
|
|
|
+ summary[
|
|
|
|
|
+ summary["entity_type"].isin([ENTITY_SELF, ENTITY_GZH])
|
|
|
|
|
+ ]["阈值样本状态"].eq("进入连续两日阈值样本池").all()
|
|
|
)
|
|
)
|
|
|
|
|
|
|
|
- def test_qiwei_is_excluded_before_metric_and_action_evaluation(self):
|
|
|
|
|
- daily = self.build_daily()
|
|
|
|
|
- candidates, _, summary = evaluate_rules(daily, DATES)
|
|
|
|
|
- self.assertTrue(summary["entity_type"].eq(ENTITY_QIWEI).any())
|
|
|
|
|
- self.assertFalse(candidates["合作方名"].str.contains("企微").any())
|
|
|
|
|
-
|
|
|
|
|
- def test_no_complete_three_day_entity_fails_clearly(self):
|
|
|
|
|
- daily = self.build_daily()
|
|
|
|
|
- incomplete = daily[daily["dt"].ne(DATES[-1])]
|
|
|
|
|
- with self.assertRaisesRegex(ValueError, "连续三日数据"):
|
|
|
|
|
- evaluate_rules(incomplete, DATES)
|
|
|
|
|
-
|
|
|
|
|
- def test_daily_fission_income_is_scaled_once_without_duplicate_addition(self):
|
|
|
|
|
|
|
+ def test_inconsistent_daily_direction_is_observe(self):
|
|
|
daily = self.build_daily()
|
|
daily = self.build_daily()
|
|
|
- mask = (
|
|
|
|
|
- daily["entity_type"].eq(ENTITY_GZH)
|
|
|
|
|
- & daily["公众号名"].eq("高ROI公众号")
|
|
|
|
|
- )
|
|
|
|
|
- daily.loc[mask, "效率收入"] = 100
|
|
|
|
|
- daily.loc[mask, "裂变效率收入"] = 200
|
|
|
|
|
- daily.loc[mask, "成本"] = 100
|
|
|
|
|
-
|
|
|
|
|
- _, _, summary = evaluate_rules(daily, DATES)
|
|
|
|
|
- target = summary[
|
|
|
|
|
- summary["entity_type"].eq(ENTITY_GZH)
|
|
|
|
|
- & summary["公众号名"].eq("高ROI公众号")
|
|
|
|
|
- ].iloc[0]
|
|
|
|
|
- expected = (
|
|
|
|
|
- 100 + 200 * FISSION_PARAMETERS.gzh_channel
|
|
|
|
|
- ) / 100
|
|
|
|
|
- self.assertEqual(target["裂变效率收入"], 600)
|
|
|
|
|
- self.assertEqual(target["T0实际裂变收入"], 600)
|
|
|
|
|
- self.assertAlmostEqual(target["ROI"], expected)
|
|
|
|
|
- self.assertAlmostEqual(target["实际ROI"], 3.0)
|
|
|
|
|
|
|
+ mask = daily["广告id"].eq("ad-0")
|
|
|
|
|
+ daily.loc[mask & daily["dt"].eq(DATES[1]), "效率收入"] = 1000
|
|
|
|
|
+ ages = pd.DataFrame({"广告id": ["ad-0"], "广告age": [10]})
|
|
|
|
|
|
|
|
- def test_self_age_blocks_actions(self):
|
|
|
|
|
- daily = self.build_daily()
|
|
|
|
|
- ages = pd.DataFrame({"广告id": ["ad-0", "ad-9"], "广告age": [2, 2]})
|
|
|
|
|
candidates, _, _ = evaluate_rules(daily, DATES, ages)
|
|
candidates, _, _ = evaluate_rules(daily, DATES, ages)
|
|
|
- blocked_ids = set(candidates.loc[candidates["entity_type"].eq(ENTITY_SELF), "广告id"])
|
|
|
|
|
- self.assertNotIn("ad-0", blocked_ids)
|
|
|
|
|
- self.assertNotIn("ad-9", blocked_ids)
|
|
|
|
|
-
|
|
|
|
|
- def test_pyodps_lowercase_aliases_are_normalized(self):
|
|
|
|
|
- daily = self.build_daily().rename(
|
|
|
|
|
- columns={"首层UV": "首层uv", "T0裂变数": "t0裂变数"}
|
|
|
|
|
- )
|
|
|
|
|
- candidates, thresholds, _ = evaluate_rules(daily, DATES)
|
|
|
|
|
- self.assertFalse(candidates.empty)
|
|
|
|
|
- self.assertEqual(len(thresholds), 1)
|
|
|
|
|
|
|
+ target = candidates[candidates["广告id"].eq("ad-0")].iloc[0]
|
|
|
|
|
+ self.assertEqual(target["动作"], "观察")
|
|
|
|
|
+ self.assertIn("表现不一致", target["动作原因"])
|
|
|
|
|
|
|
|
- def test_zero_roi_at_stop_boundary_is_stopped(self):
|
|
|
|
|
- rows = []
|
|
|
|
|
- for dt in DATES:
|
|
|
|
|
- for index, roi in enumerate([0, 0, 0, 0, 0, 1, 2, 3, 4, 5]):
|
|
|
|
|
- rows.append(row(ENTITY_SELF, SELF_CHANNEL, f"boundary-{index}", dt, roi))
|
|
|
|
|
- ages = pd.DataFrame(
|
|
|
|
|
- {
|
|
|
|
|
- "广告id": [f"boundary-{index}" for index in range(10)],
|
|
|
|
|
- "广告age": [10] * 10,
|
|
|
|
|
- }
|
|
|
|
|
|
|
+ def test_latest_day_uv_over_100_is_reported_as_observe(self):
|
|
|
|
|
+ daily = self.build_daily()
|
|
|
|
|
+ extra = row(
|
|
|
|
|
+ ENTITY_SELF,
|
|
|
|
|
+ SELF_CHANNEL,
|
|
|
|
|
+ "latest-only",
|
|
|
|
|
+ DATES[1],
|
|
|
|
|
+ 1.5,
|
|
|
|
|
+ uv=150,
|
|
|
)
|
|
)
|
|
|
- candidates, thresholds, _ = evaluate_rules(pd.DataFrame(rows), DATES, ages)
|
|
|
|
|
- self.assertEqual(thresholds.iloc[0]["t_stop"], 0)
|
|
|
|
|
- stopped = candidates[candidates["动作"].eq("关停")]
|
|
|
|
|
- self.assertTrue(stopped["广告id"].eq("boundary-0").any())
|
|
|
|
|
|
|
+ daily = pd.concat([daily, pd.DataFrame([extra])], ignore_index=True)
|
|
|
|
|
|
|
|
- def test_display_name_change_does_not_break_three_day_identity(self):
|
|
|
|
|
|
|
+ candidates, _, summary = evaluate_rules(daily, DATES)
|
|
|
|
|
+ target = summary[summary["广告id"].eq("latest-only")].iloc[0]
|
|
|
|
|
+ self.assertEqual(target["覆盖天数"], 1)
|
|
|
|
|
+ self.assertEqual(target["动作"], "观察")
|
|
|
|
|
+ self.assertEqual(target["阈值样本状态"], "观察_最新日UV达标")
|
|
|
|
|
+ self.assertAlmostEqual(target["最新日效率ROI"], 1.5)
|
|
|
|
|
+ self.assertTrue(candidates["广告id"].eq("latest-only").any())
|
|
|
|
|
+
|
|
|
|
|
+ def test_both_days_must_exceed_daily_uv_gate(self):
|
|
|
daily = self.build_daily()
|
|
daily = self.build_daily()
|
|
|
mask = daily["广告id"].eq("ad-9")
|
|
mask = daily["广告id"].eq("ad-9")
|
|
|
- daily.loc[mask & daily["dt"].eq(DATES[1]), "广告名称"] = "改名后的广告"
|
|
|
|
|
|
|
+ daily.loc[mask & daily["dt"].eq(DATES[0]), "首层UV"] = 180
|
|
|
ages = pd.DataFrame({"广告id": ["ad-9"], "广告age": [10]})
|
|
ages = pd.DataFrame({"广告id": ["ad-9"], "广告age": [10]})
|
|
|
- candidates, _, summary = evaluate_rules(daily, DATES, ages)
|
|
|
|
|
- target = summary[
|
|
|
|
|
- summary["entity_type"].eq(ENTITY_SELF)
|
|
|
|
|
- & summary["广告id"].eq("ad-9")
|
|
|
|
|
- ]
|
|
|
|
|
- self.assertEqual(len(target), 1)
|
|
|
|
|
- self.assertTrue(candidates["广告id"].eq("ad-9").any())
|
|
|
|
|
|
|
|
|
|
- def test_global_threshold_pool_only_uses_miniapp_and_gzh(self):
|
|
|
|
|
|
|
+ _, _, summary = evaluate_rules(daily, DATES, ages)
|
|
|
|
|
+ target = summary[summary["广告id"].eq("ad-9")].iloc[0]
|
|
|
|
|
+ self.assertEqual(target["动作"], "观察")
|
|
|
|
|
+ self.assertEqual(target["阈值样本状态"], "观察_最新日UV达标")
|
|
|
|
|
+
|
|
|
|
|
+ def test_stop_threshold_uses_cost_weighted_p30(self):
|
|
|
rows = []
|
|
rows = []
|
|
|
specifications = [
|
|
specifications = [
|
|
|
- (ENTITY_SELF, SELF_CHANNEL, "self-qualified", 1.0, 300),
|
|
|
|
|
- (ENTITY_GZH, GZH_CHANNEL, "gzh-qualified", 2.0, 300),
|
|
|
|
|
- (ENTITY_QIWEI, QIWEI_CHANNEL, "qiwei-qualified", 3.0, 300),
|
|
|
|
|
- (ENTITY_GZH, GZH_CHANNEL, "gzh-low-uv-excluded", 100.0, 100),
|
|
|
|
|
|
|
+ ("weighted-0", 0.1, 10),
|
|
|
|
|
+ ("weighted-1", 0.2, 10),
|
|
|
|
|
+ ("weighted-2", 1.0, 1000),
|
|
|
|
|
+ ("weighted-3", 2.0, 1000),
|
|
|
]
|
|
]
|
|
|
- for entity_type, channel, entity_id, roi, uv in specifications:
|
|
|
|
|
- for dt in DATES:
|
|
|
|
|
- rows.append(row(entity_type, channel, entity_id, dt, roi, uv=uv))
|
|
|
|
|
-
|
|
|
|
|
- _, thresholds, summary = evaluate_rules(pd.DataFrame(rows), DATES)
|
|
|
|
|
- threshold = thresholds.iloc[0]
|
|
|
|
|
- expected = pd.Series([1.0, 2.0])
|
|
|
|
|
- self.assertAlmostEqual(threshold["t_stop"], expected.quantile(0.20))
|
|
|
|
|
- self.assertAlmostEqual(threshold["t_up"], expected.quantile(0.80))
|
|
|
|
|
- self.assertEqual(threshold["阈值样本数"], 2)
|
|
|
|
|
- self.assertEqual(threshold["小程序样本数"], 1)
|
|
|
|
|
- self.assertEqual(threshold["公众号样本数"], 1)
|
|
|
|
|
- self.assertEqual(threshold["企微参考实体数"], 1)
|
|
|
|
|
- self.assertEqual(len(summary), 4)
|
|
|
|
|
-
|
|
|
|
|
- def test_actions_use_three_day_aggregate_not_each_daily_roi(self):
|
|
|
|
|
- rows = []
|
|
|
|
|
- daily_rois = [0.0, 0.0, 6.0]
|
|
|
|
|
- for dt, roi in zip(DATES, daily_rois):
|
|
|
|
|
- rows.append(row(ENTITY_GZH, GZH_CHANNEL, "波动公众号", dt, roi, uv=300))
|
|
|
|
|
for dt in DATES:
|
|
for dt in DATES:
|
|
|
- rows.append(row(ENTITY_SELF, SELF_CHANNEL, "基准创意", dt, 1.0, uv=600))
|
|
|
|
|
- rows.append(row(ENTITY_GZH, GZH_CHANNEL, "基准公众号", dt, 3.0, uv=300))
|
|
|
|
|
-
|
|
|
|
|
- candidates, thresholds, summary = evaluate_rules(pd.DataFrame(rows), DATES)
|
|
|
|
|
- target = summary[summary["公众号名"].eq("波动公众号")].iloc[0]
|
|
|
|
|
- self.assertAlmostEqual(target["ROI"], 2.0)
|
|
|
|
|
- self.assertAlmostEqual(target["t_stop"], thresholds.iloc[0]["t_stop"])
|
|
|
|
|
- self.assertAlmostEqual(target["t_up"], thresholds.iloc[0]["t_up"])
|
|
|
|
|
- self.assertNotEqual(
|
|
|
|
|
- candidates[candidates["公众号名"].eq("波动公众号")]["动作"].tolist(),
|
|
|
|
|
- ["关停"],
|
|
|
|
|
|
|
+ for entity_id, roi, cost in specifications:
|
|
|
|
|
+ rows.append(
|
|
|
|
|
+ row(
|
|
|
|
|
+ ENTITY_SELF,
|
|
|
|
|
+ SELF_CHANNEL,
|
|
|
|
|
+ entity_id,
|
|
|
|
|
+ dt,
|
|
|
|
|
+ roi,
|
|
|
|
|
+ cost=cost,
|
|
|
|
|
+ )
|
|
|
|
|
+ )
|
|
|
|
|
+ _, thresholds, _ = evaluate_rules(pd.DataFrame(rows), DATES)
|
|
|
|
|
+ self_thresholds = thresholds[
|
|
|
|
|
+ thresholds["entity_type"].eq(ENTITY_SELF)
|
|
|
|
|
+ ]
|
|
|
|
|
+ self.assertTrue(self_thresholds["t_stop"].eq(1.0).all())
|
|
|
|
|
+ self.assertTrue(
|
|
|
|
|
+ self_thresholds["关停线口径"].eq("消耗加权P25").all()
|
|
|
)
|
|
)
|
|
|
|
|
|
|
|
- def test_uv_gate_uses_three_day_average_not_each_day(self):
|
|
|
|
|
- daily = self.build_daily()
|
|
|
|
|
- target_mask = daily["广告id"].eq("ad-9")
|
|
|
|
|
- daily.loc[target_mask, "首层UV"] = [100, 250, 400]
|
|
|
|
|
- ages = pd.DataFrame({"广告id": ["ad-9"], "广告age": [10]})
|
|
|
|
|
-
|
|
|
|
|
- candidates, thresholds, summary = evaluate_rules(daily, DATES, ages)
|
|
|
|
|
- target = summary[summary["广告id"].eq("ad-9")].iloc[0]
|
|
|
|
|
- self.assertEqual(target["日均首层UV"], 250)
|
|
|
|
|
- self.assertEqual(
|
|
|
|
|
- candidates[candidates["广告id"].eq("ad-9")]["动作"].tolist(),
|
|
|
|
|
- ["扩量"],
|
|
|
|
|
|
|
+ def test_qiwei_is_display_only(self):
|
|
|
|
|
+ candidates, _, summary = evaluate_rules(self.build_daily(), DATES)
|
|
|
|
|
+ self.assertFalse(candidates["entity_type"].eq(ENTITY_QIWEI).any())
|
|
|
|
|
+ qiwei = summary[summary["entity_type"].eq(ENTITY_QIWEI)]
|
|
|
|
|
+ self.assertTrue(
|
|
|
|
|
+ qiwei["调控参与状态"].eq("仅展示_不进入阈值和调控").all()
|
|
|
)
|
|
)
|
|
|
- self.assertEqual(thresholds.iloc[0]["小程序样本数"], 10)
|
|
|
|
|
|
|
|
|
|
- def test_every_day_cost_must_be_above_100(self):
|
|
|
|
|
|
|
+ def test_report_uses_daily_rows_and_only_latest_day_is_actionable(self):
|
|
|
daily = self.build_daily()
|
|
daily = self.build_daily()
|
|
|
- target_mask = daily["广告id"].eq("ad-9")
|
|
|
|
|
- daily.loc[target_mask, "成本"] = [100, 200, 200]
|
|
|
|
|
- daily.loc[target_mask, "效率收入"] = [500, 1000, 1000]
|
|
|
|
|
- ages = pd.DataFrame({"广告id": ["ad-9"], "广告age": [10]})
|
|
|
|
|
-
|
|
|
|
|
- candidates, thresholds, summary = evaluate_rules(daily, DATES, ages)
|
|
|
|
|
- target = summary[summary["广告id"].eq("ad-9")].iloc[0]
|
|
|
|
|
- self.assertEqual(target["三日最小单日成本"], 100)
|
|
|
|
|
- self.assertFalse(candidates["广告id"].eq("ad-9").any())
|
|
|
|
|
- self.assertEqual(thresholds.iloc[0]["小程序样本数"], 9)
|
|
|
|
|
-
|
|
|
|
|
- def test_report_exposes_final_roi_and_both_fission_coefficients(self):
|
|
|
|
|
- candidates, _, _ = evaluate_rules(self.build_daily(), DATES)
|
|
|
|
|
- frame = _sheet_frame(candidates, "小程序投流")
|
|
|
|
|
-
|
|
|
|
|
- visible_columns = list(BASE_COLUMNS["小程序投流"])
|
|
|
|
|
- self.assertIn(FINAL_ROI_COLUMN, visible_columns)
|
|
|
|
|
- self.assertIn(T0_FISSION_MULTIPLIER_COLUMN, visible_columns)
|
|
|
|
|
- self.assertIn(TOTAL_FISSION_TO_FIRST_UV_COLUMN, visible_columns)
|
|
|
|
|
- self.assertIn("T0裂变效率收入", visible_columns)
|
|
|
|
|
- self.assertIn("总预估效率收入", visible_columns)
|
|
|
|
|
- self.assertNotIn("LTV预测效率收入", visible_columns)
|
|
|
|
|
- self.assertIn("裂变效率收入", frame.columns)
|
|
|
|
|
- self.assertNotIn("裂变效率收入", visible_columns)
|
|
|
|
|
|
|
+ extra = row(
|
|
|
|
|
+ ENTITY_SELF,
|
|
|
|
|
+ SELF_CHANNEL,
|
|
|
|
|
+ "latest-only",
|
|
|
|
|
+ DATES[1],
|
|
|
|
|
+ 0.01,
|
|
|
|
|
+ uv=150,
|
|
|
|
|
+ )
|
|
|
|
|
+ daily = pd.concat([daily, pd.DataFrame([extra])], ignore_index=True)
|
|
|
|
|
+ candidates, _, _ = evaluate_rules(daily, DATES)
|
|
|
|
|
+ frame = _sheet_frame(candidates, "小程序投流", DATES, 0.25)
|
|
|
|
|
+ visible = _visible_columns("小程序投流", DATES, 0.25)
|
|
|
|
|
+
|
|
|
|
|
+ for column in (
|
|
|
|
|
+ "dt",
|
|
|
|
|
+ "首层UV",
|
|
|
|
|
+ "T0裂变人数",
|
|
|
|
|
+ "T0裂变率",
|
|
|
|
|
+ "首层效率收入",
|
|
|
|
|
+ "T0裂变效率收入",
|
|
|
|
|
+ "预测总效率收入",
|
|
|
|
|
+ "成本",
|
|
|
|
|
+ "效率ROI",
|
|
|
|
|
+ T0_FISSION_MULTIPLIER_COLUMN,
|
|
|
|
|
+ TOTAL_FISSION_TO_FIRST_UV_COLUMN,
|
|
|
|
|
+ "消耗加权分位",
|
|
|
|
|
+ "消耗加权P25线",
|
|
|
|
|
+ "动作原因",
|
|
|
|
|
+ "阈值样本状态",
|
|
|
|
|
+ ):
|
|
|
|
|
+ self.assertIn(column, visible)
|
|
|
|
|
+ self.assertNotIn(FINAL_ROI_COLUMN, visible)
|
|
|
|
|
+ self.assertIn(FINAL_ROI_COLUMN, frame.columns)
|
|
|
|
|
+ latest_row = frame[
|
|
|
|
|
+ frame["广告id"].eq("ad-0") & frame["dt"].eq(DATES[1])
|
|
|
|
|
+ ].iloc[0]
|
|
|
|
|
+ self.assertEqual(latest_row["T0裂变人数"], 120)
|
|
|
|
|
+ self.assertAlmostEqual(latest_row["T0裂变率"], 0.2)
|
|
|
|
|
+ self.assertEqual(latest_row["首层效率收入"], 20)
|
|
|
|
|
+ self.assertEqual(latest_row["预测总效率收入"], 20)
|
|
|
|
|
+ self.assertEqual(set(frame["dt"]), set(DATES))
|
|
|
|
|
+ self.assertTrue((frame.groupby("广告id").size() == 2).all())
|
|
|
|
|
+
|
|
|
|
|
+ history = frame[frame["dt"].eq(DATES[0])]
|
|
|
|
|
+ latest = frame[frame["dt"].eq(DATES[1])]
|
|
|
|
|
+ self.assertTrue(history["动作"].fillna("").eq("").all())
|
|
|
|
|
+ self.assertTrue(history["审批选择"].eq("历史日").all())
|
|
|
|
|
+ self.assertTrue(history["动作幂等键"].fillna("").eq("").all())
|
|
|
|
|
+ self.assertTrue(latest["动作"].ne("").any())
|
|
|
self.assertTrue(
|
|
self.assertTrue(
|
|
|
- frame[FINAL_ROI_COLUMN].equals(frame["ROI"])
|
|
|
|
|
|
|
+ latest["阈值样本状态"].eq("观察_最新日UV达标").any()
|
|
|
|
|
+ )
|
|
|
|
|
+ self.assertEqual(
|
|
|
|
|
+ frame.iloc[0]["dt"],
|
|
|
|
|
+ DATES[1],
|
|
|
|
|
+ )
|
|
|
|
|
+ self.assertEqual(
|
|
|
|
|
+ frame.iloc[-1]["dt"],
|
|
|
|
|
+ DATES[0],
|
|
|
)
|
|
)
|
|
|
pd.testing.assert_series_equal(
|
|
pd.testing.assert_series_equal(
|
|
|
frame[TOTAL_FISSION_TO_FIRST_UV_COLUMN],
|
|
frame[TOTAL_FISSION_TO_FIRST_UV_COLUMN],
|