You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何基于分组对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)                                                                                        

解决方案

问题出在两点:

  1. generalizedESD接收含NA的列时会自动忽略NA,但返回的索引是剔除NA后的重置索引,和原列的索引不对应,直接用col.iloc[idxs]无法定位到原数据中的异常值。
  2. 直接修改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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.07 02:05:33