如何基于分组对DataFrame列执行GESD法异常值剔除?
分组DataFrame属性列的GESD异常值剔除方案
我需要对含分组的DataFrame,针对每个分组的属性列(如Property1、Property2)剔除异常值,将异常值替换为NaN。单分组场景下,我用PyAstronomy库的generalizedESD函数(GESD方法)实现了需求,代码如下:
for column in dataset.iloc[:, 1:]: # 遍历需要处理的属性列 column1 = dataset[column].dropna() # 移除缺失值 column1.reset_index(drop=True, inplace=True) # 重置索引,适配移除NA后的行 length = len(column1) # 获取有效行数 max_outlier = int(length*0.25) # 设置最大异常值数量(占比25%) if length>2: r = pyasl.generalizedESD(column1, max_outlier, 0.05, fullOutput=False) i = r[1] # 异常值的索引数组 a = (column1.loc[i]) # 异常值数值数组 for x in a: dataset.loc[dataset[column]==x, column] = np.nan # 将异常值替换为NaN
现在希望针对每个分组单独执行上述操作,最终得到统一的、已替换异常值为NaN的DataFrame。尝试将代码封装为函数后用groupby+apply,但未得到预期结果。
问题核心:处理列时用dropna()移除NA行但未正确处理索引,导致pyasl.generalizedESD返回的异常值索引与原列索引不匹配,部分异常值未被替换。
以下是含NA的测试代码:
import numpy as np import pandas as pd import random from PyAstronomy import pyasl # 创建含3个分组、带随机异常值(值≈20)的合成数据 n = 50 dataset = pd.DataFrame({ "Group": np.random.choice(["A", "B", "C"], size=n), "P1": np.random.rand(n) + np.random.choice([0., 20.], p=[0.9, 0.1], size=n), "P2": np.random.rand(n) + np.random.choice([0., 20.], p=[0.9, 0.1], size=n), "P3": np.random.rand(n) + np.random.choice([0., 20.], p=[0.9, 0.1], size=n), "P4": np.random.rand(n) + np.random.choice([0., 20.], p=[0.9, 0.1], size=n) }) # 给属性列添加NA值 p = 0.2 mask = np.random.choice([True, False], size=[50,4], p =[p,1-p]) dataset.iloc[:,1:] = dataset.iloc[:,1:].mask(mask) dataset = dataset.set_index("Group") # 按列替换异常值的函数 def replace_outliers_columnwise(col): max_outlier = len(col) // 4 # 自定义最大异常值数量,注意列行数过少的情况 _, idxs = pyasl.generalizedESD(col, max_outlier) col.iloc[idxs] = np.nan return col # 按分组处理的函数 def replace_outliers_groupwise(group): return group.apply(replace_outliers_columnwise) dataset = dataset.groupby("Group").apply(replace_outliers_groupwise) print(dataset)
解决方案
问题出在两点:
generalizedESD接收含NA的列时会自动忽略NA,但返回的索引是剔除NA后的重置索引,和原列的索引不对应,直接用col.iloc[idxs]无法定位到原数据中的异常值。- 直接修改
col(pandas Series)可能因为视图/副本问题导致修改不生效。
修正后的代码,先对列做非NA值筛选并保留原索引,处理后再映射回原分组数据:
import numpy as np import pandas as pd from PyAstronomy import pyasl # 生成测试数据(同原代码) n = 50 dataset = pd.DataFrame({ "Group": np.random.choice(["A", "B", "C"], size=n), "P1": np.random.rand(n) + np.random.choice([0., 20.], p=[0.9, 0.1], size=n), "P2": np.random.rand(n) + np.random.choice([0., 20.], p=[0.9, 0.1], size=n), "P3": np.random.rand(n) + np.random.choice([0., 20.], p=[0.9, 0.1], size=n), "P4": np.random.rand(n) + np.random.choice([0., 20.], p=[0.9, 0.1], size=n) }) # 添加NA值 p = 0.2 mask = np.random.choice([True, False], size=[50,4], p=[p,1-p]) dataset.iloc[:,1:] = dataset.iloc[:,1:].mask(mask) def process_group(group): # 遍历每个属性列 for col_name in group.columns[1:]: # 跳过Group列 col = group[col_name] # 筛选非NA值,保留原索引 non_na = col.dropna() if len(non_na) <= 2: continue # 数据量太少,不处理异常值 max_outlier = int(len(non_na) * 0.25) # 对非NA值执行GESD _, outlier_indices_in_non_na = pyasl.generalizedESD(non_na.values, max_outlier, 0.05, fullOutput=False) # 获取原数据中的异常值索引 outlier_indices = non_na.iloc[outlier_indices_in_non_na].index # 将原数据中的异常值设为NaN group.loc[outlier_indices, col_name] = np.nan return group # 按Group分组处理 dataset_cleaned = dataset.groupby("Group", group_keys=False).apply(process_group) print(dataset_cleaned)
关键修正点
- 保留原数据的索引,不提前把Group设为索引,避免分组后索引混乱。
- 对列的非NA子集处理后,通过
non_na.iloc[outlier_indices_in_non_na].index映射回原数据的真实索引,确保异常值定位准确。 - 直接在分组的DataFrame上修改,避免Series视图/副本的修改问题。
- 添加数据量判断(
len(non_na) <=2),避免数据过少时GESD无法运行。
内容的提问来源于stack exchange,提问作者user22869511
相关产品推荐
相关产品推荐

