fix(report): 细类扇图口径与版式统一

_category_mix 增加(其余细类)使简报与扇形图合计等于有效矩阵 SKU 数;§4.3 去掉易与图不一致的 Markdown 表格与规则摘要,仅保留扇形图;全部扇形图默认画布统一为 4.9×3.65 英寸。

Made-with: Cursor
This commit is contained in:
hub-gif 2026-04-16 10:38:21 +08:00
parent 99d1b23b63
commit 30a6bc4405
2 changed files with 17 additions and 44 deletions

View File

@ -1677,13 +1677,26 @@ def _category_mix(
"""
可读细类标签统计 SKU 分布 §5 ``_competitor_matrix_group_key`` 同源
仅含 ``detail_category_path`` 可解析为展示名的行
返回 ``most_common(top_k)``并将未列入 Top K 的款数合并为其余细类
使各块 SKU 数之和等于有效矩阵 SKU 总数与扇形图简报 ``category_mix_top`` 一致
"""
labels: list[str] = []
for r in rows:
k = _matrix_group_label_from_detail_path(r)
if k:
labels.append(k)
return Counter(labels).most_common(top_k)
if not labels:
return []
c = Counter(labels)
common = c.most_common(top_k)
accounted = sum(v for _, v in common)
total = sum(c.values())
rest = total - accounted
out: list[tuple[str, int]] = list(common)
if rest > 0:
out.append(("(其余细类)", rest))
return out
def _structure_shops(rows: list[dict[str, str]], *, list_export: bool) -> list[str]:
@ -1948,38 +1961,6 @@ def _lines_4_reading_shop(
return lines
def _lines_4_reading_category(
cm_structure: list[tuple[str, int]],
*,
n_merged_sku: int,
n_sku_matrix: int,
) -> list[str]:
if not cm_structure or n_sku_matrix <= 0:
return []
top_lbl, top_c = cm_structure[0]
share = top_c / float(n_sku_matrix)
lines = [
"",
"**数据解读(规则摘要)**",
"",
f"- **统计口径**:与 **§5 竞品矩阵**相同,均来自深入合并表列 ``detail_category_path`` 解析出的可读细类标签。",
f"- **有效总量**:合并表共 **{n_merged_sku}** 个 SKU其中 **{n_sku_matrix}** 个具备可解析细类标签(**有效细类 SKU**"
f"**{max(0, n_merged_sku - n_sku_matrix)}** 个无可用路径或无法解析,与 §5 一致**不纳入**本小节分布。",
f"- 扇形图与简报包按 SKU 数取 **Top 12** 细类;下表列 **Top 5**。当前「{_md_cell(top_lbl, 40)}」款数最多,"
f"约占有效细类 SKU 的 **{100 * share:.1f}%**{top_c}/{n_sku_matrix})。",
"- 若头部细类占比极高,说明当前关键词下深入样本被少数品类定义;跨品类机会需结合 §5 矩阵再核对。",
"",
"| 细类标签Top 5 | SKU 数 | 占有效细类 SKU 数 |",
"| --- | ---: | ---: |",
]
for lbl, cnt in cm_structure[:5]:
lines.append(
f"| {_md_cell(lbl, 36)} | {cnt} | {100 * cnt / float(n_sku_matrix):.1f}% |"
)
lines.append("")
return lines
def build_competitor_markdown(
*,
run_dir: Path,
@ -2415,14 +2396,8 @@ def build_competitor_markdown(
_embed_chart(
run_dir,
"chart_category_mix_pie.png",
"细类标签分布(扇形图;合并表 ``detail_category_path``,与 §5 同源)",
)
)
lines.extend(
_lines_4_reading_category(
cm_structure,
n_merged_sku=n_sku,
n_sku_matrix=n_sku_matrix,
"细类标签分布(扇形图;合并表 ``detail_category_path``,与 §5 同源;"
"Top 12 以外的细类在数据层并入「(其余细类)」;扇形图内再合并为「其他」)",
)
)
lines.append(

View File

@ -473,7 +473,7 @@ def generate_report_charts(run_dir: Path, brief: dict[str, Any]) -> list[str]:
labs, vals = _merge_tail_as_other(labels, values, max_slices=max_slices)
if not labs or not vals or sum(vals) <= 0:
return
fs = figsize if figsize is not None else (6.2, 4.5)
fs = figsize if figsize is not None else (4.9, 3.65)
fig, ax = plt.subplots(figsize=fs)
colors = plt.cm.Set3(range(len(labs)))
wedges, _t, autotexts = ax.pie(
@ -524,7 +524,6 @@ def generate_report_charts(run_dir: Path, brief: dict[str, Any]) -> list[str]:
vb,
"品牌列表曝光占比",
"chart_brand_rows_pie.png",
figsize=(4.9, 3.65),
)
shop_mix = brief.get("list_shop_mix_top") or []
@ -534,7 +533,6 @@ def generate_report_charts(run_dir: Path, brief: dict[str, Any]) -> list[str]:
vs,
"店铺列表曝光占比",
"chart_shop_rows_pie.png",
figsize=(4.9, 3.65),
)
def scenario_group_asset_slug(group: str, index: int) -> str: