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 **同一条排除规则**)。",
"- **正负面粗判§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
else "- **关注词与使用场景§8.3**:对组内评价正文做关注词子串计数(左栏条形图);对每条有效文本独立扫描**本次任务生效的场景词组**(来自报告调参或系统默认),一条可属多场景,右栏为**占该细类有效文本比例 %**(多标签下可相加 **>** 100%)。二者在 **同一张图左右并列**,与 §5 矩阵细类一一对应。"
),
@ -2744,7 +2744,7 @@ def build_competitor_markdown(
]
if _sm_score:
_sec82_block.append(
"- **四象限口径**:本批存在有效「评分」时——**12 星**计为偏负向,**45 星**计为偏正向,**3 星**计为中评,**空文本**计为中性;"
"- **正负面粗判规模**:本批存在有效「评分」时——**12 星**计为偏负向,**45 星**计为偏正向,**3 星**计为中评,**空文本**计为中性;"
"无评分的条仍按关键词子串划分;「混合」仅在**无评分**且同条兼含正/负关键词时出现。"
)
_sec82_block.extend(
@ -2778,7 +2778,7 @@ def build_competitor_markdown(
_embed_chart(
run_dir,
"chart_sentiment_overview_pie.png",
"评价语气占比(扇形图;与上表条数一致)",
"评价正负面粗判规模(扇形图;与上表条数一致)",
)
)
lines.extend(
@ -2844,6 +2844,7 @@ def build_competitor_markdown(
]
)
else:
# 仅当未嵌入文本挖掘探针_ch8_probe_sec 为空)时:原「关注词 + 场景」条图与逐细类段落
lines.extend(
[
"### 8.3 关注词与使用场景(按细类)",

View File

@ -356,7 +356,7 @@ def write_competitor_analysis_for_run_dir(
)
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 = ""
sentiment_llm_record: dict[str, Any] = {

View File

@ -256,8 +256,17 @@ def _cleanup_obsolete_report_assets(out_dir: Path) -> None:
pass
def generate_report_charts(run_dir: Path, brief: dict[str, Any]) -> list[str]:
"""生成扇形/条形 PNG。返回已写入的文件名列表不含路径"""
def generate_report_charts(
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()
import matplotlib.pyplot as plt
from matplotlib.ticker import FuncFormatter
@ -543,76 +552,87 @@ def generate_report_charts(run_dir: Path, brief: dict[str, Any]) -> list[str]:
core = "group"
return f"i{index:02d}_{core}"
scen_by_slug: dict[str, tuple[list[str], list[float], int]] = {}
by_grp = brief.get("usage_scenarios_by_matrix_group") or []
if isinstance(by_grp, list):
for item in by_grp:
if not isinstance(item, dict):
continue
slug = (item.get("chart_slug") or "").strip()
gname = str(item.get("group") or "").strip()[:24]
idx = item.get("matrix_group_index")
if not slug and gname != "" and isinstance(idx, int):
slug = scenario_group_asset_slug(gname, idx)
if not slug:
continue
scen_rows = item.get("scenarios") or []
n_unit = int(item.get("effective_text_units") or 0)
gpairs: list[tuple[str, float]] = []
if isinstance(scen_rows, list):
for r in scen_rows:
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,
)
_skip_focus_scenario_combo = bool(
isinstance(report_config, dict)
and report_config.get("chapter8_text_mining_probe")
)
if _skip_focus_scenario_combo:
for fp in out_dir.glob("chart_focus_and_scenarios_bar__*.png"):
try:
fp.unlink()
except OSError:
pass
if not _skip_focus_scenario_combo:
scen_by_slug: dict[str, tuple[list[str], list[float], int]] = {}
by_grp = brief.get("usage_scenarios_by_matrix_group") or []
if isinstance(by_grp, list):
for item in by_grp:
if not isinstance(item, dict):
continue
slug = (item.get("chart_slug") or "").strip()
gname = str(item.get("group") or "").strip()[:24]
idx = item.get("matrix_group_index")
if not slug and gname != "" and isinstance(idx, int):
slug = scenario_group_asset_slug(gname, idx)
if not slug:
continue
scen_rows = item.get("scenarios") or []
n_unit = int(item.get("effective_text_units") or 0)
gpairs: list[tuple[str, float]] = []
if isinstance(scen_rows, list):
for r in scen_rows:
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 []
if isinstance(fb, list):
for item in fb:
if not isinstance(item, dict):
continue
slug = (item.get("chart_slug") or "").strip()
gname = str(item.get("group") or "").strip()[:24]
idx = item.get("matrix_group_index")
if not slug and gname != "" and isinstance(idx, int):
slug = scenario_group_asset_slug(gname, idx)
if not slug:
continue
hk = item.get("focus_keyword_hits") or []
wl: list[str] = []
vl: list[float] = []
if isinstance(hk, list):
for row in hk[:20]:
if not isinstance(row, dict):
continue
w = str(row.get("word") or "").strip()[:32]
c = row.get("count")
if w and isinstance(c, (int, float)) and c > 0:
wl.append(w)
vl.append(float(c))
wl = wl[:18]
vl = vl[:18]
gl, gv, n_scen = scen_by_slug.get(slug, ([], [], 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
save_combo_focus_scenario_bar(
gname=gname,
slug=slug,
wl=wl,
vl=vl,
gl=gl,
gv=gv,
n_texts=n_texts,
)
fb = brief.get("consumer_feedback_by_matrix_group") or []
if isinstance(fb, list):
for item in fb:
if not isinstance(item, dict):
continue
slug = (item.get("chart_slug") or "").strip()
gname = str(item.get("group") or "").strip()[:24]
idx = item.get("matrix_group_index")
if not slug and gname != "" and isinstance(idx, int):
slug = scenario_group_asset_slug(gname, idx)
if not slug:
continue
hk = item.get("focus_keyword_hits") or []
wl: list[str] = []
vl: list[float] = []
if isinstance(hk, list):
for row in hk[:20]:
if not isinstance(row, dict):
continue
w = str(row.get("word") or "").strip()[:32]
c = row.get("count")
if w and isinstance(c, (int, float)) and c > 0:
wl.append(w)
vl.append(float(c))
wl = wl[:18]
vl = vl[:18]
gl, gv, n_scen = scen_by_slug.get(slug, ([], [], 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
save_combo_focus_scenario_bar(
gname=gname,
slug=slug,
wl=wl,
vl=vl,
gl=gl,
gv=gv,
n_texts=n_texts,
)
sent = brief.get("comment_sentiment_lexicon") or {}
if isinstance(sent, dict):