From 22dd364405fa34c3ef04d31b13722f408060120d Mon Sep 17 00:00:00 2001 From: JIANG Date: Tue, 7 Jul 2026 10:36:09 +0800 Subject: [PATCH] =?UTF-8?q?=E8=B0=83=E6=95=B4=E5=88=97=E5=90=8D=E5=B9=B6?= =?UTF-8?q?=E6=B7=BB=E5=8A=A0=E8=BE=93=E5=85=A5=E9=AA=8C=E8=AF=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .dockerignore | 26 +++++++++++++------------- Dockerfile | 1 + app/prediction.py | 31 ++++++++++++++++++++++++------- example.xlsx | Bin 13095 -> 32492 bytes tests/test_prediction.py | 17 +++++++++++++++-- 5 files changed, 53 insertions(+), 22 deletions(-) diff --git a/.dockerignore b/.dockerignore index 2ec856d..4c3bb5e 100644 --- a/.dockerignore +++ b/.dockerignore @@ -1,16 +1,16 @@ -__pycache__/ +.git +.agents +.codex +__pycache__ *.pyc *.pyo -.git/ -.agents/ -.codex/ -node_modules/ -.env - -app.log -server.out.log -server.err.log -data/ +.pytest_cache +node_modules +data +uploads +static/images pipe_survival_0331.db -uploads/ -static/images/ +app.log +server.err.log +server.out.log +.env diff --git a/Dockerfile b/Dockerfile index 1737b89..b2e09b2 100644 --- a/Dockerfile +++ b/Dockerfile @@ -22,6 +22,7 @@ COPY templates ./templates COPY static ./static COPY main.py . COPY example.xlsx . +COPY 20260630标准文本——供水管道健康状态与剩余寿命评估技术导则.pdf . COPY my_survival_forest_model_quxi-10-0331.joblib . RUN mkdir -p data static/images uploads diff --git a/app/prediction.py b/app/prediction.py index 65b4856..adbd6e2 100644 --- a/app/prediction.py +++ b/app/prediction.py @@ -57,7 +57,17 @@ FEATURES = [ ] ID_COLUMN = "管道编号" -PIPE_AGE_COLUMN = "管龄" +PIPE_AGE_COLUMN = "管龄(年)" +LEGACY_PIPE_AGE_COLUMN = "管龄" +STATUS_COLUMN = "状态" +EVENT_AGE_COLUMN = "事件/观察管龄(年)" +INPUT_COLUMNS = [ + ID_COLUMN, + PIPE_AGE_COLUMN, + STATUS_COLUMN, + EVENT_AGE_COLUMN, + *FEATURES, +] MATERIAL_COLUMN = "管材" MATERIAL_CODE_OPTIONS = [ (1, "镀锌"), @@ -183,11 +193,19 @@ def secure_upload_name(original_filename: str, run_id: str) -> tuple[str, str]: def read_input_file(path: Path, suffix: str) -> pd.DataFrame: try: if suffix == ".csv": - return pd.read_csv(path, dtype={ID_COLUMN: "string"}) - return pd.read_excel(path, dtype={ID_COLUMN: "string"}) + df = pd.read_csv(path, dtype={ID_COLUMN: "string"}) + else: + df = pd.read_excel(path, dtype={ID_COLUMN: "string"}) except Exception as exc: logging.exception("文件解析失败: %s", exc) raise PredictionError("文件解析失败,请检查编码或表格格式。") + return normalize_input_columns(df) + + +def normalize_input_columns(df: pd.DataFrame) -> pd.DataFrame: + if PIPE_AGE_COLUMN not in df.columns and LEGACY_PIPE_AGE_COLUMN in df.columns: + return df.rename(columns={LEGACY_PIPE_AGE_COLUMN: PIPE_AGE_COLUMN}) + return df def normalize_id_value(value: Any, fallback: str) -> str: @@ -201,8 +219,7 @@ def normalize_id_value(value: Any, fallback: str) -> str: def validate_input_frame(df: pd.DataFrame) -> None: if df.empty: raise PredictionError("上传文件没有可预测的数据。") - required_columns = [ID_COLUMN, *FEATURES] - missing = [col for col in required_columns if col not in df.columns] + missing = [col for col in INPUT_COLUMNS if col not in df.columns] if missing: raise PredictionError(f"缺少必要字段: {', '.join(missing)}") @@ -447,8 +464,8 @@ def render_survival_chart(df: pd.DataFrame, curves, image_path: Path) -> tuple[l ) summary_sheet_rows.append( { - "管道编号": pipe_id, - "管龄": pipe_age, + ID_COLUMN: pipe_id, + PIPE_AGE_COLUMN: pipe_age, "健康等级": grade_label, } ) diff --git a/example.xlsx b/example.xlsx index eb84766c93bd1e53e7822f8b7c90212d25f12761..f3827962cd937715826297ba5ef49dcfef230bbf 100644 GIT binary patch literal 32492 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� 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 BU}yjU diff --git a/tests/test_prediction.py b/tests/test_prediction.py index f5fd675..15e8727 100644 --- a/tests/test_prediction.py +++ b/tests/test_prediction.py @@ -11,6 +11,7 @@ from openpyxl import load_workbook from app.prediction import ( FEATURES, ID_COLUMN, + INPUT_COLUMNS, PIPE_AGE_COLUMN, PredictionError, estimate_remaining_life, @@ -59,6 +60,18 @@ class PredictionHelpersTest(unittest.TestCase): self.assertIn("缺少必要字段", ctx.exception.message) + def test_validate_input_frame_accepts_current_template_columns(self) -> None: + df = pd.DataFrame([{column: 1 for column in INPUT_COLUMNS}]) + + validate_input_frame(df) + + def test_example_workbook_columns_match_backend_contract(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(headers, INPUT_COLUMNS) + def test_read_input_file_preserves_text_pipe_ids(self) -> None: with TemporaryDirectory() as temp_dir: output_path = Path(temp_dir) / "input.xlsx" @@ -97,8 +110,8 @@ class PredictionHelpersTest(unittest.TestCase): ] summary_rows = [{"pipe_id": "P001"}, {"pipe_id": "P002"}] summary_sheet_rows = [ - {"管道编号": "P001", "管龄": "10 年", "健康概率": 0.7, "预计剩余寿命": 2.0, "健康等级": "IV级"}, - {"管道编号": "P002", "管龄": "12 年", "健康概率": 0.6, "预计剩余寿命": 1.5, "健康等级": "III级"}, + {ID_COLUMN: "P001", PIPE_AGE_COLUMN: "10 年", "健康概率": 0.7, "预计剩余寿命": 2.0, "健康等级": "IV级"}, + {ID_COLUMN: "P002", PIPE_AGE_COLUMN: "12 年", "健康概率": 0.6, "预计剩余寿命": 1.5, "健康等级": "III级"}, ] with TemporaryDirectory() as temp_dir: