大Pandas数据框架中按组剔除异常值的快速方法

2024-06-01 02:36:55 发布

您现在位置:Python中文网/ 问答频道 /正文

我有一个相对较大的DataFrame对象(大约一百万行,成百上千列),我想按组剪裁每列中的异常值。我的意思是,通过“逐组剪裁每列的异常值”,计算一个组中每列的5%和95%分位数,并剪裁该分位数范围之外的值。

以下是我当前使用的设置:

def winsorize_series(s):
    q = s.quantile([0.05, 0.95])
    if isinstance(q, pd.Series) and len(q) == 2:
        s[s < q.iloc[0]] = q.iloc[0]
        s[s > q.iloc[1]] = q.iloc[1]
    return s

def winsorize_df(df):
    return df.apply(winsorize_series, axis=0)

然后,使用名为features并由DATE索引的数据帧,我可以

grouped = features.groupby(level='DATE')
result = grouped.apply(winsorize_df)

这是可行的,只是速度很慢,可能是因为嵌套的apply调用:每个组一个,然后每个组中的每个列一个。我试图通过一次计算所有列的分位数来去掉第二个apply,但却被困在试图用不同的值对每个列设置阈值。有没有更快的方法来完成这个过程?


Tags: 对象dataframedfdatereturndefseriesfeatures
3条回答

有一个winsorize function in scipy.stats.mstats可以考虑使用。但是,请注意,它返回的值与winsorize_series略有不同:

In [126]: winsorize_series(pd.Series(range(20), dtype='float'))[0]
Out[126]: 0.95000000000000007

In [127]: mstats.winsorize(pd.Series(range(20), dtype='float'), limits=[0.05, 0.05])[0]
Out[127]: 1.0

mstats.winsorize代替winsorize_series可能(取决于N,M,p)快1.5倍:

import numpy as np
import pandas as pd
from scipy.stats import mstats

def using_mstats_df(df):
    return df.apply(using_mstats, axis=0)

def using_mstats(s):
    return mstats.winsorize(s, limits=[0.05, 0.05])

N, M, P = 10**5, 10, 10**2
dates = pd.date_range('2001-01-01', periods=N//P, freq='D').repeat(P)
df = pd.DataFrame(np.random.random((N, M))
                  , index=dates)
df.index.names = ['DATE']
grouped = df.groupby(level='DATE')

In [122]: %timeit result = grouped.apply(winsorize_df)
1 loops, best of 3: 17.8 s per loop

In [123]: %timeit mstats_result = grouped.apply(using_mstats_df)
1 loops, best of 3: 11.2 s per loop

解决这个问题的好方法是矢量化。为此,我喜欢使用np.where

import pandas as pd
import numpy as np
from scipy.stats import mstats
import timeit

data = pd.Series(range(20), dtype='float')

def WinsorizeCustom(data):
    quantiles = data.quantile([0.05, 0.95])
    q_05 = quantiles.loc[0.05]
    q_95 = quantiles.loc[0.95]

    out = np.where(data.values <= q_05,q_05, 
                                      np.where(data >= q_95, q_95, data)
                  )
    return out

为了进行比较,我将scipy中的函数包装在一个函数中:

def WinsorizeStats(data):
    out = mstats.winsorize(data, limits=[0.05, 0.05])
    return out

但正如您所看到的,尽管我的函数非常快,但它离Scipy实现还很远:

%timeit WinsorizeCustom(data)
#1000 loops, best of 3: 842 µs per loop

%timeit WinsorizeStats(data)
#1000 loops, best of 3: 212 µs per loop

如果您有兴趣阅读更多关于加速pandas代码的内容,我建议您使用Optimization Pandas for speedFrom Python to Numpy

我找到了一个非常简单的方法来实现这一点,在pandas中使用了transform方法。

from scipy.stats import mstats

def winsorize_series(group):
    return mstats.winsorize(group, limits=[lower_lim,upper_lim])

grouped = features.groupby(level='DATE')
result = grouped.transform(winsorize_series)

相关问题 更多 >