market-assistant/backend/pipeline/tests/test_competitor_brief.py
hub-gif 23ddf115ee feat(报告): 移除第八章预设口语短语情感小节并重编号
删除评价正负面粗判(扇形图、口语短语条形图、摘要及同口径 LLM 块);文本挖掘/关注词路径统一为新的 8.2;brief 不再含 comment_sentiment_lexicon;默认关闭 llm_comment_sentiment;清理旧插图文件名;同步策略与全稿提示。

Made-with: Cursor
2026-04-21 10:29:26 +08:00

206 lines
8.2 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""结构化竞品摘要:空样本烟测(不依赖真实 run_dir CSV"""
from __future__ import annotations
import tempfile
from pathlib import Path
from django.test import SimpleTestCase
from pipeline.competitor_report import jd_report as jcr
from pipeline.competitor_report.comment_sentiment import (
_comment_sentiment_lexicon,
build_comment_sentiment_llm_payload,
)
from pipeline.csv.schema import infer_total_sales_from_sales_floor
from pipeline.reporting.charts import _cn_volume_int
class BuildCompetitorBriefTests(SimpleTestCase):
def test_empty_merged_json_safe(self) -> None:
with tempfile.TemporaryDirectory() as td:
run_dir = Path(td)
(run_dir / "pc_search_raw").mkdir(parents=True)
out = jcr.build_competitor_brief(
run_dir=run_dir,
keyword="测试",
merged_rows=[],
search_export_rows=[],
comment_rows=[],
meta=None,
)
self.assertEqual(out["schema_version"], 1)
self.assertEqual(out["scope"]["merged_sku_count"], 0)
self.assertIsInstance(out["strategy_hints"], list)
self.assertEqual(out["matrix_by_group"], [])
self.assertNotIn("comment_sentiment_lexicon", out)
import json
json.dumps(out)
def test_comment_sentiment_llm_payload_has_semantic_pool(self) -> None:
texts = ["口感软硬适中很好吃", "太差了不建议"]
attr = [f"【细类ASKU1品名x店铺y】{t}" for t in texts]
pl = build_comment_sentiment_llm_payload(
texts,
attributed_texts=attr,
shuffle_seed="unit-test-seed",
semantic_pool_max=10,
)
self.assertIn("sample_reviews_semantic_pool", pl)
self.assertEqual(pl.get("sentiment_bucket_method"), "keyword_substring_heuristic")
self.assertGreaterEqual(len(pl["sample_reviews_semantic_pool"]), 1)
def test_comment_sentiment_score_then_lexeme(self) -> None:
texts = ["很好吃", "太差了", "一般般"]
scores = [5, 1, 3]
lex = _comment_sentiment_lexicon(texts, scores)
self.assertEqual(lex.get("method"), "score_then_lexeme")
self.assertEqual(lex.get("positive_only"), 1)
self.assertEqual(lex.get("negative_only"), 1)
self.assertEqual(lex.get("neutral_or_empty"), 1)
pl = build_comment_sentiment_llm_payload(texts, scores=scores)
self.assertEqual(pl.get("sentiment_bucket_method"), "score_then_lexeme")
def test_comment_sentiment_all_scores_missing_falls_back_keyword(self) -> None:
texts = ["好吃推荐", "差评"]
scores = [None, None]
lex = _comment_sentiment_lexicon(texts, scores)
self.assertEqual(lex.get("method"), "keyword_lexicon")
def test_custom_focus_words_in_report_config(self) -> None:
with tempfile.TemporaryDirectory() as td:
run_dir = Path(td)
(run_dir / "pc_search_raw").mkdir(parents=True)
out = jcr.build_competitor_brief(
run_dir=run_dir,
keyword="测试",
merged_rows=[],
search_export_rows=[],
comment_rows=[
{
"tagCommentContent": "自定义词阿尔法出现两次 自定义词阿尔法",
}
],
meta=None,
report_config={"comment_focus_words": ["自定义词阿尔法"]},
)
words = {x["word"] for x in out["comment_focus_keywords"]}
self.assertIn("自定义词阿尔法", words)
def test_matrix_groups_require_detail_category_path(self) -> None:
sku_h = "SKU(skuId)"
merged = [
{
sku_h: "111",
"detail_category_path": "食品饮料 > 休闲食品 > 饼干 > 粗粮饼干",
"标题(wareName)": "A",
},
{sku_h: "222", "标题(wareName)": "B"},
]
groups = jcr._merged_rows_grouped_for_matrix(merged)
self.assertEqual(len(groups), 1)
self.assertEqual(len(groups[0][1]), 1)
self.assertEqual(groups[0][1][0][sku_h], "111")
smap = jcr._sku_to_matrix_group_map(merged, sku_h)
self.assertEqual(smap.get("111"), "饼干")
self.assertNotIn("222", smap)
fb = jcr._consumer_feedback_by_matrix_group(
merged_rows=merged,
comment_rows=[
{"sku": "222", "tagCommentContent": "缺路径仍不应计入按细类统计"},
{"sku": "111", "tagCommentContent": "有路径进细类"},
],
sku_header=sku_h,
)
counts = {g: len(cr) for g, cr, _ in fb}
self.assertEqual(counts.get("饼干"), 1)
def test_comment_lines_with_product_context_prefix(self) -> None:
"""评价抽样须带细类/SKU/品名前缀,便于归因。"""
sku_h = "SKU(skuId)"
title_h = "标题(wareName)"
merged = [
{
sku_h: "100",
title_h: "低GI全麦饼干1kg",
"detail_brand": "B",
"detail_price_final": "29",
"detail_shop_name": "",
"detail_category_path": "休闲食品 > 饼干 > 粗粮饼干",
"detail_product_attributes": "x",
},
]
comments = [{"sku": "100", "tagCommentContent": "整体口感还差点意思"}]
lines = jcr._comment_lines_with_product_context(
comments, merged, sku_header=sku_h, title_h=title_h
)
self.assertEqual(len(lines), 1)
self.assertIn("【细类:", lines[0])
self.assertIn("SKU100", lines[0])
self.assertIn("品名:", lines[0])
self.assertIn("店铺:", lines[0])
self.assertIn("整体口感还差点意思", lines[0])
def test_scenario_groups_llm_payload_matches_section_8_4_counts(self) -> None:
sku_h = "SKU(skuId)"
merged = [
{
sku_h: "111",
"detail_category_path": "食品饮料 > 休闲食品 > 饼干 > 粗粮饼干",
"标题(wareName)": "A饼",
"detail_shop_name": "店甲",
},
]
scen = (("早餐/代餐", ("早餐",)),)
fb = jcr._consumer_feedback_by_matrix_group(
merged_rows=merged,
comment_rows=[
{"sku": "111", "tagCommentContent": "早上当早餐吃还不错"},
],
sku_header=sku_h,
)
pl = jcr.build_scenario_groups_llm_payload(
feedback_groups=fb,
scenario_groups=scen,
merged_rows=merged,
sku_header=sku_h,
title_h="标题(wareName)",
)
self.assertIn("groups", pl)
self.assertIn("scenario_lexicon", pl)
g0 = pl["groups"][0]
self.assertEqual(g0["group"], "饼干")
self.assertEqual(g0["effective_text_count"], 1)
self.assertEqual(g0["scenario_distribution"][0]["mention_rows"], 1)
self.assertEqual(
g0["scenario_distribution"][0]["scenario"], "早餐/代餐"
)
def test_cn_volume_int_parses_total_sales_trailer(self) -> None:
self.assertEqual(
_cn_volume_int("已售50万+ | good:99%好评"), 500_000
)
self.assertEqual(_cn_volume_int("2.5亿件"), 250_000_000)
def test_mix_top_remainder_sums_to_all_rows(self) -> None:
"""mix_top 各 count 之和须等于 strip 后可统计行数(与扇图同源)。"""
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)
self.assertEqual(len(jcr._structure_names_for_pie_counter(names)), 30)
def test_infer_total_sales_from_sales_floor(self) -> None:
self.assertEqual(
infer_total_sales_from_sales_floor("good:99%好评 | 已售50万+"),
"已售50万+",
)
self.assertEqual(infer_total_sales_from_sales_floor(""), "")