Python 数据分析实战:掌握 Pandas 多表合并与复杂数据清洗的艺术

17次阅读
没有评论

共计 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)

缺失值(通常表示为 NaNNone)是数据中最常见的问题之一。

  • 检测缺失值 :

    • 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]): 用列的众数填充。
        填充是保留数据量的好方法,但需谨慎选择填充策略,避免引入偏差。
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,开始你的数据分析实战之旅吧!

正文完
 0
评论(没有评论)