返回首页

Roadmap
更新于 2026年10月24日 全文 1646 字未阅读

第 5.2 节 编程练习#

预计学习时长: 50 分钟

练习方式: 新建一个 Jupyter Notebook,抄好准备代码,然后每题独立完成。这 5 题是整个课程的收官,难度比前面略高,慢慢来。

准备:两张表#

import pandas as pd
import numpy as np

orders = pd.DataFrame({
    "订单号": [1001, 1002, 1003, 1004],
    "城市":   ["北京", "上海", "北京", "广州"],
    "商品":   ["手机", "电脑", "耳机", "手机"],
    "销量":   [2, 1, 3, 2],
    "金额":   [7998.0, 7999.0, 1797.0, 7998.0],
})

clients = pd.DataFrame({
    "订单号":   [1001, 1002, 1005],
    "客户名":   ["张三", "李四", "王五"],
    "会员等级": ["金卡", "银卡", "铜卡"],
})

练习 1:根据共同字段合并两个 DataFrame#

题目: 完成下面 5 个任务:

  1. 用 merge() 按「订单号」合并两张表,打印结果(默认方式);
  2. 改成 how="left",打印结果;
  3. 改成 how="outer",并加 indicator=True,打印结果;
  4. 从第 3 题的结果里,筛出 _merge 为 "left_only" 的行;
  5. 统计 _merge 各取值的行数。

思考题: 第 1 题为什么只有 2 行?订单 1003 去哪了?

点击查看参考答案
# 1. 默认 inner
print(orders.merge(clients, on="订单号"))
   订单号  城市  商品  销量     金额  客户名 会员等级
0   1001  北京  手机   2  7998.0   张三   金卡
1   1002  上海  电脑   1  7999.0   李四   银卡
# 2. left:左表全留
print(orders.merge(clients, on="订单号", how="left"))
   订单号  城市  商品  销量     金额  客户名 会员等级
0   1001  北京  手机   2  7998.0   张三   金卡
1   1002  上海  电脑   1  7999.0   李四   银卡
2   1003  北京  耳机   3  1797.0   NaN   NaN
3   1004  广州  手机   2  7998.0   NaN   NaN
# 3. outer + indicator
m = orders.merge(clients, on="订单号", how="outer", indicator=True)
print(m)
   订单号  城市  商品  销量     金额  客户名 会员等级     _merge
0   1001  北京  手机   2  7998.0   张三   金卡       both
1   1002  上海  电脑   1  7999.0   李四   银卡       both
2   1003  北京  耳机   3  1797.0   NaN   NaN  left_only
3   1004  广州  手机   2  7998.0   NaN   NaN  left_only
4   1005   NaN   NaN  NaN     NaN   王五   铜卡 right_only
# 4. 筛出只出现在订单表的
print(m[m["_merge"] == "left_only"][["订单号", "城市"]])
   订单号  城市
2   1003  北京
3   1004  广州
# 5. 统计各匹配情况有几行
print(m["_merge"].value_counts())
left_only     2
both          2
right_only    1
dtype: int64

要点: 第 1 题只有 2 行,因为默认是 inner——两边都有的才保留。 订单 1003、1004 在客户表里查不到,所以被丢掉了。 outer 是 5 行 = 4(订单表)+ 3(客户表)− 2(重复的)。


练习 2:使用 concat() 按行或按列拼接数据#

题目: 完成下面 5 个任务:

  1. 把 orders 和 clients 用 concat(axis=0) 拼起来,打印结果的 shape;
  2. 同上,但加 ignore_index=True,打印结果的索引;
  3. 把两张表按列拼(axis=1),打印结果;
  4. 第 3 步之前,先把两张表都 set_index("订单号");
  5. 用 concat(axis=1, join="inner") 拼第 4 步的结果,打印行数。

思考题: 第 3 步(不设索引直接按列拼)和第 4 步的结果有什么不同?

点击查看参考答案
# 1. 按行拼
print(pd.concat([orders, clients]).shape)      # (7, 7)
# 2. 加 ignore_index
m = pd.concat([orders, clients], ignore_index=True)
print(m.index.tolist())
[0, 1, 2, 3, 4, 5, 6]
# 3. 直接按列拼(索引都是 0,1,2,3,所以是"按位置"对齐)
print(pd.concat([orders, clients], axis=1).head(3))
   订单号  城市  商品  销量     金额    订单号 客户名 会员等级
0   1001  北京  手机   2  7998.0  1001.0   张三   金卡
1   1002  上海  电脑   1  7999.0  1002.0   李四   银卡
2   1003  北京  耳机   3  1797.0  1005.0   王五   铜卡
# 4. 先设索引再按列拼
o = orders.set_index("订单号")
c = clients.set_index("订单号")
print(pd.concat([o, c], axis=1))
        城市  商品  销量     金额  客户名 会员等级
订单号                             
1001   北京  手机   2  7998.0   张三   金卡
1002   上海  电脑   1  7999.0   李四   银卡
1003   北京  耳机   3  1797.0   NaN   NaN
1004   广州  手机   2  7998.0   NaN   NaN
1005   NaN   NaN  NaN     NaN   王五   铜卡
# 5. 只要两边都有的索引
print(len(pd.concat([o, c], axis=1, join="inner")))     # 2

要点: 第 3 步看起来「配对成功」了,其实是巧合——两张表的索引碰巧都是 0,1,2,3,所以第 3 行的订单 1003 被配上了客户「王五」(实际王五对应的是订单 1005)。 第 4 步设了索引以后,是真正按订单号对齐,1005 那行就正确地显示了自己的客户名。 这就是 concat(axis=1) 和 merge() 的区别:前者按索引对齐,后者按你指定的字段匹配。


练习 3:将包含多个指标列的宽表转换为长表#

题目: 已知:

wide = pd.DataFrame({
    "订单号": [1, 2],
    "一季度": [100, 200],
    "二季度": [150, 250],
    "三季度": [180, 260],
})

完成下面 4 个任务:

  1. 用 melt() 把「一季度、二季度、三季度」三列转成长表,保留「订单号」,列名叫「季度」和「销售额」;
  2. 用 value_vars 只把「二季度」转成长表;
  3. 把第 1 题的结果按「季度」分组,算每个季度的总销售额;
  4. 统计每个订单号在长表里有几行。

思考题: 第 3 题如果不用 melt(),能不能直接对宽表算季度合计?

点击查看参考答案
# 1. 宽转长
long = pd.melt(wide, id_vars=["订单号"], var_name="季度", value_name="销售额")
print(long)
   订单号  季度  销售额
0     1  一季度     100
1     2  一季度     200
2     1  二季度     150
3     2  二季度     250
4     1  三季度     180
5     2  三季度     260
# 2. 只转指定的列
print(pd.melt(wide, id_vars=["订单号"], value_vars=["二季度"],
               var_name="季度", value_name="销售额"))
   订单号  季度  销售额
0     1  二季度     150
1     2  二季度     250
# 3. 长表可以直接分组统计
print(long.groupby("季度")["销售额"].sum())
季度
一季度    300
三季度    440
二季度    400
dtype: int64
# 4. 每个订单号几行
print(long.groupby("订单号").size())
订单号
1    3
2    3
dtype: int64

要点: 第 3 题其实不用 melt 也能做——直接 wide[["一季度","二季度","三季度"]].sum() 也能拿到各季度合计。 但 melt() 的价值在于:一旦你要按「季度」这个维度做别的分析(画折线图、按季度算平均、跟其它表按季度合并),就必须先转成长表,因为宽表里「季度」是列名而不是数据。


练习 4:使用 unstack() 将多级索引数据转换为宽表#

题目: 已知:

long = pd.DataFrame({
    "订单号": [1, 1, 2],
    "季度":  ["Q1", "Q2", "Q1"],
    "销售额": [100, 150, 200],
})

完成下面 5 个任务:

  1. 把「订单号」和「季度」设成多层索引,取「销售额」这列;
  2. 对上一步的结果调用 unstack(),打印结果;
  3. 用 pivot() 一步到位做出同样的结果,打印结果;
  4. 把第 2 步的结果再 stack() 回去,打印结果;
  5. 把第 2 步的结果用 reset_index() 变成普通列,打印结果。

思考题: 第 3 题的 pivot() 和第 2 题的 unstack(),结果一样吗?如果长表里有两个相同的「订单号 + 季度」,pivot() 会怎样?

点击查看参考答案
# 1. 设成多层索引
s = long.set_index(["订单号", "季度"])["销售额"]
print(s)
订单号  季度
1     Q1    100
      Q2    150
2     Q1    200
Name: 销售额, dtype: int64
# 2. unstack
print(s.unstack())
季度      Q1     Q2
订单号              
1     100.0  150.0
2     200.0    NaN
# 3. pivot,结果完全一样
print(long.pivot(index="订单号", columns="季度", values="销售额"))
季度      Q1     Q2
订单号              
1     100.0  150.0
2     200.0    NaN
# 4. stack 回去
print(s.unstack().stack())
订单号  季度
1     Q1    100.0
      Q2    150.0
2     Q1    200.0
dtype: float64
# 5. reset_index 变成普通列
print(s.unstack().reset_index())
季度  订单号     Q1     Q2
0     1  100.0  150.0
1     2  200.0    NaN

要点: 第 2 题和第 3 题结果完全一样,只是路径不同——unstack() 要求你先 set_index(),pivot() 一步到位,所以优先用 pivot()。 至于重复的「订单号 + 季度」,pivot() 会报 ValueError: Index contains duplicate entries, cannot reshape,那种情况请改用第 4 章的 pivot_table()。


练习 5:使用 apply() 对指定列或行执行自定义函数#

题目: 已知:

df = pd.DataFrame({
    "商品":   ["手机", "电脑", "耳机", "手机"],
    "单价":   [3999.0, 7999.0, 599.0, 3999.0],
    "数量":   [2, 1, 3, 5],
})

完成下面 5 个任务:

  1. 用 apply()(默认按列)算出每列的总和;
  2. 用 apply(axis=1) 算出每一行的「金额」(单价 × 数量),存成新列「金额」;
  3. 用 apply(axis=1) 给每行打标签:金额 >= 10000 标「高」,否则标「低」;
  4. 用 map() 把「商品」列的「手机」改成「智能手机」;
  5. 用 DataFrame.map() 把「单价」列的所有值都乘 2。

思考题: 第 2 题如果不用 apply,能不能用更简单的写法?

点击查看参考答案
# 1. 按列求和
print(df.apply(lambda col: col.sum()))
商品    手机电脑耳机手机
单价    16596.0
数量       11
dtype: object
# 2. 按行算金额
df["金额"] = df.apply(lambda row: row["单价"] * row["数量"], axis=1)
print(df)
   商品     单价  数量      金额
0  手机  3999.0    2   7998.0
1  电脑  7999.0    1   7999.0
2  耳机   599.0    3   1797.0
3  手机  3999.0    5  19995.0
# 3. 按行打标签
df["等级"] = df.apply(lambda row: "高" if row["金额"] >= 10000 else "低", axis=1)
print(df[["商品", "金额", "等级"]])
   商品      金额 等级
0  手机   7998.0   低
1  电脑   7999.0   低
2  耳机   1797.0   低
3  手机  19995.0   高
# 4. map 替换单列的值
df["商品"] = df["商品"].map({"手机": "智能手机", "电脑": "笔记本电脑", "耳机": "蓝牙耳机"})
print(df["商品"].tolist())
['智能手机', '笔记本电脑', '蓝牙耳机', '智能手机']
# 5. DataFrame.map 对每个单元格做变换
print(df[["单价"]].map(lambda x: x * 2))
       单价
0   7998.0
1  15998.0
2   1198.0
3   7998.0

要点: 第 2 题完全可以不用 apply:

df["金额"] = df["单价"] * df["数量"]

这就是向量化运算,比 apply 快得多。 apply 是「实在没法用现成公式」时的最后选择,因为它逐行跑 Python 函数,数据量大时会明显变慢。


加分练习:一张完整的销售宽表#

题目: 做一个「季度销售额」报表,要求:

  1. 造一张宽表 wide,有「订单号」「Q1」「Q2」「Q3」四列;
  2. 用 melt() 转成长表,列名是「订单号」「季度」「销售额」;
  3. 按「季度」分组算总销售额;
  4. 用 pivot() 把第 2 步的长表转回宽表(行是订单号,列是季度);
  5. 把每行的合计加成一列「合计」;
  6. 打印整个报表。

思考题: 第 4 步转回宽表之后,和第 1 步的原始宽表一模一样吗?

点击查看参考答案
import pandas as pd

# 1. 造宽表
wide = pd.DataFrame({
    "订单号": [1, 2, 3],
    "Q1":  [100, 200, 150],
    "Q2":  [120, 180, 160],
    "Q3":  [130, 210, 170],
})

# 2. 宽转长
long = pd.melt(wide, id_vars=["订单号"], var_name="季度", value_name="销售额")

# 3. 按季度汇总
print(long.groupby("季度")["销售额"].sum())
季度
Q1    450
Q2    460
Q3    510
dtype: int64
# 4. 长转宽
report = long.pivot(index="订单号", columns="季度", values="销售额")

# 5. 加合计列
report["合计"] = report.sum(axis=1)

# 6. 打印
print(report)
季度    Q1   Q2   Q3  合计
订单号                    
1    100  120  130  350
2    200  180  210  590
3    150  160  170  480

要点: 第 4 步的结果和第 1 步的宽表内容一样,但不完全相同:

  • 列顺序可能不同(pivot 会按列名排序,Q1/Q2/Q3 正好一样);
  • 数据类型可能变了(出现了 NaN 会变成 float);
  • 「订单号」从普通列变成了索引。

所以第 4 步后如果要把「订单号」变回普通列,记得加一句 reset_index()。


练习小结#

题目用到的核心语法
练习 1merge(on=, how=)、indicator=True、_merge
练习 2concat(axis=0/1)、ignore_index=True、join="inner"、set_index()
练习 3melt(id_vars=, value_vars=, var_name=, value_name=)
练习 4set_index([...])、unstack()、pivot()、stack()、reset_index()
练习 5apply()、apply(axis=1)、map()、DataFrame.map()

课程结业 到这里,Pandas 五大模块全部走完了:

章主题你学会的
第 1 章数据加载与查看DataFrame、shape、dtypes、head/tail
第 2 章索引与数据选择loc、iloc、布尔索引、set_index/reset_index
第 3 章数据清洗isna、fillna、dropna、drop_duplicates、rename、replace、astype
第 4 章聚合与分组groupby、agg、transform、pivot_table、crosstab
第 5 章合并与重塑merge、concat、melt、unstack/pivot、apply/map

下一步建议:拿一份你工作里真实的数据(Excel 导出就行),把上面每章的代码都敲一遍。真实数据比课程数据脏得多,跑通一遍你就真的入门了。

阅读 --