From 1d051c9fec78dce6b2320caa2970b8b6d895911e Mon Sep 17 00:00:00 2001 From: duxin Date: Thu, 9 Jul 2026 09:31:58 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=8A=A5=E5=91=8A=E7=94=9F=E6=88=90?= =?UTF-8?q?=E5=99=A8=E8=87=AA=E5=8A=A8=E6=A3=80=E6=B5=8B=E6=B0=B4=E8=89=B2?= =?UTF-8?q?=E6=8C=87=E6=95=B0=E6=A8=A1=E5=BC=8F=20=E2=80=94=20=E6=94=AF?= =?UTF-8?q?=E6=8C=81=E9=9D=9E=20ML=20=E7=AE=A1=E7=BA=BF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 问题: 报告模板仅适配 ML 管线 (steps 5-9+13), 用户跑 1,2,3,4,10,11,12 (水色指数公式管线) 时大量显示 [图片未找到]。 修复: 自动模式检测 + 分支逻辑 1. _detect_pipeline_mode(): 检测 scatter_with_confidence 文件 不存在 → 水色指数模式 2. _get_available_image_types(): 扫描实际存在的图片类型, 只报告确实生成的内容 3. 统计表格: ML→5_Data_Cleaning, 水色指数→10_WaterIndex_CSV 水色指数模式遍历所有公式 CSV 生成统计汇总表 4. 相关性热力图: 水色指数模式跳过并注明原因 5. 分布图: 利用已有 fallback 从 11_Thematic_Map 读取 --- src/postprocessing/report_word.py | 248 ++++++++++++++++++++---------- 1 file changed, 171 insertions(+), 77 deletions(-) diff --git a/src/postprocessing/report_word.py b/src/postprocessing/report_word.py index c733e3f..0c2391f 100644 --- a/src/postprocessing/report_word.py +++ b/src/postprocessing/report_word.py @@ -268,6 +268,38 @@ class WaterQualityReportGenerator: if cfg.enable_ai_analysis is not None: self.enable_ai_analysis = bool(cfg.enable_ai_analysis) + def _detect_pipeline_mode(self, vis_dir, parameters): + """检测管线模式:ML 还是水色指数公式 + + ML 模式标志:vis_dir 中存在 {param}_scatter_with_confidence.png。 + 若不存在 → 水色指数模式 (steps 10,11,12 无 ML)。 + """ + for p in parameters[:3]: # 检查前 3 个参数即可 + if (vis_dir / f"{p}_scatter_with_confidence.png").exists(): + return "ml" + return "water_index" + + def _get_available_image_types(self, vis_dir, parameters): + """扫描 vis_dir 中实际存在的图片类型,返回类型名列表""" + _all_types = ["histogram", "spectrum_comparison", "scatter_with_confidence", + "boxplot", "distribution_rendered"] + available = set() + for p in parameters[:3]: + for t in _all_types: + fname = f"{p}_{t}.png" + if (vis_dir / fname).exists(): + available.add(t) + # 也检查子目录 + if not available or t not in available: + for sub in ("boxplots", "scatter_plots", "distribution_maps"): + if (vis_dir / sub / fname).exists(): + available.add(t) + break + # 保证至少有 distribution_rendered(来自 11_Thematic_Map) + if "distribution_rendered" not in available: + available.add("distribution_rendered") + return sorted(available) + def _style_heading(self, heading, level: int): """统一一级/二级/三级标题字体(黑体)与字号。""" size_map = {1: Pt(16), 2: Pt(14), 3: Pt(12)} @@ -759,6 +791,18 @@ class WaterQualityReportGenerator: if not vis_dir.exists(): raise FileNotFoundError(f"可视化目录不存在: {vis_dir}") + + # ── 管线模式自动检测 ── + self._pipeline_mode = self._detect_pipeline_mode(vis_dir, parameters) + print(f"[报告] 管线模式: {self._pipeline_mode}") + if self._pipeline_mode == "water_index": + # 水色指数模式:只保留实际存在的图片类型 + _available_types = self._get_available_image_types(vis_dir, parameters) + self.parameter_images = { + p: [f"{p}_{t}.png" for t in _available_types] + for p in parameters + } + print(f"[报告] 水色指数可用图片类型: {_available_types}") if output_path is None: timestamp = datetime.now().strftime("%Y%m%d_%H%M%S") @@ -1613,65 +1657,113 @@ class WaterQualityReportGenerator: h2 = doc.add_heading("4.1 水质参数统计分析", level=2) self._style_heading(h2, level=2) - # 从工作目录的4_processed_data文件夹查找CSV文件 + # 从工作目录查找 CSV 统计数据文件(根据管线模式选择不同目录) work_dir_path = vis_dir.parent - processed_data_dir = work_dir_path / "5_Data_Cleaning" - - if not processed_data_dir.exists(): - doc.add_paragraph(f"未找到数据处理目录: {processed_data_dir}") + if getattr(self, '_pipeline_mode', 'ml') == 'water_index': + stats_dir = work_dir_path / "10_WaterIndex_CSV" + else: + stats_dir = work_dir_path / "5_Data_Cleaning" + + if not stats_dir.exists(): + alt_name = "10_WaterIndex_CSV" if self._pipeline_mode == 'water_index' else "5_Data_Cleaning" + doc.add_paragraph(f"未找到数据目录: {stats_dir}") doc.add_page_break() return start_figure_num - csv_files = list(processed_data_dir.glob("*.csv")) + csv_files = list(stats_dir.glob("*.csv")) if not csv_files: - doc.add_paragraph(f"在 {processed_data_dir} 目录下未找到CSV统计数据文件。") + doc.add_paragraph(f"在 {stats_dir} 目录下未找到CSV统计数据文件。") doc.add_page_break() return start_figure_num - + csv_path = csv_files[0] # 使用找到的第一个CSV文件 - + try: - df_full = pd.read_csv(csv_path, sep=',') - df = df_full.iloc[:, 2:] # 跳过前两列(纬度、经度),直接用列号 - - # 自动统计剩余列 - stats_data = [] - for i in range(df.shape[1]): - col = df.columns[i] - clean_col = str(col).strip() - try: - data = df.iloc[:, i].dropna() - if len(data) > 0: - stats_data.append({ - '参数': clean_col, - '点位数': len(data), - '最大值': f"{data.max():.4f}", - '最小值': f"{data.min():.4f}", - '平均值': f"{data.mean():.4f}", - '标准差': f"{data.std():.4f}" - }) - except Exception as e: - print(f"跳过列 {col}: {e}") - - if stats_data: - # 创建统计表格 + if getattr(self, '_pipeline_mode', 'ml') == 'water_index': + # 水色指数模式:遍历所有 CSV,每个公式一行统计 + stats_rows = [] + for cp in sorted(csv_files): + try: + df_one = pd.read_csv(cp, sep=',') + # 水色指数 CSV: proj_x, proj_y, longitude, latitude, value + # 取最后一列作为参数值 + val_col = df_one.columns[-1] + vals = pd.to_numeric(df_one[val_col], errors='coerce').dropna() + if len(vals) > 0: + stats_rows.append({ + '参数': Path(cp).stem, + '数量': len(vals), + '最小值': round(float(vals.min()), 4), + '最大值': round(float(vals.max()), 4), + '平均值': round(float(vals.mean()), 4), + '标准差': round(float(vals.std(ddof=0)), 4) if len(vals) > 1 else 0, + }) + except Exception: + pass + if stats_rows: + df = pd.DataFrame(stats_rows) + else: + doc.add_paragraph("水色指数 CSV 文件无法解析。") + doc.add_page_break() + return start_figure_num + else: + df_full = pd.read_csv(csv_path, sep=',') + df = df_full.iloc[:, 2:] # 跳过前两列(纬度、经度),直接用列号 + + if getattr(self, '_pipeline_mode', 'ml') == 'water_index': + # 水色指数模式:df 已是统计汇总表,直接写出 + if not stats_rows: + raise ValueError("无水色指数统计数据") table = doc.add_table(rows=1, cols=6, style='Table Grid') hdr_cells = table.rows[0].cells - hdr_cells[0].text = '参数' - hdr_cells[1].text = '点位数' - hdr_cells[2].text = '最大值' - hdr_cells[3].text = '最小值' - hdr_cells[4].text = '平均值' - hdr_cells[5].text = '标准差' - - for stat in stats_data: + for j, h in enumerate(['参数', '点位数', '最小值', '最大值', '平均值', '标准差']): + hdr_cells[j].text = h + header_map = {'参数': '参数', '数量': '点位数', '最小值': '最小值', + '最大值': '最大值', '平均值': '平均值', '标准差': '标准差'} + for row_dict in stats_rows: row_cells = table.add_row().cells - row_cells[0].text = stat['参数'] - row_cells[1].text = str(stat['点位数']) - row_cells[2].text = stat['最大值'] - row_cells[3].text = stat['最小值'] - row_cells[4].text = stat['平均值'] - row_cells[5].text = stat['标准差'] + for j, (col_name, hdr_name) in enumerate(header_map.items()): + row_cells[j].text = str(row_dict.get(col_name, '')) + stats_data = [{'参数': r['参数'], '点位数': r['数量']} for r in stats_rows] + else: + # ML 模式:逐列统计 + stats_data = [] + for i in range(df.shape[1]): + col = df.columns[i] + clean_col = str(col).strip() + try: + data = df.iloc[:, i].dropna() + if len(data) > 0: + stats_data.append({ + '参数': clean_col, + '点位数': len(data), + '最大值': f"{data.max():.4f}", + '最小值': f"{data.min():.4f}", + '平均值': f"{data.mean():.4f}", + '标准差': f"{data.std():.4f}" + }) + except Exception as e: + print(f"跳过列 {col}: {e}") + + if stats_data: + if getattr(self, '_pipeline_mode', 'ml') != 'water_index': + # ML 模式创建表格(水色指数模式表格已在上方创建) + table = doc.add_table(rows=1, cols=6, style='Table Grid') + hdr_cells = table.rows[0].cells + hdr_cells[0].text = '参数' + hdr_cells[1].text = '点位数' + hdr_cells[2].text = '最大值' + hdr_cells[3].text = '最小值' + hdr_cells[4].text = '平均值' + hdr_cells[5].text = '标准差' + for stat in stats_data: + row_cells = table.add_row().cells + row_cells[0].text = stat['参数'] + row_cells[1].text = str(stat['点位数']) + row_cells[2].text = stat['最大值'] + row_cells[3].text = stat['最小值'] + row_cells[4].text = stat['平均值'] + row_cells[5].text = stat['标准差'] else: doc.add_paragraph("CSV文件中未找到有效的参数数据。") @@ -1687,39 +1779,41 @@ class WaterQualityReportGenerator: doc.add_paragraph() # 表格和热力图之间的空行 - # 2. 添加相关性热力图(放在表格下方) - h3 = doc.add_heading("4.2 水质参数相关性分析", level=2) - self._style_heading(h3, level=2) - heatmap_path = vis_dir / "correlation_heatmap.png" - figure_num = start_figure_num - if heatmap_path.exists(): - try: - # 使用统一的图像插入方法 - caption_text = f"图{figure_num} 水质参数相关性热力图" - self._add_image_with_caption(doc, str(heatmap_path), caption_text, width=Inches(6.0)) - doc.add_paragraph("(颜色越深表示相关性越强,红色为正相关,蓝色为负相关)") + # 2. 添加相关性热力图(放在表格下方)—— 仅 ML 模式 + if getattr(self, '_pipeline_mode', 'ml') == 'ml': + h3 = doc.add_heading("4.2 水质参数相关性分析", level=2) + self._style_heading(h3, level=2) + heatmap_path = vis_dir / "correlation_heatmap.png" + figure_num = start_figure_num + if heatmap_path.exists(): + try: + caption_text = f"图{figure_num} 水质参数相关性热力图" + self._add_image_with_caption(doc, str(heatmap_path), caption_text, width=Inches(6.0)) + doc.add_paragraph("(颜色越深表示相关性越强,红色为正相关,蓝色为负相关)") - analysis_text = self._analyze_and_cache_image( - image_path=heatmap_path, - image_type="correlation_heatmap", - param="综合", - figure_num=figure_num, - ) - self._add_ai_analysis_paragraph(doc, analysis_text) - if all_image_analyses is not None: - all_image_analyses.append( - { - "figure_num": figure_num, - "param": "综合", - "image_type": "correlation_heatmap", - "image_name": heatmap_path.name, - "analysis": analysis_text, - } + analysis_text = self._analyze_and_cache_image( + image_path=heatmap_path, + image_type="correlation_heatmap", + param="综合", + figure_num=figure_num, ) - except Exception as e: - doc.add_paragraph(f"[相关性热力图插入失败: {e}]") + self._add_ai_analysis_paragraph(doc, analysis_text) + if all_image_analyses is not None: + all_image_analyses.append( + { + "figure_num": figure_num, + "param": "综合", + "image_type": "correlation_heatmap", + "image_name": heatmap_path.name, + "analysis": analysis_text, + } + ) + except Exception as e: + doc.add_paragraph(f"[相关性热力图插入失败: {e}]") + else: + doc.add_paragraph(f"[未找到相关性热力图: {heatmap_path.name}]") else: - doc.add_paragraph(f"[未找到相关性热力图: {heatmap_path.name}]") + doc.add_paragraph("(水色指数模式:参数相关性分析仅适用于机器学习预测流程,当前为非 ML 模式,已跳过。)") # 热力图处理结束(无论成功/失败)更新进度条 try: