共计 8672 个字符,预计需要花费 22 分钟才能阅读完成。
在当今数据驱动的世界里,数据分析师和科学家们每天都在与海量、多源且结构复杂的现实数据打交道。这些数据往往分散在不同的表格、文件或数据库中,且充满了缺失值、重复项、格式不一致等“脏数据”。面对这样的挑战,Python 语言及其强大的数据处理库 Pandas 无疑是我们的得力助手。
本文将深入探讨 Pandas 在多表合并与复杂数据清洗方面的核心技巧和最佳实践,旨在帮助读者从实战角度提升数据处理能力,为后续的数据分析和建模奠定坚实基础。
数据分析的基石:理解多表合并的需求
现实世界的数据很少是单一、独立的。想象一下一个电商平台的数据:用户基础信息、订单详情、商品信息、物流状态可能分别存储在不同的数据表中。为了分析某个用户购买了哪些商品,或者某类商品的销售额,我们就需要将这些分散的信息有效地整合起来。这就是多表合并的核心需求。
Pandas 提供了 merge() 函数,它模拟了关系型数据库中的连接(JOIN)操作,能够基于一个或多个键(key)将不同的 DataFrame 连接起来。
pd.merge():你的数据整合利器
pd.merge() 是 Pandas 中最常用的多表合并函数。其基本语法如下:
pd.merge(left, right, how='inner', on=None, left_on=None, right_on=None, suffixes=('_x', '_y'))
left,right: 需要合并的两个 DataFrame。how: 指定合并类型,非常关键,包括'inner','outer','left','right'。on: 指定用于合并的列名,要求左右两个 DataFrame 中存在同名列。left_on,right_on: 当左右两个 DataFrame 的合并列名称不同时使用。suffixes: 当合并后出现重名列时,用于添加后缀以区分。
让我们通过一个简单的例子来理解不同 how 参数的作用。
import pandas as pd
# 示例数据
df_customers = pd.DataFrame({'customer_id': [1, 2, 3, 4],
'name': ['Alice', 'Bob', 'Charlie', 'David'],
'city': ['New York', 'Los Angeles', 'Chicago', 'Houston']
})
df_orders = pd.DataFrame({'order_id': [101, 102, 103, 104, 105],
'customer_id': [1, 2, 1, 5, 2],
'product': ['Laptop', 'Mouse', 'Keyboard', 'Monitor', 'Speaker'],
'price': [1200, 25, 75, 300, 50]
})
print("df_customers:n", df_customers)
print("ndf_orders:n", df_orders)
1. 内连接 (Inner Join) – how='inner'
内连接是默认的合并方式,只保留左右两个 DataFrame 中合并键(on 参数指定的列)都存在的行。
# 合并客户和订单信息,只保留有订单的客户
inner_merge = pd.merge(df_customers, df_orders, on='customer_id', how='inner')
print("nInner Merge:n", inner_merge)
# 结果只包含 customer_id 为 1 和 2 的客户及其订单,因为 customer_id 3 和 4 没有订单,5 没有客户信息。
2. 左连接 (Left Join) – how='left'
左连接保留左侧 DataFrame 的所有行,并根据合并键匹配右侧 DataFrame 的行。如果右侧没有匹配项,则填充 NaN。
# 合并客户和订单信息,保留所有客户,即使他们没有订单
left_merge = pd.merge(df_customers, df_orders, on='customer_id', how='left')
print("nLeft Merge:n", left_merge)
# 结果包含所有客户,customer_id 为 3 和 4 的客户的订单信息为 NaN。
3. 右连接 (Right Join) – how='right'
右连接保留右侧 DataFrame 的所有行,并根据合并键匹配左侧 DataFrame 的行。如果左侧没有匹配项,则填充 NaN。
# 合并客户和订单信息,保留所有订单,即使没有对应的客户信息
right_merge = pd.merge(df_customers, df_orders, on='customer_id', how='right')
print("nRight Merge:n", right_merge)
# 结果包含所有订单,customer_id 为 5 的订单的客户信息为 NaN。
4. 外连接 (Outer Join) – how='outer'
外连接保留左右两个 DataFrame 的所有行,并填充所有不匹配的项为 NaN。它实际上是左连接和右连接的并集。
# 合并客户和订单信息,保留所有客户和所有订单
outer_merge = pd.merge(df_customers, df_orders, on='customer_id', how='outer')
print("nOuter Merge:n", outer_merge)
# 结果包含所有客户和所有订单,不匹配的部分用 NaN 填充。
pd.concat():垂直或水平堆叠数据
除了 merge() 用于基于键值合并不同维度的数据外,pd.concat() 用于沿着某个轴(行或列)堆叠多个 DataFrame。
- 垂直堆叠 (Stacking,
axis=0): 当你有多个结构相同但数据不同的 DataFrame 时(例如,不同月份的销售数据),可以使用pd.concat(dfs, axis=0)将它们堆叠起来。 - 水平堆叠 (Appending,
axis=1): 当你需要将两个具有相同索引的 DataFrame 并排连接时,可以使用pd.concat(dfs, axis=1)。这与merge(..., left_index=True, right_index=True)有些相似,但concat更侧重于直接拼接。
df_sales_q1 = pd.DataFrame({'product_id': [1, 2], 'revenue': [100, 200]})
df_sales_q2 = pd.DataFrame({'product_id': [1, 3], 'revenue': [150, 300]})
# 垂直堆叠
all_sales = pd.concat([df_sales_q1, df_sales_q2], axis=0, ignore_index=True)
print("nConcatenated (vertically):n", all_sales)
# 水平堆叠 (假设有相同索引,这里为了演示,索引保持默认)
df_info = pd.DataFrame({'product_id': [1, 2], 'category': ['A', 'B']})
df_price = pd.DataFrame({'price': [10, 20]}, index=[0, 1]) # 确保索引匹配
horizontal_concat = pd.concat([df_info, df_price], axis=1)
print("nConcatenated (horizontally):n", horizontal_concat)
复杂数据清洗技巧:让数据焕然一新
数据清洗是数据分析过程中最耗时但又至关重要的一步。一份“干净”的数据能确保后续分析的准确性和可靠性。以下是一些常见的复杂数据清洗技巧。
1. 处理缺失值 (Missing Values)
缺失值(通常表示为 NaN 或 None)是数据中最常见的问题之一。
-
检测缺失值 :
df.isnull().sum()或df.isna().sum()可以快速统计每列的缺失值数量。df.info()也能提供非空值的数量,间接反映缺失情况。
-
处理策略 :
- 删除 (Dropping):
df.dropna(axis=0): 删除包含任何缺失值的行。df.dropna(axis=1): 删除包含任何缺失值的列。df.dropna(subset=['col1', 'col2']): 只考虑特定列的缺失值来删除行。df.dropna(thresh=N): 删除非缺失值数量少于N的行。
删除通常适用于缺失值比例很小,或者缺失值对分析不重要的场景。
- 填充 (Imputation):
df.fillna(value): 用一个固定值填充所有缺失值。df.fillna(method='ffill')(forward fill): 用前一个非缺失值填充。df.fillna(method='bfill')(backward fill): 用后一个非缺失值填充。df['col'].fillna(df['col'].mean()): 用列的均值填充。df['col'].fillna(df['col'].median()): 用列的中位数填充。df['col'].fillna(df['col'].mode()[0]): 用列的众数填充。
填充是保留数据量的好方法,但需谨慎选择填充策略,避免引入偏差。
- 删除 (Dropping):
import numpy as np
df_data = pd.DataFrame({'A': [1, 2, np.nan, 4, 5],
'B': [np.nan, 2, 3, 4, np.nan],
'C': [10, 20, 30, np.nan, 50]
})
print("nOriginal DataFrame with NaNs:n", df_data)
# 填充 B 列的缺失值用均值,C 列的缺失值用前一个值
df_filled = df_data.copy()
df_filled['B'].fillna(df_filled['B'].mean(), inplace=True)
df_filled['C'].fillna(method='ffill', inplace=True)
print("nDataFrame after filling NaNs:n", df_filled)
# 删除所有含有 NaN 的行
df_dropped = df_data.dropna()
print("nDataFrame after dropping NaNs:n", df_dropped)
2. 处理重复数据 (Duplicate Data)
重复数据会扭曲统计结果,影响模型训练。
- 检测重复值 :
df.duplicated()返回一个布尔 Series,指示每一行是否是重复行(除了第一次出现)。df.duplicated().sum()统计重复行数量。 - 删除重复值 :
df.drop_duplicates()删除所有重复行,默认保留第一次出现的行。df.drop_duplicates(subset=['col1', 'col2']): 只考虑特定列的组合来检测和删除重复项。df.drop_duplicates(keep='last'): 保留最后一次出现的重复行。
df_dups = pd.DataFrame({'col1': ['A', 'B', 'A', 'C', 'B'],
'col2': [1, 2, 1, 3, 2],
'col3': [10, 20, 10, 30, 25]
})
print("nOriginal DataFrame with duplicates:n", df_dups)
# 检测重复行
print("nDuplicated rows:n", df_dups.duplicated())
# 删除所有重复行
df_no_dups = df_dups.drop_duplicates()
print("nDataFrame after dropping all duplicates:n", df_no_dups)
# 基于 col1 和 col2 的组合删除重复行
df_subset_no_dups = df_dups.drop_duplicates(subset=['col1', 'col2'])
print("nDataFrame after dropping duplicates on subset ['col1','col2']:n", df_subset_no_dups)
3. 数据类型转换 (Data Type Conversion)
不正确的数据类型会导致计算错误或内存浪费。
df.dtypes: 查看各列数据类型。df['col'].astype(new_type): 强制转换列的数据类型。pd.to_numeric(series, errors='coerce'): 将 Series 转换为数值类型,errors='coerce'会将无法转换的值变为NaN。pd.to_datetime(series, errors='coerce'): 将 Series 转换为日期时间类型。
df_types = pd.DataFrame({'id': ['1', '2', '3'],
'value': ['100', '200a', '300'],
'date_str': ['2023-01-01', '2023-01-02', 'invalid_date']
})
print("nOriginal DataFrame dtypes:n", df_types.dtypes)
# 转换 id 为整数
df_types['id'] = df_types['id'].astype(int)
# 转换 value 为数值,错误处理为 NaN
df_types['value'] = pd.to_numeric(df_types['value'], errors='coerce')
# 转换 date_str 为日期时间,错误处理为 NaT (Not a Time)
df_types['date_str'] = pd.to_datetime(df_types['date_str'], errors='coerce')
print("nDataFrame after type conversion:n", df_types)
print("nNew DataFrame dtypes:n", df_types.dtypes)
4. 文本数据清洗 (Text Data Cleaning)
文本数据(字符串)常包含格式不一致、大小写、空格或特殊字符等问题。Pandas 的 .str 访问器提供了丰富的字符串操作方法。
df['col'].str.lower(): 转换为小写。df['col'].str.upper(): 转换为大写。df['col'].str.strip(): 移除字符串两端的空格。df['col'].str.replace('old', 'new'): 替换字符串。df['col'].str.contains('pattern'): 检测是否包含某个模式(支持正则表达式)。df['col'].str.extract('pattern'): 从字符串中提取信息(支持正则表达式)。
df_text = pd.DataFrame({'product_name': ['Laptop', 'MousePAD', 'Keyboard-RGB', 'Monitor_4K']
})
print("nOriginal Text Data:n", df_text)
# 移除空格并转换为小写,替换特殊字符
df_text['product_name_cleaned'] = df_text['product_name'].str.strip().str.lower()
df_text['product_name_cleaned'] = df_text['product_name_cleaned'].str.replace('-', '_')
print("nCleaned Text Data:n", df_text)
# 提取包含数字的产品
df_text['has_number'] = df_text['product_name_cleaned'].str.contains(r'd')
print("nText Data with'has_number'column:n", df_text)
5. 异常值检测与处理 (Outlier Detection and Treatment)
异常值(Outliers)是数据集中显著偏离其他观测值的点,它们可能是真实的数据波动,也可能是测量误差。
- 检测方法 :
- 统计方法 : Z-score (适用于正态分布数据), IQR (四分位距, 适用于非正态分布数据)。
- 可视化 : 箱线图 (Box Plot)、散点图。
- 处理策略 :
- 删除 : 直接删除异常值所在的行。
- 替换 : 用均值、中位数或临近值替换异常值。
- 变换 : 对数据进行对数、平方根等数学变换,以减小异常值的影响。
# 示例:使用 IQR 方法检测和处理异常值
df_outliers = pd.DataFrame({'value': [10, 12, 11, 15, 13, 100, 14, 16]})
print("nOriginal DataFrame with outliers:n", df_outliers)
Q1 = df_outliers['value'].quantile(0.25)
Q3 = df_outliers['value'].quantile(0.75)
IQR = Q3 - Q1
lower_bound = Q1 - 1.5 * IQR
upper_bound = Q3 + 1.5 * IQR
# 识别异常值
outliers = df_outliers[(df_outliers['value'] < lower_bound) | (df_outliers['value'] > upper_bound)]
print("nDetected outliers:n", outliers)
# 处理策略:删除异常值
df_no_outliers = df_outliers[(df_outliers['value'] >= lower_bound) & (df_outliers['value'] <= upper_bound)]
print("nDataFrame after removing outliers:n", df_no_outliers)
# 处理策略:用中位数替换异常值
df_filled_outliers = df_outliers.copy()
median_val = df_outliers['value'].median()
df_filled_outliers.loc[(df_filled_outliers['value'] < lower_bound) | (df_filled_outliers['value'] > upper_bound), 'value'] = median_val
print("nDataFrame after replacing outliers with median:n", df_filled_outliers)
6. 数据转换与特征工程 (Data Transformation & Feature Engineering)
数据清洗不仅仅是“去除坏数据”,还包括将现有数据转换为更有用形式,甚至从现有数据中创建新特征,这被称为特征工程。
apply(): 对 DataFrame 的行或列应用函数。map(): 对 Series 的每个元素应用函数。groupby(): 分组操作,常用于聚合计算。
df_features = pd.DataFrame({'price': [100, 200, 150, 300],
'quantity': [2, 1, 3, 1],
'category': ['A', 'B', 'A', 'C']
})
print("nOriginal DataFrame for feature engineering:n", df_features)
# 创建新特征:总价
df_features['total_price'] = df_features['price'] * df_features['quantity']
# 使用 apply 对 category 进行转换
df_features['category_encoded'] = df_features['category'].apply(lambda x: 1 if x == 'A' else 0)
# 使用 groupby 计算每个类别的平均总价
avg_price_by_category = df_features.groupby('category')['total_price'].mean().reset_index()
print("nDataFrame after feature engineering:n", df_features)
print("nAverage total price by category:n", avg_price_by_category)
总结与展望
掌握 Pandas 的多表合并与复杂数据清洗技巧,是每一位数据专业人士的必备能力。通过 pd.merge() 和 pd.concat(),我们能有效地整合来自不同源的数据;而针对缺失值、重复项、数据类型、文本格式以及异常值的处理,则确保了我们分析结果的准确性和模型的健壮性。
数据清洗是一个迭代的过程,它需要耐心、领域知识和不断的实践。没有一劳永逸的清洗方案,每个数据集都有其独特的“脏”点。然而,一旦你熟练掌握了 Pandas 的这些强大功能,你将能够更自信、更高效地处理各种数据挑战,从而从数据中提取出真正的价值。
希望本文能为你提供一个坚实的基础和实用的指南。现在,拿起你的 Python 和 Pandas,开始你的数据分析实战之旅吧!