Pandas: 数据变换
最后更新:2026-08-26
数据清洗的最后一步是"变换"——把原始数据转成分析需要的形态:成绩转等级、价格转折扣、多步操作链式串联。Pandas 的 map / apply / pipe / replace / assign / transform 构成了从单元素到整表的多层次变换工具链。本节覆盖每个方法的适用场景和性能陷阱。
⚠️ 注意: 以下代码需在本地 Python 环境中运行。
1. 你将学到
- ❶ map 元素映射
- ❷ apply 行列函数
- ❸ pipe 管道链式
- ❹ replace 值替换
- ❺ assign 新增列
- ❻ transform 分组变换
2. Alice 的成绩等级转换
(1) 痛点:百分制成绩需转 ABCD 等级
Alice 有 100 个学生的百分制成绩,需要转成等级:
PYTHON
import pandas as pd
scores = pd.DataFrame({
'name': ['Alice', 'Bob', 'Charlie', 'Carol', 'David'],
'score': [92, 78, 65, 88, 45]
})
# How to convert 0-100 → A/B/C/D/F?
TEXT
📖 仅展示
> **输出:** 在本地 Python 环境(pandas 2.x)运行。Piston 服务器未预装 pandas,请在本机安装(`pip install pandas`)后实操对照。实际数值会因 pandas 版本略有差异。
(2) 解法:map + 字典映射
▶ 示例
TEXT
📖 仅展示
> **输出:** 在本地 Python 环境(pandas 2.x)运行。Piston 服务器未预装 pandas,请在本机安装(`pip install pandas`)后实操对照。实际数值会因 pandas 版本略有差异。
:map 字典映射(难度⭐)
PYTHON
import pandas as pd
scores = pd.DataFrame({
'name': ['Alice', 'Bob', 'Charlie', 'Carol', 'David'],
'score': [92, 78, 65, 88, 45]
})
# Step 1: Define grade mapping function
def score_to_grade(s):
if s >= 90: return 'A'
elif s >= 80: return 'B'
elif s >= 70: return 'C'
elif s >= 60: return 'D'
else: return 'F'
# Step 2: Apply with map
scores['grade'] = scores['score'].map(score_to_grade)
print(scores)
# name score grade
# 0 Alice 92 A
# 1 Bob 78 C
# 2 Charlie 65 D
# 3 Carol 88 B
# 4 David 45 F
TEXT
📖 仅展示
> **输出:** 在本地 Python 环境(pandas 2.x)运行。Piston 服务器未预装 pandas,请在本机安装(`pip install pandas`)后实操对照。实际数值会因 pandas 版本略有差异。
3. map 元素映射
(1) map 三种用法
▶ 示例
TEXT
📖 仅展示
> **输出:** 在本地 Python 环境(pandas 2.x)运行。Piston 服务器未预装 pandas,请在本机安装(`pip install pandas`)后实操对照。实际数值会因 pandas 版本略有差异。
:map 三种映射方式(难度⭐⭐)
PYTHON
import pandas as pd
s = pd.Series([1, 2, 3, 4, 5])
# 1. Dictionary mapping
category_map = {1: 'Low', 2: 'Low', 3: 'Medium', 4: 'High', 5: 'High'}
print(s.map(category_map))
# 0 Low
# 1 Low
# 2 Medium
# 3 High
# 4 High
# 2. Function mapping
print(s.map(lambda x: x ** 2))
# 0 1
# 1 4
# 2 9
# 3 16
# 4 25
# 3. Series mapping (align by index)
other = pd.Series({1: 'One', 2: 'Two', 3: 'Three'})
print(s.map(other))
# 0 NaN ← 1 exists in other → 'One'
# 1 Two
# 2 Three
# 3 NaN ← 4 not in other → NaN
# 4 NaN
TEXT
📖 仅展示
> **输出:** 在本地 Python 环境(pandas 2.x)运行。Piston 服务器未预装 pandas,请在本机安装(`pip install pandas`)后实操对照。实际数值会因 pandas 版本略有差异。
(2) map 适用场景
| 特性 | map | apply |
|---|---|---|
| 操作对象 | Series 单元素 | Series / DataFrame 行/列 |
| 输入 | 字典/函数/Series | 函数 |
| 返回 | Series(一一映射) | 任意类型 |
| 性能 | 快(字典查找 O(1)) | 较慢(逐元素/行列调用) |
| 适用 | 一对一映射 | 复杂逻辑/多列操作 |
4. apply 行列函数
(1) Series apply
▶ 示例
TEXT
📖 仅展示
> **输出:** 在本地 Python 环境(pandas 2.x)运行。Piston 服务器未预装 pandas,请在本机安装(`pip install pandas`)后实操对照。实际数值会因 pandas 版本略有差异。
:Series apply(难度⭐⭐)
PYTHON
import pandas as pd
s = pd.Series(['alice@example.com', 'bob@company.org', 'charlie@uni.edu'])
# Extract domain from email
domains = s.apply(lambda email: email.split('@')[1])
print(domains)
# 0 example.com
# 1 company.org
# 2 uni.edu
# Complex logic: classify email type
def classify_email(email):
domain = email.split('@')[1]
if '.edu' in domain: return 'Academic'
elif '.org' in domain: return 'Non-profit'
else: return 'Commercial'
email_type = s.apply(classify_email)
print(email_type)
# 0 Commercial
# 1 Non-profit
# 2 Academic
TEXT
📖 仅展示
> **输出:** 在本地 Python 环境(pandas 2.x)运行。Piston 服务器未预装 pandas,请在本机安装(`pip install pandas`)后实操对照。实际数值会因 pandas 版本略有差异。
(2) DataFrame apply
▶ 示例
TEXT
📖 仅展示
> **输出:** 在本地 Python 环境(pandas 2.x)运行。Piston 服务器未预装 pandas,请在本机安装(`pip install pandas`)后实操对照。实际数值会因 pandas 版本略有差异。
:DataFrame apply 行列操作(难度⭐⭐)
PYTHON
import pandas as pd
import numpy as np
df = pd.DataFrame({
'math': [85, 92, 78, 88, 65],
'english': [90, 85, 82, 75, 70],
'science': [88, 95, 80, 90, 60]
}, index=['Alice', 'Bob', 'Charlie', 'Carol', 'David'])
# Apply function to each COLUMN (axis=0, default)
print(df.apply(np.mean))
# math 81.6
# english 80.4
# science 82.6
# Apply function to each ROW (axis=1)
df['average'] = df.apply(lambda row: row.mean(), axis=1)
print(df['average'])
# Alice 87.67
# Bob 90.67
# Charlie 80.00
# Carol 84.33
# David 65.00
# Return Series per row (more structured)
def score_report(row):
return pd.Series({
'total': row.sum(),
'avg': row.mean(),
'max': row.max(),
'min': row.min()
})
report = df[['math', 'english', 'science']].apply(score_report, axis=1)
print(report)
TEXT
📖 仅展示
> **输出:** 在本地 Python 环境(pandas 2.x)运行。Piston 服务器未预装 pandas,请在本机安装(`pip install pandas`)后实操对照。实际数值会因 pandas 版本略有差异。
⚠️ 注意: apply 不是向量化操作——对大 DataFrame 逐行调用函数会很慢。能用向量化操作(算术运算、str 方法)就不要用 apply。apply 是"没有办法时的办法"。
5. pipe 管道链式
(1) 链式数据流
▶ 示例
TEXT
📖 仅展示
> **输出:** 在本地 Python 环境(pandas 2.x)运行。Piston 服务器未预装 pandas,请在本机安装(`pip install pandas`)后实操对照。实际数值会因 pandas 版本略有差异。
:pipe 管道操作(难度⭐⭐)
PYTHON
import pandas as pd
import numpy as np
# Raw data
df = pd.DataFrame({
'name': [' Alice ', 'BOB', 'Charlie', ' carol ', 'David'],
'age': [28, -1, 25, 30, 999],
'salary': [75000, np.nan, 68000, 88000, np.nan]
})
# Step-by-step without pipe (nested calls)
# result = assign_dept(replace_invalid(clean_names(df)))
# With pipe — readable left-to-right flow
def clean_names(df):
df = df.copy()
df['name'] = df['name'].str.strip().str.title()
return df
def replace_invalid(df):
df = df.copy()
df.loc[df['age'] < 0, 'age'] = np.nan
df.loc[df['age'] > 150, 'age'] = np.nan
return df
def fill_salary(df):
df = df.copy()
df['salary'] = df['salary'].fillna(df['salary'].median())
return df
result = (df
.pipe(clean_names)
.pipe(replace_invalid)
.pipe(fill_salary)
)
print(result)
TEXT
📖 仅展示
> **输出:** 在本地 Python 环境(pandas 2.x)运行。Piston 服务器未预装 pandas,请在本机安装(`pip install pandas`)后实操对照。实际数值会因 pandas 版本略有差异。
(2) pipe vs apply 对比
| 特性 | pipe | apply |
|---|---|---|
| 输入 | 整个 DataFrame | 行/列/Series |
| 返回 | DataFrame(变换后) | 任意类型 |
| 用途 | 多步骤流水线 | 单步元素/行列操作 |
| 链式 | ✅ 天然支持 | ❌ 需嵌套 |
| 可读性 | 高(从上到下读) | 中 |
6. replace 值替换
(1) 精确替换
▶ 示例
TEXT
📖 仅展示
> **输出:** 在本地 Python 环境(pandas 2.x)运行。Piston 服务器未预装 pandas,请在本机安装(`pip install pandas`)后实操对照。实际数值会因 pandas 版本略有差异。
:replace 值替换(难度⭐)
PYTHON
import pandas as pd
import numpy as np
df = pd.DataFrame({
'status': ['active', 'inactive', 'pending', 'active', 'unknown'],
'priority': [1, 2, 3, 1, 99]
})
# Single value replacement
df['status'] = df['status'].replace('unknown', 'inactive')
print(df['status'])
# Multiple values replacement (dict)
df['status'] = df['status'].replace({
'active': 'Active',
'inactive': 'Inactive',
'pending': 'Pending'
})
print(df['status'])
# Replace invalid sentinel value
df['priority'] = df['priority'].replace(99, np.nan)
print(df['priority'])
# Regex replacement
text = pd.Series(['Phone: 555-0100', 'Email: test@example.com'])
cleaned = text.str.replace(r'[\d-]+', '[REDACTED]', regex=True)
print(cleaned)
TEXT
📖 仅展示
> **输出:** 在本地 Python 环境(pandas 2.x)运行。Piston 服务器未预装 pandas,请在本机安装(`pip install pandas`)后实操对照。实际数值会因 pandas 版本略有差异。
(2) replace vs map 对比
| 特性 | replace | map |
|---|---|---|
| 未匹配值 | 保持不变 | 变 NaN |
| 多值替换 | ✅ 字典 | ✅ 字典 |
| 正则 | ✅ regex=True | ❌ |
| 适用 | 替换特定值 | 全量映射 |
🔥 易错: map 中字典未覆盖的值会变成 NaN!replace 中未覆盖的值保持不变。想"只替换几个值,其他不变"用 replace;想"全量映射,没映射到的就丢弃"用 map。
7. assign 新增列
(1) 链式新增多列
▶ 示例
TEXT
📖 仅展示
> **输出:** 在本地 Python 环境(pandas 2.x)运行。Piston 服务器未预装 pandas,请在本机安装(`pip install pandas`)后实操对照。实际数值会因 pandas 版本略有差异。
:assign 新增列(难度⭐⭐)
PYTHON
import pandas as pd
df = pd.DataFrame({
'price': [100, 200, 150, 80, 300],
'quantity': [5, 3, 10, 8, 2],
'discount': [0.1, 0.2, 0.0, 0.15, 0.05]
})
# Add multiple columns in one chain
result = (df
.assign(
discounted_price=lambda x: x['price'] * (1 - x['discount']),
total=lambda x: x['discounted_price'] * x['quantity'],
is_bulk=lambda x: x['quantity'] >= 5
)
)
print(result)
# price quantity discount discounted_price total is_bulk
# 0 100 5 0.10 90.0 450.0 True
# 1 200 3 0.20 160.0 480.0 False
# 2 150 10 0.00 150.0 1500.0 True
# 3 80 8 0.15 68.0 544.0 True
# 4 300 2 0.05 285.0 570.0 False
TEXT
📖 仅展示
> **输出:** 在本地 Python 环境(pandas 2.x)运行。Piston 服务器未预装 pandas,请在本机安装(`pip install pandas`)后实操对照。实际数值会因 pandas 版本略有差异。
💡 提示: assign 中用 lambda x: 引用当前 DataFrame,可以引用同一次 assign 中刚创建的列(如 total 引用 discounted_price)。这比
df['new_col'] = ... 的写法更适合链式操作。
8. transform 分组变换
(1) transform 保持原形状
▶ 示例
TEXT
📖 仅展示
> **输出:** 在本地 Python 环境(pandas 2.x)运行。Piston 服务器未预装 pandas,请在本机安装(`pip install pandas`)后实操对照。实际数值会因 pandas 版本略有差异。
:transform 分组标准化(难度⭐⭐)
PYTHON
import pandas as pd
import numpy as np
df = pd.DataFrame({
'student': ['Alice', 'Alice', 'Bob', 'Bob', 'Charlie', 'Charlie'],
'subject': ['Math', 'English', 'Math', 'English', 'Math', 'English'],
'score': [85, 90, 92, 88, 78, 82]
})
# Z-score normalization within each student
df['z_score'] = df.groupby('student')['score'].transform(
lambda x: (x - x.mean()) / x.std()
)
print(df)
# Fill with group mean (instead of global mean)
df['score_filled'] = df.groupby('student')['score'].transform('mean')
print(df[['student', 'score', 'score_filled']])
# Rank within group
df['rank_in_class'] = df.groupby('subject')['score'].transform('rank', method='min')
print(df[['student', 'subject', 'score', 'rank_in_class']])
TEXT
📖 仅展示
> **输出:** 在本地 Python 环境(pandas 2.x)运行。Piston 服务器未预装 pandas,请在本机安装(`pip install pandas`)后实操对照。实际数值会因 pandas 版本略有差异。
(2) transform vs apply 对比
| 特性 | transform | apply |
|---|---|---|
| 返回形状 | 与输入相同 | 可变 |
| 分组后 | 每组返回同长度结果 | 每组返回任意结果 |
| 聚合 | ❌ 不能缩小 | ✅ 可以 |
| 广播 | ✅ 自动广播到原行 | ❌ |
| 常用 | 标准化/填充/排名 | 聚合/复杂计算 |
9. 变换方法选择决策
(1) 6 种变换方法速查
graph TB
Q["Need to transform data?"] --> TYPE{"Transform type?"}
TYPE -->|"One-to-one mapping"| MAP["map()"]
TYPE -->|"Row/Col function"| APPLY["apply()"]
TYPE -->|"Multi-step pipeline"| PIPE["pipe()"]
TYPE -->|"Replace specific values"| REPLACE["replace()"]
TYPE -->|"Add new columns"| ASSIGN["assign()"]
TYPE -->|"Group-level transform"| TRANS["transform()"]
MAP --> NOTE1["✅ dict/function<br>❌ unmapped → NaN"]
APPLY --> NOTE2["⚠️ Slow for large DF<br>✅ Flexible"]
PIPE --> NOTE3["✅ Readable chain<br>✅ Reusable steps"]
REPLACE --> NOTE4["✅ Unmatched kept<br>✅ Regex support"]
ASSIGN --> NOTE5["✅ Chain-friendly<br>✅ Multiple columns"]
TRANS --> NOTE6["✅ Same shape output<br>✅ Group broadcast"]
TEXT
📖 仅展示
> **输出:** 在本地 Python 环境(pandas 2.x)运行。Piston 服务器未预装 pandas,请在本机安装(`pip install pandas`)后实操对照。实际数值会因 pandas 版本略有差异。
(2) 性能排序
| 排序 | 方法 | 性能 | 原因 |
|---|---|---|---|
| 1 | 向量化运算 | 最快 | C 层执行,无 Python 循环 |
| 2 | map(字典) | 快 | O(1) 查找 |
| 3 | replace | 快 | 底层优化 |
| 4 | map(函数) | 中 | 逐元素调用 |
| 5 | apply | 慢 | 逐行/列 Python 调用 |
| 6 | apply + 复杂逻辑 | 最慢 | 函数开销叠加 |
💡 提示: 能用
df['col'] * 2 就不要用 df['col'].apply(lambda x: x * 2)。向量化运算比 apply 快 10-100 倍。
10. 完整示例:电商数据变换全流程
▶ 示例
TEXT
📖 仅展示
> **输出:** 在本地 Python 环境(pandas 2.x)运行。Piston 服务器未预装 pandas,请在本机安装(`pip install pandas`)后实操对照。实际数值会因 pandas 版本略有差异。
:数据变换全流程(难度⭐⭐⭐)
PYTHON
import pandas as pd
import numpy as np
# ============================================
# Comprehensive example: E-commerce data
# transformation pipeline
# ============================================
# 1. Raw data
np.random.seed(42)
df = pd.DataFrame({
'product': [f'Item_{i:03d}' for i in range(1, 21)],
'category': np.random.choice(['Electronics', 'Clothing', 'Home'], 20),
'price': np.round(np.random.uniform(10, 500, 20), 2),
'cost': np.round(np.random.uniform(5, 250, 20), 2),
'quantity_sold': np.random.randint(1, 100, 20),
'rating': np.round(np.random.uniform(2.0, 5.0, 20), 1)
})
# 2. Step-by-step pipeline with pipe
def add_margin(df):
df = df.copy()
df['margin_pct'] = ((df['price'] - df['cost']) / df['price'] * 100).round(1)
return df
def categorize_price(df):
df = df.copy()
df['price_tier'] = df['price'].map(
lambda p: 'Budget' if p < 50 else ('Mid' if p < 200 else 'Premium')
)
return df
def add_revenue(df):
return df.assign(
revenue=lambda x: x['price'] * x['quantity_sold'],
is_top_rated=lambda x: x['rating'] >= 4.0
)
def normalize_rating(df):
df = df.copy()
df['rating_zscore'] = df.groupby('category')['rating'].transform(
lambda x: (x - x.mean()) / x.std()
)
return df
# 3. Execute pipeline
result = (df
.pipe(add_margin)
.pipe(categorize_price)
.pipe(add_revenue)
.pipe(normalize_rating)
)
# 4. Summary
print("=== Transformation Result ===")
print(result[['product', 'category', 'price_tier', 'margin_pct', 'revenue', 'is_top_rated']].head(10))
print(f"\nTotal revenue: ${result['revenue'].sum():,.2f}")
print(f"Top-rated items: {result['is_top_rated'].sum()}")
print(f"Price tier distribution:\n{result['price_tier'].value_counts()}")
TEXT
📖 仅展示
> **输出:** 在本地 Python 环境(pandas 2.x)运行。Piston 服务器未预装 pandas,请在本机安装(`pip install pandas`)后实操对照。实际数值会因 pandas 版本略有差异。
❓ 常见问题
Q map 和 apply 区别?
A map 只能用于 Series,对每个元素做一对一映射(输入字典/函数),未映射值变 NaN。apply 可用于 Series 和 DataFrame,支持行/列级别操作(axis=0/1),返回类型灵活。简单映射用 map(快),复杂逻辑用 apply(灵活)。
Q apply 为什么慢?
A apply 本质是 Python 循环——对每行/每列调用一次 Python 函数,函数调用开销巨大。向量化运算在 C 层执行,无此开销。原则:能用
df['a'] + df['b'] 就不要用 df.apply(lambda r: r['a']+r['b'], axis=1)。apply 是最后的手段。Q pipe 和 apply 区别?
A pipe 作用于整个 DataFrame,接收并返回 DataFrame,用于多步骤流水线。apply 作用于行/列/Series,用于逐元素/逐行计算。pipe 的价值是可读性——把
f3(f2(f1(df))) 变成 df.pipe(f1).pipe(f2).pipe(f3),从上到下读就是执行顺序。Q replace 和 map 区别?
A 最大区别是对未匹配值的处理——replace 保持原值不变,map 把未匹配值变成 NaN。只替换几个特定值用 replace;做全量映射(所有值都要转换)用 map。replace 还支持正则替换(regex=True),map 不支持。
Q assign 和 df['col']=... 区别?
A assign 返回新 DataFrame(不修改原数据),适合链式操作。
df['col']=... 就地修改。assign 中可以用 lambda 引用同一步创建的列,而直接赋值做不到。推荐:链式操作用 assign,简单场景直接赋值即可。Q transform 和 apply 在 groupby 后区别?
A transform 返回与原 DataFrame 相同长度的结果(每组内广播),apply 可以返回任意形状。举例:groupby + transform('mean') 返回每行的组均值(长度不变),groupby + apply('mean') 返回每组一个均值(长度=组数)。想保持原形状用 transform,想聚合用 apply/agg。
Q 向量化具体指什么?
A 向量化是用 Pandas/NumPy 的内置运算替代 Python 循环。
df['a'] + df['b'] 是向量化(C 层对整个数组运算),df.apply(lambda r: r['a']+r['b'], axis=1) 不是(Python 逐行调用)。向量化快 10-100 倍。能用内置运算就不要用 apply。📖 小节
- map 用于 Series 一对一映射,字典映射最快,未匹配值变 NaN
- apply 用于逐行/逐列复杂逻辑,灵活但慢——能用向量化就不用 apply
- pipe 用于多步骤流水线,把嵌套调用变成从上到下的可读链
- replace 替换特定值,未匹配保持不变,支持正则
- assign 链式新增列,lambda 可引用同步创建的列
- transform 分组变换,保持原形状,适合标准化/填充/排名
- 性能排序:向量化 > map(字典) > replace > map(函数) > apply
📝 作业
- 基础题(难度⭐):创建成绩 Series(0-100),用 map+字典把成绩分为 High(≥70)/Medium(≥50)/Low(<50),再用 replace 把 Medium 换成 Mid。
- 进阶题(难度⭐⭐):创建员工 DataFrame(name/salary/department),用 pipe 构建 3 步流水线:清洗 name(strip+title)→ 加税后工资列 → 按 department 标记是否高于部门均值。
- 挑战题(难度⭐⭐⭐):创建 20 行商品数据,用 assign 一次添加 3 列(revenue/margin/tier),用 groupby+transform 计算每个 category 的 z-score,最后用 pipe 整合所有步骤。