diff --git a/backend/crawler_copy/jd_pc_search/jd_competitor_report.py b/backend/crawler_copy/jd_pc_search/jd_competitor_report.py index d25c368..e4bb5b1 100644 --- a/backend/crawler_copy/jd_pc_search/jd_competitor_report.py +++ b/backend/crawler_copy/jd_pc_search/jd_competitor_report.py @@ -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( - "- **四象限口径**:本批存在有效「评分」时——**1~2 星**计为偏负向,**4~5 星**计为偏正向,**3 星**计为中评,**空文本**计为中性;" + "- **正负面粗判规模**:本批存在有效「评分」时——**1~2 星**计为偏负向,**4~5 星**计为偏正向,**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 关注词与使用场景(按细类)", diff --git a/backend/pipeline/jd/runner.py b/backend/pipeline/jd/runner.py index ab4a10f..ea90084 100644 --- a/backend/pipeline/jd/runner.py +++ b/backend/pipeline/jd/runner.py @@ -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] = { diff --git a/backend/pipeline/reporting/charts.py b/backend/pipeline/reporting/charts.py index 5a662a0..c8d8ebe 100644 --- a/backend/pipeline/reporting/charts.py +++ b/backend/pipeline/reporting/charts.py @@ -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):