数据清洗是数据分析流程中非常耗时但至关重要的一步。它涉及处理重复数据、缺失值、异常值以及不一致的数据格式等问题。pandas
提供了丰富的工具来高效地执行这些任务。
1.重复数据过滤
重复数据是指在数据集中出现多次的完全相同或部分相同的记录。处理重复数据是确保数据质量和分析准确性的重要步骤。
df.duplicated(subset=None, keep='first')
:- 返回一个布尔 Series,指示每一行是否是重复的。
True
表示该行是重复的(之前出现过)。subset
: 可选参数,指定要检查重复的列的子集。如果为None
,则检查所有列。keep
: 指定如何标记重复项。'first'
(默认):将第一次出现的重复项标记为False
,其余标记为True
。'last'
:将最后一次出现的重复项标记为False
,其余标记为True
。False
:将所有重复项(包括第一次/最后一次出现)都标记为True
。
df.drop_duplicates(subset=None, keep='first', inplace=False)
:- 删除重复的行。
inplace
: 布尔值,如果为True
,则直接修改原始 DataFrame 并返回None
;如果为False
(默认),则返回一个新的 DataFrame。- 参数
subset
和keep
的含义与duplicated()
相同。
duplicated()
和drop_duplicates()
的底层实现通常会使用哈希表来高效地识别重复行。对于每一行,它会计算一个哈希值(或基于subset
列的哈希值),并将其存储在哈希表中。当遇到相同的哈希值时,就认为是重复项。keep
参数决定了在识别重复项时,哪个副本被认为是“原始”的或“非重复”的。这些操作通常会返回新的 DataFrame(除非
inplace=True
),因为删除行会改变 DataFrame 的结构,需要重新构建。
只想根据某些关键列来判断重复时,subset
参数非常有用。例如,在客户数据中,可能允许姓名重复,但如果姓名和邮箱地址都重复,则可能认为是同一个客户的重复记录。
判断是否存在重复数据:
1
2
3
4
5
data = {"color": ['red', 'blue', 'red', 'green', 'blue', None, 'red'], 'price': [10, 20, 10, 15, 20, 0, np.nan]}
df = pd.DataFrame(data=data)
display(df)
is_dup = df.duplicated()
print(is_dup)
删除重复数据:
1
2
3
# 默认keep=first
df_no_dup = df.drop_duplicates()
df_no_dup
根据color列判断重复,保留最后一个重复项
1
2
df_drop_color = df.drop_duplicates(subset=["color"], keep='last')
df_drop_color
选择题
-
给定
df = pd.DataFrame({'A': [1, 2, 1], 'B': [10, 20, 10]})
,执行df.drop_duplicates(keep='last')
后,df
的内容是什么?A.
DataFrame
包含[1, 2, 1]
和[10, 20, 10]
。B.
DataFrame
包含[2, 1]
和[20, 10]
。C.
DataFrame
包含[1, 2]
和[10, 20]
。D.
DataFrame
包含[1, 2, 1]
和[10, 20, 10]
,但顺序不同。答案:B
-
df.duplicated()
方法的keep='False'
参数的作用是?A. 不删除任何重复项。
B. 删除所有重复项,包括第一次出现的。
C. 将所有重复项(包括第一次/最后一次出现)都标记为
True
。D. 报错。
答案:C,
df.duplicated()
方法的keep='False'
参数的作用是:keep='False'
会将所有重复行(包括第一次和最后一次出现)标记为 True。例如,对于重复的行,所有出现都会被标记为重复。
编程题
- 创建一个 DataFrame
orders
,包含'OrderID'
,'CustomerID'
,'Product'
,'Quantity'
四列,并包含一些重复的OrderID
和CustomerID
组合。 - 找出所有完全重复的行。
- 删除所有完全重复的行,只保留第一次出现的记录。
- 找出并删除那些
CustomerID
和Product
都重复的记录,保留最后一次出现的记录。 - 打印每一步操作后的 DataFrame。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
orderID = [1, 2, 1, 10, 9, 8, 5, 10]
customerID = [1, 99, 1, 10, 88, 8, 55, 10]
product = ["Apple", "Banana", "Orange", "Grapes", "Flower", "Milk", "Shoes", "Hat"]
quantity = np.random.randint(10, 50, (8, ))
order_dict = {"OrderID": orderID, "CustomerID": customerID, "Product": product, "Quantity": quantity}
orders = pd.DataFrame(data=order_dict)
# 找出所有完全重复的行
dup_line_all = orders.duplicated()
display(dup_line_all)
# 删除所有完全重复的行,只保留第一次出现的记录
orders = orders.drop_duplicates() # 默认keep=first
display(orders)
# 找出并删除那些 CustomerID 和 Product 都重复的记录,保留最后一次出现的记录
orders = orders.drop_duplicates(subset=["OrderID", "CustomerID"], keep="last")
display(orders)
2.空数据过滤
空数据(缺失值)是真实世界数据中普遍存在的问题。pandas
使用 NaN
(Not a Number)来表示缺失值,并提供了一系列工具来检测、删除和填充这些缺失值。
df.isnull()
/df.isna()
:- 返回一个布尔 DataFrame,其中缺失值的位置为
True
,非缺失值的位置为False
。 isnull()
和isna()
是等价的。
- 返回一个布尔 DataFrame,其中缺失值的位置为
df.notnull()
/df.notna()
:- 返回一个布尔 DataFrame,其中非缺失值的位置为
True
,缺失值的位置为False
。
- 返回一个布尔 DataFrame,其中非缺失值的位置为
df.dropna(axis=0, how='any', thresh=None, subset=None, inplace=False)
:- 删除包含缺失值的行或列。
axis
: 指定删除行 (0
或'index'
) 还是列 (1
或'columns'
)。how
: 指定删除策略。'any'
(默认):如果行/列中至少有一个NaN
,则删除。'all'
:如果行/列中所有值都是NaN
,则删除。
thresh
: 整数,要求行/列中至少有多少个非NaN
值才保留。subset
: 列表,指定只在这些列中查找NaN
来决定是否删除行。
df.fillna(value=None, method=None, axis=None, limit=None, inplace=False)
:- 填充缺失值。
value
: 用于填充缺失值的值(标量、字典、Series 或 DataFrame)。method
: 填充方法。'ffill'
或'pad'
:使用前一个有效观测值填充。'bfill'
或'backfill'
:使用后一个有效观测值填充。
axis
: 填充方向(0
或'index'
沿着行填充,1
或'columns'
沿着列填充)。limit
: 整数,限制连续填充的缺失值数量。
缺失值的处理是数据预处理的关键步骤。
pandas
在底层对NaN
值进行了特殊优化,使其在计算时能够被正确识别和处理(例如,sum()
会默认跳过NaN
)。
dropna()
通过遍历行或列,检查NaN
的存在性,并根据how
和thresh
参数来决定是否保留。fillna()
则根据指定的value
或method
来替换NaN
。ffill
和bfill
方法涉及到在内存中进行前向或后向填充,这在时间序列数据中非常有用。这些操作通常返回新的 DataFrame(除非
inplace=True
),因为它们会改变 DataFrame 的内容或结构。
【缺失值处理策略】
- 删除: 当缺失值占比较小或数据量足够大时,可以直接删除包含缺失值的行或列。
- 填充:
- 常数填充: 用 0、平均值、中位数、众数等填充。
- 插值填充: 使用
'ffill'
,'bfill'
或更复杂的插值方法(如线性插值interpolate()
)。 - 模型预测填充: 使用机器学习模型预测缺失值。
- 标记: 有时,缺失本身就是一种信息,可以创建一个新的布尔列来标记缺失值的位置。
【1】判断是否存在空数据:
1
2
3
4
data = {"color": ['red', 'blue', 'red', 'green', 'blue', None, 'red'], 'price': [10, 20, 10, 15, 20, 0, np.nan]}
df = pd.DataFrame(data=data)
display(df)
df.isnull()
【2】删除空数据:
1
2
3
# 只要有值是nan就删除
drop_na_df = df.dropna(how="any")
drop_na_df
【3】填充空数据:
1
2
3
4
5
6
7
8
9
10
data = {
"color": ['red', 'blue', 'red', 'green', 'blue', None, 'red'],
'price': [10, 20, 10, 15, 20, 0, np.nan],
'col3': [10, 20, np.nan, np.nan, np.nan, np.nan, np.nan],
'col4': [10, 20, np.nan, np.nan, np.nan, np.nan, np.nan]
}
df = pd.DataFrame(data=data)
display(df)
df_fill_ = df.fillna(value=111)
display(df_fill_)
也可以用前一个有效值/后一个有效值填充:
现在可以直接用
df.ffill()
或者df.bfill()
进行填充。
1
2
3
4
5
display(df)
df_fill_f = df.ffill()
display(df_fill_f) # 用前一个值进行填充
df_fill_b = df.bfill()
display(df_fill_b) # 用后一个值进行填充
用均值填充:
1
2
3
4
5
display(df)
# price这一列的填充(用均值)
display(np.mean(df.price))
df_mean_fill = df.fillna(value=df["price"].mean())
display(df_mean_fill)
选择题
-
给定
df = pd.DataFrame({'A': [1, np.nan], 'B': [3, 4]})
,执行df.dropna(how='all')
后,df
的内容是什么?A.
DataFrame
包含[1, np.nan]
和[3, 4]
。B.
DataFrame
包含[1]
和[3]
。C.
DataFrame
包含[1, np.nan]
和[3, 4]
,但顺序不同。D. 报错。
答案:A,
df.dropna(how='all')
会删除所有列均为 NaN 的行。 -
以下哪个方法用于使用前一个有效观测值填充缺失值?
A.
df.fillna(value=0)
B.
df.fillna(method='bfill')
C.
df.fillna(method='ffill')
D.
df.dropna()
答案:C,
df.fillna(value=0)
:用 0 填充缺失值。df.fillna(method='bfill')
:用后一个有效值填充(后向填充)。df.fillna(method='ffill')
:用前一个有效值填充(前向填充)。df.dropna()
:删除包含缺失值的行。
编程题
- 创建一个 DataFrame
sensor_readings
,包含'Temperature'
,'Humidity'
,'Pressure'
三列,以及 5 行数据。在'Temperature'
和'Humidity'
列中随机插入一些np.nan
值。 - 检查 DataFrame 中是否存在任何缺失值。
- 删除所有包含至少一个缺失值的行。
- 重新创建原始 DataFrame,并使用每列的均值来填充该列的缺失值。
- 打印每一步操作后的 DataFrame。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
t = [38, 26, np.nan, np.nan, 21]
humid = [90, np.nan, 90, 80, np.nan]
pressure = [89, 90, 65, 89, 28]
# 创建df:'Temperature', 'Humidity', 'Pressure' 三列,以及 5 行数据
s_data = {"Temperature": t, "Humidity": humid, "Pressure": pressure}
df = pd.DataFrame(data=s_data)
display(df)
# 检查 DataFrame 中是否存在任何缺失值
display(df.isnull())
# 删除所有包含至少一个缺失值的行
df_drop_na = df.dropna(how="any")
display(df_drop_na)
# 重新创建原始 DataFrame,并使用每列的均值来填充该列的缺失值
df_new = pd.DataFrame()
df_new["Temperature"] = df["Temperature"].fillna(df["Temperature"].mean()).round(2)
df_new["Humidity"] = df["Humidity"].fillna(df["Humidity"].mean()).round(2)
df_new["Pressure"] = df["Pressure"]
display(df_new)
3.指定行或者列过滤
除了基于条件过滤,我们还可以直接通过列名或行索引来删除指定的行或列。
- 删除列:
del df['ColumnName']
:直接删除指定的列。这是 Python 原生的del
关键字,会直接修改 DataFrame。df.drop(labels, axis=1, inplace=False)
:删除指定的列。labels
: 要删除的列名(单个字符串或列表)。axis=1
(或'columns'
):表示删除列。inplace
: 布尔值,如果为True
,则直接修改原始 DataFrame。
- 删除行:
df.drop(labels, axis=0, inplace=False)
:删除指定的行。labels
: 要删除的行索引(单个值或列表)。axis=0
(或'index'
):表示删除行。
删除行或列涉及到 DataFrame 内部数据结构的重组。
- 删除列: 当删除一列时,
pandas
会从内部的 Series 字典中移除对应的 Series,并更新列索引。这个操作通常比较高效,因为 Series 本身是独立的。- 删除行: 当删除行时,
pandas
需要创建一个新的 DataFrame,将未被删除的行复制到新的内存空间中。这是因为 DataFrame 的行通常是连续存储的,删除中间的行会破坏这种连续性,需要重新排列。因此,删除行通常会涉及数据复制。
inplace=True
是一个方便的参数,它允许函数直接修改原始 DataFrame,而不需要返回新的 DataFrame。这可以节省内存,但在链式操作中需要谨慎使用,因为它会改变原始对象。在现代 pandas
实践中,更推荐返回新 DataFrame,然后将其赋值给变量,以提高代码的可读性和可预测性。
【1】使用del 删除指定的列,会改变原来的df:
1
2
3
4
5
6
7
8
9
10
11
data = {
"color": ['red', 'blue', 'red', 'green', 'blue', None, 'red'],
'price': [10, 20, 10, 15, 20, 0, np.nan],
'col3': [10, 20, np.nan, np.nan, 80, np.nan, np.nan],
'col4': [10, 20, 90, 80, np.nan, np.nan, np.nan]
}
df = pd.DataFrame(data=data)
display(df)
# del df[""]
del df["col4"]
display(df)
【2】df.drop()
删除一列,会返回新的df:
1
2
df_drop_col3 = df.drop(labels=["col3"], axis=1)
display(df_drop_col3, df)
也可以删除多列:
1
2
3
4
5
6
7
8
9
10
11
12
data = {
"color": ['red', 'blue', 'red', 'green', 'blue', None, 'red'],
'price': [10, 20, 10, 15, 20, 0, np.nan],
'col3': [10, 20, np.nan, np.nan, 80, np.nan, np.nan],
'col4': [10, 20, 90, 80, np.nan, np.nan, np.nan],
"col5": np.random.randint(0, 101, (7, ))
}
df = pd.DataFrame(data=data)
display(df)
# 删除col3,col5这2列:
df_ = df.drop(labels=["col3", "col5"], axis=1)
display(df_, df)
【3】删除行:
1
2
3
4
5
6
7
8
9
10
11
data = {
"color": ['red', 'blue', 'red', 'green', 'blue', None, 'red'],
'price': [10, 20, 10, 15, 20, 0, np.nan],
'col3': [10, 20, np.nan, np.nan, 80, np.nan, np.nan],
'col4': [10, 20, 90, 80, np.nan, np.nan, np.nan],
"col5": np.random.randint(0, 101, (7, ))
}
df = pd.DataFrame(data=data)
display(df)
df_ = df.drop(labels=[0, 1, 4], axis=0)
display(df_, df)
【4】inplace=True
会直接修改原来的df
1
2
3
4
5
6
7
8
9
10
11
data = {
"color": ['red', 'blue', 'red', 'green', 'blue', None, 'red'],
'price': [10, 20, 10, 15, 20, 0, np.nan],
'col3': [10, 20, np.nan, np.nan, 80, np.nan, np.nan],
'col4': [10, 20, 90, 80, np.nan, np.nan, np.nan],
"col5": np.random.randint(0, 101, (7, ))
}
df = pd.DataFrame(data=data)
display(df)
df.drop(labels=[0, 1, 4], axis=0, inplace=True)
display(df)
选择题
-
给定
df = pd.DataFrame({'A': [1, 2], 'B': [3, 4]})
,以下哪个操作会删除'B'
列并修改原始 DataFrame?A.
df.drop(columns='B')
B.
del df['B']
C.
df.drop(labels='B', axis=1, inplace=False)
D.
df.drop('B', axis=0)
答案:B
-
df.drop(labels=[0, 2], axis=0)
的作用是什么?A. 删除列名为
0
和2
的列。B. 删除行索引为
0
和2
的行。C. 删除第 0 列和第 2 列。
D. 删除第 0 行和第 2 行。
答案:B
编程题
- 创建一个 DataFrame
inventory
,包含'Item'
,'Category'
,'Stock'
三列和 6 行数据。 - 删除
'Category'
列。 - 删除行索引为 2 和 4 的行。
- 打印每一步操作后的 DataFrame。
1
2
3
4
5
6
7
8
9
10
11
12
item = ["Apple", "Banana", "Orange", "Hat", "Shoes", "Flower"]
category = ["fruit", "fruit", "fruit", "clothes", "clothes", "dec"]
stock = np.random.randint(0, 151, (6, ))
data = {"Item": item, "Category": category, "Stock": stock}
df = pd.DataFrame(data=data)
display(df)
# 删除 'Category' 列。
del df["Category"]
display(df)
# 删除行索引为 2 和 4 的行。
df_ = df.drop(labels=[2, 4], axis=0)
display(df_, df)
4.函数 filter
使用
df.filter()
方法提供了一种灵活的方式来根据索引或列名进行筛选,支持精确匹配、模糊匹配和正则表达式匹配。
- 基本语法:
df.filter(items=None, like=None, regex=None, axis=None)
items
: 列表,用于精确匹配行索引或列标签。只保留列表中存在的标签。like
: 字符串,用于模糊匹配行索引或列标签。保留包含该字符串的标签。regex
: 字符串,用于正则表达式匹配行索引或列标签。保留符合正则表达式的标签。axis
: 指定在哪个轴上进行过滤。0
(或'index'
):在行索引上过滤。1
(或'columns'
):在列标签上过滤。None
(默认):如果提供了items
,则默认为columns
;如果提供了like
或regex
,则默认为index
。为了明确起见,建议始终指定axis
。
filter()
方法在底层会遍历指定轴上的所有标签,并根据items
、like
或regex
参数提供的条件进行匹配。匹配成功的标签会被保留,不匹配的则被过滤掉。这个操作会返回一个新的 DataFrame,其中只包含满足条件的行或列。
filter
的实用场景
- 选择特定模式的列: 例如,选择所有以
_id
结尾的列,或所有包含temp
的列。 - 选择特定模式的行: 例如,选择所有以
user_
开头的用户 ID 的行。 - 批量重命名前的预筛选: 在对大量列进行操作前,先用
filter
筛选出感兴趣的列。
【1】使用items精确筛选列标签:
1
2
3
4
5
6
7
8
data = np.array([
[3, 7, 1],
[2, 8, 256]
])
df = pd.DataFrame(data=data, index=["dog_A", "dog_B"], columns=["China_ID", "America_Code", "France_key"])
display(df)
df1 = df.filter(items=["China_ID", "France_key"])
display(df1)
【2】正则表达式筛选列标签:(以e结尾的列名)
1
2
df2 = df.filter(regex="e$", axis=1)
display(df2)
还有比如,包含ID的列名:
1
2
df3 = df.filter(regex="ID", axis = 1)
df3
1
2
df6 = df.filter(regex="China|America", axis=1)
df6
【3】like模糊匹配:
1
2
df4 = df.filter(like="og", axis=0)
df4
1
2
df5 = df.filter(like="B", axis=0)
df5
选择题
-
给定
df = pd.DataFrame({'col_A': [1], 'col_B': [2], 'another_col': [3]})
,执行df.filter(like='col', axis=1)
的结果是什么?A. 包含
col_A
,col_B
,another_col
三列。B. 包含
col_A
,col_B
两列。C. 包含
col_A
一列。D. 报错。
答案:A
-
df.filter(regex='^C', axis=0)
的作用是?A. 选择所有以
C
开头的列。B. 选择所有以
C
开头的行索引。C. 选择所有包含
C
的列。D. 选择所有包含
C
的行索引。答案:B
编程题
- 创建一个 DataFrame
log_data
,包含'user_id_123'
,'event_type'
,'timestamp'
,'user_id_456'
四列,以及 5 行数据。 - 使用
df.filter()
筛选出所有列名中包含'user_id'
的列。 - 使用
df.filter()
筛选出所有行索引中包含数字的行(假设行索引是默认整数索引)。 - 打印每一步操作后的 DataFrame。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
uid123 = np.arange(0, 5)
event_type = list("ABCDE")
time = ["2025-07-23"] * 5
uid456 = np.arange(-1, 4)
log_data = pd.DataFrame(data={
"user_id_123": uid123,
"event_type": event_type,
"timestamp": time,
"user_id_456": uid456
})
display(log_data)
# 使用 df.filter() 筛选出所有列名中包含 'user_id' 的列。
res1 = log_data.filter(regex="user_id", axis=1)
display(res1)
# 使用 df.filter() 筛选出所有行索引中包含数字的行(假设行索引是默认整数索引)。
res2 = log_data.filter(regex=r"\d+", axis=0)
display(res2)
5.异常值过滤
异常值(Outliers)是数据集中与大多数数据点显著不同的观测值。它们可能是数据输入错误、测量误差,也可能是真实但罕见的事件。识别和处理异常值是数据清洗的关键步骤,因为异常值会对统计分析和机器学习模型的性能产生负面影响。
- 3\(\sigma\) 原则(经验法则):
- 对于服从正态分布的数据,约 99.7% 的数据点落在均值 (\(\mu\)) 的 pm3 倍标准差 (\(\sigma\)) 范围内。
- 因此,任何超出 (\(\mu−3\sigma,\mu+3\sigma\)) 范围的数据点都可以被视为异常值。
- 计算:
df.mean()
:计算均值。df.std()
:计算标准差。
- 条件筛选: 一旦识别出异常值,可以使用布尔索引来选择或删除它们。
df[condition]
:选择满足条件的行。df.drop(labels=index_to_drop, axis=0)
:根据行索引删除异常值行。
- 其他异常值检测方法:
- IQR (Interquartile Range) 方法: 对于非正态分布数据更鲁棒。异常值通常定义为小于 \(Q_1−1.5*IQR\) 或大于 \(Q_3+1.5*IQR\)的数据点,其中\(Q_1\) 是第一四分位数,\(Q_3\) 是第三四分位数,\(IQR=Q_3−Q_1\)。
- Z-score 方法: 衡量数据点距离均值有多少个标准差。通常,Z-score 超过 2 或 3 的数据点被视为异常值。
- 可视化方法: 箱线图(Box Plot)、散点图等可以直观地帮助识别异常值。
- 基于模型的方法: 孤立森林(Isolation Forest)、局部异常因子(Local Outlier Factor, LOF)等机器学习算法。
异常值过滤的原理是基于统计学或数据分布的假设。
- 3\(\sigma\) 原则: 假设数据近似服从正态分布,那么超出 3 倍标准差范围的数据点出现的概率非常低,因此被认为是异常的。
- IQR 方法: 这种方法不依赖于正态分布假设,而是基于数据的四分位数,因此对偏斜数据更具鲁棒性。它关注数据集中间 50% 的范围。
- 布尔索引的应用: 一旦识别出异常值的条件,就可以将其转换为布尔 Series 或 DataFrame,然后利用
pandas
的布尔索引功能来高效地选择或删除这些数据点。
【异常值的处理策】
- 删除: 最简单直接,但可能丢失信息,尤其是在样本量较小或异常值本身有意义时。
- 转换: 对异常值进行数学变换(如对数变换),使其更接近正态分布。
- 替换/填充: 用均值、中位数、或更复杂的插值方法替换异常值。
- 不处理: 如果异常值是真实且重要的,可能需要保留它们,但要在模型选择时考虑使用对异常值不敏感的模型(如基于树的模型)。
比如下面这个例子:
1
2
3
4
5
6
7
df2 = pd.DataFrame(data = np.random.randn(10000,3)) # 正态分布数据
display(df2)
# 3σ过滤异常值,σ即是标准差
cond = (df2 > 3*df2.std()).any(axis = 1)
index = df2[cond].index # 不满足条件的行索引
df3 = df2.drop(labels=index,axis = 0) # 根据行索引,进行数据删除
display(df3)