mirror of
https://github.com/primedigitaltech/market-assistant.git
synced 2026-07-22 08:01:34 +08:00
fix(charts): 探针模式下跳过 §8.3 关注词场景条图并清理旧 PNG
Made-with: Cursor
This commit is contained in:
parent
31ac2f1ac7
commit
b8516662a8
@ -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(
|
||||||
"- **四象限口径**:本批存在有效「评分」时——**1~2 星**计为偏负向,**4~5 星**计为偏正向,**3 星**计为中评,**空文本**计为中性;"
|
"- **正负面粗判规模**:本批存在有效「评分」时——**1~2 星**计为偏负向,**4~5 星**计为偏正向,**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 关注词与使用场景(按细类)",
|
||||||
|
|||||||
@ -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] = {
|
||||||
|
|||||||
@ -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):
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user