feat: 报告生成器自动检测水色指数模式 — 支持非 ML 管线

问题: 报告模板仅适配 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 读取
This commit is contained in:
duxin
2026-07-09 09:31:58 +08:00
parent d18da41505
commit 1d051c9fec

View File

@ -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: