Pandas: 数据变换

最后更新:2026-08-26

数据清洗的最后一步是"变换"——把原始数据转成分析需要的形态:成绩转等级、价格转折扣、多步操作链式串联。Pandas 的 map / apply / pipe / replace / assign / transform 构成了从单元素到整表的多层次变换工具链。本节覆盖每个方法的适用场景和性能陷阱。

⚠️ 注意: 以下代码需在本地 Python 环境中运行。

1. 你将学到


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 种变换方法速查

100%
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。

📖 小节


📝 作业

  1. 基础题(难度⭐):创建成绩 Series(0-100),用 map+字典把成绩分为 High(≥70)/Medium(≥50)/Low(<50),再用 replace 把 Medium 换成 Mid。
  2. 进阶题(难度⭐⭐):创建员工 DataFrame(name/salary/department),用 pipe 构建 3 步流水线:清洗 name(strip+title)→ 加税后工资列 → 按 department 标记是否高于部门均值。
  3. 挑战题(难度⭐⭐⭐):创建 20 行商品数据,用 assign 一次添加 3 列(revenue/margin/tier),用 groupby+transform 计算每个 category 的 z-score,最后用 pipe 整合所有步骤。

← 上一课:重复数据处理 · 下一课:分组聚合 →

Web-Tutorial.com

Web-Tutorial 技术团队

由多位开发者共同维护的编程教程平台。每篇教程由对应领域的开发者编写和审核,确保内容准确可靠。如发现任何问题,欢迎向我们反馈。

100%

🙏 帮我们做得更好

我们是刚上线的编程教程站,几个人的小团队,精力有限。页面虽经检查,难免还有疏漏——链接失效、排版错乱、内容有误、语言生硬……

如果您发现了,麻烦告诉我们,我们会在收到反馈后第一时间进行修复,再次感谢您的光临 🙏