在pandas DataFrame中检测和排除异常值

389
我有一个带有几列的pandas数据框。 现在我知道某些行是基于某列值的异常值。 例如 列Vol的所有值都在12xx左右,而一个值是4000(异常值)。 我想要排除那些具有这样的Vol列的行。
因此,本质上我需要在数据框上设置一个过滤器,以便选择所有某列值在平均值的3个标准差范围内的行。
有什么优雅的方法可以实现这个?
19个回答

428

使用{{link1:scipy.stats.zscore}}函数

删除所有至少有一个列中存在异常值的行

如果您的数据框中有多个列,并且希望删除所有至少有一个列中存在异常值的行,可以使用以下表达式一次性完成:

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

df = pd.DataFrame(np.random.randn(100, 3))

df[(np.abs(stats.zscore(df)) < 3).all(axis=1)]

描述:

  • 对于每一列,首先计算每个值相对于列的均值和标准差的Z分数。
  • 然后取绝对值的Z分数,因为方向无关紧要,只要它低于阈值。
  • ( < 3).all(axis=1) 检查每一行的所有列值是否都在均值的3个标准差范围内。
  • 最后,将此条件的结果用于索引数据框。

根据单个列过滤其他列

与上述相同,但指定一个列作为zscore,例如df[0],并去除.all(axis=1)

df[np.abs(stats.zscore(df[0])) < 3]

11
你能解释一下这段代码在做什么吗?或许可以提供一些想法,如何删除所有在单个指定列中有异常值的行?这将会很有帮助。谢谢。 - samthebrand
24
对于每一列,首先计算该列中每个值相对于列均值和标准差的Z得分。然后取Z得分的绝对值,因为方向无关紧要,只要它低于阈值即可。.all(axis=1) 确保对于每一行,所有列都满足约束条件。最后,将此条件的结果用于索引数据帧。 - rafaelvalle
7
当列中存在空值/缺失值时,您将如何处理这种情况? 如何忽略它们? - asimo
11
针对此解决方案,我们如何处理字符串列?如果某些列是非数字的,并且我们想基于所有数字列删除异常值。 - ssp
6
出现错误:"TypeError: unsupported operand type(s) for /: 'str' and 'int'"。意思是无法将字符串和整数进行除法运算。 - sak
显示剩余16条评论

236

对于您的数据框每一列,您可以使用以下方法获取分位数:

q = df["col"].quantile(0.99)

然后使用过滤器:

df[df["col"] < q]
如果需要删除较低和较高的异常值,请使用AND语句将条件组合起来:

如果需要删除较低和较高的异常值,请使用AND语句将条件组合起来:

q_low = df["col"].quantile(0.01)
q_hi  = df["col"].quantile(0.99)

df_filtered = df[(df["col"] < q_hi) & (df["col"] > q_low)]

6
本文提供了一个非常好的异常值去除技术概述。https://machinelearningmastery.com/how-to-use-statistics-to-identify-outliers-in-data/ - user6903745
3
这可能仅会从上限中删除异常值,而不是从下限中删除? - indolentdeveloper
3
@indolentdeveloper 你是正确的,只需要反转不等式来消除低值异常值,或者使用OR运算符将它们与其他条件结合起来。 - user6903745
@user6903745,使用AND语句还是OR语句? - A.B
@user6903745 df_filtered = df[(df["col"] < q_hi) & (df["col"] > q_low)] 我猜这个语句足以去除上下两端的异常值。我不知道为什么这还不够。 - user10424859
显示剩余4条评论

191

像在 numpy.array 中一样使用布尔索引。

df = pd.DataFrame({'Data':np.random.normal(size=200)})
# example dataset of normally distributed data. 

df[np.abs(df.Data-df.Data.mean()) <= (3*df.Data.std())]
# keep only the ones that are within +3 to -3 standard deviations in the column 'Data'.

df[~(np.abs(df.Data-df.Data.mean()) > (3*df.Data.std()))]
# or if you prefer the other way around

对于一个系列,情况类似:

S = pd.Series(np.random.normal(size=200))
S[~((S-S.mean()).abs() > 3*S.std())]

6
他们有一个 "DataFrame.abs()" FYI,还有 "DataFrame.clip()"。 - Jeff
8
对于 clip() 函数,在使用 df.SOME_DATA.clip(-3std,+3std) 进行调用时,Jeff,数据的边界值并不会被删除,而是将超出范围的值分配到 +3std 或 -3std 上。请注意保持原意不变,并尽可能使翻译通俗易懂。 - CT Zhu
1
那几乎是一样的,@AMM - CT Zhu
1
如果我们的Pandas数据框有100列,我们该如何做同样的事情? - DreamerP
3
非常感谢@CTZhu提供的答案。 @DreamerP您可以使用以下代码将其应用于整个DataFrame:df_new = df[np.abs(df - df.mean()) <= (3 * df.std())]。但是,与将其应用于Series或单个列相反,这将使用np.nan替换异常值并保持DataFrame的形状,因此可能需要插值来填充缺失的值。 - JE_Muc
显示剩余3条评论

52

这个答案与@tanemaki提供的答案类似,但是使用了lambda表达式而不是scipy stats

df = pd.DataFrame(np.random.randn(100, 3), columns=list('ABC'))

standard_deviations = 3
df[df.apply(lambda x: np.abs(x - x.mean()) / x.std() < standard_deviations)
   .all(axis=1)]

要筛选DataFrame,仅保留一个列(如'B')在三个标准差范围内的数据:

df[((df['B'] - df['B'].mean()) / df['B'].std()).abs() < standard_deviations]

查看此处了解如何在滚动基础上应用此Z分数:应用于pandas数据框的滚动Z分数


你好,能否看一下这个问题: https://dev59.com/kcPra4cB1Zd3GeqPios5 - Aaditya Ura

51

在回答实际问题之前,根据数据的性质,我们应该问另一个非常相关的问题:

什么是异常值?

想象一系列数值[3, 2, 3, 4, 999](其中999似乎不合适),并分析各种异常值检测方法

Z-Score

问题在于,所讨论的值严重扭曲了我们的度量方式meanstd,导致Z-score大约为[-0.5, -0.5, -0.5, -0.5, 2.0],保持每个值在平均值的两个标准偏差内。一个非常大的异常值可能会扭曲您对异常值的整体评估。我不鼓励使用这种方法。

Quantile Filter

更为稳健的方法是此答案,消除数据底部和顶部的1%。但是,这样会消除相对固定的一部分数据,而独立于这些数据是否真的是异常值。您可能会失去很多有效的数据,并且另一方面,如果您的数据中有1%或2%以上的数据是异常值,则仍会保留一些异常值。

中位数到IQR距离

这是分位数原理的更稳健版本:消除所有距离数据的中位数超过f四分位距。例如,sklearnRobustScaler使用这种变换。IQR和中位数对异常值具有鲁棒性,因此您可以避免Z-score方法的问题。

在正常分布中,我们大约有iqr=1.35*s,因此您将把z-score筛选器的z=3转换为iqr-filter的f=2.22。这将删除上述示例中的999

基本假设是至少您的数据的“中间一半”有效,并且很好地反映了该分布,而您也会搞砸,如果您的分布具有宽尾巴和一个窄的q_25%到q_75%区间。

高级统计方法

当然,还有一些花哨的数学方法,例如佩尔斯准则格鲁布斯检验迪克森Q检验等,也适用于非正态分布数据。它们中没有一个容易实现,因此不再进一步讨论。

代码

将所有数字列的所有异常值替换为np.nan在示例数据框上。该方法对Pandas提供的所有数据类型

import pandas as pd
import numpy as np                                     

# sample data of all dtypes in pandas (column 'a' has an outlier)         # dtype:
df = pd.DataFrame({'a': list(np.random.rand(8)) + [123456, np.nan],       # float64
                   'b': [0,1,2,3,np.nan,5,6,np.nan,8,9],                  # int64
                   'c': [np.nan] + list("qwertzuio"),                     # object
                   'd': [pd.to_datetime(_) for _ in range(10)],           # datetime64[ns]
                   'e': [pd.Timedelta(_) for _ in range(10)],             # timedelta[ns]
                   'f': [True] * 5 + [False] * 5,                         # bool
                   'g': pd.Series(list("abcbabbcaa"), dtype="category")}) # category
cols = df.select_dtypes('number').columns  # limits to a (float), b (int) and e (timedelta)
df_sub = df.loc[:, cols]


# OPTION 1: z-score filter: z-score < 3
lim = np.abs((df_sub - df_sub.mean()) / df_sub.std(ddof=0)) < 3

# OPTION 2: quantile filter: discard 1% upper / lower values
lim = np.logical_and(df_sub < df_sub.quantile(0.99, numeric_only=False),
                     df_sub > df_sub.quantile(0.01, numeric_only=False))

# OPTION 3: iqr filter: within 2.22 IQR (equiv. to z-score < 3)
iqr = df_sub.quantile(0.75, numeric_only=False) - df_sub.quantile(0.25, numeric_only=False)
lim = np.abs((df_sub - df_sub.median()) / iqr) < 2.22


# replace outliers with nan
df.loc[:, cols] = df_sub.where(lim, np.nan)

删除包含至少一个NaN值的所有行:

df.dropna(subset=cols, inplace=True) # drop rows with NaN in numerical columns
# or
df.dropna(inplace=True)  # drop rows with NaN in any column

使用 pandas 1.3 函数:


1
为避免删除非数值列中的NaN行,请使用df.dropna(how='any', subset=cols, inplace=True)。 - till Kadabra
1
我认为np.logical_or应该改为np.logical_and才能正常工作(选项2)。 - user1259201

45
#------------------------------------------------------------------------------
# accept a dataframe, remove outliers, return cleaned data in a new dataframe
# see http://www.itl.nist.gov/div898/handbook/prc/section1/prc16.htm
#------------------------------------------------------------------------------
def remove_outlier(df_in, col_name):
    q1 = df_in[col_name].quantile(0.25)
    q3 = df_in[col_name].quantile(0.75)
    iqr = q3-q1 #Interquartile range
    fence_low  = q1-1.5*iqr
    fence_high = q3+1.5*iqr
    df_out = df_in.loc[(df_in[col_name] > fence_low) & (df_in[col_name] < fence_high)]
    return df_out

我在代码行 "df_out = df_in.loc[(df_in[col_name] > fence_low) & (df_in[col_name] < fence_high)]" 中遇到了错误 "ValueError: Cannot index with multidimensional key"。你能帮忙吗? - Imran Ahmad Ghazali

31

由于我没有看到处理数字和非数字属性的答案,这里是一份补充答案。

您可能只想在数字属性上删除异常值(分类变量很难成为异常值)。

函数定义

我已经扩展了@tanemaki的建议以处理存在非数值属性的数据:

from scipy import stats

def drop_numerical_outliers(df, z_thresh=3):
    # Constrains will contain `True` or `False` depending on if it is a value below the threshold.
    constrains = df.select_dtypes(include=[np.number]) \
        .apply(lambda x: np.abs(stats.zscore(x)) < z_thresh, reduce=False) \
        .all(axis=1)
    # Drop (inplace) values set to be rejected
    df.drop(df.index[~constrains], inplace=True)

使用方法

drop_numerical_outliers(df)

示例

假设有一个包含房屋信息(如:巷道、土地状况、销售价格等)的数据集 df,例如:数据文档

首先,您想在散点图上可视化该数据(使用 z-score 阈值=3):

# Plot data before dropping those greater than z-score 3. 
# The scatterAreaVsPrice function's definition has been removed for readability's sake.
scatterAreaVsPrice(df)

Before - Gr Liv Area Versus SalePrice

# Drop the outliers on every attributes
drop_numerical_outliers(train_df)

# Plot the result. All outliers were dropped. Note that the red points are not
# the same outliers from the first plot, but the new computed outliers based on the new data-frame.
scatterAreaVsPrice(train_df)

After - Gr Liv Area Versus SalePrice


3
好的解决方案!提醒一下,自 pandas 版本 0.23.0 起,reduce=False 已被弃用。 - RK1
2
reduce=False替换为result_type='reduce' - Ekaba Bisong
3
@KeyMaker00,我非常希望使用这个,但是我遇到了以下错误:ValueError: No axis named 1 for object type Series。 - flashliquid

21

对于数据框中的每个系列,您可以使用betweenquantile来去除异常值。

x = pd.Series(np.random.normal(size=200)) # with outliers
x = x[x.between(x.quantile(.25), x.quantile(.75))] # without outliers

3
在这里,您只选择在四分位距(IQR)内的数据,但请记住,可能存在不是异常值但超出此范围的值。 - BCArg
2
选择例如0.1和0.9应该是相当安全的。像这样使用between和分位数是一种相当简洁的语法。 - PascalVKooten

15

scipy.statstrim1()trimboth()方法,可以根据排名和指定的移除数据百分比在单个行中剪裁异常值。


1
trimboth 对我来说是最容易的。 - wordsforthewise

10

如果你喜欢使用方法链,你可以像这样获取所有数字列的布尔条件:

df.sub(df.mean()).div(df.std()).abs().lt(3)

每列的每个值将根据其是否距离平均值小于三个标准差来转换为True/False


这应该是le(3),因为它正在去除异常值。这样您就可以得到异常值的True。除此之外+1和这个答案应该更靠前 - Erfan

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