使用百分位数在Pandas DataFrame中删除异常值

31

我有一个名为df的DataFrame,其中有40个列和许多记录。

df:

User_id | Col1 | Col2 | Col3 | Col4 | Col5 | Col6 | Col7 |...| Col39

对于除了“user_id”列之外的每一列,我希望检查是否存在异常值,并在出现异常值时删除整条记录。

为了在每行中进行离群值检测,我决定简单地使用第5个百分位数和第95个百分位数(我知道这不是最好的统计方法):

到目前为止,我的代码如下:

P = np.percentile(df.Col1, [5, 95])
new_df = df[(df.Col1 > P[0]) & (df.Col1 < P[1])]

问题: 我该如何将这种方法应用于除User_id之外的所有列,而不需要手动操作?我的目标是获取一个没有异常值记录的数据框。

谢谢!

5个回答

104

使用此代码,不要浪费您的时间:

Q1 = df.quantile(0.25)
Q3 = df.quantile(0.75)
IQR = Q3 - Q1

df = df[~((df < (Q1 - 1.5 * IQR)) |(df > (Q3 + 1.5 * IQR))).any(axis=1)]

如果您想要特定的列:

cols = ['col_1', 'col_2'] # one or more

Q1 = df[cols].quantile(0.25)
Q3 = df[cols].quantile(0.75)
IQR = Q3 - Q1

df = df[~((df[cols] < (Q1 - 1.5 * IQR)) |(df[cols] > (Q3 + 1.5 * IQR))).any(axis=1)]

5
感谢您发布这篇文章,我无法用点赞来表达我的感激之情。 - exmatelote
谢谢,这非常有帮助。 - Bawantha
7
为什么要用加减1.5倍的四分位距(IQR)? - Carl
1
@Carl 离群值是指落在四分位距(Q3-Q1)的1.5倍以外的数据点。因此,+和-1.5*IQR意味着我们只考虑在这些限制内的数据。 - stuckoverflow
2
这个有解释吗? - Themasterhimself
显示剩余5条评论

37

初始数据集。

print(df.head())

   Col0  Col1  Col2  Col3  Col4  User_id
0    49    31    93    53    39       44
1    69    13    84    58    24       47
2    41    71     2    43    58       64
3    35    56    69    55    36       67
4    64    24    12    18    99       67

首先删除User_id列。

filt_df = df.loc[:, df.columns != 'User_id']

然后,计算百分位数。

low = .05
high = .95
quant_df = filt_df.quantile([low, high])
print(quant_df)

       Col0   Col1  Col2   Col3   Col4
0.05   2.00   3.00   6.9   3.95   4.00
0.95  95.05  89.05  93.0  94.00  97.05

接下来是基于计算出的百分位数对值进行过滤。为此,我通过列使用apply即可完成!

filt_df = filt_df.apply(lambda x: x[(x>quant_df.loc[low,x.name]) & 
                                    (x < quant_df.loc[high,x.name])], axis=0)

带回User_id

filt_df = pd.concat([df.loc[:,'User_id'], filt_df], axis=1)

最后,具有NaN值的行可以简单地像这样删除。

filt_df.dropna(inplace=True)
print(filt_df.head())

   User_id  Col0  Col1  Col2  Col3  Col4
1       47    69    13    84    58    24
3       67    35    56    69    55    36
5        9    95    79    44    45    69
6       83    69    41    66    87     6
9       87    50    54    39    53    40

检查结果

print(filt_df.head())

   User_id  Col0  Col1  Col2  Col3  Col4
0       44    49    31   NaN    53    39
1       47    69    13    84    58    24
2       64    41    71   NaN    43    58
3       67    35    56    69    55    36
4       67    64    24    12    18   NaN

print(filt_df.describe())

          User_id       Col0       Col1       Col2       Col3       Col4
count  100.000000  89.000000  88.000000  88.000000  89.000000  89.000000
mean    48.230000  49.573034  45.659091  52.727273  47.460674  57.157303
std     28.372292  25.672274  23.537149  26.509477  25.823728  26.231876
min      0.000000   3.000000   5.000000   7.000000   4.000000   5.000000
25%     23.000000  29.000000  29.000000  29.500000  24.000000  36.000000
50%     47.000000  50.000000  40.500000  52.500000  49.000000  59.000000
75%     74.250000  69.000000  67.000000  75.000000  70.000000  79.000000
max     99.000000  95.000000  89.000000  92.000000  91.000000  97.000000

如何生成测试数据集

np.random.seed(0)
nb_sample = 100
num_sample = (0,100)

d = dict()
d['User_id'] = np.random.randint(num_sample[0], num_sample[1], nb_sample)
for i in range(5):
    d['Col' + str(i)] = np.random.randint(num_sample[0], num_sample[1], nb_sample)

df = DataFrame.from_dict(d)

干得好!但在我的情况下,我不得不将(lambda x: x[(x>quant_df.loc[low,x.name]) & (x < quant_df.loc[high,x.name])], axis=0) 更改为 (lambda x: x[(x >= quant_df.loc[low,x.name]) & (x <= quant_df.loc[high,x.name])], axis=0)。否则,所有记录都将被删除。我有一些非常接近于零的中位数,例如0.00001,也许这就是原因。 - Mi Funk
太好了!我看不出这两个lambda之间有什么区别,除了换行符。 - Romain
1
我使用了">="和"<="代替了">"和"<"来包括上限和下限。 - Mi Funk
之后为什么会出现很多原始数据集中没有的“NaN”?我们该如何处理它们? - DreamerP
但是不同的列会不会导致行被打乱? - Rohan Bhale

7
您所描述的类似于winsorizing的过程,它会剪切值(例如,在第5个和第95个百分位处),而不是完全消除它们。

以下是一个示例:

import pandas as pd
from scipy.stats import mstats
%matplotlib inline

test_data = pd.Series(range(30))
test_data.plot()

Original data

# Truncate values to the 5th and 95th percentiles
transformed_test_data = pd.Series(mstats.winsorize(test_data, limits=[0.05, 0.05])) 
transformed_test_data.plot()

Winsorized data


2
使用内连接。像这样的语句应该可以工作。
cols = df.columns.tolist()
cols.remove('user_id') #remove user_id from list of columns

P = np.percentile(df[cols[0]], [5, 95])
new_df = df[(df[cols[0] > P[0]) & (df[cols[0]] < P[1])]
for col in cols[1:]:
    P = np.percentile(df[col], [5, 95])
    new_df = new_df.join(df[(df[col] > P[0]]) & (df[col] < P[1])], how='inner')

-1

如果要根据单个列来修剪整个DataFrame,这里有一种更简单的方法。在排序后从顶部和底部删除n行。

nb_to_trim = round(len(df.index) * 0.05)
df = df.sort_values(col1).iloc[nb_to_trim:-nb_to_trim, :]

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接