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

396 投票
18 回答
626653 浏览
提问于 2025-04-18 03:36

我有一个 pandas 数据框,里面有几列数据。现在我知道根据某一列的值,有些行是异常值。比如说,Vol 这一列的值大多在 12xx 附近,但有一个值是 4000(就是异常值)。我想把这些 Vol 列中这样的行排除掉。

所以,简单来说,我需要在数据框上设置一个过滤器,选择所有某一列的值在平均值的 3 个标准差范围内的行。

有什么优雅的方法可以做到这一点呢?

18 个回答

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)]

如果你想过滤数据表,只保留某一列(比如说'B')在三个标准差范围内的数据,可以这样做:

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

想了解如何在滚动计算中应用这个z-score,可以查看这里:在pandas数据表中应用滚动Z-score

54

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

什么是异常值?

想象一下这个数值序列 [3, 2, 3, 4, 999](其中的 999 看起来不太合适),我们来分析一下识别异常值的不同方法。

Z-分数

这里的问题是,那个值会严重扭曲我们的平均值 mean 和标准差 std,导致得到的 z-分数大约是 [-0.5, -0.5, -0.5, -0.5, 2.0],这让所有值都保持在平均值的两个标准差之内。因此,一个非常大的异常值可能会影响你对异常值的整体评估。我不建议使用这种方法。

分位数过滤

一种更稳健的方法是 这个答案,它会去掉数据中最低和最高的1%。但是,这样做会固定去掉一部分数据,而不管这些数据是否真的异常。你可能会丢失很多有效的数据,另一方面,如果你的数据中有超过1%或2%的异常值,仍然可能会保留一些异常值。

中位数的四分位距距离

这是分位数原则的一个更稳健的版本:去掉所有距离数据中位数超过 f 倍的 四分位距 的数据。这也是 sklearnRobustScaler 使用的转换方法。例如,在正态分布中,我们大约有 iqr=1.35*s,所以你可以把 z-分数过滤中的 z=3 转换为 f=2.22 的四分位距过滤。这将去掉上面例子中的 999

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

高级统计方法

当然,还有一些复杂的数学方法,比如 Peirce标准Grubb检验Dixon的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 的函数:

192

使用 boolean 索引,就像你在 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())]
243

对于你每一列的数据表,你可以用下面的方式来获取分位数:

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

然后你可以用下面的方式来筛选数据:

df[df["col"] < q]

如果你想去掉上下的异常值,可以把条件用“与”这个逻辑连接起来:

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)]
432

使用 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]

撰写回答