sino-bc-cli 1.8.26 → 1.8.27
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +6 -0
- package/dist/SKILL.md +10 -10
- package/dist/affordance/project-analysis.md +18 -0
- package/dist/affordance/user.md +8 -2
- package/dist/api-doc/oapi/project-analysis.md +86 -0
- package/dist/api-doc/oapi/user.md +19 -2
- package/dist/generated/api-catalog.json +779 -4
- package/dist/index.js +1 -1
- package/dist/skills/custom/biz-line-strategy-generate/SKILL.md +36 -17
- package/dist/skills/custom/biz-line-strategy-generate/references/api-flow.md +44 -22
- package/dist/skills/custom/biz-line-strategy-generate/references/four-strategies.md +18 -74
- package/dist/skills/custom/biz-line-strategy-generate/references/metrics.md +20 -32
- package/dist/skills/custom/biz-line-strategy-generate/references/output-excel.md +12 -7
- package/dist/skills/custom/biz-line-strategy-generate/references/runtime.md +41 -4
- package/dist/skills/custom/biz-line-strategy-generate/references/spec.md +176 -6
- package/dist/skills/custom/biz-line-strategy-generate/scripts/build_report.py +17 -6
- package/dist/skills/custom/biz-line-strategy-generate/scripts/cli_json.py +36 -17
- package/dist/skills/custom/biz-line-strategy-generate/scripts/fetch_data.py +44 -17
- package/dist/skills/custom/biz-line-strategy-generate/scripts/run_workflow.py +134 -37
- package/dist/skills/custom/biz-line-strategy-generate/scripts/strategy_output.py +277 -0
- package/dist/skills/custom/biz-line-strategy-generate/scripts/summarize.py +84 -146
- package/dist/skills/custom/biz-line-strategy-generate/scripts/test_database_output.py +401 -0
- package/dist/skills/custom/biz-line-strategy-generate/scripts/test_strategy_rules.py +18 -8
- package/dist/skills/custom/biz-line-strategy-generate/scripts/test_workflow.py +138 -8
- package/dist/skills/oapi/sino-bc-project-analysis/SKILL.md +13 -1
- package/dist/skills/oapi/sino-bc-project-analysis/references/analysis-workflow.md +4 -0
- package/dist/skills/oapi/sino-bc-project-analysis/references/filters-and-metrics.md +2 -2
- package/dist/skills/oapi/sino-bc-user/SKILL.md +6 -4
- package/package.json +1 -1
|
@@ -14,6 +14,7 @@ SCRIPTS = Path(__file__).resolve().parent
|
|
|
14
14
|
if str(SCRIPTS) not in sys.path:
|
|
15
15
|
sys.path.insert(0, str(SCRIPTS))
|
|
16
16
|
|
|
17
|
+
from cli_json import learning_text, amount_change
|
|
17
18
|
from cli_json import (
|
|
18
19
|
bid_adjust_stats,
|
|
19
20
|
budget_abs_share,
|
|
@@ -73,7 +74,7 @@ def load_merged(bl: Path, stem: str) -> list:
|
|
|
73
74
|
|
|
74
75
|
|
|
75
76
|
def object_name_maps(rows, id_key, name_key) -> tuple[dict[str, str], dict[str, str]]:
|
|
76
|
-
"""id → 明细展示名(空则回退 id),以及原始 display(空名不当作 id
|
|
77
|
+
"""id → 明细展示名(空则回退 id),以及原始 display(空名不当作 id,避免分布在拆 ID)。"""
|
|
77
78
|
names: dict[str, str] = {}
|
|
78
79
|
raw: dict[str, str] = {}
|
|
79
80
|
for row in rows:
|
|
@@ -164,7 +165,7 @@ def bid_changes(rows, fields):
|
|
|
164
165
|
|
|
165
166
|
|
|
166
167
|
def new_items(curr_rows, prev_rows, key, id_key=None):
|
|
167
|
-
"""按稳定 ID
|
|
168
|
+
"""按稳定 ID/维度值聚合基础指标,再识别新增的对象。只为新增对象保留行。"""
|
|
168
169
|
def identity(row):
|
|
169
170
|
return str(row.get(id_key) or "") if id_key else disp(row, key)
|
|
170
171
|
previous: dict[str, str] = {}
|
|
@@ -195,7 +196,7 @@ def dim_layer_text(title: str, items: list, stop_labs: list, rule=None):
|
|
|
195
196
|
rule = rule or core_rule("")
|
|
196
197
|
metric = rule["metric"]
|
|
197
198
|
if not items and not stop_labs:
|
|
198
|
-
return f"{title}\n
|
|
199
|
+
return f"{title}\n本期无新增。", None, 0.0, 0.0
|
|
199
200
|
lines = [title]
|
|
200
201
|
usable = [it for it in items if it["spend"] >= 50]
|
|
201
202
|
for it in usable[:DETAIL_LIMIT]:
|
|
@@ -208,7 +209,7 @@ def dim_layer_text(title: str, items: list, stop_labs: list, rule=None):
|
|
|
208
209
|
if len(items) > len(usable):
|
|
209
210
|
lines.append(f"· 另有 {len(items) - len(usable)} 个新增消耗不足 $50,不纳入聚合")
|
|
210
211
|
if stop_labs:
|
|
211
|
-
lines.append(f"·
|
|
212
|
+
lines.append(f"· 暂停 {len(stop_labs)} 个:{examples(stop_labs)}")
|
|
212
213
|
records = [r for it in usable for r in it.get("rows", [])]
|
|
213
214
|
value = metric_value(records, metric)
|
|
214
215
|
hit = rule_hit(value, rule)
|
|
@@ -217,27 +218,10 @@ def dim_layer_text(title: str, items: list, stop_labs: list, rule=None):
|
|
|
217
218
|
|
|
218
219
|
|
|
219
220
|
def setting_line(title, curr_set, prev_set, first_period=False) -> str:
|
|
220
|
-
|
|
221
|
-
lines = [title]
|
|
221
|
+
"""主表仅保留集合数量;成员名称统一在明细列出。"""
|
|
222
222
|
if first_period:
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
main, _extra = summarize_countries(labs)
|
|
226
|
-
lines.append(f"首期展示当期配置:{main};示例 {examples(labs)}")
|
|
227
|
-
else:
|
|
228
|
-
lines.append(f"首期展示当期 {len(labs)} 个:{examples(labs)}")
|
|
229
|
-
return "\n".join(lines)
|
|
230
|
-
if news:
|
|
231
|
-
if title.startswith("【国家】"):
|
|
232
|
-
main, _extra = summarize_countries(news)
|
|
233
|
-
lines.append(f"新增 {len(news)} 个({main}):{examples(news)}")
|
|
234
|
-
else:
|
|
235
|
-
lines.append(f"新增 {len(news)} 个:{examples(news)}")
|
|
236
|
-
if stops:
|
|
237
|
-
lines.append(f"本期未出现 {len(stops)} 个:{examples(stops)}")
|
|
238
|
-
if len(lines) == 1:
|
|
239
|
-
lines.append("未发现新增或本期未出现")
|
|
240
|
-
return "\n".join(lines)
|
|
223
|
+
return f'{title}\n当期 {len(curr_set)} 个'
|
|
224
|
+
return f'{title}\n新增 {len(curr_set - prev_set)} 个,暂停 {len(prev_set - curr_set)} 个'
|
|
241
225
|
|
|
242
226
|
|
|
243
227
|
def ad_keywords(names, n=3):
|
|
@@ -255,6 +239,7 @@ def ad_keywords(names, n=3):
|
|
|
255
239
|
|
|
256
240
|
def summarize_request(req: Path):
|
|
257
241
|
"""仅使用本次通过分页校验的文件,固定生成摘要和完整明细。"""
|
|
242
|
+
from strategy_output import effect_code
|
|
258
243
|
biz = load_json_file(req / "biz-line.json") or {}
|
|
259
244
|
detail = load_json_file(req / "biz-line-detail.out.json") or {}
|
|
260
245
|
detail_data = detail.get("data") if isinstance(detail.get("data"), dict) else {}
|
|
@@ -281,12 +266,8 @@ def summarize_request(req: Path):
|
|
|
281
266
|
if len(curr) == 2 and len(prev) == 2:
|
|
282
267
|
period = f"本期:{curr[0]}~{curr[1]}\n上期:{prev[0]}~{prev[1]}"
|
|
283
268
|
else:
|
|
284
|
-
#
|
|
285
|
-
|
|
286
|
-
ce = biz.get("periodEnd") or detail_data.get("periodEnd")
|
|
287
|
-
ps = biz.get("prevPeriodStart") or detail_data.get("prevPeriodStart")
|
|
288
|
-
pe = biz.get("prevPeriodEnd") or detail_data.get("prevPeriodEnd")
|
|
289
|
-
period = f"本期:{cs}~{ce}\n上期:{ps}~{pe}" if cs and ce and ps and pe else ""
|
|
269
|
+
# 缺少请求周期时不能以业务线周期冒充本次分析范围。
|
|
270
|
+
raise ValueError('缺少有效 periods.json,请通过统一入口重新拉取数据')
|
|
290
271
|
|
|
291
272
|
spend = {"curr": {}, "prev": {}}
|
|
292
273
|
imps = {"curr": {}, "prev": {}}
|
|
@@ -354,7 +335,7 @@ def summarize_request(req: Path):
|
|
|
354
335
|
spend_verdict = "待观察"
|
|
355
336
|
gate_note = f"本期有效投放 {eff_days} 天、消耗 ${curr_total:,.0f}、安装 {curr_inst:,.0f}"
|
|
356
337
|
qoq_pct_s = "" if qoq_pct is None else f"{qoq_pct * 100:+.1f}%"
|
|
357
|
-
details: list[dict] = [
|
|
338
|
+
details: list[dict] = [] # KPI 原文仅参与内部判定,不写入任一 sheet。
|
|
358
339
|
coverage_notes = []
|
|
359
340
|
from datetime import date, timedelta
|
|
360
341
|
for per, bounds in (("curr", periods.get("curr")), ("prev", periods.get("prev"))):
|
|
@@ -398,12 +379,7 @@ def summarize_request(req: Path):
|
|
|
398
379
|
f"{label}:上调 {stats['raise_n']} 个广告组,下调 {stats['lower_n']} 个;"
|
|
399
380
|
f"期内变化但首末持平 {stats['flat_n']} 个;幅度中位数 {stats['median_abs']:.2f}"
|
|
400
381
|
]
|
|
401
|
-
|
|
402
|
-
for b in stats["details"]:
|
|
403
|
-
lines.append(
|
|
404
|
-
f"· {short_label(b.get('name') or '')}:{b.get('action')} "
|
|
405
|
-
f"{b.get('from')} → {b.get('to')}({b.get('times')} 次)"
|
|
406
|
-
)
|
|
382
|
+
lines.append('分布在:' + naming_patterns([b.get('name') or '' for b in items]))
|
|
407
383
|
return "\n".join(lines)
|
|
408
384
|
|
|
409
385
|
for ch in ("fb", "gg", "tt"):
|
|
@@ -424,6 +400,7 @@ def summarize_request(req: Path):
|
|
|
424
400
|
budget_strategy = "\n".join(
|
|
425
401
|
[
|
|
426
402
|
"【调整出价】",
|
|
403
|
+
f"合计 {sum(len(items) for items in bids.values())} 个广告组调整出价",
|
|
427
404
|
bid_text("fb"),
|
|
428
405
|
bid_text("gg"),
|
|
429
406
|
bid_text("tt"),
|
|
@@ -450,53 +427,41 @@ def summarize_request(req: Path):
|
|
|
450
427
|
prev_share=prev_share["tt"],
|
|
451
428
|
curr_share=curr_share["tt"],
|
|
452
429
|
),
|
|
453
|
-
f"消耗合计:本期 ${curr_total:,.0f} / 上期 ${prev_total:,.0f}",
|
|
430
|
+
f"消耗合计:本期 ${curr_total:,.0f} / 上期 ${prev_total:,.0f};{amount_change(curr_total, prev_total)}",
|
|
454
431
|
]
|
|
455
432
|
)
|
|
456
433
|
cpi_s = f"全渠道 {metric}:本期 {fmt_cpm(core_c)} / 上期 {fmt_cpm(core_p)}"
|
|
457
434
|
cpi_s += "(美元消耗 / 平台安装量,非客户激活或留存成本)" if metric == "CPI" else "(同粒度基础指标合计后计算)"
|
|
458
|
-
budget_learning =
|
|
459
|
-
[
|
|
460
|
-
f"结论:{spend_verdict}(第二期起跟环比,KPI 只作依据)" if not first_period else f"结论:{spend_verdict}(首期)",
|
|
461
|
-
f"依据:{cpi_s},环比{qoq_label}" + (f" {qoq_pct_s}" if qoq_pct_s else ""),
|
|
462
|
-
f"客户 KPI:{core_index or '未填写'};{rule['reason']}",
|
|
463
|
-
*([f"门槛:{gate_note} → 待观察"] if gate_note else []),
|
|
464
|
-
"",
|
|
465
|
-
"Spend",
|
|
466
|
-
f"· {cpi_s}",
|
|
467
|
-
f"· 参考 CPM:本期 {fmt_cpm(overall_c)} / 上期 {fmt_cpm(overall_p)}(不作结论轴)",
|
|
468
|
-
f"· {budget_abs_share('FB', prev_abs=spend['prev']['fb'], curr_abs=spend['curr']['fb'], prev_share=prev_share['fb'], curr_share=curr_share['fb'])}",
|
|
469
|
-
f"· {budget_abs_share('GG', prev_abs=spend['prev']['gg'], curr_abs=spend['curr']['gg'], prev_share=prev_share['gg'], curr_share=curr_share['gg'])}",
|
|
470
|
-
f"· {budget_abs_share('TT', prev_abs=spend['prev']['tt'], curr_abs=spend['curr']['tt'], prev_share=prev_share['tt'], curr_share=curr_share['tt'])}",
|
|
471
|
-
"",
|
|
472
|
-
"调整出价",
|
|
473
|
-
*[
|
|
474
|
-
f"· {CH_LABEL[ch]}:上调 {bid_stats[ch]['raise_n']} / 下调 {bid_stats[ch]['lower_n']},"
|
|
475
|
-
f"幅度中位数 {bid_stats[ch]['median_abs']:.2f}"
|
|
476
|
-
for ch in ("fb", "gg", "tt")
|
|
477
|
-
],
|
|
478
|
-
"",
|
|
479
|
-
advice_of(spend_verdict),
|
|
480
|
-
]
|
|
481
|
-
)
|
|
435
|
+
budget_learning = learning_text(spend_verdict, rule)
|
|
482
436
|
|
|
483
437
|
def share_line(title, dim_key, stem, channels):
|
|
484
|
-
|
|
485
|
-
moved = False
|
|
438
|
+
"""占比同时展示对应消耗;主表汇总前三类,其余合并,明细保留全量。"""
|
|
439
|
+
segs, moved = [], False
|
|
486
440
|
for ch in channels:
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
441
|
+
current, current_total = share_map(iter_stem_rows(bl, f'met_{ch}_curr_{stem}'), dim_key)
|
|
442
|
+
previous, previous_total = share_map(iter_stem_rows(bl, f'met_{ch}_prev_{stem}'), dim_key)
|
|
443
|
+
moved = moved or share_moved(current, previous)
|
|
444
|
+
keys = [key for key in sorted(set(current) | set(previous), key=lambda key: (-current.get(key, 0), key))
|
|
445
|
+
if not is_ghost_enum(current.get(key, 0), previous.get(key, 0))]
|
|
446
|
+
def aggregate_line(label, members):
|
|
447
|
+
cp, pp = sum(current.get(k, 0) for k in members), sum(previous.get(k, 0) for k in members)
|
|
448
|
+
ca, pa = cp * current_total / 100, pp * previous_total / 100
|
|
449
|
+
return f'{label}:消耗 ${pa:,.0f} → ${ca:,.0f},占比 {pp:.1f}% → {cp:.1f}%;{amount_change(ca, pa)}'
|
|
450
|
+
for key in keys:
|
|
451
|
+
details.append({'bizLine': name, 'kind': title.strip('【】') + '/分布', 'action': '对比',
|
|
452
|
+
'channel': CH_LABEL[ch], 'name': key, 'extra': aggregate_line(key, [key])})
|
|
453
|
+
parts = [aggregate_line(key, [key]) for key in keys[:3]]
|
|
454
|
+
if len(keys) > 3:
|
|
455
|
+
parts.append(aggregate_line('其余类别合计', keys[3:]))
|
|
456
|
+
if parts:
|
|
457
|
+
segs.append(CH_LABEL[ch] + ':' + ';'.join(parts))
|
|
458
|
+
return ('\n'.join([title] + segs) if segs else ''), moved
|
|
494
459
|
|
|
495
460
|
p_obj, m1 = share_line("【广告目标】", "objective", "objective", ["fb", "tt"])
|
|
496
461
|
p_ch, m2 = share_line("【广告系列类型】", "advertising_channel_type", "advertising_channel_type", ["gg"])
|
|
497
462
|
p_opt, m3 = share_line("【成效目标】", "optimization_goal", "optimization_goal", ["fb", "gg", "tt"])
|
|
498
463
|
p_bid, m4 = share_line("【出价方式】", "bid_strategy", "bid_strategy", ["fb", "gg", "tt"])
|
|
499
|
-
product_strategy = "\n
|
|
464
|
+
product_strategy = "\n".join(x for x in [p_obj, p_ch, p_opt, p_bid] if x) or '本期无可识别的广告产品策略数据'
|
|
500
465
|
product_share_moved = any([m1, m2, m3, m4])
|
|
501
466
|
|
|
502
467
|
def dim_sets(stem, key, channels):
|
|
@@ -538,7 +503,7 @@ def summarize_request(req: Path):
|
|
|
538
503
|
{
|
|
539
504
|
"bizLine": name,
|
|
540
505
|
"kind": kind,
|
|
541
|
-
"action": "
|
|
506
|
+
"action": "暂停",
|
|
542
507
|
"channel": channel,
|
|
543
508
|
"name": lab,
|
|
544
509
|
"extra": "",
|
|
@@ -551,9 +516,12 @@ def summarize_request(req: Path):
|
|
|
551
516
|
add_setting_details("国家", country_curr, country_prev)
|
|
552
517
|
add_setting_details("兴趣词", interest_curr, interest_prev)
|
|
553
518
|
add_setting_details("关键词", kw_curr, kw_prev)
|
|
519
|
+
for label, field in (("年龄", "targeting_age"), ("性别", "targeting_genders")):
|
|
520
|
+
add_setting_details(label, *dim_sets(field, field, ["fb", "gg", "tt"]))
|
|
554
521
|
|
|
555
522
|
def object_strategy(kind, id_key, name_key):
|
|
556
523
|
bits = []
|
|
524
|
+
total_new = total_absent = 0
|
|
557
525
|
for ch in CH_LABEL:
|
|
558
526
|
stem = f"{id_key}+{name_key}"
|
|
559
527
|
|
|
@@ -563,20 +531,25 @@ def summarize_request(req: Path):
|
|
|
563
531
|
c, c_raw = index_objects("curr")
|
|
564
532
|
p, _p_raw = index_objects("prev")
|
|
565
533
|
new, absent = sorted(c.keys() - p.keys()), sorted(p.keys() - c.keys())
|
|
566
|
-
|
|
534
|
+
total_new += len(new)
|
|
535
|
+
total_absent += len(absent)
|
|
536
|
+
if not c and not p:
|
|
537
|
+
continue
|
|
538
|
+
bits.append(f"{CH_LABEL[ch]}:新增 {len(new)} 个,暂停 {len(absent)} 个;新增分布在:{naming_patterns([c_raw[i] for i in new])};暂停分布在:{naming_patterns([_p_raw[i] for i in absent])}")
|
|
567
539
|
for ident in sorted(c.keys() | p.keys()):
|
|
568
540
|
record_name = c.get(ident) or p[ident]
|
|
569
|
-
action = "当期" if first_period else ("新增" if ident in new else ("
|
|
541
|
+
action = "当期" if first_period else ("新增" if ident in new else ("暂停" if ident in absent else "持续"))
|
|
570
542
|
details.append({"bizLine": name, "kind": kind, "action": action, "channel": CH_LABEL[ch], "name": record_name, "extra": f"ID={ident};集合差异不等同创建/暂停操作"})
|
|
571
|
-
return bits
|
|
543
|
+
return [f'合计新增 {total_new} 个{kind},暂停 {total_absent} 个'] + bits
|
|
572
544
|
|
|
545
|
+
audience_type_text, audience_type_moved = share_line("【受众类型】", "audience_type", "audience_type", ["fb", "gg", "tt"])
|
|
573
546
|
ag_bits = object_strategy("广告组", "adgroup_id", "adgroup_name")
|
|
574
|
-
audience_strategy = "\n
|
|
547
|
+
audience_strategy = "\n".join(
|
|
575
548
|
x
|
|
576
549
|
for x in [
|
|
577
550
|
setting_line("【国家】", country_curr, country_prev, first_period=first_period),
|
|
578
551
|
setting_line("【兴趣词包】", interest_curr, interest_prev, first_period=first_period),
|
|
579
|
-
|
|
552
|
+
audience_type_text,
|
|
580
553
|
setting_line("【关键词】", kw_curr, kw_prev, first_period=first_period),
|
|
581
554
|
setting_line("【年龄】", *dim_sets("targeting_age", "targeting_age", ["fb", "gg", "tt"]), first_period=first_period),
|
|
582
555
|
setting_line("【性别】", *dim_sets("targeting_genders", "targeting_genders", ["fb", "gg", "tt"]), first_period=first_period),
|
|
@@ -589,12 +562,11 @@ def summarize_request(req: Path):
|
|
|
589
562
|
creative_fmt = share_line("【创意类型】", "ad_format", "ad_format", ["fb", "gg", "tt"])[0] or ""
|
|
590
563
|
creative_strategy = "【广告】\n" + "\n".join(ad_bits) + (("\n\n" + creative_fmt) if creative_fmt else "")
|
|
591
564
|
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
565
|
+
# 文案和效果在同一判断点产出,不扫描 learning 文本反推状态。
|
|
566
|
+
learning_effects = {}
|
|
567
|
+
|
|
568
|
+
def strategy_learning(dim_specs, allow_share_fallback=False, share_did_move=False, strategy_key=''):
|
|
596
569
|
for title, stem, key, channels, id_key in dim_specs:
|
|
597
|
-
items_all, stops_all = [], []
|
|
598
570
|
for ch in channels:
|
|
599
571
|
curr_stem = f"met_{ch}_curr_{stem}"
|
|
600
572
|
prev_stem = f"met_{ch}_prev_{stem}"
|
|
@@ -613,18 +585,6 @@ def summarize_request(req: Path):
|
|
|
613
585
|
hit = rule_hit(value, rule)
|
|
614
586
|
verdict_text = '不可判定' if hit is None else ('达标' if hit else '未达标')
|
|
615
587
|
details.append({"bizLine": name, "kind": f"新增成效/{title}", "action": "新增", "channel": CH_LABEL[ch], "name": item["label"], "extra": f"ID={item['id']};消耗美元 {item['spend']:.2f};{metric} {fmt_cpm(value)};KPI {verdict_text}"})
|
|
616
|
-
items_all.extend(items)
|
|
617
|
-
stops_all.extend(stops)
|
|
618
|
-
if items_all:
|
|
619
|
-
any_new = True
|
|
620
|
-
if stops_all and not items_all:
|
|
621
|
-
only_stop = True
|
|
622
|
-
text, _hit, hs, ts = dim_layer_text(title, items_all, stops_all, rule)
|
|
623
|
-
texts.append(text)
|
|
624
|
-
if ts > 0:
|
|
625
|
-
hit_spend += hs
|
|
626
|
-
all_spend += ts
|
|
627
|
-
detail = "\n\n".join(texts)
|
|
628
588
|
verdict = period_verdict(
|
|
629
589
|
first_period=first_period,
|
|
630
590
|
qoq_label=qoq_label,
|
|
@@ -633,55 +593,8 @@ def summarize_request(req: Path):
|
|
|
633
593
|
)
|
|
634
594
|
if gate_note:
|
|
635
595
|
verdict = "待观察"
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
+ "。此为整体环比观察,不代表本次单一动作的因果效果;" + rule["reason"]
|
|
639
|
-
)
|
|
640
|
-
if not any_new and only_stop:
|
|
641
|
-
return "\n".join(
|
|
642
|
-
[
|
|
643
|
-
f"结论:{verdict}",
|
|
644
|
-
"口径:本期无新增,走本期未出现",
|
|
645
|
-
f"依据:{evidence}",
|
|
646
|
-
"",
|
|
647
|
-
detail,
|
|
648
|
-
"",
|
|
649
|
-
advice_of(verdict),
|
|
650
|
-
]
|
|
651
|
-
)
|
|
652
|
-
if not any_new:
|
|
653
|
-
if allow_share_fallback and share_did_move:
|
|
654
|
-
return "\n".join(
|
|
655
|
-
[
|
|
656
|
-
f"结论:{verdict}",
|
|
657
|
-
"口径:无新增枚举,占比变化超过 ±2%,按可环比",
|
|
658
|
-
f"依据:{evidence}",
|
|
659
|
-
"",
|
|
660
|
-
detail,
|
|
661
|
-
"",
|
|
662
|
-
advice_of(verdict),
|
|
663
|
-
]
|
|
664
|
-
)
|
|
665
|
-
return "\n".join(
|
|
666
|
-
[
|
|
667
|
-
f"结论:{verdict}",
|
|
668
|
-
"口径:本期无实质调整动作",
|
|
669
|
-
f"依据:{evidence}",
|
|
670
|
-
"",
|
|
671
|
-
detail,
|
|
672
|
-
]
|
|
673
|
-
)
|
|
674
|
-
ratio = (hit_spend / all_spend) if all_spend else 0.0
|
|
675
|
-
return "\n".join(
|
|
676
|
-
[
|
|
677
|
-
f"结论:{verdict}",
|
|
678
|
-
f"依据:{evidence}",
|
|
679
|
-
"",
|
|
680
|
-
detail,
|
|
681
|
-
"",
|
|
682
|
-
advice_of(verdict),
|
|
683
|
-
]
|
|
684
|
-
)
|
|
596
|
+
learning_effects[strategy_key] = effect_code(verdict)
|
|
597
|
+
return learning_text(verdict, rule)
|
|
685
598
|
|
|
686
599
|
product_learning = strategy_learning(
|
|
687
600
|
[
|
|
@@ -692,6 +605,7 @@ def summarize_request(req: Path):
|
|
|
692
605
|
],
|
|
693
606
|
allow_share_fallback=True,
|
|
694
607
|
share_did_move=product_share_moved,
|
|
608
|
+
strategy_key='product',
|
|
695
609
|
)
|
|
696
610
|
audience_learning = strategy_learning(
|
|
697
611
|
[
|
|
@@ -702,13 +616,13 @@ def summarize_request(req: Path):
|
|
|
702
616
|
("年龄", "targeting_age", "targeting_age", ["fb", "gg", "tt"], None),
|
|
703
617
|
("性别", "targeting_genders", "targeting_genders", ["fb", "gg", "tt"], None),
|
|
704
618
|
("广告组", "adgroup_id+adgroup_name", "adgroup_name", ["fb", "gg", "tt"], "adgroup_id"),
|
|
705
|
-
]
|
|
619
|
+
], strategy_key='audience',
|
|
706
620
|
)
|
|
707
621
|
creative_learning = strategy_learning(
|
|
708
622
|
[
|
|
709
623
|
("广告", "ad_id+ad_name", "ad_name", ["fb", "gg", "tt"], "ad_id"),
|
|
710
624
|
("创意类型", "ad_format", "ad_format", ["fb", "gg", "tt"], None),
|
|
711
|
-
]
|
|
625
|
+
], strategy_key='creative',
|
|
712
626
|
)
|
|
713
627
|
|
|
714
628
|
row = {
|
|
@@ -724,7 +638,30 @@ def summarize_request(req: Path):
|
|
|
724
638
|
"audienceLearning": audience_learning,
|
|
725
639
|
"creativeStrategy": creative_strategy,
|
|
726
640
|
"creativeLearning": creative_learning,
|
|
641
|
+
"budgetEffect": effect_code(spend_verdict),
|
|
642
|
+
**{key + 'Effect': value for key, value in learning_effects.items()},
|
|
727
643
|
}
|
|
644
|
+
# 证据只来自实际快照或集合/占比变化;不把消耗下降推断为主动减预算。
|
|
645
|
+
evidence = {'budget': [], 'product': [], 'audience': []}
|
|
646
|
+
if any(bids.values()):
|
|
647
|
+
evidence['budget'].append('出价调整')
|
|
648
|
+
for field, action in [('raise_n', '上调出价'), ('lower_n', '下调出价')]:
|
|
649
|
+
if any(stats[field] for stats in bid_stats.values()):
|
|
650
|
+
evidence['budget'].append(action)
|
|
651
|
+
if not first_period:
|
|
652
|
+
for moved, label in zip((m1, m2, m3, m4), ('广告目标调整', '广告系列类型调整', '成效目标调整', '出价方式调整')):
|
|
653
|
+
if moved:
|
|
654
|
+
evidence['product'].append(label)
|
|
655
|
+
for current, previous, label in [(country_curr, country_prev, '国家调整'),
|
|
656
|
+
(interest_curr, interest_prev, '兴趣词调整'), (kw_curr, kw_prev, '关键词调整')]:
|
|
657
|
+
if current != previous:
|
|
658
|
+
evidence['audience'].append(label)
|
|
659
|
+
for stem, label in [('targeting_age', '年龄调整'), ('targeting_genders', '性别调整')]:
|
|
660
|
+
current, previous = dim_sets(stem, stem, ['fb', 'gg', 'tt'])
|
|
661
|
+
if current != previous:
|
|
662
|
+
evidence['audience'].append(label)
|
|
663
|
+
if audience_type_moved:
|
|
664
|
+
evidence['audience'].append('受众类型调整')
|
|
728
665
|
payload = {"rows": [row], "details": details}
|
|
729
666
|
(req / "payload.json").write_text(json.dumps(payload, ensure_ascii=False, indent=2), encoding="utf-8")
|
|
730
667
|
summary = {
|
|
@@ -738,6 +675,7 @@ def summarize_request(req: Path):
|
|
|
738
675
|
"spend_verdict": spend_verdict,
|
|
739
676
|
"bid_counts": {ch: len(bids[ch]) for ch in bids},
|
|
740
677
|
"detail_count": len(details),
|
|
678
|
+
"tag_evidence": evidence,
|
|
741
679
|
"request_dir": str(req),
|
|
742
680
|
}
|
|
743
681
|
(req / "summary.json").write_text(json.dumps(summary, ensure_ascii=False, indent=2), encoding="utf-8")
|