mirror of
https://github.com/primedigitaltech/market-assistant.git
synced 2026-07-21 23:41:39 +08:00
feat(报告): 移除第八章预设口语短语情感小节并重编号
删除评价正负面粗判(扇形图、口语短语条形图、摘要及同口径 LLM 块);文本挖掘/关注词路径统一为新的 8.2;brief 不再含 comment_sentiment_lexicon;默认关闭 llm_comment_sentiment;清理旧插图文件名;同步策略与全稿提示。 Made-with: Cursor
This commit is contained in:
parent
7506f9795c
commit
23ddf115ee
@ -61,7 +61,7 @@ _COMMENT_CSV_SKU = COMMENT_CSV_COLUMNS[0]
|
||||
_COMMENT_CSV_BODY = COMMENT_CSV_COLUMNS[3]
|
||||
_COMMENT_CSV_SCORE = COMMENT_CSV_COLUMNS[7] # 「评分」→ commentScore
|
||||
|
||||
# 评价星级与 §8.2 分桶:先按评分筛正负,再在对应子集内统计口语短语(无评分时回退关键词)
|
||||
# 评价星级分桶(comment_sentiment 模块):仍用于可选的情感 LLM 载荷;报告正文与 brief 已不再输出同口径图
|
||||
_COMMENT_SCORE_NEG_MAX = 2 # 1~2 星 → 偏负向
|
||||
_COMMENT_SCORE_POS_MIN = 4 # 4~5 星 → 偏正向(3 星为中评,归入中性)
|
||||
|
||||
|
||||
@ -20,7 +20,7 @@ def _comment_lines_with_product_context(
|
||||
sku_header: str,
|
||||
title_h: str,
|
||||
) -> list[str]:
|
||||
"""与 ``comment_rows`` 顺序对齐:带细类/SKU/品名/店铺前缀,供 §8.2 大模型抽样。"""
|
||||
"""与 ``comment_rows`` 顺序对齐:带细类/SKU/品名/店铺前缀,供可选的情感/评论 LLM 载荷抽样。"""
|
||||
sku_meta: dict[str, tuple[str, str, str]] = {}
|
||||
for row in merged_rows:
|
||||
sku = _cell(row, sku_header).strip()
|
||||
|
||||
@ -58,9 +58,7 @@ from pipeline.competitor_report.price_promo import ( # noqa: E402
|
||||
_markdown_price_promotion_section,
|
||||
)
|
||||
from pipeline.competitor_report.comment_sentiment import ( # noqa: E402
|
||||
build_comment_sentiment_llm_payload,
|
||||
_comment_keyword_hits,
|
||||
_comment_sentiment_lexicon,
|
||||
_iter_comment_text_units,
|
||||
_iter_comment_text_units_and_scores,
|
||||
_merge_comment_previews,
|
||||
@ -225,7 +223,6 @@ def build_competitor_markdown(
|
||||
comment_texts, comment_scores = _iter_comment_text_units_and_scores(
|
||||
comment_rows, merged_rows
|
||||
)
|
||||
sentiment_lex = _comment_sentiment_lexicon(comment_texts, comment_scores)
|
||||
scen_counts, scen_n_texts = _comment_scenario_counts(
|
||||
comment_texts, scenario_groups
|
||||
)
|
||||
@ -292,9 +289,9 @@ def build_competitor_markdown(
|
||||
"- **评价主题词**:对评价正文做**预设词表子串计数**,非分词主题模型,适合扫方向,**需抽样人工验证**。",
|
||||
"- **用途/场景**:对每条评价独立判断是否命中预设场景词;一条可计入多个场景,统计的是「提及该场景的评价条数」而非用户数。",
|
||||
(
|
||||
"- **用户画像(第八章)**:正负面粗判含**口语短语**级摘录;**第八章第三节**另用分词、词频与主题模型等对评论做**补充分析**(与**第八章第二节**条形图口径不同、互为补充),可选词云并由大模型归纳要点。"
|
||||
"- **用户画像(第八章)**:**不再**使用星级子集内**预设口语短语**条形图、正负面扇形图及同口径摘要;**第八章第二节**为分词、词频、共现与主题等**文本补充分析**(可选词云及大模型解读),结论须结合抽样原文理解。"
|
||||
if _ch8_probe_sec
|
||||
else "- **用户画像(第八章)**:正负面粗判含**口语短语**级摘录;关注词与场景**仅按细类**以**同图左右并列**展示(左为关注词命中次数,右为场景占有效文本 **%**);见**第八章第三节**。"
|
||||
else "- **用户画像(第八章)**:关注词与场景**仅按细类**以**同图左右并列**展示(左为关注词命中次数,右为场景占有效文本 **%**);见**第八章第二节**。"
|
||||
),
|
||||
"- **细类划分(第五至第八章)**:**仅**依据合并表中的**商品详情页类目路径**;该信息缺失或无法读出细类名称的 SKU **不参与**竞品矩阵与按细类评价统计(相关评价条亦**不进入**按细类图表)。",
|
||||
"- **检索结果规模**:来自京东 PC 搜索返回的「结果条数」类指标,表示平台侧申报的匹配数量级,**不等于**动销、库存或独立 SKU 数。",
|
||||
@ -389,7 +386,7 @@ def build_competitor_markdown(
|
||||
)
|
||||
if multi_feedback_cat and (hits or scen_n_texts > 0):
|
||||
exec_bullets.append(
|
||||
"评价侧写(关注词、用途/场景)已按**第五章同一细类划分**分节,见**第八章第三节**(同图并列)。"
|
||||
"评价侧写(关注词、用途/场景)已按**第五章同一细类划分**分节,见**第八章第二节**(同图并列)。"
|
||||
)
|
||||
elif hits:
|
||||
top3 = "、".join(f"「{w}」({n})" for w, n in hits.most_common(3))
|
||||
@ -743,13 +740,7 @@ def build_competitor_markdown(
|
||||
]
|
||||
)
|
||||
|
||||
_sm_score = sentiment_lex.get("method") == "score_then_lexeme"
|
||||
_sec82_title = (
|
||||
"### 8.2 评价正负面粗判(评分优先 + 关键词回退)"
|
||||
if _sm_score
|
||||
else "### 8.2 评价正负面粗判(关键词规则)"
|
||||
)
|
||||
_sec82_block: list[str] = [
|
||||
_sec8_open: list[str] = [
|
||||
"---",
|
||||
"",
|
||||
"## 八、消费者反馈与用户画像(按细分类目)",
|
||||
@ -758,123 +749,30 @@ def build_competitor_markdown(
|
||||
"",
|
||||
"- **细类划分**:与**第五章「竞品矩阵」**相同,**仅**依据合并表中的**商品详情页类目路径**解析为「饼干 / 西式糕点 / …」等(规则见第五章开头说明)。",
|
||||
"- **归因**:每条评价按其 SKU 对应到深入样本,再映射到该 SKU 所属细类;SKU 不在合并表中的评价单独归入说明性分组;**在合并表中但该 SKU 缺少类目路径或读不出细类名称的,该评价不进入按细类统计**(与第五章**同一条排除规则**)。",
|
||||
"- **正负面粗判(第八章第二节)**:若评价含有效「评分」列则**先按星级**粗分正负与中评,再在对应子集内统计口语短语;无评分时仍按关键词子串;若任务开启**大模型评价情感分析**,可附**大模型对抽样原文的主题归因**,与条形图互补。",
|
||||
(
|
||||
"- **文本补充分析(第八章第三节)**:本任务已用中文分词与统计工具做了开放词表分析(词频、关键词突出度、词对共现、主题归纳等,可选词云),与**第八章第二节**规则词表条形图**不同**、**互补**;**不再**输出原「关注词次数 + 场景占比」左右并列条图。"
|
||||
"- **评论文本补充分析(第八章第二节)**:本任务已用中文分词与统计工具做了开放词表分析(词频、关键词突出度、词对共现、主题归纳等,可选词云),**不再**在报告中使用「星级子集内预设口语短语」条形图、正负面扇形图及同口径摘要;**不再**输出原「关注词次数 + 场景占比」左右并列条图。"
|
||||
if _ch8_probe_sec
|
||||
else "- **关注词与使用场景(第八章第三节)**:对组内评价正文做关注词子串计数(左栏条形图);对每条有效文本独立扫描**本次任务生效的场景词组**(来自报告调参或系统默认),一条可属多场景,右栏为**占该细类有效文本比例 %**(多标签下可相加 **>** 100%)。二者在 **同一张图左右并列**,与第五章矩阵细类一一对应。"
|
||||
else "- **关注词与使用场景(第八章第二节)**:对组内评价正文做关注词子串计数(左栏条形图);对每条有效文本独立扫描**本次任务生效的场景词组**(来自报告调参或系统默认),一条可属多场景,右栏为**占该细类有效文本比例 %**(多标签下可相加 **>** 100%)。二者在 **同一张图左右并列**,与第五章矩阵细类一一对应。"
|
||||
),
|
||||
"",
|
||||
_sec82_title,
|
||||
"",
|
||||
f"- **有效文本条数**:{sentiment_lex.get('text_units', 0)}(与第八章第一节**归因规则**一致)。",
|
||||
]
|
||||
if _sm_score:
|
||||
_sec82_block.append(
|
||||
"- **正负面粗判规模**:本批存在有效「评分」时——**1~2 星**计为偏负向,**4~5 星**计为偏正向,**3 星**计为中评,**空文本**计为中性;"
|
||||
"无评分的条仍按关键词子串划分;「混合」仅在**无评分**且同条兼含正/负关键词时出现。"
|
||||
)
|
||||
_sec82_block.extend(
|
||||
[
|
||||
f"- **偏正向**:{sentiment_lex.get('positive_only', 0)} 条"
|
||||
+ ("(主要为 4~5 星)" if _sm_score else "(仅命中正向词表)")
|
||||
+ ";"
|
||||
f"**偏负向**:{sentiment_lex.get('negative_only', 0)} 条"
|
||||
+ ("(主要为 1~2 星)" if _sm_score else "(仅命中负向词表)")
|
||||
+ ";"
|
||||
f"**混合**:{sentiment_lex.get('mixed_positive_and_negative', 0)} 条"
|
||||
+ ("(无评分且同条兼含正/负关键词)" if _sm_score else "(同条兼含正/负词)")
|
||||
+ ";"
|
||||
f"**中性或空文本**:{sentiment_lex.get('neutral_or_empty', 0)} 条"
|
||||
+ ("(含 3 星中评及无关键词命中)" if _sm_score else "")
|
||||
+ "。",
|
||||
"- **说明**:"
|
||||
+ (
|
||||
"星级与正文可能不一致(如五星长文吐槽);口语短语条形图仅在对应星级子集内统计;正式结论请**人工抽样**阅读原文。"
|
||||
if _sm_score
|
||||
else "词表为方向性粗判,讽刺、省略与错别字会导致误判;正式结论请**人工抽样**阅读原文。"
|
||||
),
|
||||
]
|
||||
)
|
||||
lines.extend(_sec82_block)
|
||||
_scope = (sentiment_lex.get("lexeme_scope_note") or "").strip()
|
||||
if _scope:
|
||||
lines.append(f"- **词根统计说明**:{_scope}")
|
||||
lines.extend(["", ""])
|
||||
lines.extend(
|
||||
_embed_chart(
|
||||
run_dir,
|
||||
"chart_sentiment_overview_pie.png",
|
||||
"评价正负面粗判规模(扇形图;与上表条数一致)",
|
||||
)
|
||||
)
|
||||
lines.extend(
|
||||
_embed_chart(
|
||||
run_dir,
|
||||
"chart_positive_lexemes_bar.png",
|
||||
(
|
||||
"正向评价里**最常出现的口语短语**(在 **4~5 星** 评价条内统计;条形图)"
|
||||
if _sm_score
|
||||
else "正向评价里**最常出现的口语短语**(在偏正向或混合评价条内统计;条形图)"
|
||||
),
|
||||
)
|
||||
)
|
||||
lines.extend(
|
||||
_embed_chart(
|
||||
run_dir,
|
||||
"chart_negative_lexemes_bar.png",
|
||||
(
|
||||
"负向评价里**最常出现的口语短语**(在 **1~2 星** 评价条内统计;条形图)"
|
||||
if _sm_score
|
||||
else "负向评价里**最常出现的口语短语**(在偏负向或混合评价条内统计;条形图)"
|
||||
),
|
||||
)
|
||||
)
|
||||
pos_h = sentiment_lex.get("positive_tone_lexeme_hits") or []
|
||||
neg_h = sentiment_lex.get("negative_tone_lexeme_hits") or []
|
||||
if pos_h:
|
||||
frag = ";".join(
|
||||
f"「{x.get('word', '')}」{x.get('texts_matched', 0)} 条"
|
||||
for x in pos_h[:6]
|
||||
if isinstance(x, dict)
|
||||
)
|
||||
lines.append(f"- **正向语境高频短语(摘要)**:{frag}。")
|
||||
if neg_h:
|
||||
frag_n = ";".join(
|
||||
f"「{x.get('word', '')}」{x.get('texts_matched', 0)} 条"
|
||||
for x in neg_h[:6]
|
||||
if isinstance(x, dict)
|
||||
)
|
||||
lines.append(f"- **负向语境高频短语(摘要)**:{frag_n}。")
|
||||
_llm_s = (llm_sentiment_section_md or "").strip()
|
||||
if _llm_s:
|
||||
lines.extend(
|
||||
[
|
||||
"",
|
||||
"#### 大模型深入解读(主题归因,与词频统计互补)",
|
||||
"",
|
||||
"> **说明**:基于与上节**同一套评分优先或关键词归类规则**抽样的评价原文,由大模型归纳**用户在说什么**(尤其是负向的具体事由),与上列条数、条形图**互补**;引文以原评论为准。",
|
||||
"",
|
||||
_llm_s,
|
||||
]
|
||||
)
|
||||
lines.append("")
|
||||
lines.extend(_sec8_open)
|
||||
if _ch8_probe_sec:
|
||||
lines.extend(
|
||||
[
|
||||
"### 8.3 评论文本补充分析(词频、关键词与共现、主题归纳)",
|
||||
"### 8.2 评论文本补充分析(词频、关键词与共现、主题归纳)",
|
||||
"",
|
||||
"> **说明**:与**第八章第二节**口语短语条形图(规则词表)**口径不同**、**互补**;**不再**输出本章原「关注词 + 场景」左右并列条图;插图位于本批次报告附图文件夹中。",
|
||||
"> **说明**:开放词表统计与分词主题探索;**勿**与已废弃的预设口语短语条形图混读;插图位于本批次报告附图文件夹中。",
|
||||
"",
|
||||
_ch8_probe_sec,
|
||||
"",
|
||||
]
|
||||
)
|
||||
else:
|
||||
# 仅当未嵌入第八章第三节补充分析(_ch8_probe_sec 为空)时:原「关注词 + 场景」条图与逐细类段落
|
||||
# 仅当未嵌入第八章第二节探针补充分析(_ch8_probe_sec 为空)时:原「关注词 + 场景」条图与逐细类段落
|
||||
lines.extend(
|
||||
[
|
||||
"### 8.3 关注词与使用场景(按细类)",
|
||||
"### 8.2 关注词与使用场景(按细类)",
|
||||
"",
|
||||
"每细类一张**左右并列图**(与报告附图文件夹中的 ``chart_focus_and_scenarios_bar__*.png`` 同源):"
|
||||
"**左**为配置关注词子串命中次数(同一评价可出现多次,为次数而非去重条数);"
|
||||
@ -931,9 +829,9 @@ def build_competitor_markdown(
|
||||
lines.extend(
|
||||
[
|
||||
"",
|
||||
"#### 使用场景要点归纳(大模型,与第八章第三节右栏图表互补)",
|
||||
"#### 使用场景要点归纳(大模型,与第八章第二节右栏图表互补)",
|
||||
"",
|
||||
"> **说明**:与第八章第三节**相同**的预设场景词组与子串命中规则;**各场景条数与占比以正文图右栏为准**。",
|
||||
"> **说明**:与第八章第二节**相同**的预设场景词组与子串命中规则;**各场景条数与占比以正文图右栏为准**。",
|
||||
"",
|
||||
_llm_sg,
|
||||
"",
|
||||
@ -945,9 +843,9 @@ def build_competitor_markdown(
|
||||
lines.extend(
|
||||
[
|
||||
"",
|
||||
"#### 细类评价与关注词要点归纳(大模型,与第八章第三节左栏图表互补)",
|
||||
"#### 细类评价与关注词要点归纳(大模型,与第八章第二节左栏图表互补)",
|
||||
"",
|
||||
"> **说明**:归纳各细类反馈主题与配置关注词命中;**次数与第八章第三节图左栏以正文为准**。",
|
||||
"> **说明**:归纳各细类反馈主题与配置关注词命中;**次数与第八章第二节图左栏以正文为准**。",
|
||||
"",
|
||||
_llm_cg,
|
||||
"",
|
||||
@ -1090,9 +988,6 @@ def build_competitor_brief(
|
||||
comment_texts, comment_scores = _iter_comment_text_units_and_scores(
|
||||
comment_rows, merged_rows
|
||||
)
|
||||
comment_sentiment_lexicon = _comment_sentiment_lexicon(
|
||||
comment_texts, comment_scores
|
||||
)
|
||||
scen_counts, scen_n_texts = _comment_scenario_counts(
|
||||
comment_texts, scenario_groups
|
||||
)
|
||||
@ -1311,11 +1206,9 @@ def build_competitor_brief(
|
||||
"strategy_hints": hints,
|
||||
"matrix_by_group": matrix_groups,
|
||||
"consumer_feedback_by_matrix_group": feedback_by_group,
|
||||
"comment_sentiment_lexicon": comment_sentiment_lexicon,
|
||||
"notes": [
|
||||
"与在线分析报告各章**计数规则**一致;关注词与场景以任务中的分析规则为准(子串命中统计,非深度主题模型)。",
|
||||
"价格来自页面展示字段抽取,含促销与规格差异;促销与标价对齐等为启发式摘录,仅供对照。",
|
||||
"评价语气为关键词粗判,非深度学习情感模型。",
|
||||
"「集中度」中:默认按**列表行**计数(同一 SKU 多页曝光会重复计);`shops_from_list.unique_sku_basis` 为按**去重 SKU** 的对照口径。二者均**不是**销量、库存或全渠道市场份额。",
|
||||
],
|
||||
}
|
||||
|
||||
@ -2,7 +2,7 @@
|
||||
第八章「评论文本补充分析」独立脚本(**不修改**主报告核心逻辑)。
|
||||
|
||||
流程(按细类分组):清洗(中文分词 + 停用词)→ **词云图(可选)** → 词频 / 关键词突出度 → 词对共现 → 主题归纳
|
||||
→ 规则化叙事小结 → 文末可选 **专用 LLM**(结构化 JSON + ``PROBE_TEXT_MINING_SYSTEM`` + ``_call_llm``;**非** ``COMMENT_GROUPS_SYSTEM``、**非**第八章第二节情感)。
|
||||
→ 规则化叙事小结 → 文末可选 **专用 LLM**(结构化 JSON + ``PROBE_TEXT_MINING_SYSTEM`` + ``_call_llm``;**非** ``COMMENT_GROUPS_SYSTEM``、**非**已废弃的星级子集预设口语短语情感条形图)。
|
||||
|
||||
依赖(请自行安装)::
|
||||
|
||||
@ -17,7 +17,7 @@
|
||||
|
||||
输出:默认写入 ``<run_dir>/chapter8_text_mining_probe.md``。
|
||||
|
||||
嵌入竞品报告:流水线默认开启(``get_default_report_config`` 中 ``chapter8_text_mining_probe``: true);若任务显式关闭则为 false。开启时会生成本稿并调用 ``markdown_embed_body_for_competitor_report`` 写入 ``competitor_analysis.md`` 的 **第八章第三节**,替代原「关注词 + 场景」条图及对应两段大模型;**第八章第二节与「大模型深入解读(主题归因…)」保留**。
|
||||
嵌入竞品报告:流水线默认开启(``get_default_report_config`` 中 ``chapter8_text_mining_probe``: true);若任务显式关闭则为 false。开启时会生成本稿并调用 ``markdown_embed_body_for_competitor_report`` 写入 ``competitor_analysis.md`` 的 **第八章第二节(评论文本补充分析)**,替代原「关注词 + 场景」条图及对应两段大模型;**不再**嵌入原「评价正负面粗判」预设口语短语扇形图/条形图及同口径大模型块。
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
@ -91,10 +91,10 @@ _STOP_BASIC: frozenset[str] = frozenset(
|
||||
""".split()
|
||||
)
|
||||
|
||||
# --- 评论文本补充分析 · 专用 LLM(与正式报告 ``COMMENT_GROUPS_SYSTEM`` / 第八章第二节均不同)---
|
||||
# --- 评论文本补充分析 · 专用 LLM(与正式报告 ``COMMENT_GROUPS_SYSTEM`` / 已废弃的预设口语短语情感口径均不同)---
|
||||
PROBE_TEXT_MINING_SYSTEM = """你是用户研究与文本挖掘方向的助手。
|
||||
|
||||
输入 JSON 为「第八章第三节评论文本补充分析」的**专用**结果(``schema_version``=1),**不是**正式竞品报告里的关注词规则统计、也不是第八章第二节情感 Lexicon。其中的数字与词表来自**中文分词 + 统计工具**(词频、关键词突出度、共现、主题归纳),与业务侧子串计数**口径不同**。
|
||||
输入 JSON 为「第八章第二节评论文本补充分析」的**专用**结果(``schema_version``=1),**不是**正式竞品报告里的关注词规则统计、也不是已废弃的星级子集预设口语短语 Lexicon。其中的数字与词表来自**中文分词 + 统计工具**(词频、关键词突出度、共现、主题归纳),与业务侧子串计数**口径不同**。
|
||||
|
||||
每个 ``groups`` 元素含:
|
||||
- ``probe_status``:``ok`` 表示该细类已完成分词与统计;``skipped`` 表示样本过少等未下钻。
|
||||
@ -551,7 +551,7 @@ def build_markdown(
|
||||
"## 8.0 说明",
|
||||
"",
|
||||
"本稿为**独立补充分析**,流程为:清洗评论 → 词云(可选)→ 词频与关键词 → 词对共现 → 主题归纳 → 文字小结;"
|
||||
"文末可选用**专用提示词**由大模型解读(与第八章第二节所用口径不同)。"
|
||||
"文末可选用**专用提示词**由大模型解读(与已废弃的预设口语短语情感口径不同)。"
|
||||
"**细类划分与 SKU 归因**与主报告一致;其余为中文分词与统计工具做的开放词表分析,**不替代**正式报告中的规则统计。",
|
||||
"",
|
||||
"---",
|
||||
@ -696,7 +696,7 @@ def build_markdown(
|
||||
"keyword": kw,
|
||||
"probe_note": (
|
||||
"中文分词 + 停用词;关键词突出度/共现/主题归纳为统计库;"
|
||||
"与正式报告的关注词规则统计、第八章第二节情感口径均不同;"
|
||||
"与正式报告的关注词规则统计、已废弃的预设口语短语情感口径均不同;"
|
||||
"评价摘录字段合并自 build_comment_groups_llm_payload,供语境对照;"
|
||||
"「使用场景」若出现,须仅能从本 JSON 内统计与摘录推断,不接入正式场景分组。"
|
||||
),
|
||||
@ -730,7 +730,7 @@ def build_markdown(
|
||||
|
||||
def markdown_embed_body_for_competitor_report(full_probe_md: str) -> str:
|
||||
"""
|
||||
将独立补充分析稿转为可嵌入 ``build_competitor_markdown`` 的 **第八章第三节正文**(不含 ``### 8.3`` 标题行):
|
||||
将独立补充分析稿转为可嵌入 ``build_competitor_markdown`` 的 **第八章第二节正文**(不含 ``### 8.2`` 标题行;由 ``jd_report`` 统一加标题):
|
||||
从 ``## 8.0 说明`` 起至文末,并把 ``## …`` 降为 ``#### …``,避免与宿主 ``## 八、`` 冲突。
|
||||
"""
|
||||
lines = (full_probe_md or "").splitlines()
|
||||
|
||||
@ -196,7 +196,7 @@ def main() -> int:
|
||||
if report_uses_chapter8_text_mining_probe(rc):
|
||||
payload["structured_brief_omission_note"] = (
|
||||
"已启用第八章文本挖掘(探针为主):structured_brief 已省略「关注词/场景子串计数」、按细类 feedback 中的 focus_keyword_hits/scenarios_top、"
|
||||
"`strategy_hints`、`comment_sentiment_lexicon`(规则词表/条形图同源),避免与报告 §8 文本挖掘主口径冲突。**不得**再以这类子串计数或预设场景占比作为论据。"
|
||||
"`strategy_hints` 等;全量 brief 已**不再**包含 ``comment_sentiment_lexicon``。**不得**再以星级子集预设口语短语计数或预设场景占比作为论据。"
|
||||
"用户与评论侧须依报告 §8 文本挖掘归纳及 `report_matrix_group_evidence_md`;**促销、满减、券价差**须与报告第六章、`price_promotion_signals` 及下方 `report_strategy_excerpt`(第九章)对齐,不得省略报告已写明的活动建议。"
|
||||
)
|
||||
|
||||
|
||||
@ -3,9 +3,9 @@
|
||||
|
||||
- §5 后:``generate_matrix_group_summaries_llm``
|
||||
- §6 后:``generate_price_group_summaries_llm``、``generate_promo_group_summaries_llm``
|
||||
- §8.2:``generate_comment_sentiment_analysis_llm``
|
||||
- (可选、默认关闭)``generate_comment_sentiment_analysis_llm``:已不再嵌入报告正文
|
||||
- §8末细类评价:``generate_comment_group_summaries_llm``
|
||||
- §8.3 右栏后使用场景:``generate_scenario_group_summaries_llm``
|
||||
- §8.2 关注词/场景路径下右栏后使用场景:``generate_scenario_group_summaries_llm``
|
||||
- §9 策略与机会:``generate_strategy_opportunities_llm``(``build_competitor_brief`` + 可选 ``chapter_llm_narratives`` 与各章归纳对齐)
|
||||
- §8.5 类全文补充(独立长文):``generate_competitor_report_markdown_llm``
|
||||
|
||||
@ -38,6 +38,9 @@ if str(JCR_ROOT) not in sys.path:
|
||||
sys.path.insert(0, str(JCR_ROOT))
|
||||
|
||||
from pipeline.competitor_report import jd_report as jcr # noqa: E402
|
||||
from pipeline.competitor_report.comment_sentiment import ( # noqa: E402
|
||||
build_comment_sentiment_llm_payload,
|
||||
)
|
||||
import jd_keyword_pipeline as kpl # noqa: E402
|
||||
|
||||
from pipeline.csv.schema import MERGED_FIELD_TO_CSV_HEADER # noqa: E402
|
||||
@ -199,7 +202,7 @@ def main() -> None:
|
||||
attr_units = list(comment_units)
|
||||
|
||||
def _sent() -> str:
|
||||
pl = jcr.build_comment_sentiment_llm_payload(
|
||||
pl = build_comment_sentiment_llm_payload(
|
||||
comment_units,
|
||||
scores=comment_scores,
|
||||
attributed_texts=attr_units,
|
||||
@ -210,7 +213,7 @@ def main() -> None:
|
||||
return generate_comment_sentiment_analysis_llm(pl)
|
||||
|
||||
_run_one(
|
||||
"§8.2 评价正负面(llm_comment_sentiment)",
|
||||
"已弃用嵌入 · 评价情感 LLM 演示(llm_comment_sentiment)",
|
||||
_sent,
|
||||
live=args.live,
|
||||
preview_chars=args.preview_chars,
|
||||
@ -333,7 +336,7 @@ def main() -> None:
|
||||
return generate_scenario_group_summaries_llm(pl_sg, keyword=keyword)
|
||||
|
||||
_run_one(
|
||||
"§8.3 使用场景归纳(scenario_groups)",
|
||||
"§8.2 路径 · 使用场景归纳(scenario_groups)",
|
||||
_sg,
|
||||
live=args.live,
|
||||
preview_chars=args.preview_chars,
|
||||
|
||||
@ -34,7 +34,7 @@ def merge_llm_supplement_with_rules_report(llm_md: str, rules_md: str) -> str:
|
||||
marker = "\n---\n\n## 九、策略与机会提示(假设清单,待验证)"
|
||||
insert = (
|
||||
"\n\n---\n\n"
|
||||
"### 8.5 大模型深度补充(与第二章至第八章第三节正文中的定量内容互补)\n\n"
|
||||
"### 8.5 大模型深度补充(与第二章至第八章第二节正文中的定量内容互补)\n\n"
|
||||
"> **说明**:本段位于**第八章末**;**竞品矩阵、价盘表、统计图与第八章第二、三节等各节正文以相应章节为准**,"
|
||||
"此处为跨小节语义整合,便于衔接第九章。\n\n"
|
||||
f"{sup.strip()}\n"
|
||||
@ -50,7 +50,7 @@ def merge_llm_supplement_with_rules_report(llm_md: str, rules_md: str) -> str:
|
||||
if alt in body and marker not in body:
|
||||
return body.replace(
|
||||
alt,
|
||||
"\n\n---\n\n### 8.5 大模型深度补充(与第二章至第八章第三节正文中的定量内容互补)\n\n"
|
||||
"\n\n---\n\n### 8.5 大模型深度补充(与第二章至第八章第二节正文中的定量内容互补)\n\n"
|
||||
"> **说明**:位于第八章末;**矩阵与图表以正文为准**。\n\n"
|
||||
f"{sup.strip()}\n"
|
||||
+ alt,
|
||||
@ -59,7 +59,7 @@ def merge_llm_supplement_with_rules_report(llm_md: str, rules_md: str) -> str:
|
||||
app = "\n## 附录 A:数据留存说明"
|
||||
if app in body:
|
||||
tail = (
|
||||
"\n\n---\n\n### 8.5 大模型深度补充(与第二章至第八章第三节正文中的定量内容互补)\n\n"
|
||||
"\n\n---\n\n### 8.5 大模型深度补充(与第二章至第八章第二节正文中的定量内容互补)\n\n"
|
||||
f"{sup.strip()}\n"
|
||||
)
|
||||
return body.replace(app, tail + app, 1)
|
||||
@ -167,7 +167,7 @@ def get_default_report_config() -> dict[str, Any]:
|
||||
"""与 ``pipeline.competitor_report.jd_report`` 模块常量一致的默认报告调参(供前端回填)。"""
|
||||
jcr, _ = _jd_crawler_modules()
|
||||
return {
|
||||
"llm_comment_sentiment": True,
|
||||
"llm_comment_sentiment": False,
|
||||
"llm_matrix_group_summaries": True,
|
||||
"llm_comment_group_summaries": True,
|
||||
"llm_scenario_group_summaries": True,
|
||||
@ -728,10 +728,6 @@ def write_competitor_analysis_for_run_dir(
|
||||
from ..llm.generate import generate_strategy_opportunities_llm
|
||||
|
||||
_strategy_narratives: dict[str, str] = {}
|
||||
if (llm_sentiment_md or "").strip():
|
||||
_strategy_narratives["sec8_2_sentiment_theme_attribution"] = (
|
||||
llm_sentiment_md
|
||||
)
|
||||
if (llm_matrix_md or "").strip():
|
||||
_strategy_narratives["sec5_matrix_group_summaries"] = llm_matrix_md
|
||||
if (llm_price_md or "").strip():
|
||||
|
||||
@ -26,11 +26,10 @@ REPORT_SYSTEM = """你是业务与产品读者顾问。输入 JSON 含 `keyword`
|
||||
**请输出**(仅输出将置于 8.5 小节 下的正文,不要自造「### 8.5」标题行):
|
||||
- **Markdown**,约 **800~1500 字**;
|
||||
- 建议用 ``####`` 组织:**执行摘要级要点**、**竞争与价盘**、**用户声量与负向事由**(须归纳用户在抱怨什么类型的问题,而非只堆关键词)、**后续可验证动作(假设)**(不写第九章目录或重复策略章内容);
|
||||
- 若有 `comment_sentiment_lexicon`,概括正/负向粗判局限;**负向**写清事由类型(口感、价格、物流等);
|
||||
- **归因与引语(硬性)**:`consumer_feedback_by_matrix_group` 与 `comment_sentiment_lexicon` 等均为**跨 SKU/跨店铺的关键词子串或条数统计**,**不能**单独据此推断「某一店铺某一单品」的结论。
|
||||
- **具体体验句式(含口感、包装等)**须以正文 **第八章第二节** 中带 ``【细类|SKU|品名|店铺】`` 前缀的抽样为准;本段**不要**新增无前缀、无店铺/品名/SKU 指向的「」引语。
|
||||
- 若写口感、包装、物流、价格等**聚合**维度,须**写明统计范围**(如「在已合并的评价文本中,『物流』『价格』类关键词命中较多,为全样本子串计数」);可结合 `matrix_overview_for_llm` 谈细类结构;**可一句**引导读者「见第八章第二节按店铺/品名的负向举例」。
|
||||
- 若 第八章第二节 已归纳带店铺与 SKU 的负向主题,本段**只做执行摘要级收束**,勿重复编造新引文。
|
||||
- **归因与引语(硬性)**:`consumer_feedback_by_matrix_group` 等为**跨 SKU/跨店铺的关键词子串或条数统计**,**不能**单独据此推断「某一店铺某一单品」的结论。
|
||||
- **具体体验句式(含口感、包装等)**须以正文 **第八章** 中带 ``【细类|SKU|品名|店铺】`` 前缀的抽样或文本挖掘归纳为准;本段**不要**新增无前缀、无店铺/品名/SKU 指向的「」引语。
|
||||
- 若写口感、包装、物流、价格等**聚合**维度,须**写明统计范围**;可结合 `matrix_overview_for_llm` 谈细类结构;**可一句**引导读者「见第八章按店铺/品名的评价归纳」。
|
||||
- 若第八章已归纳带店铺与 SKU 的负向主题,本段**只做执行摘要级收束**,勿重复编造新引文。
|
||||
- 语气专业、中文;某类信息在输入中缺失时**一句带过数据缺口**即可,**禁止**输出「本段未提供该项」等套话占位。"""
|
||||
|
||||
REPORT_USER_PREFIX = """请根据以下 JSON 撰写上文所述 8.5 小节 嵌入段落(Markdown 正文,勿加 ### 8.5 标题)。\n\n"""
|
||||
|
||||
@ -220,7 +220,7 @@ def generate_strategy_draft_markdown_llm(
|
||||
if report_uses_chapter8_text_mining_probe(report_config):
|
||||
payload["structured_brief_omission_note"] = (
|
||||
"已启用第八章文本挖掘(探针为主):structured_brief 已省略「关注词/场景子串计数」、按细类 feedback 中的 focus_keyword_hits/scenarios_top、"
|
||||
"``strategy_hints``、``comment_sentiment_lexicon``(规则词表/条形图同源),避免与报告 §8 文本挖掘主口径冲突。**不得**再以这类子串计数、短语条形图或预设场景占比作为论据。"
|
||||
"``strategy_hints`` 等;报告已**不再**输出 ``comment_sentiment_lexicon``(星级子集预设口语短语)及同口径图。**不得**再以这类子串计数、短语条形图或预设场景占比作为论据。"
|
||||
"用户与评论侧须依报告 §8 文本挖掘归纳及 `report_matrix_group_evidence_md`;**促销、满减、券价差**须与报告第六章、`price_promotion_signals` 及下方 `report_strategy_excerpt`(第九章)对齐,不得省略报告已写明的活动建议。"
|
||||
)
|
||||
raw = json.dumps(payload, ensure_ascii=False)
|
||||
@ -290,7 +290,7 @@ STRATEGY_OPPORTUNITIES_SYSTEM = (
|
||||
- **定性主题**(各细类讨论焦点、正负向体验、场景与关注词归纳、配料/卖点叙事、促销形态描述等)须与 ``prior_chapter_llm_narratives`` 中已出现的表述**方向一致**,**禁止**另写一套与节选**明显矛盾**的品类判断、品牌举例或用户痛点主题。
|
||||
- **定量与可核验事实**(价带分位数、店铺/品牌占比、条数、评论统计字段等)**以** ``competitor_brief`` **为准**;若节选与 brief 数字冲突,**采纳 brief**,且勿复述与数字冲突的节选句。
|
||||
- 若某键未出现在 ``prior_chapter_llm_narratives`` 或内容为空,则该维度**不得**编造与可能存在的报告其他章冲突的细节;仅依据 ``competitor_brief`` 或明确写「输入中未体现」。
|
||||
- **转化与体验**小节:正负向体验线索须**优先呼应** ``sec8_2_sentiment_theme_attribution`` 与 **第八章第三节 侧**节选(``sec8_3_comment_focus_summaries`` 或 ``sec8_3_text_mining_probe``,视何者存在);**禁止**将节选未提及的具体抱怨/品类问题写成**主要结论**;可写「假设:待结合业务验证」。
|
||||
- **转化与体验**小节:正负向体验线索须**优先呼应** **第八章第二节 侧**节选(``sec8_3_comment_focus_summaries`` 或 ``sec8_3_text_mining_probe``,视何者存在;内部键名仍沿用 ``sec8_3_*``);**禁止**将节选未提及的具体抱怨/品类问题写成**主要结论**;可写「假设:待结合业务验证」。
|
||||
|
||||
**标题与措辞(硬性)**:
|
||||
- **禁止**在正文开头或任何位置重复宿主已有章名/小节名,包括但不限于:「第九章」「第9章」「九、」「策略与机会提示」「策略与机会建议」「策略与机会」等;**不要**自造 ``##`` 一级标题;
|
||||
|
||||
@ -225,6 +225,10 @@ _OBSOLETE_REPORT_ASSETS: frozenset[str] = frozenset(
|
||||
"chart_focus_keywords_pie.png",
|
||||
"chart_comment_focus_global_bar.png",
|
||||
"chart_usage_scenarios_global_bar.png",
|
||||
"chart_sentiment_overview_pie.png",
|
||||
"chart_sentiment.png",
|
||||
"chart_positive_lexemes_bar.png",
|
||||
"chart_negative_lexemes_bar.png",
|
||||
}
|
||||
)
|
||||
|
||||
@ -265,7 +269,7 @@ def generate_report_charts(
|
||||
"""生成扇形/条形 PNG。返回已写入的文件名列表(不含路径)。
|
||||
|
||||
若 ``report_config["chapter8_text_mining_probe"]`` 为真,**不**生成 ``chart_focus_and_scenarios_bar__*.png``
|
||||
(与竞品报告 §8.3 文本挖掘探针互斥,避免无效产出)。
|
||||
(与竞品报告 §8.2 文本挖掘探针互斥,避免无效产出)。
|
||||
"""
|
||||
_setup_matplotlib_cjk()
|
||||
import matplotlib.pyplot as plt
|
||||
@ -634,62 +638,6 @@ def generate_report_charts(
|
||||
n_texts=n_texts,
|
||||
)
|
||||
|
||||
sent = brief.get("comment_sentiment_lexicon") or {}
|
||||
if isinstance(sent, dict):
|
||||
_score_mode = (sent.get("method") or "") == "score_then_lexeme"
|
||||
pie_labs = (
|
||||
["偏正向(4~5星)", "偏负向(1~2星)", "关键词混合", "中性/空"]
|
||||
if _score_mode
|
||||
else ["偏正向", "偏负向", "正负混合", "中性/空"]
|
||||
)
|
||||
pie_vals = [
|
||||
float(sent.get("positive_only") or 0),
|
||||
float(sent.get("negative_only") or 0),
|
||||
float(sent.get("mixed_positive_and_negative") or 0),
|
||||
float(sent.get("neutral_or_empty") or 0),
|
||||
]
|
||||
pl = [a for a, b in zip(pie_labs, pie_vals) if b > 0]
|
||||
pv = [b for b in pie_vals if b > 0]
|
||||
save_pie(pl, pv, "评价语气四象限占比", "chart_sentiment_overview_pie.png")
|
||||
save_bar_h(
|
||||
pl,
|
||||
pv,
|
||||
"评价正负面粗判(条数)",
|
||||
"chart_sentiment.png",
|
||||
"条数",
|
||||
)
|
||||
|
||||
pos_h = sent.get("positive_tone_lexeme_hits") or []
|
||||
neg_h = sent.get("negative_tone_lexeme_hits") or []
|
||||
plx, pvx = _label_count_pairs(
|
||||
pos_h, key_label="word", key_count="texts_matched", cap=16
|
||||
)
|
||||
save_bar_h(
|
||||
plx,
|
||||
pvx,
|
||||
(
|
||||
"4~5星语境 · 正向口语短语命中条数"
|
||||
if _score_mode
|
||||
else "正向/混合语境 · 正向口语短语命中条数"
|
||||
),
|
||||
"chart_positive_lexemes_bar.png",
|
||||
"条数",
|
||||
)
|
||||
nlx, nvx = _label_count_pairs(
|
||||
neg_h, key_label="word", key_count="texts_matched", cap=16
|
||||
)
|
||||
save_bar_h(
|
||||
nlx,
|
||||
nvx,
|
||||
(
|
||||
"1~2星语境 · 负向口语短语命中条数"
|
||||
if _score_mode
|
||||
else "负向/混合语境 · 负向口语短语命中条数"
|
||||
),
|
||||
"chart_negative_lexemes_bar.png",
|
||||
"条数",
|
||||
)
|
||||
|
||||
matrix_groups = brief.get("matrix_by_group") or []
|
||||
if isinstance(matrix_groups, list):
|
||||
for gi, block in enumerate(matrix_groups):
|
||||
|
||||
@ -7,6 +7,10 @@ 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
|
||||
|
||||
@ -30,8 +34,7 @@ class BuildCompetitorBriefTests(SimpleTestCase):
|
||||
self.assertEqual(out["scope"]["merged_sku_count"], 0)
|
||||
self.assertIsInstance(out["strategy_hints"], list)
|
||||
self.assertEqual(out["matrix_by_group"], [])
|
||||
self.assertIn("comment_sentiment_lexicon", out)
|
||||
self.assertEqual(out["comment_sentiment_lexicon"].get("text_units"), 0)
|
||||
self.assertNotIn("comment_sentiment_lexicon", out)
|
||||
import json
|
||||
|
||||
json.dumps(out)
|
||||
@ -39,7 +42,7 @@ class BuildCompetitorBriefTests(SimpleTestCase):
|
||||
def test_comment_sentiment_llm_payload_has_semantic_pool(self) -> None:
|
||||
texts = ["口感软硬适中很好吃", "太差了不建议"]
|
||||
attr = [f"【细类:A|SKU:1|品名:x|店铺:y】{t}" for t in texts]
|
||||
pl = jcr.build_comment_sentiment_llm_payload(
|
||||
pl = build_comment_sentiment_llm_payload(
|
||||
texts,
|
||||
attributed_texts=attr,
|
||||
shuffle_seed="unit-test-seed",
|
||||
@ -52,18 +55,18 @@ class BuildCompetitorBriefTests(SimpleTestCase):
|
||||
def test_comment_sentiment_score_then_lexeme(self) -> None:
|
||||
texts = ["很好吃", "太差了", "一般般"]
|
||||
scores = [5, 1, 3]
|
||||
lex = jcr._comment_sentiment_lexicon(texts, scores)
|
||||
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 = jcr.build_comment_sentiment_llm_payload(texts, scores=scores)
|
||||
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 = jcr._comment_sentiment_lexicon(texts, scores)
|
||||
lex = _comment_sentiment_lexicon(texts, scores)
|
||||
self.assertEqual(lex.get("method"), "keyword_lexicon")
|
||||
|
||||
def test_custom_focus_words_in_report_config(self) -> None:
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user