From fac2a2a2d2b58e5bcd2c5abd253710f66845873e Mon Sep 17 00:00:00 2001 From: Huarch Date: Mon, 20 Jul 2026 20:50:37 +0800 Subject: [PATCH] feat: align prediction report with guidance --- app/prediction.py | 18 ++++++++++-------- docker-compose.yml | 1 + example.xlsx | Bin 43387 -> 36279 bytes tests/test_prediction.py | 39 ++++++++++++++++++++++++++++----------- 4 files changed, 39 insertions(+), 19 deletions(-) diff --git a/app/prediction.py b/app/prediction.py index 2215b07..410815e 100644 --- a/app/prediction.py +++ b/app/prediction.py @@ -56,7 +56,10 @@ FEATURES = [ ID_COLUMN = "管道编号" PIPE_AGE_COLUMN = "管龄(年)" -REMAINING_LIFE_COLUMN = "预计剩余寿命(年)(管道健康状态 ≥0.5 的剩余年数)" +REMAINING_LIFE_COLUMN = ( + "预计剩余寿命(年)(管道健康状态 ≥0.5 的剩余年数;" + "如管道健康状态在73年内均高于0.5,则为73减去当前管龄)" +) LEGACY_PIPE_AGE_COLUMN = "管龄" COLUMN_ALIASES = { "ID": ID_COLUMN, @@ -402,7 +405,7 @@ def make_analysis_text(summary_rows: list[dict[str, Any]]) -> str: return ( "曲线表示模型对不同管道随管龄变化的健康状态动态预测。" f"当前样本中风险最高管道为 {worst['pipe_id']}({worst['grade_label']})," - f"当前健康状态最高管道为 {best['pipe_id']}({best['health_state']:.1%})。" + f"当前健康状态最高管道为 {best['pipe_id']}({best['health_state']:.2f})。" ) @@ -460,9 +463,9 @@ def float_list(values: Any) -> list[float]: return [float(value) for value in list(values)] -def format_years(value: float | None, current_age: float, max_age: float) -> str | float: +def format_years(value: float | None, current_age: float, max_age: float) -> float: if value is None: - return f">{max(max_age - current_age, 0.0):g}" + return round(max(max_age - current_age, 0.0), 3) return round(float(value), 3) @@ -497,7 +500,7 @@ def render_survival_chart(predictions: list[dict[str, Any]], image_path: Path) - summary_sheet_rows.append( { ID_COLUMN: pipe_id, - PIPE_AGE_COLUMN: pipe_age, + PIPE_AGE_COLUMN: current_age, "当前健康状态": current_health_state, "健康等级": grade_label, REMAINING_LIFE_COLUMN: format_years(remaining_life, current_age, max_age), @@ -509,7 +512,6 @@ def render_survival_chart(predictions: list[dict[str, Any]], image_path: Path) - font_kwargs = chinese_font_kwargs() plt.xlabel("管龄(年)", **font_kwargs) plt.ylabel("管道健康状态", **font_kwargs) - plt.title("供水管道健康状态曲线", **font_kwargs) plt.figtext(0.5, 0.02, display_note, ha="center", fontsize=9, color="#475569", **font_kwargs) plt.grid(alpha=0.18) if min(len(summary_rows), CHART_DISPLAY_LIMIT) <= 12: @@ -561,7 +563,7 @@ def write_prediction_workbook( sample_worksheet = writer.book["样本数据"] summary_worksheet = writer.book["结果摘要"] for cell in summary_worksheet["C"][1:]: - cell.number_format = "0.0%" + cell.number_format = "0.00" sample_worksheet.freeze_panes = "A2" sample_worksheet.auto_filter.ref = sample_worksheet.dimensions sample_worksheet.column_dimensions["A"].width = 12 @@ -569,7 +571,7 @@ def write_prediction_workbook( header_cell = column_cells[0] sample_worksheet.column_dimensions[header_cell.column_letter].width = max(12, len(str(header_cell.value)) + 2) for cell in column_cells[1:]: - cell.number_format = "0.0%" + cell.number_format = "0.00" for sheet_name in ("结果摘要", "样本数据"): worksheet = writer.book[sheet_name] diff --git a/docker-compose.yml b/docker-compose.yml index a27ef44..ad50df9 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -22,3 +22,4 @@ services: - ./data:/app/data - ./data/uploads:/app/uploads - ./data/images:/app/static/images + - ./example.xlsx:/app/example.xlsx:ro diff --git a/example.xlsx b/example.xlsx index a370edfbcae01931ad0ae09aa44e0b38c9a58405..500aadcbe7af1e8302cbe6754dd78f5c91cbb891 100644 GIT binary patch literal 36279 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 literal 43387 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
aC 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 HevJMDEdnMO diff --git a/tests/test_prediction.py b/tests/test_prediction.py index 1ccb059..2482d8c 100644 --- a/tests/test_prediction.py +++ b/tests/test_prediction.py @@ -18,8 +18,10 @@ from app.prediction import ( REMAINING_LIFE_COLUMN, PredictionError, estimate_remaining_life, + format_years, grade_info, interpolate_probability, + make_analysis_text, prepare_model_features, read_input_file, render_survival_chart, @@ -60,6 +62,18 @@ class PredictionHelpersTest(unittest.TestCase): def test_probability_helpers(self) -> None: self.assertEqual(interpolate_probability([1, 5, 10], [0.9, 0.8, 0.6], 6), 0.6) self.assertEqual(estimate_remaining_life([1, 5, 10], [0.9, 0.4, 0.2]), 5.0) + self.assertEqual(format_years(None, 10, 73), 63.0) + + def test_analysis_text_displays_health_state_as_decimal(self) -> None: + text = make_analysis_text( + [ + {"pipe_id": "P001", "health_state": 0.734, "grade_label": "IV级"}, + {"pipe_id": "P002", "health_state": 0.2, "grade_label": "I级"}, + ] + ) + + self.assertIn("0.73", text) + self.assertNotIn("%", text) def test_grade_boundaries(self) -> None: self.assertEqual(grade_info(0.2)[0], "I级") @@ -79,13 +93,12 @@ class PredictionHelpersTest(unittest.TestCase): validate_input_frame(df) - def test_example_workbook_columns_match_backend_contract(self) -> None: + def test_example_workbook_contains_backend_contract_columns(self) -> None: workbook = load_workbook(Path(__file__).resolve().parents[1] / "example.xlsx", read_only=True) worksheet = workbook["Template"] headers = [cell.value for cell in next(worksheet.iter_rows(min_row=1, max_row=1))] - self.assertEqual(len(headers), len(INPUT_COLUMNS)) - self.assertEqual(set(headers), set(INPUT_COLUMNS)) + self.assertTrue(set(INPUT_COLUMNS).issubset(headers)) def test_read_input_file_preserves_text_pipe_ids(self) -> None: with TemporaryDirectory() as temp_dir: @@ -183,8 +196,8 @@ class PredictionHelpersTest(unittest.TestCase): ] summary_rows = [{"pipe_id": "P001"}, {"pipe_id": "P002"}] summary_sheet_rows = [ - {ID_COLUMN: "P001", PIPE_AGE_COLUMN: "10 年", "当前健康状态": 0.7, "健康等级": "IV级", REMAINING_LIFE_COLUMN: ">63"}, - {ID_COLUMN: "P002", PIPE_AGE_COLUMN: "12 年", "当前健康状态": 0.6, "健康等级": "III级", REMAINING_LIFE_COLUMN: ">61"}, + {ID_COLUMN: "P001", PIPE_AGE_COLUMN: 10, "当前健康状态": 0.7, "健康等级": "IV级", REMAINING_LIFE_COLUMN: 63}, + {ID_COLUMN: "P002", PIPE_AGE_COLUMN: 12, "当前健康状态": 0.6, "健康等级": "III级", REMAINING_LIFE_COLUMN: 61}, ] with TemporaryDirectory() as temp_dir: @@ -199,6 +212,8 @@ class PredictionHelpersTest(unittest.TestCase): summary_data.columns.tolist(), [ID_COLUMN, PIPE_AGE_COLUMN, "当前健康状态", "健康等级", REMAINING_LIFE_COLUMN], ) + self.assertEqual(summary_data[PIPE_AGE_COLUMN].tolist(), [10, 12]) + self.assertEqual(summary_data[REMAINING_LIFE_COLUMN].tolist(), [63, 61]) sample_data = pd.read_excel(output_path, sheet_name="样本数据") self.assertEqual(sample_data.columns.tolist(), ["管龄(年)", "P001", "P002"]) @@ -209,15 +224,19 @@ class PredictionHelpersTest(unittest.TestCase): openpyxl_workbook = load_workbook(output_path) sample_worksheet = openpyxl_workbook["样本数据"] + summary_worksheet = openpyxl_workbook["结果摘要"] self.assertEqual(sample_worksheet.freeze_panes, "A2") self.assertEqual(sample_worksheet.auto_filter.ref, "A1:C3") - self.assertEqual(sample_worksheet["B2"].number_format, "0.0%") + self.assertEqual(summary_worksheet["C2"].number_format, "0.00") + self.assertEqual(sample_worksheet["B2"].number_format, "0.00") + self.assertIn("73减去当前管龄", summary_worksheet["E1"].value) + self.assertNotIn("73 - 当前管龄", summary_worksheet["E1"].value) def test_prediction_workbook_writes_pipe_ids_as_excel_text(self) -> None: predictions = [dummy_prediction("00123", [1], [0.9], 10, 0.9)] summary_rows = [{"pipe_id": "00123"}] summary_sheet_rows = [ - {ID_COLUMN: "00123", PIPE_AGE_COLUMN: "10 年", "当前健康状态": 0.9, "健康等级": "V级", REMAINING_LIFE_COLUMN: ">63"} + {ID_COLUMN: "00123", PIPE_AGE_COLUMN: 10, "当前健康状态": 0.9, "健康等级": "V级", REMAINING_LIFE_COLUMN: 63} ] with TemporaryDirectory() as temp_dir: @@ -243,19 +262,17 @@ class PredictionHelpersTest(unittest.TestCase): patch("app.prediction.plt.xlabel") as xlabel, patch("app.prediction.plt.ylabel") as ylabel, patch("app.prediction.plt.figtext") as figtext, - patch("app.prediction.plt.title") as title, ): - summary_rows, _ = render_survival_chart([dummy_prediction()], output_path) + summary_rows, summary_sheet_rows = render_survival_chart([dummy_prediction()], output_path) self.assertAlmostEqual(summary_rows[0]["health_state"], 0.4) self.assertEqual(summary_rows[0]["grade_label"], "II级") + self.assertEqual(summary_sheet_rows[0][PIPE_AGE_COLUMN], 12.0) xlabel.assert_called_once() self.assertEqual(xlabel.call_args.args[0], "管龄(年)") ylabel.assert_called_once() self.assertEqual(ylabel.call_args.args[0], "管道健康状态") - title.assert_called_once() - self.assertEqual(title.call_args.args[0], "供水管道健康状态曲线") figtext.assert_called_once() self.assertIn(f"前{CHART_DISPLAY_LIMIT}条管道的示例数据", figtext.call_args.args[2])