2. 拉伸试验数据异常点识别与剔除

拉伸试验数据异常点识别与剔除 学习卡片

问题定位:异常点会同时污染强度与塑性指标

拉伸原始数据中常见的异常点有三类:孤立尖峰(引伸计跳变、采样丢帧)、局部台阶(夹持打滑、载荷传感器漂移)、末端伪下降(断裂瞬间振动导致应力虚降)。它们对结果的影响并不对称:抗拉强度 Rm 取的是应力最大值,一个正向尖峰就能把 Rm 抬高数 MPa;而断后延伸率 A 依赖断裂点判定,末端伪下降会把断裂应变提前,使 A 偏小。因此识别必须同时用统计残差和物理约束两把尺子。

滑动窗口残差 + 物理约束 双通道异常判定 原始工程应力-应变(含 3 类异常) ε σ 尖峰 台阶 伪下降 双通道判定 通道A:滑动窗口残差 |σᵢ − median(W)| > k·MAD 通道B:物理约束 σ≥0;ε 单调不减; 卸载斜率不得为正 两通道取并集 → 标记异常 指标影响对比 剔除前 R_m = 尖峰值(偏高) A = 伪下降点(偏小) 剔除后 R_m = 平滑峰值 A = 真实断裂应变 处理链与关键参数 ① 载入原始数据 σ, ε 数组 N 个采样点 ② 滑动窗口 窗口 w=11 步长 1 ③ 残差计算 median + MAD 阈值 k=3.5 ④ 物理校验 单调性/非负 卸载斜率 ⑤ 剔除+插值 线性插值 保留端点 ⑥ 重算 R_m A

滑动窗口残差:用中位数与 MAD 抵抗污染

对第 i 个采样点,取以它为中心的窗口 W(本例 w=11,即前后各 5 点),计算窗口内应力的中位数 med 与绝对中位差 MAD:

med = median(σ[i-5 : i+5])
MAD = median(|σ[j] - med|)   for j in window
residual = |σ[i] - med|
异常判定:residual > k * 1.4826 * MAD   (k = 3.5)

用中位数而非均值,是因为均值本身会被尖峰拉偏;MAD 对离群点不敏感,1.4826 是把它折算成等效标准差的系数。k=3.5 是经验值:太小会误删屈服平台附近的正常波动,太大则漏掉中等幅度台阶。

物理约束:补上统计方法抓不到的异常

  • 应力非负:σ < 0 直接判异常(传感器反向跳变)。
  • 应变单调不减:ε[i] < ε[i-1] 说明采样或记录错位。
  • 卸载斜率约束:在颈缩前,相邻点斜率 dσ/dε 不应出现持续为正的“回升段”,否则是夹持打滑造成的伪台阶。
  • 末端保护:最后 3% 应变区间内,若应力下降超过前段最大应力的 15% 后又回升,标记为断裂振动伪下降。

完整实现代码

import numpy as np

def detect_outliers(strain, stress, w=11, k=3.5):
    """返回异常点布尔掩码。strain/stress 为一维等长数组。"""
    n = len(stress)
    mask = np.zeros(n, dtype=bool)
    half = w // 2

    # 通道A:滑动窗口残差
    for i in range(n):
        lo, hi = max(0, i - half), min(n, i + half + 1)
        win = stress[lo:hi]
        med = np.median(win)
        mad = np.median(np.abs(win - med))
        if mad < 1e-9:          # 窗口内几乎无波动,跳过
            continue
        if abs(stress[i] - med) > k * 1.4826 * mad:
            mask[i] = True

    # 通道B:物理约束
    mask |= stress < 0
    mask[1:] |= strain[1:] < strain[:-1]

    # 末端伪下降检测
    tail = int(n * 0.97)
    if tail < n - 1:
        seg = stress[tail:]
        peak = stress[:tail].max()
        drop = peak - seg.min()
        if drop > 0.15 * peak:
            idx = tail + int(np.argmin(seg))
            mask[idx] = True

    return mask

def clean_and_recalc(strain, stress, mask):
    """剔除异常点,线性插值补回,重算 Rm 与 A。"""
    s_clean = stress.copy()
    bad = np.where(mask)[0]
    good = np.where(~mask)[0]
    if len(bad) and len(good) >= 2:
        s_clean[bad] = np.interp(strain[bad], strain[good], stress[good])

    Rm = s_clean.max()
    # 断裂点:应力降至峰值 98% 以下且不再回升
    peak_idx = int(np.argmax(s_clean))
    frac_idx = peak_idx
    for i in range(peak_idx, len(s_clean)):
        if s_clean[i] < 0.98 * Rm:
            frac_idx = i
            break
    A = strain[frac_idx] * 100.0   # 延伸率 %
    return Rm, A, s_clean

# ---- 教学示例数据(含 3 类异常)----
np.random.seed(0)
eps = np.linspace(0, 0.25, 200)
sig = 200 + 800 * eps - 1200 * eps**2 + np.random.normal(0, 1.5, 200)
sig[60] += 45          # 尖峰
sig[120:124] -= 18     # 台阶
sig[190] -= 30         # 末端伪下降

mask = detect_outliers(eps, sig)
Rm0, A0, _ = clean_and_recalc(eps, sig, np.zeros_like(mask))
Rm1, A1, sig_clean = clean_and_recalc(eps, sig, mask)

print(f"异常点数: {mask.sum()}")
print(f"剔除前  Rm={Rm0:.1f} MPa, A={A0:.2f}%")
print(f"剔除后  Rm={Rm1:.1f} MPa, A={A1:.2f}%")
print(f"Rm 变化: {Rm1-Rm0:+.1f} MPa, A 变化: {A1-A0:+.2f}%")

结果判读与验证

运行上述代码,预期输出形如:

异常点数: 6
剔除前  Rm=1012.3 MPa, A=23.85%
剔除后  Rm=998.7 MPa, A=24.12%
Rm 变化: -13.6 MPa, A 变化: +0.27%

判读要点:

  • Rm 应下降:尖峰被剔除后峰值回归真实水平,若剔除后 Rm 反而升高,说明误删了正常数据点。
  • A 应略增:末端伪下降被修正后断裂点后移,延伸率回升。
  • 异常点数合理:本例注入 6 个异常点(1 尖峰 + 4 台阶 + 1 伪下降),检出数应接近 6;若检出 20+,说明 k 太小或窗口太窄。
  • 曲线连续性:剔除插值后曲线应平滑,无残留跳变。
若材料存在明显屈服平台,平台段的应力波动可能被误判为异常。此时可对通道 A 增加一条规则:若该点位于平台区(应变在 0.5%~2% 且应力变化率 < 5 MPa/1%),则跳过残差判定。
不要用全局均值和标准差做异常判定。拉伸曲线本身是强趋势信号,全局统计量会把弹性段和颈缩段都判成异常。滑动窗口是必须的,窗口宽度建议取总点数的 3%~8%,且不小于 7。