第 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 个任务:
- 用
merge()按「订单号」合并两张表,打印结果(默认方式); - 改成
how="left",打印结果; - 改成
how="outer",并加indicator=True,打印结果; - 从第 3 题的结果里,筛出
_merge为"left_only"的行; - 统计
_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 个任务:
- 把
orders和clients用concat(axis=0)拼起来,打印结果的shape; - 同上,但加
ignore_index=True,打印结果的索引; - 把两张表按列拼(
axis=1),打印结果; - 第 3 步之前,先把两张表都
set_index("订单号"); - 用
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 个任务:
- 用
melt()把「一季度、二季度、三季度」三列转成长表,保留「订单号」,列名叫「季度」和「销售额」; - 用
value_vars只把「二季度」转成长表; - 把第 1 题的结果按「季度」分组,算每个季度的总销售额;
- 统计每个订单号在长表里有几行。
思考题: 第 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 个任务:
- 把「订单号」和「季度」设成多层索引,取「销售额」这列;
- 对上一步的结果调用
unstack(),打印结果; - 用
pivot()一步到位做出同样的结果,打印结果; - 把第 2 步的结果再
stack()回去,打印结果; - 把第 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 个任务:
- 用
apply()(默认按列)算出每列的总和; - 用
apply(axis=1)算出每一行的「金额」(单价 × 数量),存成新列「金额」; - 用
apply(axis=1)给每行打标签:金额 >= 10000 标「高」,否则标「低」; - 用
map()把「商品」列的「手机」改成「智能手机」; - 用
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 函数,数据量大时会明显变慢。
加分练习:一张完整的销售宽表#
题目: 做一个「季度销售额」报表,要求:
- 造一张宽表
wide,有「订单号」「Q1」「Q2」「Q3」四列; - 用
melt()转成长表,列名是「订单号」「季度」「销售额」; - 按「季度」分组算总销售额;
- 用
pivot()把第 2 步的长表转回宽表(行是订单号,列是季度); - 把每行的合计加成一列「合计」;
- 打印整个报表。
思考题: 第 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()。
练习小结#
| 题目 | 用到的核心语法 |
|---|---|
| 练习 1 | merge(on=, how=)、indicator=True、_merge |
| 练习 2 | concat(axis=0/1)、ignore_index=True、join="inner"、set_index() |
| 练习 3 | melt(id_vars=, value_vars=, var_name=, value_name=) |
| 练习 4 | set_index([...])、unstack()、pivot()、stack()、reset_index() |
| 练习 5 | apply()、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 导出就行),把上面每章的代码都敲一遍。真实数据比课程数据脏得多,跑通一遍你就真的入门了。