ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Python异常值检测实战:从数学建模到数据科学的核心技能

Python异常值检测实战:从数学建模到数据科学的核心技能 1. 项目缘起从一道国赛真题说起几年前我第一次带队参加全国大学生数学建模竞赛CUMCM碰上的就是2011年的A题。题目是关于城市表层土壤重金属污染的分析数据给出来是一堆采样点的经纬度和八种重金属元素的浓度。乍一看数据规整任务明确无非是画图、插值、评价。但真上手处理时一个看似简单却足以颠覆结论的问题就摆在了面前那些浓度高得离谱的采样点到底是真的“污染源”还是仪器故障、记录错误产生的“异常值”如果直接拿原始数据去做空间插值画出来的污染分布图可能会因为一两个异常高的点导致整个区域“一片红”严重误导污染源定位和风险评估。这就是异常值处理的威力——它不是一个可做可不做的“数据美容”步骤而是决定你模型结论是否可靠、论文能否拿奖的生死线。很多新手包括当年的我最容易犯的错误就是直奔主题套上算法就开始跑。用Pandas读入数据用Matplotlib画个箱线图看到几个点落在“胡须”外面鼠标一点直接删除然后长舒一口气觉得大功告成。这种做法非常危险。你删掉的可能恰恰是揭示关键污染事件如非法倾倒的宝贵证据而你留下的也可能隐藏着其他类型的异常。处理异常值核心不是“剔除”而是“诊断”与“决策”。它要求我们结合问题背景、数据特性和分析方法做出有依据的判断。今天我就以这道经典的国赛题为背景抛开那些花哨的算法库用最基础的Python数据分析栈NumPy, Pandas, Matplotlib, SciPy带你走一遍异常值处理的完整逻辑链条。你会发现真正有用的技术往往藏在那些对数据和业务的理解里。2. 理解你的数据异常值的“病理学”分类在动手写任何代码之前我们必须先搞清楚我们要对付的“异常值”到底是什么。在统计学和数据科学中异常值通常分为三类处理方式也截然不同。第一类点异常Point Anomalies。这是最常见的一种指某个单一的观测值明显偏离数据集中其他大部分观测值。在我们这个土壤重金属数据集中可能就是某个采样点的砷As浓度达到了背景值的几百倍。这类异常最直观也最容易用统计方法检测。第二类上下文异常Contextual Anomalies。这种异常值在全局看可能不奇怪但在特定上下文时间、空间、分组下就显得异常。比如在工业区铜浓度高是正常的但如果在同一个工业区内某个点的铜浓度比其他点突然高出一个数量级这就是上下文异常。对于CUMCM 2011 A题我们需要特别注意空间上下文。一个在郊区的采样点出现极高浓度远比在工业区出现同样值更值得警惕。第三类集体异常Collective Anomalies。单个数据点看起来正常但一组数据点联合起来看就表现出异常模式。例如一片连续区域的所有采样点某种重金属浓度都处于“正常范围”的上限虽然单个点未触发警报但整体空间聚集性暗示了面源污染的可能。这类异常是空间分析的重点但简单的单变量异常检测方法会失效。对于数学建模竞赛我们面对的数据通常是“干净”的竞赛数据第一类点异常是主要矛盾。但具备识别后两类的意识能在论文中体现你对问题的深度思考是加分项。我们的处理流程将主要围绕检测和处置第一类异常值展开同时兼顾空间属性的考量。3. 数据初窥与描述性统计建立“正常”的基准拿到数据假设文件为soil_heavy_metals.csv别急着找异常。先全面认识它建立数据的“健康档案”。import pandas as pd import numpy as np import matplotlib.pyplot as plt import seaborn as sns from scipy import stats # 设置中文显示和美观的样式 plt.rcParams[font.sans-serif] [SimHei, DejaVu Sans] plt.rcParams[axes.unicode_minus] False sns.set_style(whitegrid) # 加载数据 df pd.read_csv(soil_heavy_metals.csv) print(数据形状:, df.shape) print(\n前5行数据:) print(df.head()) print(\n数据基本信息:) print(df.info()) print(\n描述性统计重点关注数值型列:) print(df.describe())运行这段代码你会得到行数、列数、数据类型、缺失值情况以及最重要的——每个重金属浓度字段的统计摘要均值、标准差、最小值、25%分位数、中位数、75%分位数、最大值。中位数和四分位距IQR是我们后续判断异常值的基石。比如你看到砷As的均值是25.3 mg/kg但最大值却达到了1200 mg/kg而75%分位数只有45.2 mg/kg。这个1200显然非常扎眼但它是否一定是错误还需要结合其他信息。注意df.describe()默认只给出数值列的统计。如果你的数据包含经纬度它们也会被计算进来这通常是合理的因为我们需要检查坐标是否有明显错误比如纬度大于90。接下来可视化是必须的。直方图配合密度曲线能让你直观感受数据的分布形态。# 假设我们关心的重金属元素列为As, Cd, Cr, Cu, Hg, Ni, Pb, Zn heavy_metals [As, Cd, Cr, Cu, Hg, Ni, Pb, Zn] fig, axes plt.subplots(2, 4, figsize(16, 8)) axes axes.ravel() # 将二维坐标轴数组展平为一维 for idx, col in enumerate(heavy_metals): ax axes[idx] # 绘制直方图 ax.hist(df[col].dropna(), bins30, edgecolorblack, alpha0.7, densityTrue) # 绘制核密度估计曲线 df[col].dropna().plot(kindkde, axax, colorred, linewidth2) ax.set_title(f{col}浓度分布) ax.set_xlabel(浓度 (mg/kg)) ax.set_ylabel(密度) plt.tight_layout() plt.show()这个图能立刻告诉你数据是近似正态分布还是严重右偏有长尾。重金属浓度数据通常服从对数正态分布或严重右偏因为污染往往导致少数点浓度极高。如果你看到某个元素的分布出现诡异的双峰或多峰这可能暗示数据来源于两个不同的总体例如背景区域和污染区域混合此时用全局的统一标准去判断异常值就需要格外小心。4. 单变量异常检测从3σ准则到箱线图建立了初步印象我们开始正式的异常值检测。对于单变量即逐个元素分析最经典的方法是标准差法和箱线图法。4.1 3σ准则拉依达准则这个方法假设数据服从正态分布。对于正态分布约有99.73%的数据落在均值上下3个标准差的范围内。因此可以将超出此范围的数据视为异常值。def detect_outliers_3sigma(data_series, n_sigmas3): 使用3σ准则检测异常值。 参数: data_series: pandas Series待检测的数据列。 n_sigmas: 整数标准差倍数默认为3。 返回: outlier_mask: 布尔型SeriesTrue表示对应位置为异常值。 if data_series.isnull().all(): return pd.Series(False, indexdata_series.index) mean_val data_series.mean() std_val data_series.std() lower_bound mean_val - n_sigmas * std_val upper_bound mean_val n_sigmas * std_val # 异常值判断小于下限或大于上限 outlier_mask (data_series lower_bound) | (data_series upper_bound) return outlier_mask # 对每种重金属应用3σ准则 outliers_3sigma {} for metal in heavy_metals: outliers_3sigma[metal] detect_outliers_3sigma(df[metal]) print(f{metal}: 检测到 {outliers_3sigma[metal].sum()} 个异常值3σ准则)但这里有个大坑3σ准则对异常值本身非常敏感。因为均值和标准差都会受到极端值的影响。如果一个数据集中存在一个巨大的异常值它会拉高均值同时极大地膨胀标准差导致真正的异常值可能逃过检测因为上下界被撑宽了。因此在数据明显偏态或已知存在极端值时慎用3σ准则。更稳健的方法是使用箱线图法。4.2 箱线图Boxplot与IQR法则箱线图基于四分位数对极端值不敏感是更稳健的异常值检测工具。其原理是Q1第一四分位数25%分位数Q3第三四分位数75%分位数IQR四分位距 Q3 - Q1正常值范围[Q1 - 1.5 * IQR, Q3 1.5 * IQR]温和异常值Mild Outlier位于[Q1 - 1.5*IQR, Q1 - 3*IQR)或(Q3 1.5*IQR, Q3 3*IQR]区间通常箱线图用点表示极端异常值Extreme Outlier超出[Q1 - 3 * IQR, Q3 3 * IQR]范围箱线图也用点表示可能形状不同在实践和多数数学建模场景中我们通常将超出[Q1 - 1.5*IQR, Q3 1.5*IQR]范围的点都视为需要关注的异常值。def detect_outliers_iqr(data_series, k1.5): 使用IQR法则检测异常值。 参数: data_series: pandas Series待检测的数据列。 k: 浮点数IQR的倍数默认为1.5箱线图常用值。 返回: outlier_mask: 布尔型SeriesTrue表示对应位置为异常值。 Q1 data_series.quantile(0.25) Q3 data_series.quantile(0.75) IQR Q3 - Q1 lower_bound Q1 - k * IQR upper_bound Q3 k * IQR outlier_mask (data_series lower_bound) | (data_series upper_bound) return outlier_mask # 对每种重金属应用IQR法则 outliers_iqr {} for metal in heavy_metals: outliers_iqr[metal] detect_outliers_iqr(df[metal]) print(f{metal}: 检测到 {outliers_iqr[metal].sum()} 个异常值IQR, k1.5) # 可视化箱线图 plt.figure(figsize(12, 6)) # 使用DataFrame直接绘制pandas会调用matplotlib df[heavy_metals].plot(kindbox, vertTrue, patch_artistTrue) plt.title(重金属浓度箱线图检测异常值) plt.ylabel(浓度 (mg/kg)) # 由于浓度可能差异大考虑使用对数坐标以便观察 # plt.yscale(log) plt.xticks(rotation45) plt.tight_layout() plt.show()箱线图能一目了然地展示每个变量的中位数、四分位范围以及那些落在“胡须”之外的异常点。对比3σ和IQR的结果你通常会发现IQR法检测出的异常值更多因为它对偏态分布更敏感且不受极端值影响。5. 多变量与空间上下文更高级的异常洞察单变量检测是基础但割裂了变量间的联系和样本的空间属性。在CUMCM A题中我们需要做得更好。5.1 基于马氏距离的多元异常检测马氏距离考虑了变量之间的相关性。一个点在多维空间中的马氏距离衡量的是它相对于数据分布中心的距离同时考虑了各维度的方差和协方差。距离越大越可能是异常值。from scipy.spatial.distance import mahalanobis from scipy.linalg import inv def mahalanobis_distance(df, columns): 计算数据集中每个样本点到中心点的马氏距离。 注意此函数为演示原理实际计算可能需处理协方差矩阵奇异问题。 data df[columns].dropna().values # 去除缺失值简化处理 # 计算均值向量和协方差矩阵 mean_vec np.mean(data, axis0) cov_mat np.cov(data, rowvarFalse) # 防止协方差矩阵奇异添加一个很小的正则项 try: inv_cov_mat inv(cov_mat) except np.linalg.LinAlgError: # 如果矩阵奇异使用伪逆或添加正则化 cov_mat_reg cov_mat np.eye(cov_mat.shape[0]) * 1e-6 inv_cov_mat inv(cov_mat_reg) # 计算每个样本的马氏距离 mahal_dist [] for i in range(data.shape[0]): diff data[i] - mean_vec dist np.sqrt(np.dot(np.dot(diff, inv_cov_mat), diff.T)) mahal_dist.append(dist) return np.array(mahal_dist) # 计算八种重金属的马氏距离 mahal_dists mahalanobis_distance(df, heavy_metals) # 将距离添加到DataFrame中注意与原始数据行索引对齐已去除缺失值行 # 这里简化处理假设数据无缺失。实际情况需要处理索引对齐。 df_non_missing df[heavy_metals].dropna() df_non_missing[Mahalanobis_Distance] mahal_dists # 通常将马氏距离的平方近似服从卡方分布作为判断依据 # 自由度为变量个数k选择显著性水平如0.975分位数作为阈值 import scipy.stats as stats k len(heavy_metals) threshold stats.chi2.ppf(0.975, dfk) # 卡方分布0.975分位数 print(f马氏距离平方的卡方阈值 (df{k}, p0.975): {threshold:.2f}) # 标记多元异常值 df_non_missing[Multivariate_Outlier] df_non_missing[Mahalanobis_Distance]**2 threshold print(f基于马氏距离检测到 {df_non_missing[Multivariate_Outlier].sum()} 个多元异常值)这个方法能找出在多个变量上组合异常的样本。比如一个点每种重金属浓度都不算单变量异常但它们的组合比例非常奇怪如砷极高而铅极低也可能被标记为异常。5.2 结合空间位置的异常分析对于地理数据空间自相关性是重要特性。一个点的值很可能与其邻近点的值相似。我们可以利用这一点来发现空间上下文异常。一种简单有效的方法是计算每个点的值与它周围一定距离内邻居点值的差异。from sklearn.neighbors import BallTree import numpy as np # 假设df中包含Lon经度和Lat纬度列 coords df[[Lon, Lat]].values # 使用BallTree进行高效的空间邻居查找 tree BallTree(coords, leaf_size15, metrichaversine) # 注意haversine计算球面距离单位是弧度 # 为简化我们使用欧氏距离适用于小范围平面近似 tree_euclidean BallTree(coords, leaf_size15, metriceuclidean) # 为每个点查找固定半径内的邻居例如半径R1公里需要根据数据实际单位调整 # 这里假设坐标单位是度1度约111公里我们取0.01度约1.1公里作为搜索半径 radius_km 1.0 radius_deg radius_km / 111.0 # 非常粗略的换算仅用于演示 spatial_anomaly_flags [] for metal in heavy_metals: anomaly_flags np.zeros(len(df), dtypebool) for i, (coord, value) in enumerate(zip(coords, df[metal].values)): # 查找半径内的邻居索引不包括自身 indices tree_euclidean.query_radius([coord], rradius_deg)[0] indices indices[indices ! i] # 排除自身 if len(indices) 0: neighbor_values df.iloc[indices][metal].values # 简单规则如果该点值超过邻居平均值的3倍标准差则视为空间异常 neighbor_mean neighbor_values.mean() neighbor_std neighbor_values.std() if neighbor_std 0: # 避免除零 z_score_local (value - neighbor_mean) / neighbor_std if abs(z_score_local) 3: anomaly_flags[i] True spatial_anomaly_flags.append(anomaly_flags) print(f{metal}: 基于局部空间分析检测到 {anomaly_flags.sum()} 个潜在异常点) # 可以将结果汇总例如一个点在任意元素上被标记为空间异常则视为空间异常点 df[Spatial_Anomaly] np.any(np.column_stack(spatial_anomaly_flags), axis1)这个方法能有效识别出“孤立的污染点”。例如在一片浓度普遍较低的区域突然出现一个浓度极高的点它就会被标记出来。这比全局的单变量检测更有说服力。6. 异常值的诊断与处置策略不是一删了之检测出异常值只是第一步更重要的是诊断其产生原因并决定如何处理。这是一个需要结合领域知识这里是环境科学和数据分析的决策过程。6.1 诊断为什么这个点异常数据错误检查原始记录。可能是单位错误如将µg/kg记成mg/kg、小数点错位、数据录入错误。这是最理想的状况可以直接修正。测量误差仪器故障、采样污染、实验室分析误差。如果可能应追溯原始记录或进行复测。在竞赛中我们通常假设数据是准确的但可以在论文中提出这种可能性作为分析局限性。真实但特殊的现象点源污染工厂排污口、垃圾填埋场渗漏点。这正是研究要寻找的这类异常值包含关键信息绝不能简单删除。应该保留并在后续建模中作为特殊对象处理例如在空间插值时采用抗差方法或单独标识。自然高背景值地质原因导致某区域本底值就高。需要查阅文献了解该区域的地球化学背景。如果确认则该点不属于“污染”异常而是“地质”异常处理方式也不同。样本混淆采样点定位错误实际采自污染源。这属于数据质量问题但难以验证。对于CUMCM A题我们应首先假设异常值可能是真实的污染信号。可以通过以下方法辅助判断可视化将检测出的异常点在地图上用显著颜色标出观察其空间分布。是否聚集在工业区、河流沿岸如果是则很可能是真实污染。多变量关联查看被标记为异常的点在其他重金属元素上是否也异常如果多种元素同时异常且具有特定的比例关系如Cu、Zn、Pb常伴生则更可能是真实的工业污染。对比背景值查找国家《土壤环境质量标准》或当地土壤元素背景值。如果异常值远超背景值范围尤其是警戒线则污染可能性大。6.2 处置五大策略及其应用场景处置策略具体方法适用场景在CUMCM A题中的考量保留不进行任何处理在后续分析中使用稳健模型。异常值是真实且有价值的信息如污染源。首选策略。对于空间分析异常点可能就是污染热点删除会掩盖问题。应在论文中说明哪些点被识别为异常并解释其可能的环境意义。修正根据可追溯的信息纠正错误值。确认为数据录入或单位错误。竞赛数据通常已校正此情况较少。但可提及若在实际工作中发现明显错误如浓度值为负应予以修正。删除将异常样本从数据集中移除。确认为无意义的测量误差且该样本量很小删除不影响整体分布。慎用。仅在极端情况下如某个点的所有元素值都与其他点格格不入且无法用任何地理或环境因素解释可考虑删除。必须在论文中详细说明删除的理由和数量。替换用其他值替代异常值。既不能删除样本重要又确认是误差。在时间序列分析中常用。在空间数据中可考虑用空间插值得到的估计值替换需谨慎会平滑掉真实热点或用局部邻域的中位数/均值替换。转换对数据进行数学变换缩小异常值的影响。数据严重偏态且分析模型对异常值敏感如基于最小二乘的回归。强烈推荐。对于重金属浓度这种通常右偏的数据进行对数变换log1p是标准操作。这能使分布更接近正态减弱极端值的影响同时保留所有数据点。变换后许多统计方法和模型如克里金插值的效果会更好。6.3 针对本题的实操决策流程建议数据变换先行对所有重金属浓度数据df[heavy_metals]进行log1p变换即log(1 x)防止x0时无定义。将变换后的数据用于后续的统计分析、建模和绘图。这能有效缓解异常值的影响而不丢失信息。df_log df.copy() df_log[heavy_metals] np.log1p(df[heavy_metals])异常点标识与分类综合运用IQR法则在原始尺度或对数尺度上和空间局部Z值法识别出异常点。为每个点打上标签如单变量异常、多元异常、空间异常。空间可视化诊断制作带异常点标记的散点图或地图。plt.figure(figsize(10, 8)) scatter plt.scatter(df[Lon], df[Lat], cdf_log[As], cmapviridis, s20, alpha0.7) # 标记被IQR法检测为异常的点 as_outliers df[outliers_iqr[As]] plt.scatter(as_outliers[Lon], as_outliers[Lat], colorred, s100, edgecolorsblack, markero, labelAs异常点(IQR)) plt.colorbar(scatter, labelLog(As1)浓度) plt.xlabel(经度) plt.ylabel(纬度) plt.title(砷(As)浓度空间分布及异常点标识) plt.legend() plt.tight_layout() plt.show()分情况处理对于空间聚集的异常点群很可能代表污染区域保留并在论文中重点分析。对于孤立的、且无法用任何空间因素解释的极高异常点检查其所有元素值。如果仅单一元素奇高其他元素正常可能是误差可考虑用局部邻域中位数替换或视为缺失值并用空间插值填充。如果多个元素同时异常则更可能是真实污染保留。无论是否处理都必须在论文的“数据预处理”部分清晰说明采用了何种方法检测异常值、检测出了多少个、基于何种理由决定保留/替换/删除并附上处理前后的对比图如箱线图、空间分布图。7. 处理后的效果验证与建模影响处理完异常值或决定保留但进行变换后必须评估处理对后续分析的影响。7.1 描述性统计对比比较处理前后或变换前后关键统计量的变化。# 以砷(As)为例假设我们决定对IQR检测出的极端异常值进行盖帽法处理Winsorization而非删除 def winsorize_series(series, limits(0.05, 0.05)): 盖帽法将两端超出分位数的值缩回到指定分位数 # 这里简化我们使用IQR的上下界进行盖帽 Q1 series.quantile(0.25) Q3 series.quantile(0.75) IQR Q3 - Q1 lower_bound Q1 - 1.5 * IQR upper_bound Q3 1.5 * IQR capped_series series.clip(lowerlower_bound, upperupper_bound) return capped_series df[As_original] df[As] df[As_winsorized] winsorize_series(df[As]) print(砷(As)处理前后统计对比:) print(pd.DataFrame({ 原始数据: df[As_original].describe(), 盖帽处理后: df[As_winsorized].describe(), 对数变换后: df_log[As].describe() }))观察均值、标准差、最大值的变化。盖帽法会显著降低标准差和最大值而对数变换则会改变数据的分布形态。7.2 分布形态对比绘制处理前后的分布直方图与Q-Q图Quantile-Quantile Plot检查是否更接近正态分布。import scipy.stats as stats fig, axes plt.subplots(2, 2, figsize(12, 10)) # 原始数据直方图与Q-Q图 axes[0, 0].hist(df[As_original], bins30, edgecolorblack, alpha0.7) axes[0, 0].set_title(原始As浓度分布) axes[0, 0].set_xlabel(浓度) axes[0, 0].set_ylabel(频数) stats.probplot(df[As_original].dropna(), distnorm, plotaxes[0, 1]) axes[0, 1].set_title(原始As Q-Q图) # 对数变换后数据直方图与Q-Q图 axes[1, 0].hist(df_log[As], bins30, edgecolorblack, alpha0.7) axes[1, 0].set_title(对数变换后As分布) axes[1, 0].set_xlabel(log(1浓度)) stats.probplot(df_log[As].dropna(), distnorm, plotaxes[1, 1]) axes[1, 1].set_title(对数变换后As Q-Q图) plt.tight_layout() plt.show()Q-Q图中点越接近对角线说明数据越符合正态分布。通常对数变换能显著改善重金属浓度数据的正态性。7.3 对后续建模的关键影响空间插值如克里金法异常值会严重扭曲变异函数模型导致插值结果在异常点附近产生“牛眼”效应虚假的峰值或低谷。处理异常值或进行对数变换后变异函数更稳定插值结果更能反映真实的区域趋势。污染评价与源解析基于原始数据的污染指数如单因子指数、内梅罗指数会被少数极高值主导可能夸大整体污染水平。使用处理后的数据或中位数等稳健统计量进行计算评价结果更可靠。在因子分析PCA等源解析方法中异常值会拉偏主成分方向影响污染源贡献率的计算。统计检验与回归分析许多参数检验如t检验、方差分析和经典线性回归都假设残差正态分布且方差齐性。异常值会破坏这些假设导致检验效能下降或回归系数估计不准。处理异常值能提升这些模型的可靠性。在CUMCM论文中你需要展示处理前后的对比并论证你所采用的处理方法如何使后续的建模步骤空间插值、污染评价、源解析结果更合理、更稳健。这体现了你完整的数据科学思维链条是从众多参赛论文中脱颖而出的关键。8. 完整代码框架与竞赛实战要点最后我将一个完整的、面向竞赛的异常值处理流程封装成函数并附上关键的实战建议。def comprehensive_outlier_analysis(df, value_columns, coord_columns[Lon, Lat], id_columnSampleID): 针对地理空间数据的综合异常值分析流程。 参数: df: 包含数据和坐标的DataFrame。 value_columns: list需要分析的值列如重金属浓度。 coord_columns: list坐标列名。 id_column: str样本ID列名。 返回: result_df: 添加了多种异常标签和变换后数据的DataFrame。 report: 包含各类异常统计信息的字典。 result_df df.copy() report {} # 1. 描述性统计 report[descriptive_stats] result_df[value_columns].describe() # 2. 单变量异常检测 (IQR) iqr_outliers {} for col in value_columns: outlier_mask detect_outliers_iqr(result_df[col], k1.5) iqr_outliers[col] outlier_mask result_df[f{col}_IQR_Outlier] outlier_mask report[iqr_outlier_counts] {col: mask.sum() for col, mask in iqr_outliers.items()} # 3. 多变量异常检测 (马氏距离) - 简化版需确保数据无缺失且协方差矩阵可逆 try: data_for_mahal result_df[value_columns].dropna() if not data_for_mahal.empty: mahal_dists mahalanobis_distance(data_for_mahal, value_columns) k len(value_columns) threshold stats.chi2.ppf(0.975, dfk) multivariate_mask (mahal_dists**2 threshold) # 注意对齐索引 result_df[Multivariate_Outlier] False result_df.loc[data_for_mahal.index, Multivariate_Outlier] multivariate_mask report[multivariate_outlier_count] multivariate_mask.sum() except Exception as e: print(f多元异常检测计算失败: {e}) report[multivariate_outlier_count] None # 4. 空间局部异常检测 (简化演示仅对第一列进行) if coord_columns[0] in result_df.columns and coord_columns[1] in result_df.columns: coords result_df[coord_columns].values tree BallTree(coords, metriceuclidean, leaf_size15) radius 0.01 # 示例半径需根据实际调整 spatial_flags np.zeros(len(result_df), dtypebool) target_col value_columns[0] # 示例对第一个元素进行空间分析 for i, (coord, val) in enumerate(zip(coords, result_df[target_col].values)): indices tree.query_radius([coord], rradius)[0] indices indices[indices ! i] if len(indices) 2: # 至少需要几个邻居才有意义 neighbor_vals result_df.iloc[indices][target_col].values n_mean neighbor_vals.mean() n_std neighbor_vals.std() if n_std 0: local_z (val - n_mean) / n_std if abs(local_z) 3: spatial_flags[i] True result_df[Spatial_Local_Outlier] spatial_flags report[spatial_outlier_count] spatial_flags.sum() # 5. 数据变换建议 result_df[[flog_{col} for col in value_columns]] np.log1p(result_df[value_columns]) report[transformation_applied] Log1p # 6. 生成综合异常标签 (例如被任何方法标记) outlier_cols [col for col in result_df.columns if Outlier in col] result_df[Is_Any_Outlier] result_df[outlier_cols].any(axis1) report[any_outlier_count] result_df[Is_Any_Outlier].sum() return result_df, report # 使用示例 result_df, report comprehensive_outlier_analysis(df, heavy_metals) print(异常值统计报告:) for key, value in report.items(): if key ! descriptive_stats: print(f{key}: {value})竞赛实战要点与避坑指南方法选择与理由在论文中不要只写“我们使用了箱线图法”。要解释为什么选择IQR而不是3σ因为数据偏态为什么要考虑空间局部异常因为数据具有空间属性。这体现了你的思考深度。参数透明化明确写出你使用的参数如IQR的k1.5马氏距离的显著性水平α0.05空间邻域半径r1km。并简要说明选择这些值的依据如1.5是箱线图标准1km是考虑到采样点密度和污染扩散范围。可视化至上一图胜千言。一定要附上处理前后的箱线图对比、空间分布图用不同颜色/形状标记异常点、分布直方图/Q-Q图对比。这些图能让你论文的“数据预处理”部分非常出彩。处理策略的论述这是最容易得分也最容易失分的地方。详细阐述你对每个被标记异常点的诊断思路例如“点A的砷浓度虽高但位于已知化工厂下风向且铜、锌也同步升高符合该厂排污特征故予以保留作为污染点源点B的汞浓度极高但孤立且其他元素无异常可能为采样或分析误差采用其周围5个点浓度的中位数进行替换”。对后续分析的影响用一小节说明异常值处理如何影响了你的插值结果、污染评价指数或源解析因子载荷。可以展示一小幅处理前后插值结果的对比图直观说明处理使结果更合理。代码与附录将核心的数据预处理代码包括异常值检测和处理整理好放在论文附录中。评委有时会看代码的逻辑是否清晰。记住在数学建模竞赛中处理异常值的目的不是为了得到一个“干净”的数据集而是为了得到一个“可靠”的数据集从而支撑起一个更稳固、更可信的模型结论。整个过程从检测到诊断再到处置每一步都需要严谨的推理和清晰的表达这正是评委最看重的“模型准备”工作。
返回列表