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 eb84766..f382796 100644 Binary files a/example.xlsx and b/example.xlsx differ 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: