在大型pandas DataFrame中按组移除异常值的更快方法

yts*_*aig 13 python pandas

我有一个相对较大的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)
Run Code Online (Sandbox Code Playgroud)

然后,通过我的DataFrame调用features和索引DATE,我可以做到

grouped = features.groupby(level='DATE')
result = grouped.apply(winsorize_df)
Run Code Online (Sandbox Code Playgroud)

这是有效的,除了它非常慢,可能是由于嵌套apply调用:每个组一个,然后每个组中的每个列一个.我试图apply通过一次计算所有列的分位数来摆脱第二个,但是试图将每个列的阈值设置为不同的值.有没有更快的方法来完成此过程?

unu*_*tbu 7

您可以考虑使用scipy.stats.mstats中winsorize函数.但请注意,它返回的值略有不同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
Run Code Online (Sandbox Code Playgroud)

使用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')
Run Code Online (Sandbox Code Playgroud)
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
Run Code Online (Sandbox Code Playgroud)

  • @YT正如你在OP中提到的,pandas现在有一个`.clip()`函数应该适合你,特别是与`.quantile()`结合使用时. (4认同)

小智 5

我找到了一个相当简单的方法来让它工作,使用 pandas 中的转换方法。

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)
Run Code Online (Sandbox Code Playgroud)


tnf*_*tnf 5

这是一个不使用 scipy.stats.mstats 的解决方案:

def clip_series(s, lower, upper):
   clipped = s.clip(lower=s.quantile(lower), upper=s.quantile(upper), axis=1)
   return clipped

# Manage list of features to be winsorized
feature_list = list(features.columns)

for f in feature_list:
   features[f] = clip_series(features[f], 0.05, 0.95)
Run Code Online (Sandbox Code Playgroud)

  • 你能添加一个简短的描述吗? (9认同)