Более быстрый способ удаления выбросов по группам в больших pandas DataFrame

У меня есть относительно большой объект 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)

а затем, с моим DataFrame, который называется features и индексируется DATE, я могу сделать

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

Это работает, за исключением того, что он очень медленный, предположительно из-за вложенных вызовов apply: по одному в каждой группе, а затем по одному для каждого столбца в каждой группе. Я попытался избавиться от второго apply, вычислив квантили для всех столбцов сразу, но застрял, пытаясь порождать каждый столбец другим значением. Есть ли более быстрый способ выполнить эту процедуру?

Ответ 1

Существует функция winsorize в 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.5x быстрее:

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

Ответ 2

Я нашел довольно простой способ заставить это работать, используя метод преобразования в 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)

Ответ 3

Хороший способ приблизиться к этому - с векторизации. И для этого я люблю использовать 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, я бы предложил Оптимизация pandas для скорости и От Python to Numpy.

Ответ 4

Вот решение без использования 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)