跳到主要内容

数据转换

数据转换是数据分析的核心环节,Pandas 提供了丰富的工具来转换、映射和重塑数据。本章介绍如何使用 applymaptransform 等方法高效地进行数据转换。

函数应用概述

Pandas 提供了多个层次的数据转换方法:

方法应用范围返回值适用场景
map()Series 元素级Series单列值映射
apply()Series 或 DataFrame 行/列Series/DataFrame复杂行/列操作
applymap()DataFrame 元素级DataFrame所有元素转换
transform()Series 或 DataFrameSeries/DataFrame与 groupby 配合使用

map 方法

map() 是 Series 专用的方法,用于将 Series 中的每个值映射为新值。它接受函数、字典或 Series 作为映射规则。

使用函数映射

import pandas as pd
import numpy as np

# 创建示例数据
s = pd.Series(['apple', 'banana', 'cherry', 'date'])

# 使用函数转换
result = s.map(str.upper)
print(result)
# 0 APPLE
# 1 BANANA
# 2 CHERRY
# 3 DATE

# 使用 lambda 函数
result = s.map(lambda x: x[:3].upper())
print(result)
# 0 APP
# 1 BAN
# 2 CHA
# 3 DAT

使用字典映射

字典映射是 map() 最常见的用法,特别适合类别转换:

# 创建员工数据
df = pd.DataFrame({
'name': ['张三', '李四', '王五', '赵六'],
'department': ['技术', '销售', '技术', '人事'],
'level': ['A', 'B', 'A', 'C']
})

# 使用字典映射部门代码
dept_code = {'技术': 'TECH', '销售': 'SALES', '人事': 'HR'}
df['dept_code'] = df['department'].map(dept_code)
print(df)
# name department level dept_code
# 0 张三 技术 A TECH
# 1 李四 销售 B SALES
# 2 王五 技术 A TECH
# 3 赵六 人事 C HR

# 级别对应薪资
level_salary = {'A': 20000, 'B': 15000, 'C': 10000}
df['base_salary'] = df['level'].map(level_salary)

字典中不存在的键会被映射为 NaN,如果需要保留原值,可以结合 fillna()

# 不完整的映射字典
partial_map = {'技术': 'TECH'}
result = df['department'].map(partial_map)
print(result)
# 0 TECH
# 1 NaN
# 2 TECH
# 3 NaN

# 保留原值
result = df['department'].map(partial_map).fillna(df['department'])
print(result)
# 0 TECH
# 1 销售
# 2 TECH
# 3 人事

使用 Series 映射

Series 也可以作为映射规则,其索引作为键:

# 创建映射 Series
mapping = pd.Series({'技术': 'Technology', '销售': 'Sales', '人事': 'HR'})
df['dept_en'] = df['department'].map(mapping)

apply 方法

apply() 是最灵活的转换方法,可以对 Series 或 DataFrame 应用自定义函数。

Series 的 apply

对 Series 使用 apply() 时,函数会应用到每个元素:

# 示例数据
s = pd.Series([1, 2, 3, 4, 5])

# 简单计算
result = s.apply(lambda x: x ** 2)
print(result)
# 0 1
# 1 4
# 2 9
# 3 16
# 4 25

# 条件转换
def categorize(x):
if x < 3:
return '低'
elif x < 4:
return '中'
else:
return '高'

result = s.apply(categorize)
print(result)
# 0 低
# 1 低
# 2 中
# 3 高
# 4 高

DataFrame 的 apply

对 DataFrame 使用时,可以指定轴向(axis):

# 创建示例数据
df = pd.DataFrame({
'A': [1, 2, 3],
'B': [4, 5, 6],
'C': [7, 8, 9]
})

# 默认 axis=0,对每列应用函数
col_sum = df.apply(lambda x: x.sum())
print(col_sum)
# A 6
# B 15
# C 24

# axis=1,对每行应用函数
row_sum = df.apply(lambda x: x.sum(), axis=1)
print(row_sum)
# 0 12
# 1 15
# 2 18

# 返回多个值的聚合
def describe_column(col):
return pd.Series({
'mean': col.mean(),
'std': col.std(),
'min': col.min(),
'max': col.max()
})

stats = df.apply(describe_column)
print(stats)
# A B C
# mean 2.0 5.0 8.0
# std 1.0 1.0 1.0
# min 1.0 4.0 7.0
# max 3.0 6.0 9.0

复杂行操作

apply 特别适合需要同时访问多列的复杂操作:

# 创建员工数据
df = pd.DataFrame({
'name': ['张三', '李四', '王五'],
'base_salary': [10000, 15000, 20000],
'bonus_rate': [0.1, 0.15, 0.2]
})

# 计算总薪资(需要多列参与)
def calculate_total(row):
base = row['base_salary']
bonus = base * row['bonus_rate']
return base + bonus

df['total_salary'] = df.apply(calculate_total, axis=1)
print(df)
# name base_salary bonus_rate total_salary
# 0 张三 10000 0.10 11000.0
# 1 李四 15000 0.15 17250.0
# 2 王五 20000 0.20 24000.0

性能优化提示

apply 虽然灵活,但性能不如向量化操作:

# ❌ 慢:使用 apply
df['total'] = df.apply(lambda row: row['A'] + row['B'], axis=1)

# ✅ 快:使用向量化操作
df['total'] = df['A'] + df['B']

# ❌ 慢:逐元素 apply
df['squared'] = df['A'].apply(lambda x: x ** 2)

# ✅ 快:向量化运算
df['squared'] = df['A'] ** 2

applymap 方法

applymap() 对 DataFrame 的每个元素应用函数(Pandas 2.1+ 推荐使用 map() 替代):

# 创建示例数据
df = pd.DataFrame({
'A': [1.123, 2.456, 3.789],
'B': [4.111, 5.222, 6.333]
})

# 格式化所有元素
formatted = df.applymap(lambda x: f'{x:.2f}')
print(formatted)
# A B
# 0 1.12 4.11
# 1 2.46 5.22
# 2 3.79 6.33

# Pandas 2.1+ 可以使用 df.map()
formatted = df.map(lambda x: f'{x:.2f}')

transform 方法

transform() 返回与输入相同形状的结果,常与 groupby 配合使用。

基本用法

# 示例数据
df = pd.DataFrame({
'group': ['A', 'A', 'B', 'B', 'C'],
'value': [1, 2, 3, 4, 5]
})

# 与 groupby 配合:标准化每组的值
df['normalized'] = df.groupby('group')['value'].transform(
lambda x: (x - x.mean()) / x.std()
)
print(df)
# group value normalized
# 0 A 1 -1.0
# 1 A 2 1.0
# 2 B 3 -1.0
# 3 B 4 1.0
# 4 C 5 NaN # 单个值无法计算标准差

# 计算每组的排名
df['rank_in_group'] = df.groupby('group')['value'].transform('rank')

多函数转换

# 同时应用多个转换
transformed = df.groupby('group')['value'].transform(['mean', 'std', 'count'])
print(transformed)
# mean std count
# 0 1.5 0.5 2
# 1 1.5 0.5 2
# 2 3.5 0.5 2
# 3 3.5 0.5 2
# 4 5.0 NaN 1

# 添加前缀
df['group_mean'] = df.groupby('group')['value'].transform('mean')
df['group_max'] = df.groupby('group')['value'].transform('max')

transform vs apply

# transform:返回与原数据相同形状
df['group_mean'] = df.groupby('group')['value'].transform('mean')
# 结果形状与 df['value'] 相同

# apply:返回聚合后的形状
group_means = df.groupby('group')['value'].apply('mean')
# 结果形状为每组一行

数据类型转换

astype 方法

# 创建示例数据
df = pd.DataFrame({
'int_col': [1, 2, 3],
'float_col': [1.1, 2.2, 3.3],
'str_col': ['1', '2', '3'],
'bool_col': [True, False, True]
})

# 转换单列
df['int_col'] = df['int_col'].astype(float)

# 转换多列
df = df.astype({
'int_col': 'float64',
'str_col': 'int64'
})

# 转换为字符串
df['int_col'] = df['int_col'].astype(str)

# 转换为分类类型(节省内存)
df['category_col'] = df['str_col'].astype('category')

智能类型推断

# to_numeric:智能转换为数值
s = pd.Series(['1', '2.5', 'three', '4'])

# 默认遇到错误会抛出异常
# pd.to_numeric(s) # ValueError

# 忽略错误
result = pd.to_numeric(s, errors='ignore')
print(result)
# 0 1
# 1 2.5
# 2 three
# 3 4

# 强制转换(错误变为 NaN)
result = pd.to_numeric(s, errors='coerce')
print(result)
# 0 1.0
# 1 2.5
# 2 NaN
# 3 4.0

# to_datetime:转换为日期时间
dates = pd.Series(['2024-01-01', '2024-02-15', '2024-03-20'])
df['date'] = pd.to_datetime(dates)

# 指定格式
df['date'] = pd.to_datetime(dates, format='%Y-%m-%d')

字符串操作

Pandas 提供了 .str 访问器来处理字符串列:

# 创建示例数据
df = pd.DataFrame({
'name': [' 张三 ', '李四', '王 五 '],
'email': ['[email protected]', '[email protected]', '[email protected]'],
'phone': ['138-1234-5678', '139 5678 9012', '13612345678']
})

# 去除空白
df['name_clean'] = df['name'].str.strip()

# 大小写转换
df['email_lower'] = df['email'].str.lower()
df['email_upper'] = df['email'].str.upper()
df['email_title'] = df['email'].str.title()

# 字符串替换
df['phone_clean'] = df['phone'].str.replace(r'[-\s]', '', regex=True)

# 字符串分割
df['email_parts'] = df['email'].str.split('@')
df['email_domain'] = df['email'].str.split('@').str[1]

# 字符串包含
mask = df['email'].str.contains('example', case=False)

# 正则表达式提取
df['area_code'] = df['phone'].str.extract(r'(\d{3})')

# 字符串长度
df['name_len'] = df['name'].str.len()

# 判断是否以某字符开头/结尾
mask_start = df['email'].str.startswith('ZHANG')
mask_end = df['email'].str.endswith('.com')

条件转换

np.where

import numpy as np

df = pd.DataFrame({
'score': [85, 62, 91, 45, 78]
})

# 简单条件
df['pass'] = np.where(df['score'] >= 60, '及格', '不及格')

# 多条件
df['grade'] = np.where(df['score'] >= 90, 'A',
np.where(df['score'] >= 80, 'B',
np.where(df['score'] >= 60, 'C', 'D')))

pd.cut 分箱

# 连续变量分箱
df['score_level'] = pd.cut(
df['score'],
bins=[0, 60, 70, 80, 90, 100],
labels=['不及格', '及格', '中等', '良好', '优秀']
)

# 等频分箱
df['score_quartile'] = pd.qcut(df['score'], q=4, labels=['Q1', 'Q2', 'Q3', 'Q4'])

自定义函数最佳实践

向量化优先

# ❌ 慢:逐元素操作
df['total'] = df['price'].apply(lambda x: x * 1.1)

# ✅ 快:向量化操作
df['total'] = df['price'] * 1.1

使用 numpy 函数

# ❌ 慢:Python 内置函数
df['log_value'] = df['value'].apply(lambda x: np.log(x))

# ✅ 快:numpy 向量化
df['log_value'] = np.log(df['value'])

缓存中间结果

# ❌ 慢:重复计算
df['result'] = df.apply(
lambda row: complex_calc(row['A']) + complex_calc(row['A']) * 2,
axis=1
)

# ✅ 快:缓存中间结果
cache = {}
def cached_calc(x):
if x not in cache:
cache[x] = complex_calc(x)
return cache[x]

df['result'] = df['A'].apply(cached_calc) * 3

实战示例

示例 1:数据清洗

# 原始数据
df = pd.DataFrame({
'name': [' 张三 ', '李四 ', '王 五'],
'phone': ['13812345678', '139-5678-9012', '136 1234 5678'],
'email': ['[email protected]', '[email protected]', '[email protected] ']
})

# 清洗流程
def clean_data(df):
# 去除空白
df['name'] = df['name'].str.strip()
df['email'] = df['email'].str.strip()

# 标准化电话号码
df['phone'] = df['phone'].str.replace(r'[-\s]', '', regex=True)

# 标准化邮箱
df['email'] = df['email'].str.lower()

return df

df_clean = clean_data(df)

示例 2:特征工程

# 原始数据
df = pd.DataFrame({
'transaction_time': pd.to_datetime([
'2024-01-15 09:30:00',
'2024-01-15 14:20:00',
'2024-01-16 20:15:00'
]),
'amount': [100, 5000, 200]
})

# 提取时间特征
df['year'] = df['transaction_time'].dt.year
df['month'] = df['transaction_time'].dt.month
df['day'] = df['transaction_time'].dt.day
df['hour'] = df['transaction_time'].dt.hour
df['day_of_week'] = df['transaction_time'].dt.dayofweek
df['is_weekend'] = df['day_of_week'].isin([5, 6])

# 时间段分类
df['time_period'] = pd.cut(
df['hour'],
bins=[0, 6, 12, 18, 24],
labels=['凌晨', '上午', '下午', '晚上']
)

# 金额分类
df['amount_level'] = pd.cut(
df['amount'],
bins=[0, 100, 1000, 10000],
labels=['小额', '中额', '大额']
)

示例 3:复杂转换

# 计算移动平均和标准化
df = pd.DataFrame({
'date': pd.date_range('2024-01-01', periods=10),
'value': [10, 12, 15, 14, 18, 20, 19, 22, 25, 23]
})

# 移动平均
df['ma_3'] = df['value'].rolling(window=3).mean()

# 指数移动平均
df['ema'] = df['value'].ewm(span=3).mean()

# Z-score 标准化
df['z_score'] = (df['value'] - df['value'].mean()) / df['value'].std()

# 最大最小归一化
df['normalized'] = (df['value'] - df['value'].min()) / (df['value'].max() - df['value'].min())

小结

方法选择指南

需求推荐方法
单列值映射map()
行/列级复杂操作apply()
元素级操作applymap()map()
与分组配合的转换transform()
类型转换astype()to_numeric()
字符串处理.str 访问器

性能建议

  • 优先使用向量化操作
  • 避免 apply 中的循环
  • 使用 numpy 函数代替 Python 内置函数
  • 对于大数据集考虑分块处理

练习

  1. 使用 map() 将员工级别转换为薪资等级
  2. 使用 apply() 计算每行的加权平均
  3. 使用 transform() 计算每个分组内的排名
  4. 创建一个字符串清洗函数,处理空白、大小写和特殊字符
  5. 使用 pd.cut() 将连续年龄变量转换为年龄段

下一步

掌握了数据转换后,让我们学习 数据合并