mirror of
https://github.com/primedigitaltech/market-assistant.git
synced 2026-07-21 23:41:39 +08:00
fix(pipeline): 区分店铺列表行占比与去重SKU占比,避免误导
- 列表导出增加 unique_sku_basis(每 SKU 计 1 次),与按行计的集中度并列写入 brief 与执行摘要。 - 第四章解读与扇图说明强调「列表可见度」非市占;8.5 大模型提示禁止将行占比写成 SKU 款数占比或自营占比。 - 分析页摘要兼容 first_share 并展示去重 SKU 口径。 Made-with: Cursor
This commit is contained in:
parent
f09a46a79e
commit
ed28e00e74
@ -91,6 +91,7 @@ from pipeline.competitor_report.list_mix import ( # noqa: E402
|
||||
_brand_cr,
|
||||
_counter_mix_top_rows_with_remainder,
|
||||
_search_list_proxies,
|
||||
_shop_concentration_by_unique_sku,
|
||||
_structure_brands,
|
||||
_structure_names_for_pie_counter,
|
||||
_structure_shops,
|
||||
@ -167,6 +168,11 @@ def build_competitor_markdown(
|
||||
shops_for_cr = _structure_names_for_pie_counter(shops_s)
|
||||
brands_for_cr = _structure_names_for_pie_counter(brands_s)
|
||||
cr1_shop, cr3_shop, top_shop_s, _ = _brand_cr(shops_for_cr)
|
||||
shop_unique_sku_basis = (
|
||||
_shop_concentration_by_unique_sku(search_export_rows)
|
||||
if list_export
|
||||
else None
|
||||
)
|
||||
cr1_list_brand, cr3_list_brand, top_list_brand, _ = _brand_cr(brands_for_cr)
|
||||
# §4.3 类目分布:深入合并表(与 §5 竞品矩阵同一细类划分,非搜索列表行)
|
||||
cm_structure = _category_mix(merged_rows, top_k=12)
|
||||
@ -320,12 +326,21 @@ def build_competitor_markdown(
|
||||
if cr3_shop is not None:
|
||||
exec_bullets.append(
|
||||
f"竞争结构({src},第四章):**店铺** 第一大店铺份额 ≈ **{100 * cr1_shop:.1f}%**(「{top_shop_s}」),"
|
||||
f"前三店铺合计份额 ≈ **{100 * cr3_shop:.1f}%**(按列表行计,同一 SKU 多行会重复计)。"
|
||||
f"前三店铺合计份额 ≈ **{100 * cr3_shop:.1f}%**(**按列表行计**,同一 SKU 多行会重复计;**勿与「去重 SKU 款数占比」或市占混淆**)。"
|
||||
)
|
||||
else:
|
||||
exec_bullets.append(
|
||||
f"竞争结构({src},第四章):**店铺** 第一大店铺份额 ≈ **{100 * cr1_shop:.1f}%**(「{top_shop_s}」)。"
|
||||
f"竞争结构({src},第四章):**店铺** 第一大店铺份额 ≈ **{100 * cr1_shop:.1f}%**(「{top_shop_s}」;**按列表行计**)。"
|
||||
)
|
||||
if shop_unique_sku_basis and shop_unique_sku_basis.get("n_unique_skus"):
|
||||
u = shop_unique_sku_basis
|
||||
fs = u.get("first_share")
|
||||
lab = (u.get("top_label") or "").strip()
|
||||
if fs is not None and lab:
|
||||
exec_bullets.append(
|
||||
f"**对照(按去重 SKU)**:同批列表共 **{u['n_unique_skus']}** 个去重 SKU,第一大店铺「{lab}」"
|
||||
f"约占 **{100 * float(fs):.1f}%**(每 SKU 计 1 次;**不是**全渠道销量或市占)。"
|
||||
)
|
||||
elif not list_export and cr1_deep is not None and top_brand_deep:
|
||||
if cr3_deep is not None:
|
||||
exec_bullets.append(
|
||||
@ -548,7 +563,7 @@ def build_competitor_markdown(
|
||||
_embed_chart(
|
||||
run_dir,
|
||||
"chart_shop_rows_pie.png",
|
||||
"店铺列表曝光占比(扇形图;按整理后的店铺名计数,与结构化摘要中的店铺占比统计一致;"
|
||||
"店铺列表曝光占比(扇形图;**按列表行计**的店铺名计数,与结构化摘要一致;若与「按去重 SKU」口径差异大,见下文规则解读;"
|
||||
"长尾并入「(其余店铺)」;扇形内再合并为「其他」)",
|
||||
)
|
||||
)
|
||||
@ -559,6 +574,7 @@ def build_competitor_markdown(
|
||||
top=top_shop_s or "",
|
||||
shop_rows_n=shop_rows_n,
|
||||
n_structure=n_structure,
|
||||
unique_sku_basis=shop_unique_sku_basis,
|
||||
)
|
||||
)
|
||||
lines.append(
|
||||
@ -1019,6 +1035,11 @@ def build_competitor_brief(
|
||||
shops_for_cr = _structure_names_for_pie_counter(shops_s)
|
||||
brands_for_cr = _structure_names_for_pie_counter(brands_s)
|
||||
cr1_shop, cr3_shop, top_shop_s, top_shop_share = _brand_cr(shops_for_cr)
|
||||
shop_unique_sku_basis = (
|
||||
_shop_concentration_by_unique_sku(search_export_rows)
|
||||
if list_export
|
||||
else None
|
||||
)
|
||||
cr1_list_brand, cr3_list_brand, top_list_brand, _ = _brand_cr(brands_for_cr)
|
||||
cm_structure = _category_mix(merged_rows, top_k=12)
|
||||
min_brand_rows = max(5, int(0.02 * n_structure)) if n_structure else 5
|
||||
@ -1238,6 +1259,7 @@ def build_competitor_brief(
|
||||
"top_three_combined_share": cr3_shop,
|
||||
"top_label": top_shop_s,
|
||||
"top_share_pct": top_shop_share,
|
||||
"unique_sku_basis": shop_unique_sku_basis,
|
||||
},
|
||||
"list_brand_field": list_brand_block,
|
||||
"detail_brand_among_merged": {
|
||||
@ -1294,7 +1316,7 @@ def build_competitor_brief(
|
||||
"与在线分析报告各章**计数规则**一致;关注词与场景以任务中的分析规则为准(子串命中统计,非深度主题模型)。",
|
||||
"价格来自页面展示字段抽取,含促销与规格差异;促销与标价对齐等为启发式摘录,仅供对照。",
|
||||
"评价语气为关键词粗判,非深度学习情感模型。",
|
||||
"「集中度」中:最大一家占比、前三名合计占比为小数(如 0.12 表示约 12%),对应列表或深入样本中的相关行。",
|
||||
"「集中度」中:默认按**列表行**计数(同一 SKU 多页曝光会重复计);`shops_from_list.unique_sku_basis` 为按**去重 SKU** 的对照口径。二者均**不是**销量、库存或全渠道市场份额。",
|
||||
],
|
||||
}
|
||||
return _sanitize_json_numbers(out)
|
||||
|
||||
@ -99,6 +99,42 @@ def _search_list_proxies(rows: list[dict[str, str]]) -> dict[str, Any]:
|
||||
}
|
||||
|
||||
|
||||
def _shop_concentration_by_unique_sku(
|
||||
rows: list[dict[str, str]],
|
||||
) -> dict[str, Any] | None:
|
||||
"""
|
||||
列表导出专用:按 **去重 SKU** 归属店铺后计集中度(每个 SKU 计 1 次;店铺名取该 SKU **首次出现** 的列表行)。
|
||||
|
||||
与 ``_structure_names_for_pie_counter(_structure_shops(...))`` 的「按列表行计」可形成对照:
|
||||
同一 SKU 在多页重复曝光时,行占比会高于去重 SKU 占比。**不是**销量、库存或全渠道市场份额。
|
||||
"""
|
||||
if not rows:
|
||||
return None
|
||||
sku_k = JD_SEARCH_CSV_HEADERS["sku_id"]
|
||||
shop_k = JD_SEARCH_CSV_HEADERS["shop_name"]
|
||||
sku_to_shop: dict[str, str] = {}
|
||||
for r in rows:
|
||||
sid = _cell(r, sku_k)
|
||||
sh = _cell(r, shop_k, _LEGACY_SHOP_NAME_KEY)
|
||||
if not sid or not sh:
|
||||
continue
|
||||
sid = sid.strip()
|
||||
sh = sh.strip()
|
||||
if not sid or not sh:
|
||||
continue
|
||||
if sid not in sku_to_shop:
|
||||
sku_to_shop[sid] = sh
|
||||
if not sku_to_shop:
|
||||
return None
|
||||
c1, c3, top, _ = _brand_cr(list(sku_to_shop.values()))
|
||||
return {
|
||||
"first_share": c1,
|
||||
"top_three_combined_share": c3,
|
||||
"top_label": top,
|
||||
"n_unique_skus": len(sku_to_shop),
|
||||
}
|
||||
|
||||
|
||||
def _structure_shops(rows: list[dict[str, str]], *, list_export: bool) -> list[str]:
|
||||
if list_export:
|
||||
return [
|
||||
@ -132,6 +168,7 @@ __all__ = [
|
||||
"_brand_cr",
|
||||
"_counter_mix_top_rows_with_remainder",
|
||||
"_search_list_proxies",
|
||||
"_shop_concentration_by_unique_sku",
|
||||
"_structure_brands",
|
||||
"_structure_names_for_pie_counter",
|
||||
"_structure_shops",
|
||||
|
||||
@ -167,6 +167,7 @@ def _lines_4_reading_shop(
|
||||
top: str,
|
||||
shop_rows_n: int,
|
||||
n_structure: int,
|
||||
unique_sku_basis: dict[str, Any] | None = None,
|
||||
) -> list[str]:
|
||||
if not shop_rows_n:
|
||||
return []
|
||||
@ -178,13 +179,26 @@ def _lines_4_reading_shop(
|
||||
]
|
||||
if cr1 is not None and (top or "").strip():
|
||||
lines.append(
|
||||
f"- 第一大店铺「{_md_cell(top.strip(), 40)}」约占 **{100 * cr1:.1f}%**;"
|
||||
"该指标刻画的是**列表可见度**而非销量,适合用于判断货架被哪些店铺占据。"
|
||||
f"- 第一大店铺「{_md_cell(top.strip(), 40)}」约占 **{100 * cr1:.1f}%**(**按列表行计**,同一 SKU 多行重复曝光会重复计);"
|
||||
"该指标刻画的是**列表可见度**而非销量或全渠道市占。"
|
||||
)
|
||||
if cr3 is not None:
|
||||
lines.append(
|
||||
f"- 前三店铺合计约 **{100 * cr3:.1f}%**;若集中度高,可考虑从店铺矩阵、旗舰店/专营店布局等角度拆解竞争。"
|
||||
)
|
||||
if isinstance(unique_sku_basis, dict) and unique_sku_basis.get("n_unique_skus"):
|
||||
u1 = unique_sku_basis.get("first_share")
|
||||
u3 = unique_sku_basis.get("top_three_combined_share")
|
||||
utop = (unique_sku_basis.get("top_label") or "").strip()
|
||||
nuk = unique_sku_basis.get("n_unique_skus")
|
||||
if isinstance(u1, (int, float)) and utop:
|
||||
lines.append(
|
||||
f"- **按去重 SKU 计**(列表内共 **{nuk}** 个 SKU,每个计 1 次;店铺取该 SKU 首次出现行):"
|
||||
f"第一大店铺「{_md_cell(utop, 40)}」约占 **{100 * float(u1):.1f}%**。"
|
||||
"若与上行「按行计」差异大,通常因同一 SKU 在多页重复出现;**勿将行占比误称为「SKU 款数占比」或「市场份额」**。"
|
||||
)
|
||||
if isinstance(u3, (int, float)):
|
||||
lines.append(f"- 前三店铺合计(按去重 SKU)约 **{100 * float(u3):.1f}%**。")
|
||||
lines.append("")
|
||||
return lines
|
||||
|
||||
|
||||
@ -21,6 +21,7 @@ REPORT_SYSTEM = """你是业务与产品读者顾问。输入 JSON 含 `keyword`
|
||||
- **不要**输出完整报告目录或复述「研究范围与方法」长章;
|
||||
- **不要**撰写 Markdown 表格版「竞品对比矩阵」或罗列 SKU 明细——**正文已含矩阵**,此处只做分组级语义归纳;
|
||||
- **不要使用** CR1、CR3 等集中度缩写作主表述;集中度请用「第一大店铺/品牌份额」「前三家合计份额」;输入中的英文字段名勿照抄进正文,请写成中文业务用语。
|
||||
- **店铺集中度**:若 `competitor_brief.concentration.shops_from_list` 含 `unique_sku_basis`,须区分 **按列表行计** 与 **按去重 SKU 计**;**禁止**将「列表行曝光占比」写成「SKU 款数占比」「自营 SKU 占比」或「市场份额」。
|
||||
|
||||
**请输出**(仅输出将置于 8.5 小节 下的正文,不要自造「### 8.5」标题行):
|
||||
- **Markdown**,约 **800~1500 字**;
|
||||
|
||||
@ -84,11 +84,24 @@ def markdown_summary_from_brief(brief: dict[str, Any]) -> str:
|
||||
[
|
||||
"## 店铺集中度(列表)",
|
||||
"",
|
||||
f"- **第一大店铺份额**:{_pct(concentration_first_share(shops))}(第一店铺:{shops.get('top_label') or '—'})",
|
||||
f"- **前三店铺合计份额**:{_pct(concentration_top_three_share(shops))}",
|
||||
f"- **第一大店铺份额(按列表行)**:{_pct(concentration_first_share(shops))}(第一店铺:{shops.get('top_label') or '—'})",
|
||||
f"- **前三店铺合计份额(按列表行)**:{_pct(concentration_top_three_share(shops))}",
|
||||
"",
|
||||
]
|
||||
)
|
||||
usb = shops.get("unique_sku_basis")
|
||||
if isinstance(usb, dict) and usb.get("n_unique_skus"):
|
||||
u1 = usb.get("first_share")
|
||||
u3 = usb.get("top_three_combined_share")
|
||||
if u1 is not None:
|
||||
lines.extend(
|
||||
[
|
||||
f"- **按去重 SKU 计**:共 **{_num(usb.get('n_unique_skus'))}** 个 SKU;"
|
||||
f"第一大店铺 **{_pct(u1)}**(「{usb.get('top_label') or '—'}」)",
|
||||
f"- **前三店铺合计(按去重 SKU)**:{_pct(u3)}",
|
||||
"",
|
||||
]
|
||||
)
|
||||
dbrand = conc.get("detail_brand_among_merged") or {}
|
||||
if concentration_first_share(dbrand) is not None or dbrand.get("top_label"):
|
||||
lines.extend(
|
||||
|
||||
@ -11,6 +11,15 @@ function pctShare(x) {
|
||||
return `${(n * 100).toFixed(1)}%`
|
||||
}
|
||||
|
||||
/** 集中度块:新键 first_share / top_three_combined_share,旧键 cr1 / cr3 */
|
||||
function concShare(block, key) {
|
||||
if (!block || typeof block !== 'object') return null
|
||||
const v0 = key === 'first' ? block.first_share : block.top_three_combined_share
|
||||
if (v0 != null && v0 !== '') return v0
|
||||
const legacy = key === 'first' ? block.cr1 : block.cr3
|
||||
return legacy != null && legacy !== '' ? legacy : null
|
||||
}
|
||||
|
||||
function briefHumanSummary(j) {
|
||||
const rows = []
|
||||
if (!j || typeof j !== 'object') return rows
|
||||
@ -31,46 +40,69 @@ function briefHumanSummary(j) {
|
||||
if (conc && typeof conc === 'object') {
|
||||
const shops = conc.shops_from_list
|
||||
if (shops && typeof shops === 'object') {
|
||||
if (shops.top_label && shops.cr1 != null) {
|
||||
const cr1 = concShare(shops, 'first')
|
||||
const cr3 = concShare(shops, 'top3')
|
||||
if (shops.top_label && cr1 != null) {
|
||||
rows.push({
|
||||
label: '第一大店铺(占列表行比例)',
|
||||
value: `${pctShare(shops.cr1)} · ${shops.top_label}`,
|
||||
value: `${pctShare(cr1)} · ${shops.top_label}`,
|
||||
})
|
||||
}
|
||||
if (shops.cr3 != null) {
|
||||
if (cr3 != null) {
|
||||
rows.push({
|
||||
label: '前三大店铺合计(占列表行比例)',
|
||||
value: pctShare(shops.cr3),
|
||||
value: pctShare(cr3),
|
||||
})
|
||||
}
|
||||
const usb = shops.unique_sku_basis
|
||||
if (usb && typeof usb === 'object' && usb.n_unique_skus != null) {
|
||||
const u1 = concShare(usb, 'first')
|
||||
const u3 = concShare(usb, 'top3')
|
||||
if (usb.top_label && u1 != null) {
|
||||
rows.push({
|
||||
label: '第一大店铺(占去重 SKU 比例)',
|
||||
value: `${pctShare(u1)} · ${usb.top_label} · 共 ${usb.n_unique_skus} 个 SKU`,
|
||||
})
|
||||
}
|
||||
if (u3 != null) {
|
||||
rows.push({
|
||||
label: '前三大店铺合计(占去重 SKU)',
|
||||
value: pctShare(u3),
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
const lb = conc.list_brand_field
|
||||
if (lb && typeof lb === 'object') {
|
||||
if (lb.top_label && lb.cr1 != null) {
|
||||
const l1 = concShare(lb, 'first')
|
||||
const l3 = concShare(lb, 'top3')
|
||||
if (lb.top_label && l1 != null) {
|
||||
rows.push({
|
||||
label: '第一大品牌(列表侧,按行)',
|
||||
value: `${pctShare(lb.cr1)} · ${lb.top_label}`,
|
||||
value: `${pctShare(l1)} · ${lb.top_label}`,
|
||||
})
|
||||
}
|
||||
if (lb.cr3 != null) {
|
||||
if (l3 != null) {
|
||||
rows.push({
|
||||
label: '前三大品牌合计(列表侧)',
|
||||
value: pctShare(lb.cr3),
|
||||
value: pctShare(l3),
|
||||
})
|
||||
}
|
||||
}
|
||||
const db = conc.detail_brand_among_merged
|
||||
if (db && typeof db === 'object') {
|
||||
if (db.top_label && db.cr1 != null) {
|
||||
const d1 = concShare(db, 'first')
|
||||
const d3 = concShare(db, 'top3')
|
||||
if (db.top_label && d1 != null) {
|
||||
rows.push({
|
||||
label: '第一大品牌(深入样本)',
|
||||
value: `${pctShare(db.cr1)} · ${db.top_label}`,
|
||||
value: `${pctShare(d1)} · ${db.top_label}`,
|
||||
})
|
||||
}
|
||||
if (db.cr3 != null) {
|
||||
if (d3 != null) {
|
||||
rows.push({
|
||||
label: '前三大品牌合计(深入样本)',
|
||||
value: pctShare(db.cr3),
|
||||
value: pctShare(d3),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user