提升数据分析效率:Python Pandas 多表合并与复杂数据清洗实战指南

6次阅读
没有评论

共计 8544 个字符,预计需要花费 22 分钟才能阅读完成。

在当今数据驱动的世界里,数据分析师和数据科学家扮演着至关重要的角色。然而,原始数据往往是杂乱无章、来源多样且充满瑕疵的。要从中提取有价值的洞见,必须经过两道关键工序:多表合并 复杂数据清洗。Python 的 Pandas 库,以其强大的数据结构和丰富的功能,成为了处理这些挑战的瑞士军刀。

本文将作为一份详尽的实战指南,深入探讨如何利用 Pandas 库高效地进行多表合并,以及如何运用一系列高级技巧来应对真实世界中复杂的 数据清洗 问题。无论你是数据分析新手,还是希望提升 Pandas 数据处理 能力的资深人士,本文都将为你提供宝贵的见解和可操作的策略。

第一部分:Pandas 多表合并实战

现实世界的数据通常不是单一的表格,而是分散在多个不同的数据源中。例如,你可能有客户信息表、订单明细表、产品目录表等等。要进行全面的分析,比如找出哪个地区的客户购买了最畅销的产品,你就需要将这些表格有效地关联起来。Pandas 多表合并 功能正是为此而生。

1. pd.merge():基于键的合并利器

pd.merge() 函数是 Pandas 中实现类似 SQL JOIN 操作的核心工具。它允许你通过一个或多个公共列(键)将两个或多个 DataFrame 连接起来。

import pandas as pd
import numpy as np

# 创建示例数据
df_customers = pd.DataFrame({'customer_id': [1, 2, 3, 4, 5],
    'name': ['Alice', 'Bob', 'Charlie', 'David', 'Eve'],
    'city': ['New York', 'Los Angeles', 'Chicago', 'Houston', 'Phoenix']
})

df_orders = pd.DataFrame({'order_id': [101, 102, 103, 104, 105, 106],
    'customer_id': [1, 2, 1, 3, 2, 6], # 注意客户 ID 6 不在 df_customers 中
    'product': ['Laptop', 'Mouse', 'Keyboard', 'Monitor', 'Printer', 'Webcam'],
    'amount': [1200, 25, 75, 300, 150, 50]
})

print("客户数据 (df_customers):n", df_customers)
print("n 订单数据 (df_orders):n", df_orders)

合并类型 (how 参数):

  • inner (内连接):只保留两个 DataFrame 中都存在的合并键对应的行。这是默认设置。
    inner_merge = pd.merge(df_customers, df_orders, on='customer_id', how='inner')
    print("n 内连接 (inner_merge):n", inner_merge)
    # 结果只包含 customer_id 1, 2, 3 的订单,因为它们在两个表中都有。
  • left (左连接):保留左边 DataFrame 的所有行,并匹配右边 DataFrame 中对应的行。如果右边没有匹配项,则填充 NaN
    left_merge = pd.merge(df_customers, df_orders, on='customer_id', how='left')
    print("n 左连接 (left_merge):n", left_merge)
    # 结果包含所有客户的信息,即使他们没有订单 (customer_id 4, 5 的订单信息为 NaN)。
  • right (右连接):保留右边 DataFrame 的所有行,并匹配左边 DataFrame 中对应的行。如果左边没有匹配项,则填充 NaN
    right_merge = pd.merge(df_customers, df_orders, on='customer_id', how='right')
    print("n 右连接 (right_merge):n", right_merge)
    # 结果包含所有订单的信息,即使客户信息不存在 (customer_id 6 的客户信息为 NaN)。
  • outer (外连接):保留两个 DataFrame 中的所有行。如果任一表没有匹配项,则填充 NaN
    outer_merge = pd.merge(df_customers, df_orders, on='customer_id', how='outer')
    print("n 外连接 (outer_merge):n", outer_merge)
    # 结果包含所有客户和所有订单的信息,不匹配的部分用 NaN 填充。

其他重要参数:

  • left_on, right_on:当两个 DataFrame 的合并键列名不同时使用。
  • suffixes:当两个 DataFrame 中存在同名但非合并键的列时,用于添加后缀以区分。

2. pd.concat():堆叠与拼接的艺术

pd.concat() 函数用于将多个 DataFrame 沿指定轴(行或列)进行堆叠或拼接。它更像是 SQL 中的 UNION ALL 或简单的列添加操作。

# 创建更多示例数据
df_sales_q1 = pd.DataFrame({'month': ['Jan', 'Feb'], 'sales': [100, 120]})
df_sales_q2 = pd.DataFrame({'month': ['Mar', 'Apr'], 'sales': [110, 130]})

# 沿行堆叠 (axis=0, 默认)
combined_sales = pd.concat([df_sales_q1, df_sales_q2], ignore_index=True)
print("n 沿行堆叠 (combined_sales):n", combined_sales)

# 沿列拼接 (axis=1)
df_info = pd.DataFrame({'id': [1, 2], 'name': ['A', 'B']})
df_data = pd.DataFrame({'value1': [10, 20], 'value2': [30, 40]})
combined_cols = pd.concat([df_info, df_data], axis=1)
print("n 沿列拼接 (combined_cols):n", combined_cols)

pd.concat() 的一个常见用途是合并结构相同但数据分批存储的文件(例如,按月份保存的销售数据),或者将具有不同但互补信息的 DataFrame 组合起来。ignore_index=True 参数在行堆叠时非常有用,它可以重置索引,避免重复。

3. pd.join():索引合并的简洁之道

pd.join() 方法是 pd.merge() 在特定场景下的一个语法糖,它默认使用 DataFrame 的索引进行合并。当一个 DataFrame 的索引是另一个 DataFrame 的列时,或者当两个 DataFrame 具有相同的索引时,join 会显得非常简洁。

df_a = pd.DataFrame({'value': [1, 2, 3]}, index=['a', 'b', 'c'])
df_b = pd.DataFrame({'data': [10, 20]}, index=['a', 'c'])

joined_df = df_a.join(df_b, how='left') # 默认 left join
print("n 使用 join 合并 (joined_df):n", joined_df)

多表合并的实战场景与注意事项:

  • 选择合适的合并方法merge 适用于基于键的复杂关系(一对一、一对多、多对多),而 concat 更适用于简单的数据堆叠或基于索引的直接拼接。
  • 性能考虑:对于非常大的数据集,合并操作可能会消耗大量内存和时间。确保合并键的数据类型一致,并考虑使用更高效的数据结构或分块处理。
  • 数据完整性检查:合并后,检查行数是否符合预期,是否有意外的 NaN 值产生,这可以帮助你发现合并逻辑中的错误。

第二部分:Pandas 复杂数据清洗技巧

原始数据中充满了各种问题,如缺失值、重复项、格式不一致、类型错误、异常值以及逻辑错误。仅仅删除或简单填充缺失值远远不够。本节将深入探讨 Pandas 数据清洗 的各种高级技巧,帮助你将杂乱的数据转化为可分析的资产。

1. 处理缺失值:策略与艺术

除了简单的 dropna()fillna(),Pandas 提供了更灵活的缺失值处理策略。

df_data = pd.DataFrame({'category': ['A', 'B', 'A', 'C', 'B', 'A', 'C'],
    'value': [10, 20, np.nan, 40, 50, np.nan, 70],
    'price': [100, 120, 110, np.nan, 130, 105, 140]
})
print("n 原始数据 (df_data):n", df_data)

# 1. 均值 / 中位数 / 众数填充
# df_data['value'] = df_data['value'].fillna(df_data['value'].mean())

# 2. 前向 / 后向填充
# df_data['value'] = df_data['value'].fillna(method='ffill')
# df_data['value'] = df_data['value'].fillna(method='bfill')

# 3. 按组填充:例如,按 'category' 填充各自的均值
df_data['value_filled_by_group'] = df_data.groupby('category')['value'].transform(lambda x: x.fillna(x.mean()))
print("n 按组填充缺失值:n", df_data)

# 4. 插值填充:适用于时间序列或有序数据
# df_data['price'] = df_data['price'].interpolate()
# print("n 插值填充缺失值:n", df_data)

按组填充在很多场景下非常有用,比如按地区、产品类型或时间段填充缺失值,可以保持数据的内在结构。

2. 识别与处理重复值

重复数据会扭曲统计结果,必须加以处理。

df_duplicates = pd.DataFrame({'col1': ['A', 'B', 'A', 'C', 'B'],
    'col2': [1, 2, 1, 3, 2],
    'col3': ['x', 'y', 'x', 'z', 'p']
})
print("n 含重复值数据 (df_duplicates):n", df_duplicates)

# 识别重复行
print("n 重复行布尔掩码:n", df_duplicates.duplicated()) # 默认判断所有列

# 识别基于特定列的重复
print("n 基于 col1 和 col2 的重复布尔掩码:n", df_duplicates.duplicated(subset=['col1', 'col2']))

# 删除重复行 (默认保留第一个)
df_no_duplicates = df_duplicates.drop_duplicates()
print("n 删除所有列重复后的数据:n", df_no_duplicates)

# 删除基于特定列的重复,保留最后一个
df_no_duplicates_subset = df_duplicates.drop_duplicates(subset=['col1', 'col2'], keep='last')
print("n 删除基于'col1','col2'重复并保留最后一个:n", df_no_duplicates_subset)

3. 统一数据格式与类型转换

数据类型不匹配是常见问题,特别是从外部源导入数据时。

df_types = pd.DataFrame({'id': ['1', '2', '3a', '4'],
    'price_str': ['10.5', '20.0', '15.75', 'Error'],
    'date_str': ['2023-01-01', '2023-02-15', '2023-3-5', 'invalid date'],
    'description': ['Apple', 'Banana', 'orange', 'apple']
})
print("n 原始数据 (df_types):n", df_types)

# 强制类型转换 (astype)
# df_types['id'] = df_types['id'].astype(int) # 会报错因为 '3a'
# 更好的做法是使用 pd.to_numeric 并处理错误
df_types['id_numeric'] = pd.to_numeric(df_types['id'], errors='coerce') # '3a' 会变成 NaN

# 字符串清洗
df_types['description_cleaned'] = df_types['description'].str.strip().str.lower()
df_types['contains_apple'] = df_types['description'].str.contains('apple', case=False)

# 日期时间转换
df_types['date_parsed'] = pd.to_datetime(df_types['date_str'], errors='coerce') # 'invalid date' 会变成 NaT
df_types['year'] = df_types['date_parsed'].dt.year # 提取年份
df_types['month'] = df_types['date_parsed'].dt.month # 提取月份

# 数值字符串安全转换
df_types['price_numeric'] = pd.to_numeric(df_types['price_str'], errors='coerce')

print("n 清洗后的数据 (df_types):n", df_types)

pd.to_numeric()pd.to_datetime()errors='coerce' 参数非常有用,它能将无法转换的值变为 NaNNaT,而不是抛出错误,从而允许你进一步处理这些无效值。

4. 异常值处理:识别与纠正

异常值(或离群值)可能极大地影响分析结果。识别它们并决定如何处理至关重要。

  • 识别方法
    • 统计方法
      • Z-score:如果数据服从正态分布,超出几个标准差(例如,2 或 3 个)的值可能是异常值。
      • IQR (四分位距):通常,低于 Q1 – 1.5 IQR 或高于 Q3 + 1.5 IQR 的值被认为是异常值。
    • 可视化方法:箱线图 (Box plot) 是识别异常值最直观的方法。散点图也可以帮助发现多变量的异常模式。
  • 处理策略
    • 删除:如果异常值是数据录入错误或损坏,且数量不多,可以考虑删除。
    • 替换:用均值、中位数或某个边界值替换异常值。
    • 转换:对数据进行对数或平方根转换,以减少异常值的影响。
# 示例:使用 IQR 识别异常值
df_outliers = pd.DataFrame({'data': np.random.normal(loc=100, scale=10, size=50),
    'values': [90, 95, 100, 105, 110, 1000, 10, 115, 120, 80] + list(np.random.normal(loc=100, scale=10, size=40))
})

Q1 = df_outliers['values'].quantile(0.25)
Q3 = df_outliers['values'].quantile(0.75)
IQR = Q3 - Q1

lower_bound = Q1 - 1.5 * IQR
upper_bound = Q3 + 1.5 * IQR

# 标记异常值
df_outliers['is_outlier'] = (df_outliers['values'] < lower_bound) | (df_outliers['values'] > upper_bound)
print("n 异常值标记 (使用 IQR):n", df_outliers[df_outliers['is_outlier']])

# 处理策略:用中位数替换异常值
df_outliers.loc[df_outliers['is_outlier'], 'values_cleaned'] = df_outliers['values'].median()
df_outliers['values_cleaned'].fillna(df_outliers['values'], inplace=True) # 将非异常值复制过来
# print("n 替换异常值后的数据:n", df_outliers)

5. 数据一致性与规范化

对于分类或文本数据,一致性是确保准确分析的关键。

df_consistency = pd.DataFrame({'gender': ['Male', 'male', 'Female', 'M', 'F', 'male'],
    'product_name': ['iphone 15', 'iPhone 15', 'Galaxy S23', 'Iphone 15 Pro']
})

# 统一分类数据:使用 map 或 replace
gender_mapping = {'Male': 'Male', 'male': 'Male', 'M': 'Male', 'Female': 'Female', 'F': 'Female'}
df_consistency['gender_cleaned'] = df_consistency['gender'].map(gender_mapping)

# 统一文本数据:去除空格,统一大小写
df_consistency['product_name_cleaned'] = df_consistency['product_name'].str.strip().str.lower().str.replace('iphone', 'iPhone')
print("n 数据一致性清洗后:n", df_consistency)

# 更复杂的统一(例如,模糊匹配):可能需要第三方库如 fuzzywuzzy

对于更复杂的文本数据,如产品名称、地址等,可能需要结合正则表达式 (str.extract()) 和字符串相似度算法(例如,使用 fuzzywuzzy 库)来纠正错别字或合并同义词。

6. 高级清洗技巧:条件逻辑与函数应用

当内置方法无法满足需求时,自定义函数和条件逻辑是强大的补充。

  • apply(): 对 Series 或 DataFrame 的行 / 列应用函数。
  • applymap(): 对 DataFrame 的每个元素应用函数 (Python 3.11+ 推荐 df.map)。
  • pipe(): 链式调用函数,使代码更具可读性。
  • np.where()df.loc[]: 基于条件进行值替换或赋值。
# 示例:使用 apply 根据条件创建新列
df_complex = pd.DataFrame({'score': [75, 88, 62, 91, 45, 70],
    'attempts': [1, 2, 1, 3, 1, 2]
})

def assign_grade(row):
    if row['score'] >= 90:
        return 'A'
    elif row['score'] >= 80:
        return 'B'
    elif row['score'] >= 70:
        return 'C'
    else:
        return 'D'

df_complex['grade'] = df_complex.apply(assign_grade, axis=1)

# 使用 np.where 进行条件赋值
df_complex['pass_fail'] = np.where(df_complex['score'] >= 60, 'Pass', 'Fail')
print("n 高级清洗技巧应用:n", df_complex)

apply() 在处理复杂逻辑时非常灵活,允许你定义任何 Python 函数来转换数据。np.where() 则是进行简单条件判断并赋值的简洁方式。

最佳实践与建议

  1. 备份原始数据:在进行任何清洗操作之前,始终保留一份原始数据的副本。
  2. 逐步清洗,记录过程:每次只处理一个清洗任务,并记录下你所做的每一步,这对于重现结果和调试非常重要。
  3. 可视化辅助:利用 matplotlib, seaborn, plotly 等库进行数据可视化,可以直观地发现数据中的模式、异常值和问题。
  4. 分块处理大数据:对于内存无法一次性加载的大型数据集,考虑分块读取和处理。
  5. 数据字典和元数据:维护一个数据字典,清晰地定义每一列的含义、数据类型和允许的范围。
  6. 代码模块化:将常用的清洗函数封装起来,提高代码的复用性和可维护性。

总结

Python 数据分析实战 中,Pandas 多表合并 复杂数据清洗 是构建可靠数据分析管道的基石。通过熟练掌握 pd.merge()pd.concat() 等合并工具,以及缺失值处理、重复值删除、类型转换、字符串操作、异常值识别和高级函数应用等清洗技巧,你将能够自信地应对各种真实世界的数据挑战。

数据清洗并非一劳永逸的过程,它需要耐心、经验和对数据的深刻理解。然而,投入时间和精力进行彻底的清洗,将极大地提升后续分析的质量和洞见的准确性。现在,是时候将这些 Pandas 技巧 付诸实践,让你的数据分析之旅更加高效和富有成效!

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