fix(report): align shop/brand pie denominators with §4 CR table via mix tail bucket

Made-with: Cursor
This commit is contained in:
hub-gif 2026-04-14 17:02:42 +08:00
parent e20250e7a6
commit c3fdb2b970
2 changed files with 47 additions and 8 deletions

View File

@ -1322,6 +1322,26 @@ def _brand_cr(cnames: list[str]) -> tuple[float | None, float | None, str, str]:
return cr1, cr3, top1, f"{100.0 * top1_n / total:.1f}%" return cr1, cr3, top1, f"{100.0 * top1_n / total:.1f}%"
def _counter_mix_top_rows_with_remainder(
row_names: list[str], *, top_n: int, remainder_label: str
) -> list[tuple[str, int]]:
"""
§4 集中度表饼图一致按行计数``most_common(top_n)`` 未覆盖的长尾合并为
``remainder_label``保证 ``sum(count) == len(row_names)``仅统计非空名行
"""
c = Counter((x or "").strip() for x in row_names if (x or "").strip())
if not c:
return []
total = sum(c.values())
common = c.most_common(top_n)
accounted = sum(v for _, v in common)
rest = total - accounted
out: list[tuple[str, int]] = list(common)
if rest > 0:
out.append((remainder_label, rest))
return out
def _price_stats_extended(prices: list[float]) -> dict[str, Any]: def _price_stats_extended(prices: list[float]) -> dict[str, Any]:
if not prices: if not prices:
return {} return {}
@ -2136,7 +2156,7 @@ def build_competitor_markdown(
_embed_chart( _embed_chart(
run_dir, run_dir,
"chart_brand_rows_pie.png", "chart_brand_rows_pie.png",
"品牌列表曝光占比(扇形图Top 段合并为「其他」;与上表集中度同源", "品牌列表曝光占比(扇形图;与上表按行计同源,长尾并入「(其余品牌)」;扇形内再合并为「其他」",
) )
) )
lines.extend( lines.extend(
@ -2178,7 +2198,7 @@ def build_competitor_markdown(
_embed_chart( _embed_chart(
run_dir, run_dir,
"chart_shop_rows_pie.png", "chart_shop_rows_pie.png",
"店铺列表曝光占比(扇形图;与上表同源)", "店铺列表曝光占比(扇形图;与上表按行计同源,长尾并入「(其余店铺)」",
) )
) )
lines.extend( lines.extend(
@ -2798,15 +2818,19 @@ def build_competitor_brief(
], ],
"list_brand_mix_top": [ "list_brand_mix_top": [
{"label": k, "count": v} {"label": k, "count": v}
for k, v in Counter( for k, v in _counter_mix_top_rows_with_remainder(
b for b in brands_s if (b or "").strip() brands_s,
).most_common(24) top_n=24,
remainder_label="(其余品牌)",
)
], ],
"list_shop_mix_top": [ "list_shop_mix_top": [
{"label": k, "count": v} {"label": k, "count": v}
for k, v in Counter( for k, v in _counter_mix_top_rows_with_remainder(
s for s in shops_s if (s or "").strip() shops_s,
).most_common(24) top_n=24,
remainder_label="(其余店铺)",
)
], ],
"price_stats": pst, "price_stats": pst,
"price_stats_source": price_stats_source, "price_stats_source": price_stats_source,

View File

@ -200,3 +200,18 @@ class BuildCompetitorBriefTests(SimpleTestCase):
"已售50万+", "已售50万+",
) )
self.assertEqual(infer_total_sales_from_sales_floor(""), "") self.assertEqual(infer_total_sales_from_sales_floor(""), "")
def test_mix_top_remainder_sums_to_all_rows(self) -> None:
"""饼图与 §4 表同源mix_top 各 count 之和须等于含名行数。"""
root = Path(settings.CRAWLER_JD_ROOT).resolve()
if str(root) not in sys.path:
sys.path.insert(0, str(root))
import jd_competitor_report as jcr # noqa: WPS433
names = [f"{i}" for i in range(30)]
mix = jcr._counter_mix_top_rows_with_remainder(
names, top_n=24, remainder_label="(其余店铺)"
)
self.assertEqual(sum(v for _, v in mix), 30)
self.assertEqual(mix[-1][0], "(其余店铺)")
self.assertEqual(mix[-1][1], 6)