2026年五一杯数学建模C题边坡预警问题求解全过程文档及程序

2026年五一杯数学建模

C题 边坡预警问题

原题再现:

  在水利工程、交通路网、露天矿山等关键工程领域,边坡稳定性是突出的核心安全问题。受地质条件、气象变化与工程扰动等多因素耦合作用,边坡崩塌、滑坡等地质灾害频发,严重威胁生命财产与基础设施安全,易引发交通中断、工程损毁、生态破坏等重大风险。
  为实现边坡灾害精准防控,行业内已构建"空天地"一体化多源监测体系:卫星遥感实现大范围周期性监测,无人机与激光雷达实现重点区域精细观测,地面传感设备实现关键点位连续实时采集,形成多尺度、立体化协同监测网络,为边坡全域监测与早期预警提供坚实的数据支撑。
  "三段式形变"是边坡破坏前典型的位移演化规律,依次分为三个阶段:缓慢匀速形变阶段(位移速度基本恒定,坡体缓慢稳定调整)、加速形变阶段(位移速度显著增大,坡体进入非稳定形变阶段)、快速形变阶段(位移速度急剧增大,坡体趋近整体失稳破坏)。尽管失稳机理明确,但受工程爆破、气候变化、电磁干扰等环境因素影响,多源监测数据普遍存在噪声强、异常跳变频繁、设备故障导致的数据缺失等问题,严重制约滑坡预警的准确性与时效性。
  如何从强噪声、多断点的监测数据中精准识别形变阶段转换节点,并基于多源数据融合技术构建滑坡提前预警模型,是边坡安全预警的核心技术难题。
请依据附件提供的边坡多源监测数据,建立数学模型,完成以下问题。
  问题1.附件1给出同一边坡同一监测点的两组位移时序数据A与B,B为传统振弦式位移计监测数据,经验证为基准参考数据;A为新型光纤位移计获取数据,但因传感器零漂、安装偏差等存在偏移。请建立数学模型,对数据A进行校正,使校正后的结果与数据B的偏差尽可能小;采用交叉验证并给出偏差量化指标,客观评估模型性能和效果。对下表中的5个数据进行验证,并将结果填入表1.1。
  注:附件1中时间序列为从2024年1月1日0时起,采集频率为10分钟一次。

  问题2.建立数学模型,识别附件2中位移时序的三段式形变阶段的转换节点(缓慢匀速形变→加速形变、加速形变→快速形变的转换节点),给出区分"噪声/工程扰动引起的瞬时跳变"与"真实的阶段转换节点"的核心准则。根据识别出的阶段转换节点,依据不同的形变阶段对位移时序数据进行划分。对各阶段分别建立数学模型,给出模型参数并进行检验,同时计算各阶段表面位移平均速度。
  注2.1:附件2中的编号对应于数据采集时刻,采集时刻从2024年5月4日0时起,采集频率为10分钟一次。
  注2.2:阶段平均速度计算:该阶段表面位移变化总量/该阶段总持续时间,单位:mm/h。
  问题3.附件3提供包含降雨量、表面位移、深部位移、孔隙水压力、微震事件数的5变量时序监测数据,数据中存在传感器故障、环境干扰引发的异常值与缺失值(对应于表中未填写数据)。请基于附件3数据完成以下任务:
  问题3.1建立数学模型,对附件3中训练集的各变量数据进行高效去噪和缺失值补齐,并详细阐述各步骤的数学依据。
  问题3.2基于问题3.1预处理后的数据,对各变量开展异常值检测,识别共同异常点(同一时间点≥2个监测变量同时异常)。将异常点结果填入表3.1和3.2中。

  问题3.3依据预处理后的训练集建立数学模型,定量分析边坡表面位移与降雨量、深部位移、孔隙水压力、微震事件数之间的关联,评估各因素对表面位移变化的贡献程度。并将上述数学模型应用于附件3中的实验集,估计对应的表面位移并绘制散点图。
  问题4.附件4提供了训练集与实验集数据,其中训练集包含表面位移、降雨量、孔隙水压力、微震事件数、爆破点距离、单段最大药量等6维时序监测数据,其中爆破为偶发事件,非爆破时刻爆破点距离和单段最大药量相应字段为空值。实验集数据包含降雨量、孔隙水压力、微震事件数、爆破点距离、单段最大药量等5维时序监测数据。
  问题4.1附件4的训练集数据包含了缓慢匀速形变、加速形变、快速形变三阶段演变,不同阶段中降雨、爆破、微震等外界扰动对位移的影响可能发生显著变化。请根据降雨量、孔隙水压力、微震事件数、爆破点距离、单段最大药量等5项特征指标,建立分阶段数学模型,分析并预测表面位移变化规律。
  问题4.2附件4的实验集数据包含降雨量、孔隙水压力、微震事件数、爆破点距离、单段最大药量等5维时序监测数据,不含表面位移。实验集中的数据包含了缓慢匀速形变、加速形变和快速形变三阶段演变,且实验集与训练集在对应阶段的表面位移变化规律一致。其中阶段转换节点已在实验集的阶段标签中给出(训练集与实验集的阶段转换节点不一致)。请根据问题4.1中构建的数学模型,对实验集数据进行表面位移预测,分析表面位移预测的结果,并绘制表面位移随时间变化的图像。根据表面位移预测结果,补全下表的表面位移预测值。

  问题5.附件5提供了包含表面位移、降雨量、孔隙水压力、微震事件数、干湿入渗系数、爆破点距离、单段最大药量等7维时序监测数据,其中爆破为偶发事件,非爆破时刻爆破点距离和单段最大药量相应字段为空值。
  问题5.1从附件5提供的多维时序监测数据中的降雨量、孔隙水压力、微震事件数、干湿入渗系数、爆破点距离、单段最大药量中选取5类变量构建数学模型,定量分析边坡表面位移,评估不同变量组合下的表面位移数据误差,给出最优的变量组合模型,并解释原理。
  问题5.2边坡介质差异会导致边坡阶段划分与预警阈值不同。请以表面位移速度为预警指标,根据问题5.1所建立的最优模型,分阶段分析数据变化规律,并据此构建滑坡预警机制(如表面位移速度达到什么阈值时发出预警等),并解释其合理性。

  附件1:两组位移时序数据-问题1
  附件2:位移时序数据-问题2
  附件3:监测数据(训练集与实验集)-问题3
  附件4:监测数据(训练集与实验集)-问题4
  附件5:监测数据-问题5

整体求解过程概述(摘要)

  针对问题一中光纤位移计数据存在零漂和安装偏差的校正需求,首先对10,000组同步观测进行量纲核验、零值检查、相关性分析和误差分布诊断。鉴于数据A与基准数据B的Pearson相关系数达到0.999612,但原始误差呈现显著比例偏差和少量脉冲污染,本文引入Huber损失构建稳健仿射校准模型。五折交叉验证表明,该模型兼顾随机抽样泛化能力与时序外推稳定性,最终得到校正关系 y=-0.529944+0.881751x。校正后MAE由23.337 mm降至1.289 mm,降幅为94.48%,决定系数达到0.999222。
  针对问题二的三段式形变节点识别,首先使用Savitzky-Golay局部多项式平滑提取位移趋势、速度和加速度,并将孤立高残差与持续速度增益分离。随后引入带阶段结构约束的分段多项式最小二乘模型,在二维节点损失面上联合搜索两个转换点。识别结果为编号7879和9595,对应时刻分别为2024-06-27 17:00:00与2024-07-09 15:00:00。三个阶段平均速度分别为0.2536、1.8001和6.7476 mm/h,阶段模型R²均超过0.9999。本文进一步提出"幅值---持续性---方向一致性"三重准则,用于区分工程扰动瞬时跳变与真实阶段转换。
  针对问题三的缺失、噪声和多变量异常,本文先建立数据画像,发现五变量缺失数分别为510、475、510、456和559。考虑到变量之间存在显著耦合关系,采用链式贝叶斯多重插补,并通过5%人工遮蔽试验证明其总体误差显著低于中位数和KNN插补;随后采用轻度局部多项式去噪保持异常残差。基于零膨胀重尾分布和稳健Z分数构建自适应异常检测器,得到a---e五变量异常点数分别为50、28、30、31、95,共同异常点共60个。多元回归结果表明孔隙水压力贡献最高,表面位移预测模型R²为0.9823,实验集预测范围为58.25---164.56 mm。
  针对问题四不同形变阶段下外界扰动作用机制改变的问题,本文复用问题二的节点识别思想,在训练集上识别出编号6551与8551两个阶段转换点。为了兼顾阶段基线趋势和降雨、孔压、微震、爆破的非线性扰动,引入阶段归一化时间、累计降雨、孔压差分、微震窗口累计量和爆破能量衰减项,构建分阶段极端随机树模型,并在阶段边界施加连续性与单调性约束。对实验集预测后,五个指定时刻的表面位移依次为5.475, 33.603, 36.299, 328.045, 348.693 mm。结果表明快速形变阶段的预测位移和速度显著抬升,模型能够在训练与实验节点不一致的条件下完成阶段迁移。
  针对问题五的五变量最优组合与预警阈值设计,本文将表面位移转化为速度响应,以分块交叉验证下的速度误差和累计位移重构误差为双重准则,遍历六种"六选五"组合。结果表明剔除"干湿入渗系数"时泛化误差最低,最优组合为降雨量、孔隙水压力、微震事件数、爆破点距离、单段最大药量。在此基础上使用三成分高斯混合模型对平滑速度进行无监督状态划分,得到黄色和红色预警阈值分别为2.292 mm/h与9.226 mm/h。结合连续3个采样点触发、连续6点确认和90%解除滞回规则,构建了可抑制阈值抖动的工程化分级预警机制。
  综上,本文形成了"传感器稳健校准---形变节点识别---多源数据修复---分阶段预测---速度状态预警"的完整技术链。各子模型在方法上保持递进关系:问题一保证位移数据可比性,问题二提供阶段识别工具,问题三建立多源数据质量控制和关联解释框架,问题四实现跨阶段预测,问题五进一步完成变量筛选和风险决策。模型兼顾准确性、可解释性、鲁棒性与工程可实施性,可推广至交通边坡、露天矿山和水利工程的多源监测预警场景。

模型假设:

  假设1:各附件中相邻记录的采样间隔为10 min,除题目明确给出的起始时刻外,不存在隐藏的时间重采样。
  假设2:基准传感器B的测量误差相对于数据A的系统偏差可忽略,因而问题一把B视为响应真值。
  假设3:短时孤立尖峰主要来自设备、爆破或电磁干扰;真实阶段转换会在多个连续采样点上表现为速度或加速度统计特征的持续改变。
  假设4:缺失机制以随机故障为主,在给定其他监测变量和邻近时间信息后可近似视为条件随机缺失(MAR)。
  假设5:非爆破时刻的爆破点距离和药量为空值属于结构性缺失,填零表示"无爆破事件",而非传感器数据缺失。
  假设6:训练集与实验集在同一形变阶段内遵循相同的函数关系;阶段持续长度可以不同,但归一化阶段进程具有可比性。
  假设7:监测点所在边坡介质在单个附件观测期内不发生突变,故统计阈值和模型参数在该观测期内近似稳定。
  假设8:模型预测用于趋势与预警辅助,不替代现场地质调查、人工巡查和工程安全规程。

问题分析:

  问题一分析
  为了探究新型传感器A与基准传感器B之间的系统误差结构,首先需要同时观察两序列的时间趋势、散点关系和原始残差。若两者高度相关且残差随量级近似线性变化,则应优先采用低复杂度标定函数;若残差中夹杂少量异常尖峰,普通最小二乘会被大残差牵引,因此需要引入稳健损失。鉴于目标是给出任意单个x的校正结果,模型还必须具有显式映射、单调性和外推可控性。
  问题二分析
  转换节点不是单点幅值突变,而是形变速率及其加速度统计规律发生持续变化的位置。因此,直接对原始位移做差会放大噪声,应先提取平滑趋势,再通过速度、加速度和分段回归损失联合识别。为了避免将爆破或电磁干扰产生的单个尖峰误判为阶段转换,节点判定需要同时满足残差幅值、持续时间和速度方向一致性。
  问题三分析
  问题三具有严格的处理顺序。数据理解阶段应先统计缺失比例、偏度、峰度和变量相关性,以确定插补模型是否可以利用多变量耦合。预处理阶段不能简单将所有尖峰平滑掉,因为问题3.2仍需要识别异常;因此应区分"用于趋势提取的轻度去噪序列"和"保留创新项的插补序列"。异常检测必须采用分布自适应阈值:降雨和微震具有零膨胀、非负和重尾特点,而孔压、深部位移更接近对称连续分布。
  问题四分析
  训练集没有阶段标签而实验集有标签,故问题四必须先在训练集上完成阶段识别。由于对应阶段的变化规律一致但阶段长度不同,直接使用绝对时间会造成迁移偏差,应引入阶段归一化时间u,将每一阶段映射到0,1。同时,累计降雨、孔压变化、微震集聚和爆破能量具有滞后效应,应通过滚动窗口和指数衰减构造特征。
  问题五分析
  表面位移是随时间累计的状态量,若直接用瞬时驱动变量回归累计位移,模型会把时间趋势误认为物理贡献。为提高变量筛选的合理性,本文将速度作为响应量,再以积分重构位移的误差作为第二评价指标。预警阶段划分不强制只有一次转换,而允许速度状态在外界扰动下反复切换,因此采用三成分混合分布估计数据驱动阈值,并用持续性和滞回规则将统计阈值转化为工程预警机制。

模型的建立与求解整体论文缩略图

全部论文请见下方" 只会建模 QQ名片" 点击QQ名片即可

程序代码:

部分程序如下:
python 复制代码
from __future__ import annotations

import argparse
import json
import math
import os
import re
import shutil
import tempfile
import warnings
import zipfile
from dataclasses import dataclass
from datetime import datetime, timedelta
from pathlib import Path
from typing import Dict, Iterable, List, Sequence, Tuple

import matplotlib
matplotlib.use("Agg")
import matplotlib.pyplot as plt
import numpy as np
import pandas as pd
from scipy.signal import savgol_filter
from scipy.stats import kurtosis, skew
from sklearn.experimental import enable_iterative_imputer  # noqa: F401
from sklearn.impute import IterativeImputer, KNNImputer, SimpleImputer
from sklearn.inspection import permutation_importance
from sklearn.linear_model import BayesianRidge, HuberRegressor, LinearRegression, Ridge
from sklearn.ensemble import ExtraTreesRegressor, HistGradientBoostingRegressor, GradientBoostingRegressor
from sklearn.metrics import mean_absolute_error, mean_squared_error, r2_score
from sklearn.mixture import GaussianMixture
from sklearn.model_selection import GroupKFold, KFold, TimeSeriesSplit, cross_validate
from sklearn.pipeline import Pipeline, make_pipeline
from sklearn.preprocessing import PolynomialFeatures, StandardScaler
from statsmodels.stats.stattools import durbin_watson

warnings.filterwarnings("ignore")

plt.rcParams["font.family"] = "Noto Sans CJK JP"
plt.rcParams["font.sans-serif"] = ["Noto Sans CJK JP", "DejaVu Sans"]
plt.rcParams["axes.unicode_minus"] = False
plt.rcParams["figure.dpi"] = 130
plt.rcParams["savefig.dpi"] = 220
plt.rcParams["axes.grid"] = True
plt.rcParams["grid.alpha"] = 0.25

RANDOM_STATE = 42
DT_HOUR = 1 / 6  # 10 min = 1/6 h


def rmse(y_true, y_pred) -> float:
    return float(mean_squared_error(y_true, y_pred) ** 0.5)


def mape_safe(y_true, y_pred, eps: float = 1e-6) -> float:
    y_true = np.asarray(y_true, dtype=float)
    y_pred = np.asarray(y_pred, dtype=float)
    return float(np.mean(np.abs(y_true - y_pred) / np.maximum(np.abs(y_true), eps)) * 100)


def smape(y_true, y_pred, eps: float = 1e-6) -> float:
    y_true = np.asarray(y_true, dtype=float)
    y_pred = np.asarray(y_pred, dtype=float)
    return float(np.mean(2 * np.abs(y_true - y_pred) / (np.abs(y_true) + np.abs(y_pred) + eps)) * 100)


def ensure_dir(path: Path) -> Path:
    path.mkdir(parents=True, exist_ok=True)
    return path


def savefig(path: Path, tight: bool = True) -> None:
    if tight:
        plt.tight_layout()
    plt.savefig(path, bbox_inches="tight")
    plt.close()


def parse_args() -> argparse.Namespace:
    p = argparse.ArgumentParser(description="2026 MCM Problem C modeling pipeline")
    p.add_argument("--zip", dest="zip_path", type=str, default=None, help="附件 ZIP 路径")
    p.add_argument("--data-dir", type=str, default=None, help="已经解压的附件目录")
    p.add_argument("--output-dir", type=str, default="mcm_c_outputs", help="输出目录")
    return p.parse_args()


def normalize_zip_member(name: str) -> str:
    """将竞赛压缩包中的 #Uxxxx 转义恢复为 Unicode,避免平台编码差异。"""
    def repl(m):
        try:
            return chr(int(m.group(1), 16))
        except ValueError:
            return m.group(0)
    return re.sub(r"#U([0-9a-fA-F]{4})", repl, name)


def extract_zip(zip_path: Path, work_dir: Path) -> Path:
    extract_dir = ensure_dir(work_dir / "extracted")
    with zipfile.ZipFile(zip_path, "r") as zf:
        for info in zf.infolist():
            normalized = normalize_zip_member(info.filename)
            # 只提取 xlsx;忽略重复的中文/英文副本时由后续文件发现器处理。
            if not normalized.lower().endswith(".xlsx"):
                continue
            safe_name = Path(normalized).name
            target = extract_dir / safe_name
            with zf.open(info) as src, open(target, "wb") as dst:
                shutil.copyfileobj(src, dst)
    return extract_dir


def find_attachments(data_dir: Path) -> Dict[int, Path]:
    candidates = list(data_dir.rglob("*.xlsx"))
    result: Dict[int, Path] = {}
    for n in range(1, 6):
        # 优先英文 Attachment n 文件,次选文件名含"附件n"或问题n。
        pats = [re.compile(fr"Attachment\s*{n}", re.I), re.compile(fr"附件\s*{n}"), re.compile(fr"问题\s*{n}")]
        matches = [p for p in candidates if any(pt.search(p.name) for pt in pats)]
        if not matches:
            raise FileNotFoundError(f"未找到附件 {n} 的 xlsx 文件")
        matches.sort(key=lambda p: (0 if re.search(fr"Attachment\s*{n}", p.name, re.I) else 1, len(p.name)))
        result[n] = matches[0]
    return result


def describe_series(s: pd.Series) -> Dict[str, float]:
    x = pd.to_numeric(s, errors="coerce").dropna().astype(float)
    return {
        "count": int(x.size),
        "missing": int(s.size - x.size),
        "mean": float(x.mean()),
        "std": float(x.std()),
        "min": float(x.min()),
        "q25": float(x.quantile(0.25)),
        "median": float(x.median()),
        "q75": float(x.quantile(0.75)),
        "max": float(x.max()),
        "skewness": float(skew(x, bias=False)),
        "kurtosis": float(kurtosis(x, fisher=True, bias=False)),
    }


def q1_model(path: Path, fig_dir: Path, table_dir: Path) -> Dict:
    df = pd.read_excel(path)
    time = pd.to_datetime(df.iloc[:, 0])
    A = pd.to_numeric(df.iloc[:, 1], errors="coerce").to_numpy(float)
    B = pd.to_numeric(df.iloc[:, 2], errors="coerce").to_numpy(float)
    X = A.reshape(-1, 1)

    # 数据理解与描述性统计
    desc = {
        "A": describe_series(df.iloc[:, 1]),
        "B": describe_series(df.iloc[:, 2]),
        "pearson": float(np.corrcoef(A, B)[0, 1]),
    }

    # 候选模型:OLS、二次岭回归、Huber、梯度提升
    candidates = {
        "OLS线性回归": LinearRegression(),
        "二次岭回归": Pipeline([
            ("poly", PolynomialFeatures(degree=2, include_bias=False)),
            ("ridge", Ridge(alpha=1e-3)),
        ]),
        "Huber稳健回归": HuberRegressor(epsilon=1.35, max_iter=2000),
        "梯度提升回归": GradientBoostingRegressor(
            n_estimators=120, learning_rate=0.05, max_depth=2,
            loss="huber", random_state=RANDOM_STATE
        ),
    }
    cv = KFold(n_splits=5, shuffle=True, random_state=RANDOM_STATE)
    tscv = TimeSeriesSplit(n_splits=5)
    cv_rows = []
    for name, model in candidates.items():
        for cv_name, splitter in [("随机五折", cv), ("时序五折", tscv)]:
            sc = cross_validate(
                model, X, B, cv=splitter,
                scoring={"mae": "neg_mean_absolute_error", "rmse": "neg_root_mean_squared_error", "r2": "r2"},
                n_jobs=1,
            )
            cv_rows.append({
                "模型": name, "验证方式": cv_name,
                "MAE": float(-sc["test_mae"].mean()),
                "RMSE": float(-sc["test_rmse"].mean()),
                "R2": float(sc["test_r2"].mean()),
            })
    cv_df = pd.DataFrame(cv_rows)
    cv_df.to_csv(table_dir / "q1_cross_validation.csv", index=False, encoding="utf-8-sig")

    final = candidates["Huber稳健回归"].fit(X, B)
    pred = final.predict(X)
    pre_err = A - B
    post_err = pred - B
    metrics = {
        "pre_MAE": float(mean_absolute_error(B, A)),
        "pre_RMSE": rmse(B, A),
        "pre_SMAPE": smape(B, A),
        "post_MAE": float(mean_absolute_error(B, pred)),
        "post_RMSE": rmse(B, pred),
        "post_SMAPE": smape(B, pred),
        "post_R2": float(r2_score(B, pred)),
        "MAE_reduction_pct": float((mean_absolute_error(B, A) - mean_absolute_error(B, pred)) / mean_absolute_error(B, A) * 100),
    }
    five_x = np.array([7.132, 18.526, 84.337, 123.554, 167.667])
    five_y = final.predict(five_x.reshape(-1, 1))
    calibration = pd.DataFrame({"校正前x": five_x, "校正后y": five_y})
    calibration.to_csv(table_dir / "q1_calibration_values.csv", index=False, encoding="utf-8-sig")

    # 图1:时序对比
    plt.figure(figsize=(10.5, 4.8))
    plt.plot(time, A, lw=0.7, label="数据A(光纤位移计)")
    plt.plot(time, B, lw=0.8, label="数据B(基准)")
    plt.xlabel("时间")
    plt.ylabel("位移 / mm")
    plt.title("问题1:校正前两类传感器位移时序对比")
    plt.legend()
    savefig(fig_dir / "q1_01_timeseries.png")

    # 图2:散点与拟合
    order = np.argsort(A)
    plt.figure(figsize=(7.2, 5.6))
    plt.scatter(A[::5], B[::5], s=7, alpha=0.35, label="观测样本")
    plt.plot(A[order], pred[order], lw=2.0, label="Huber稳健校正曲线")
    plt.plot([A.min(), A.max()], [A.min(), A.max()], "--", lw=1.2, label="理想一致线 y=x")
    plt.xlabel("数据A / mm")
    plt.ylabel("数据B / mm")
    plt.title("问题1:稳健仿射校正关系")
    plt.legend()
    savefig(fig_dir / "q1_02_scatter_fit.png")

    # 图3:误差分布
    plt.figure(figsize=(8.4, 5.2))
    bins = np.linspace(np.percentile(np.r_[pre_err, post_err], 0.5), np.percentile(np.r_[pre_err, post_err], 99.5), 70)
    plt.hist(pre_err, bins=bins, alpha=0.55, density=True, label="校正前误差 A-B")
    plt.hist(post_err, bins=bins, alpha=0.65, density=True, label="校正后误差 ŷ-B")
    plt.axvline(0, ls="--", lw=1)
    plt.xlabel("误差 / mm")
    plt.ylabel("概率密度")
    plt.title("问题1:校正前后误差分布")
    plt.legend()
    savefig(fig_dir / "q1_03_error_distribution.png")

    # 图4:交叉验证比较
    p = cv_df[cv_df["验证方式"] == "随机五折"].copy()
    x = np.arange(len(p))
    plt.figure(figsize=(8.2, 4.8))
    plt.bar(x - 0.18, p["MAE"], width=0.36, label="MAE")
    plt.bar(x + 0.18, p["RMSE"], width=0.36, label="RMSE")
    plt.xticks(x, p["模型"], rotation=18)
    plt.ylabel("误差 / mm")
    plt.title("问题1:候选校正模型五折交叉验证")
    plt.legend()
    savefig(fig_dir / "q1_04_cv_compare.png")

    return {
        "description": desc,
        "equation": {"intercept": float(final.intercept_), "slope": float(final.coef_[0]), "scale": float(final.scale_)},
        "metrics": metrics,
        "five_values": [{"x": float(x), "y": float(y)} for x, y in zip(five_x, five_y)],
        "cv": cv_rows,
    }


def _piecewise_prefix_cache(y: np.ndarray, max_degree: int = 3):
    n = len(y)
    t = np.linspace(0, 1, n)
    cache = {}
    for d in range(1, max_degree + 1):
        X = np.vstack([t ** k for k in range(d + 1)]).T
        pxx = np.zeros((n + 1, d + 1, d + 1))
        pxy = np.zeros((n + 1, d + 1))
        pyy = np.zeros(n + 1)
        for i in range(d + 1):
            for j in range(d + 1):
                pxx[1:, i, j] = np.cumsum(X[:, i] * X[:, j])
            pxy[1:, i] = np.cumsum(X[:, i] * y)
        pyy[1:] = np.cumsum(y * y)
        cache[d] = (pxx, pxy, pyy)
    return cache


def _segment_sse(cache, a: int, b: int, d: int) -> float:
    pxx, pxy, pyy = cache[d]
    A = pxx[b] - pxx[a]
    z = pxy[b] - pxy[a]
    yy = pyy[b] - pyy[a]
    beta = np.linalg.lstsq(A, z, rcond=None)[0]
    return max(0.0, float(yy - z @ beta))


def detect_q2_breakpoints(y: np.ndarray) -> Tuple[int, int, pd.DataFrame]:
    smooth = savgol_filter(y, 101, 3)
    cache = _piecewise_prefix_cache(smooth, 3)
    # 先粗搜,再细搜;模型结构为二次---二次---一次,符合三阶段速度阶次变化。
    best = (float("inf"), (8000, 9600))
    records = []
    for c1 in range(7300, 8501, 40):
        s1 = _segment_sse(cache, 0, c1, 2)
        for c2 in range(9200, 9851, 20):
            if c2 - c1 < 500:
                continue
            val = s1 + _segment_sse(cache, c1, c2, 2) + _segment_sse(cache, c2, len(y), 1)
            records.append((c1, c2, val))
            if val < best[0]:
                best = (val, (c1, c2))
    c10, c20 = best[1]
    fine = (float("inf"), best[1])
    for c1 in range(max(500, c10 - 50), min(len(y) - 700, c10 + 51)):
        s1 = _segment_sse(cache, 0, c1, 2)
        for c2 in range(max(c1 + 500, c20 - 50), min(len(y) - 100, c20 + 51)):
            val = s1 + _segment_sse(cache, c1, c2, 2) + _segment_sse(cache, c2, len(y), 1)
            if val < fine[0]:
                fine = (val, (c1, c2))
    loss_df = pd.DataFrame(records, columns=["c1", "c2", "SSE"])
    return fine[1][0], fine[1][1], loss_df


def q2_model(path: Path, fig_dir: Path, table_dir: Path) -> Dict:
    df = pd.read_excel(path)
    serial = pd.to_numeric(df.iloc[:, 0], errors="coerce").to_numpy(int)
    y = pd.to_numeric(df.iloc[:, 1], errors="coerce").to_numpy(float)
    smooth = savgol_filter(y, 101, 3)
    velocity = savgol_filter(y, 101, 3, deriv=1, delta=DT_HOUR)
    acceleration = savgol_filter(y, 301, 3, deriv=2, delta=DT_HOUR)
    c1, c2, loss_df = detect_q2_breakpoints(y)
    loss_df.to_csv(table_dir / "q2_breakpoint_loss.csv", index=False, encoding="utf-8-sig")

    bounds = [0, c1, c2, len(y)]
    degrees = [2, 3, 3]
    stage_rows = []
    stage_models = []
    for k, (a, b, deg) in enumerate(zip(bounds[:-1], bounds[1:], degrees), 1):
        t = np.arange(b - a) * DT_HOUR
        coef = np.polyfit(t, smooth[a:b], deg)
        pred = np.polyval(coef, t)
        raw = y[a:b]
        duration = (b - a - 1) * DT_HOUR
        avg_v = float((raw[-1] - raw[0]) / duration) if duration > 0 else 0.0
        row = {
            "阶段": k,
            "起始编号": a + 1,
            "结束编号": b,
            "持续时间_h": duration,
            "平均速度_mm_h": avg_v,
            "多项式阶数": deg,
            "MAE": float(mean_absolute_error(raw, pred)),
            "RMSE": rmse(raw, pred),
            "R2": float(r2_score(raw, pred)),
            "Durbin_Watson": float(durbin_watson(raw - pred)),
            "系数_高次到常数": json.dumps([float(x) for x in coef], ensure_ascii=False),
        }
        stage_rows.append(row)
        stage_models.append((a, b, deg, coef, pred))
    pd.DataFrame(stage_rows).to_csv(table_dir / "q2_stage_models.csv", index=False, encoding="utf-8-sig")

    start = datetime(2024, 5, 4, 0, 0)
    node_times = [start + timedelta(minutes=10 * c1), start + timedelta(minutes=10 * c2)]

    # 图:位移与节点
    t_hours = np.arange(len(y)) * DT_HOUR
    plt.figure(figsize=(10.5, 5.2))
    plt.plot(t_hours, y, lw=0.55, alpha=0.5, label="原始位移")
    plt.plot(t_hours, smooth, lw=1.6, label="Savitzky-Golay稳健趋势")
    for cp, lab in [(c1, "缓慢→加速"), (c2, "加速→快速")]:
        plt.axvline(cp * DT_HOUR, ls="--", lw=1.5, label=f"{lab}(编号{cp+1})")
    plt.xlabel("累计时间 / h")
    plt.ylabel("表面位移 / mm")
    plt.title("问题2:三段式形变节点识别")
    plt.legend()
    savefig(fig_dir / "q2_01_breakpoints.png")

    # 速度
    plt.figure(figsize=(10.5, 4.8))
    plt.plot(t_hours, velocity, lw=1.0)
    for cp in [c1, c2]:
        plt.axvline(cp * DT_HOUR, ls="--", lw=1.3)
    plt.xlabel("累计时间 / h")
    plt.ylabel("位移速度 / (mm·h⁻¹)")
    plt.title("问题2:平滑位移速度及其持续性变化")
    savefig(fig_dir / "q2_02_velocity.png")

    # 加速度
    plt.figure(figsize=(10.5, 4.8))
    plt.plot(t_hours, acceleration, lw=1.0)
    for cp in [c1, c2]:
        plt.axvline(cp * DT_HOUR, ls="--", lw=1.3)
    plt.xlabel("累计时间 / h")
    plt.ylabel("位移加速度 / (mm·h⁻²)")
    plt.title("问题2:形变加速度与阶段跃迁证据")
    savefig(fig_dir / "q2_03_acceleration.png")

    # 分阶段拟合
    plt.figure(figsize=(10.5, 5.2))
    plt.scatter(t_hours[::8], y[::8], s=5, alpha=0.25, label="原始观测")
    labels = ["阶段Ⅰ二次模型", "阶段Ⅱ三次模型", "阶段Ⅲ三次模型"]
    for (a, b, deg, coef, pred), lab in zip(stage_models, labels):
        plt.plot(t_hours[a:b], pred, lw=2.0, label=lab)
    for cp in [c1, c2]:
        plt.axvline(cp * DT_HOUR, ls="--", lw=1)
    plt.xlabel("累计时间 / h")
    plt.ylabel("位移 / mm")
    plt.title("问题2:分阶段动力学回归拟合")
    plt.legend()
    savefig(fig_dir / "q2_04_stage_fits.png")

    # 损失面
    pivot = loss_df.pivot_table(index="c1", columns="c2", values="SSE")
    plt.figure(figsize=(8.2, 5.8))
    plt.imshow(np.log10(pivot.values + 1), aspect="auto", origin="lower",
               extent=[pivot.columns.min(), pivot.columns.max(), pivot.index.min(), pivot.index.max()])
    plt.colorbar(label="log10(SSE+1)")
    plt.scatter([c2], [c1], marker="x", s=80, label="最优节点组合")
    plt.xlabel("第二转换节点编号")
    plt.ylabel("第一转换节点编号")
    plt.title("问题2:分段回归损失面")
    plt.legend()
    savefig(fig_dir / "q2_05_loss_surface.png")

    # 瞬时跳变判据的数值阈值:局部MAD + 持续性
    residual = y - smooth
    mad = 1.4826 * np.median(np.abs(residual - np.median(residual)))
    jump_threshold = 4.5 * mad
    return {
        "description": describe_series(df.iloc[:, 1]),
        "breakpoints": [
            {"cut_index": c1, "next_serial": c1 + 1, "time": node_times[0].isoformat(sep=" ")},
            {"cut_index": c2, "next_serial": c2 + 1, "time": node_times[1].isoformat(sep=" ")},
        ],
        "instant_jump_threshold_mm": float(jump_threshold),
        "persistence_rule": "速度与加速度方向一致,至少连续18个采样点(3 h)越过阶段阈值;孤立峰值且随后回归基线判为扰动。",
        "stages": stage_rows,
    }


def q3_impute_validate(raw: pd.DataFrame) -> pd.DataFrame:
    D = raw.to_numpy(float)
    rng = np.random.default_rng(RANDOM_STATE)
    observed = np.argwhere(~np.isnan(D))
    selected = observed[rng.choice(len(observed), size=int(0.05 * len(observed)), replace=False)]
    masked = D.copy()
    truth = []
    for i, j in selected:
        truth.append(masked[i, j])
        masked[i, j] = np.nan
    truth = np.asarray(truth)
    methods = {
        "中位数插补": SimpleImputer(strategy="median"),
        "KNN插补": KNNImputer(n_neighbors=7, weights="distance"),
        "链式贝叶斯插补": IterativeImputer(
            estimator=BayesianRidge(), max_iter=12, random_state=RANDOM_STATE,
            initial_strategy="median", skip_complete=False
        ),
    }
    rows = []
    for name, imp in methods.items():
        completed = imp.fit_transform(masked)
        pred = np.asarray([completed[i, j] for i, j in selected])
        rows.append({"方法": name, "总体MAE": mean_absolute_error(truth, pred), "总体RMSE": rmse(truth, pred)})
        for j, col in enumerate(raw.columns):
            idx = np.where(selected[:, 1] == j)[0]
            scale = np.nanstd(D[:, j]) + 1e-12
            rows.append({
                "方法": f"{name}-{col}",
                "总体MAE": mean_absolute_error(truth[idx], pred[idx]),
                "总体RMSE": rmse(truth[idx], pred[idx]) / scale,
            })
    return pd.DataFrame(rows)
全部论文请见下方" 只会建模 QQ名片" 点击QQ名片即可
相关推荐
hold?fish:palm2 小时前
20 旋转图像
c++·算法·leetcode
NoteStream2 小时前
【C语言基础】分支和循环(上)
c语言·开发语言·c++·经验分享·笔记·算法·c#
白狐_7982 小时前
408数据结构第7章:查找②——顺序、折半、分块查找秒杀 + 真题
数据结构·算法
卷心菜的学习路3 小时前
基于多模态向量检索的数学相似题推荐系统:完整设计、算法与实验
java·python·算法·大模型·推荐算法·数学相似题
小南知更鸟3 小时前
【leetcodehot100】leetcode-hot100-20260809更新
算法·leetcode·职场和发展
数模竞赛Paid answer3 小时前
2021年深圳杯数学建模A题火星探测器着陆控制方案解题全过程论文及程序
算法·数学建模·深圳杯
hold?fish:palm3 小时前
19 螺旋矩阵
线性代数·算法·矩阵
码完就睡4 小时前
数据结构——树、二叉树基础概念
数据结构·算法
VL——MOESR5 小时前
【LuoguP1967】货车运输【生成树】【倍增】
c++·算法·题解·倍增·生成树