fix(charts): 探针模式下跳过 §8.3 关注词场景条图并清理旧 PNG

Made-with: Cursor
This commit is contained in:
hub-gif 2026-04-16 16:32:05 +08:00
parent 31ac2f1ac7
commit b8516662a8
3 changed files with 96 additions and 75 deletions

View File

@ -2733,7 +2733,7 @@ def build_competitor_markdown(
"- **归因**:每条评价按其 SKU 对应到深入样本,再映射到该 SKU 所属细类SKU 不在合并表中的评价单独归入说明性分组;**在合并表中但该 SKU 缺 ``detail_category_path`` 或路径无法解析为可读细类的,该评价不进入按细类统计**(与 §5 **同一条排除规则**)。", "- **归因**:每条评价按其 SKU 对应到深入样本,再映射到该 SKU 所属细类SKU 不在合并表中的评价单独归入说明性分组;**在合并表中但该 SKU 缺 ``detail_category_path`` 或路径无法解析为可读细类的,该评价不进入按细类统计**(与 §5 **同一条排除规则**)。",
"- **正负面粗判§8.2**:若评价含有效「评分」列则**先按星级**粗分正负与中评,再在对应子集内统计口语短语;无评分时仍按关键词子串;若任务开启 **llm_comment_sentiment**,可附**大模型对抽样原文的主题归因**,与条形图互补。", "- **正负面粗判§8.2**:若评价含有效「评分」列则**先按星级**粗分正负与中评,再在对应子集内统计口语短语;无评分时仍按关键词子串;若任务开启 **llm_comment_sentiment**,可附**大模型对抽样原文的主题归因**,与条形图互补。",
( (
"- **文本挖掘探§8.3**:本任务已启用 **jieba + sklearn** 的开放词表分析(词频 / TF-IDF / 共现 / LDA可选词云与 §8.2 规则词表条形图**口径不同**、**互补****不再**输出原「关注词次数 + 场景占比」左右并列条图。" "- **文本挖掘探§8.3**:本任务已启用 **jieba + sklearn** 的开放词表分析(词频 / TF-IDF / 共现 / LDA可选词云与 §8.2 规则词表条形图**不同**、**互补****不再**输出原「关注词次数 + 场景占比」左右并列条图。"
if _ch8_probe_sec if _ch8_probe_sec
else "- **关注词与使用场景§8.3**:对组内评价正文做关注词子串计数(左栏条形图);对每条有效文本独立扫描**本次任务生效的场景词组**(来自报告调参或系统默认),一条可属多场景,右栏为**占该细类有效文本比例 %**(多标签下可相加 **>** 100%)。二者在 **同一张图左右并列**,与 §5 矩阵细类一一对应。" else "- **关注词与使用场景§8.3**:对组内评价正文做关注词子串计数(左栏条形图);对每条有效文本独立扫描**本次任务生效的场景词组**(来自报告调参或系统默认),一条可属多场景,右栏为**占该细类有效文本比例 %**(多标签下可相加 **>** 100%)。二者在 **同一张图左右并列**,与 §5 矩阵细类一一对应。"
), ),
@ -2744,7 +2744,7 @@ def build_competitor_markdown(
] ]
if _sm_score: if _sm_score:
_sec82_block.append( _sec82_block.append(
"- **四象限口径**:本批存在有效「评分」时——**12 星**计为偏负向,**45 星**计为偏正向,**3 星**计为中评,**空文本**计为中性;" "- **正负面粗判规模**:本批存在有效「评分」时——**12 星**计为偏负向,**45 星**计为偏正向,**3 星**计为中评,**空文本**计为中性;"
"无评分的条仍按关键词子串划分;「混合」仅在**无评分**且同条兼含正/负关键词时出现。" "无评分的条仍按关键词子串划分;「混合」仅在**无评分**且同条兼含正/负关键词时出现。"
) )
_sec82_block.extend( _sec82_block.extend(
@ -2778,7 +2778,7 @@ def build_competitor_markdown(
_embed_chart( _embed_chart(
run_dir, run_dir,
"chart_sentiment_overview_pie.png", "chart_sentiment_overview_pie.png",
"评价语气占比(扇形图;与上表条数一致)", "评价正负面粗判规模(扇形图;与上表条数一致)",
) )
) )
lines.extend( lines.extend(
@ -2844,6 +2844,7 @@ def build_competitor_markdown(
] ]
) )
else: else:
# 仅当未嵌入文本挖掘探针_ch8_probe_sec 为空)时:原「关注词 + 场景」条图与逐细类段落
lines.extend( lines.extend(
[ [
"### 8.3 关注词与使用场景(按细类)", "### 8.3 关注词与使用场景(按细类)",

View File

@ -356,7 +356,7 @@ def write_competitor_analysis_for_run_dir(
) )
from ..reporting.charts import generate_report_charts from ..reporting.charts import generate_report_charts
generate_report_charts(run_dir, brief_final) generate_report_charts(run_dir, brief_final, report_config=eff_rc)
llm_sentiment_md = "" llm_sentiment_md = ""
sentiment_llm_record: dict[str, Any] = { sentiment_llm_record: dict[str, Any] = {

View File

@ -256,8 +256,17 @@ def _cleanup_obsolete_report_assets(out_dir: Path) -> None:
pass pass
def generate_report_charts(run_dir: Path, brief: dict[str, Any]) -> list[str]: def generate_report_charts(
"""生成扇形/条形 PNG。返回已写入的文件名列表不含路径""" run_dir: Path,
brief: dict[str, Any],
*,
report_config: dict[str, Any] | None = None,
) -> list[str]:
"""生成扇形/条形 PNG。返回已写入的文件名列表不含路径
``report_config["chapter8_text_mining_probe"]`` 为真****生成 ``chart_focus_and_scenarios_bar__*.png``
与竞品报告 §8.3 文本挖掘探针互斥避免无效产出
"""
_setup_matplotlib_cjk() _setup_matplotlib_cjk()
import matplotlib.pyplot as plt import matplotlib.pyplot as plt
from matplotlib.ticker import FuncFormatter from matplotlib.ticker import FuncFormatter
@ -543,76 +552,87 @@ def generate_report_charts(run_dir: Path, brief: dict[str, Any]) -> list[str]:
core = "group" core = "group"
return f"i{index:02d}_{core}" return f"i{index:02d}_{core}"
scen_by_slug: dict[str, tuple[list[str], list[float], int]] = {} _skip_focus_scenario_combo = bool(
by_grp = brief.get("usage_scenarios_by_matrix_group") or [] isinstance(report_config, dict)
if isinstance(by_grp, list): and report_config.get("chapter8_text_mining_probe")
for item in by_grp: )
if not isinstance(item, dict): if _skip_focus_scenario_combo:
continue for fp in out_dir.glob("chart_focus_and_scenarios_bar__*.png"):
slug = (item.get("chart_slug") or "").strip() try:
gname = str(item.get("group") or "").strip()[:24] fp.unlink()
idx = item.get("matrix_group_index") except OSError:
if not slug and gname != "" and isinstance(idx, int): pass
slug = scenario_group_asset_slug(gname, idx) if not _skip_focus_scenario_combo:
if not slug: scen_by_slug: dict[str, tuple[list[str], list[float], int]] = {}
continue by_grp = brief.get("usage_scenarios_by_matrix_group") or []
scen_rows = item.get("scenarios") or [] if isinstance(by_grp, list):
n_unit = int(item.get("effective_text_units") or 0) for item in by_grp:
gpairs: list[tuple[str, float]] = [] if not isinstance(item, dict):
if isinstance(scen_rows, list): continue
for r in scen_rows: slug = (item.get("chart_slug") or "").strip()
if not isinstance(r, dict): gname = str(item.get("group") or "").strip()[:24]
continue idx = item.get("matrix_group_index")
lb = str(r.get("scenario") or "").strip()[:48] if not slug and gname != "" and isinstance(idx, int):
c = r.get("count") slug = scenario_group_asset_slug(gname, idx)
if lb and isinstance(c, (int, float)) and c > 0: if not slug:
gpairs.append((lb, float(c))) continue
gpairs = _merge_labeled_counts_tail(gpairs, max_items=14) scen_rows = item.get("scenarios") or []
if gpairs and n_unit > 0: n_unit = int(item.get("effective_text_units") or 0)
scen_by_slug[slug] = ( gpairs: list[tuple[str, float]] = []
[p[0] for p in gpairs], if isinstance(scen_rows, list):
[p[1] for p in gpairs], for r in scen_rows:
n_unit, if not isinstance(r, dict):
) continue
lb = str(r.get("scenario") or "").strip()[:48]
c = r.get("count")
if lb and isinstance(c, (int, float)) and c > 0:
gpairs.append((lb, float(c)))
gpairs = _merge_labeled_counts_tail(gpairs, max_items=14)
if gpairs and n_unit > 0:
scen_by_slug[slug] = (
[p[0] for p in gpairs],
[p[1] for p in gpairs],
n_unit,
)
fb = brief.get("consumer_feedback_by_matrix_group") or [] fb = brief.get("consumer_feedback_by_matrix_group") or []
if isinstance(fb, list): if isinstance(fb, list):
for item in fb: for item in fb:
if not isinstance(item, dict): if not isinstance(item, dict):
continue continue
slug = (item.get("chart_slug") or "").strip() slug = (item.get("chart_slug") or "").strip()
gname = str(item.get("group") or "").strip()[:24] gname = str(item.get("group") or "").strip()[:24]
idx = item.get("matrix_group_index") idx = item.get("matrix_group_index")
if not slug and gname != "" and isinstance(idx, int): if not slug and gname != "" and isinstance(idx, int):
slug = scenario_group_asset_slug(gname, idx) slug = scenario_group_asset_slug(gname, idx)
if not slug: if not slug:
continue continue
hk = item.get("focus_keyword_hits") or [] hk = item.get("focus_keyword_hits") or []
wl: list[str] = [] wl: list[str] = []
vl: list[float] = [] vl: list[float] = []
if isinstance(hk, list): if isinstance(hk, list):
for row in hk[:20]: for row in hk[:20]:
if not isinstance(row, dict): if not isinstance(row, dict):
continue continue
w = str(row.get("word") or "").strip()[:32] w = str(row.get("word") or "").strip()[:32]
c = row.get("count") c = row.get("count")
if w and isinstance(c, (int, float)) and c > 0: if w and isinstance(c, (int, float)) and c > 0:
wl.append(w) wl.append(w)
vl.append(float(c)) vl.append(float(c))
wl = wl[:18] wl = wl[:18]
vl = vl[:18] vl = vl[:18]
gl, gv, n_scen = scen_by_slug.get(slug, ([], [], 0)) gl, gv, n_scen = scen_by_slug.get(slug, ([], [], 0))
n_unit_fb = int(item.get("effective_comment_text_units") or 0) n_unit_fb = int(item.get("effective_comment_text_units") or 0)
n_texts = n_scen if n_scen > 0 else n_unit_fb n_texts = n_scen if n_scen > 0 else n_unit_fb
save_combo_focus_scenario_bar( save_combo_focus_scenario_bar(
gname=gname, gname=gname,
slug=slug, slug=slug,
wl=wl, wl=wl,
vl=vl, vl=vl,
gl=gl, gl=gl,
gv=gv, gv=gv,
n_texts=n_texts, n_texts=n_texts,
) )
sent = brief.get("comment_sentiment_lexicon") or {} sent = brief.get("comment_sentiment_lexicon") or {}
if isinstance(sent, dict): if isinstance(sent, dict):