feat: validate uploaded pipeline data

This commit is contained in:
2026-08-03 15:50:26 +08:00
parent 277595621c
commit 82a8b4187a
2 changed files with 141 additions and 2 deletions
+89 -2
View File
@@ -1,6 +1,7 @@
from __future__ import annotations
import logging
import math
import sys
import uuid
from dataclasses import dataclass
@@ -290,6 +291,12 @@ def fill_defects_from_detail_sheet(path: Path, df: pd.DataFrame) -> pd.DataFrame
for index in df.index[needs_fill]:
excel_row = int(df.at[index, "_excel_row"])
template_id = normalize_id_value(df.at[index, ID_COLUMN], "") if ID_COLUMN in df.columns else ""
detail_id = normalize_id_value(defect_sheet.cell(excel_row, 1).value, "")
if template_id and template_id != detail_id:
raise PredictionError(
f"Template 与缺陷计算工作表的管道编号不一致,Excel行:{excel_row}"
)
if pd.isna(df.at[index, "结构缺陷"]):
value = numeric_or_none(defect_sheet.cell(excel_row, 5).value)
if value is None:
@@ -310,13 +317,91 @@ def fill_defects_from_detail_sheet(path: Path, df: pd.DataFrame) -> pd.DataFrame
return df
def validate_input_frame(df: pd.DataFrame) -> None:
def blank_values(series: pd.Series) -> pd.Series:
return series.isna() | series.astype("string").str.strip().eq("")
def source_rows(df: pd.DataFrame, mask: pd.Series) -> list[int]:
rows: list[int] = []
for index in df.index[mask]:
if "_excel_row" in df.columns and pd.notna(df.at[index, "_excel_row"]):
rows.append(int(df.at[index, "_excel_row"]))
else:
rows.append(int(df.index.get_loc(index)) + 2)
return rows
def raise_for_rows(message: str, df: pd.DataFrame, mask: pd.Series) -> None:
if mask.any():
raise PredictionError(f"{message}Excel行:{source_rows(df, mask)}")
def validate_input_frame(df: pd.DataFrame, max_pipe_age: float | None = None) -> None:
if df.empty:
raise PredictionError("上传文件没有可预测的数据。")
missing = [col for col in REQUIRED_INPUT_COLUMNS if col not in df.columns]
if missing:
raise PredictionError(f"缺少必要字段: {', '.join(missing)}")
blank_id = blank_values(df[ID_COLUMN])
raise_for_rows("管道编号不能为空", df, blank_id)
normalized_ids = df[ID_COLUMN].astype("string").str.strip()
duplicate_ids = normalized_ids.duplicated(keep=False)
raise_for_rows("管道编号不能重复", df, duplicate_ids)
for column in (PIPE_AGE_COLUMN, MATERIAL_COLUMN, "管径"):
raise_for_rows(f"{column}不能为空", df, blank_values(df[column]))
numeric_columns = [
PIPE_AGE_COLUMN,
"管径",
"流速",
"压力",
"温度",
"降雨量",
"结构缺陷",
"功能缺陷",
]
numeric_values: dict[str, pd.Series] = {}
for column in numeric_columns:
if column not in df.columns:
continue
blank = blank_values(df[column])
numeric = pd.to_numeric(df[column], errors="coerce")
finite = numeric.map(lambda value: math.isfinite(value) if pd.notna(value) else False)
invalid = ~blank & (numeric.isna() | ~finite)
raise_for_rows(f"{column}必须为有限数值", df, invalid)
numeric_values[column] = numeric
pipe_age = numeric_values[PIPE_AGE_COLUMN]
raise_for_rows("管龄(年)不能小于0", df, pipe_age < 0)
if max_pipe_age is not None:
raise_for_rows(
f"管龄(年)不能超过模型支持上限{max_pipe_age:g}",
df,
pipe_age > max_pipe_age,
)
diameter = numeric_values["管径"]
raise_for_rows("管径必须大于0", df, diameter <= 0)
if "结构缺陷" in numeric_values:
structural = numeric_values["结构缺陷"]
raise_for_rows(
"结构缺陷必须在05范围内",
df,
structural.notna() & ~structural.between(0, 5),
)
else:
structural = pd.Series(pd.NA, index=df.index, dtype="Float64")
if "功能缺陷" in numeric_values:
functional = numeric_values["功能缺陷"]
raise_for_rows(
"功能缺陷必须在05范围内",
df,
functional.notna() & ~functional.between(0, 5),
)
raise_for_rows("填写功能缺陷时必须同时填写结构缺陷", df, functional.notna() & structural.isna())
def normalize_category_value(
value: Any,
@@ -429,7 +514,9 @@ def run_prediction(uploaded: FileStorage, user_id: int, model) -> PredictionArti
try:
df = read_input_file(original_path, suffix)
validate_input_frame(df)
display_age_range = getattr(model, "config", {}).get("display_age_range", [])
max_pipe_age = float(display_age_range[1]) if len(display_age_range) > 1 else None
validate_input_frame(df, max_pipe_age=max_pipe_age)
x_test = prepare_model_features(df)
try:
predictions = model.predict(x_test, variant="defect_sensitive")