3. 竞价异常波动复盘:波动幅度与显著性初筛

任务对象与数据准备

本节处理某搜索广告账户连续 7 天(D1–D7)的分小时竞价数据,字段为:日期、小时、消耗(元)、转化数。目标是先做波动幅度初筛,再用 Z-score 判断显著性,输出待复盘时段清单。为避免与口径定义、数据清洗重复,这里假定数据已通过完整性校验,直接进入计算环节。

关键约定:

  • 波动幅度用相对变化率:Δ = (X_t − X_base) / X_base × 100%,正为突增、负为骤降。
  • 基线取同小时的历史均值(D1–D6 同一小时均值),避免把日内正常节律误判为异常。
  • Z-score 用稳健版本:Z = (X_t − median) / (1.4826 × MAD),MAD 为中位数绝对偏差,对少量极端值不敏感。
  • 显著性阈值:|Δ| ≥ 30% 且 |Z| ≥ 3 同时满足才进入待复盘清单,减少单指标误报。

计算实现

下面代码用纯 Python 完成 7 天 × 24 小时数据的构造、基线计算、幅度与 Z-score 计算、双条件筛选,可直接运行验证。

import statistics as st

# 示例数据:D1-D7 每天 24 小时消耗(元),此处仅展示 0-5 时与 20-23 时片段
# 实际使用时替换为报表导出的完整 7×24 数据
data = {
    "D1": [12, 10, 9, 8, 11, 15, 30, 55, 80, 95, 100, 98, 90, 88, 92, 96, 99, 105, 110, 120, 130, 90, 60, 30],
    "D2": [11, 9, 8, 9, 10, 14, 28, 52, 78, 92, 98, 96, 88, 86, 90, 94, 97, 102, 108, 118, 128, 88, 58, 28],
    "D3": [13, 11, 10, 9, 12, 16, 32, 58, 82, 97, 102, 100, 92, 90, 94, 98, 101, 107, 112, 122, 132, 92, 62, 32],
    "D4": [12, 10, 9, 8, 11, 15, 30, 55, 80, 95, 100, 98, 90, 88, 92, 96, 99, 105, 110, 120, 130, 90, 60, 30],
    "D5": [11, 9, 8, 9, 10, 14, 28, 52, 78, 92, 98, 96, 88, 86, 90, 94, 97, 102, 108, 118, 128, 88, 58, 28],
    "D6": [12, 10, 9, 8, 11, 15, 30, 55, 80, 95, 100, 98, 90, 88, 92, 96, 99, 105, 110, 120, 130, 90, 60, 30],
    # D7 为待检测日:20 时突增、22 时骤降
    "D7": [12, 10, 9, 8, 11, 15, 30, 55, 80, 95, 100, 98, 90, 88, 92, 96, 99, 105, 110, 120, 210, 90, 20, 30],
}

days = list(data.keys())
target = "D7"
base_days = days[:-1]

# 1) 同小时基线:D1-D6 同一小时的均值与稳健统计量
baseline, medians, mads = {}, {}, {}
for h in range(24):
    vals = [data[d][h] for d in base_days]
    baseline[h] = st.mean(vals)
    medians[h] = st.median(vals)
    mad = st.median([abs(v - medians[h]) for v in vals])
    mads[h] = mad if mad > 0 else 1e-6  # 防止除零

# 2) 计算 D7 各小时的 Δ 与 Z
rows = []
for h in range(24):
    x = data[target][h]
    base = baseline[h]
    delta = (x - base) / base * 100
    z = (x - medians[h]) / (1.4826 * mads[h])
    flag = abs(delta) >= 30 and abs(z) >= 3
    rows.append((h, x, round(base, 1), round(delta, 1), round(z, 2), flag))

# 3) 输出待复盘清单
print("小时  消耗  基线   Δ%     Z     入清单")
for h, x, base, delta, z, flag in rows:
    if flag:
        print(f"{h:>2}   {x:>4}  {base:>5}  {delta:>6}  {z:>6}   {'是' if flag else '否'}")

示例数据下,D7 的 20 时消耗 210 元、基线约 130 元,Δ≈+61.5%,Z 显著为正;22 时消耗 20 元、基线约 60 元,Δ≈−66.7%,Z 显著为负。其余小时 Δ 与 Z 均在阈值内,不进入清单。这组数值属于教学示例,实际结果以真实报表计算为准。

筛选逻辑与判读

双条件筛选的意义在于:只看幅度,低基线小时(如凌晨 3 时消耗 8 元变 12 元,Δ=+50%)会被误报;只看 Z-score,高波动但幅度小的时段又可能被漏掉。两者同时满足,才说明该时段既偏离基线幅度大、又在统计上显著。

D7 分小时波动初筛:幅度 × 显著性 小时(0–23) Δ 波动幅度(%) +30% −30% +61.5% 20时 22时 −66.7% 正常波动(|Δ|<30%) 突增且显著(入清单) 骤降且显著(入清单)

待复盘清单与后续动作

按上述逻辑,D7 的待复盘清单为两条:

日期小时消耗同小时基线ΔZ-score方向建议下一步
D720210130.0+61.5%显著正突增按小时拆分计划,查是否有加预算/提价/竞争加剧
D7222060.0−66.7%显著负骤降查预算耗尽、审核拒登、出价被压或定向收窄

清单只回答“哪些时段值得复盘”,不回答“为什么”。下一步应把这两个小时作为切片入口,进入计划层级的下钻排查。若某小时 Δ 达标但 Z 未达标,说明基线本身波动大,可先复核该小时历史数据质量,再决定是否纳入。

稳健 Z-score 中 MAD 为 0 时(历史同小时消耗完全一致),代码用 1e-6 兜底,此时任何微小变化都会得到极大 Z 值。遇到这种情况应改用标准差或直接以幅度阈值判断,避免机械套用。