第 4.2 节 编程练习#
预计学习时长: 45 分钟
练习方式: 新建一个 Jupyter Notebook,抄好准备代码,然后每题独立完成。做完记得
print()出结果,和答案对一下。
准备:先把数据建好#
import pandas as pd
data = {
"订单号": [1001, 1002, 1003, 1004, 1005, 1006, 1007, 1008],
"下单日期": ["2024-01-05", "2024-01-08", "2024-01-12", "2024-02-03",
"2024-02-15", "2024-03-02", "2024-03-20", "2024-04-08"],
"城市": ["北京", "上海", "北京", "广州", "上海", "北京", "深圳", "广州"],
"商品": ["手机", "电脑", "耳机", "手机", "键盘", "显示器", "耳机", "手机"],
"销量": [2, 1, 3, 2, 5, 1, 4, 2],
"单价": [3999.0, 7999.0, 599.0, 3999.0, 299.0, 1299.0, 599.0, 3999.0],
"金额": [7998.0, 7999.0, 1797.0, 7998.0, 1495.0, 1299.0, 2396.0, 7998.0],
"客户评分": [4.5, 4.0, 4.8, 3.9, 4.2, 4.6, 4.1, 4.9],
}
df = pd.DataFrame(data)练习 1:按照指定类别对数据进行分组#
题目: 完成下面 4 个步骤:
- 按「城市」分组,统计每个城市有几条记录(用
size()); - 按「商品」分组,统计每个商品有几条记录;
- 按「城市」分组但保持原始出现顺序(加
sort=False),打印每个城市的记录数; - 打印第 1 步结果的
index.name,确认分组键变成了索引。
思考题: 第 1 题和第 2 题结果的行顺序一样吗?为什么?
点击查看参考答案
# 1. 按城市分组数行数
print(df.groupby("城市").size())城市
上海 2
北京 3
广州 2
深圳 1
dtype: int64# 2. 按商品分组
print(df.groupby("商品").size())商品
手机 3
耳机 2
电脑 1
键盘 1
显示器 1
dtype: int64# 3. 不排序,按原始出现顺序
print(df.groupby("城市", sort=False).size())城市
北京 3
上海 2
广州 2
深圳 1
dtype: int64# 4. 分组键变成了索引
print(df.groupby("城市").size().index.name) # 城市要点: 第 1 题和第 2 题顺序不一样,因为 groupby() 默认按分组键排序(按中文的 Unicode 码点排,「上」在「北」前面)。
第 3 题加 sort=False 后就按数据里第一次出现的顺序排了。
练习 2:统计每组的数量、均值、总和和最大值#
题目: 完成下面 4 个统计:
- 每个城市的订单数、平均金额、总金额、最大金额;
- 每个城市的订单数和总销量;
- 每个城市的客单价(总金额 ÷ 总销量);
- 按城市统计金额后,找出金额最高的城市。
思考题: 第 3 题的客单价为什么要用 transform() 而不能直接除?
点击查看参考答案
# 1. 一次算多个统计量
print(df.groupby("城市")["金额"].agg(["count", "mean", "sum", "max"])) count mean sum max
城市
上海 2 4747.0 9494.0 7999.0
北京 3 3698.0 11094.0 7998.0
广州 2 7998.0 15996.0 7998.0
深圳 1 2396.0 2396.0 2396.0# 2. 不同列用不同聚合函数
print(df.groupby("城市").agg({"订单号": "count", "销量": "sum"})) 订单号 销量
城市
上海 2 6
北京 3 6
广州 2 4
深圳 1 4# 3. 客单价 = 总金额 / 总销量(两个 transform 相除)
d = df.copy()
total_amount = d.groupby("城市")["金额"].transform("sum")
total_qty = d.groupby("城市")["销量"].transform("sum")
d["客单价"] = total_amount / total_qty
print(d.groupby("城市")["客单价"].first().round(2))城市
上海 1582.33
北京 1849.00
广州 3999.00
深圳 599.00
dtype: float64# 4. 金额最高的城市
best = df.groupby("城市")["金额"].sum().idxmax()
print(best) # 广州要点: 第 3 题不能用 sum() 的结果直接相除,因为 sum() 返回的 Series 只有 4 行(每个城市一行),和原表的 8 行对不上。
transform() 的结果和原表一样长,逐行相除才对得上。
另外第 3 题用 first() 是因为同一城市的所有行算出来的客单价都相同,取第一个就行;也可以先 drop_duplicates("城市") 再用。
练习 3:使用 pivot_table() 生成汇总报表#
题目: 完成下面 4 个透视表:
- 行是「城市」、列是「商品」、值是「金额」之和;
- 同上,但把空格子填成 0;
- 行是「城市」、列是「商品」、值是「销量」之和;
- 只按「城市」统计金额之和,并且加一行合计。
思考题: 第 4 题如果不写 aggfunc,算出来的是求和还是平均?
点击查看参考答案
# 1. 城市 × 商品 金额透视表
print(pd.pivot_table(df, index="城市", columns="商品", values="金额", aggfunc="sum"))商品 手机 显示器 电脑 耳机 键盘
城市
上海 NaN NaN 7999.0 NaN 1495.0
北京 7998.0 1299.0 NaN 1797.0 NaN
广州 15996.0 NaN NaN NaN NaN
深圳 NaN NaN NaN 2396.0 NaN# 2. 空格子填 0
print(pd.pivot_table(df, index="城市", columns="商品", values="金额",
aggfunc="sum", fill_value=0))商品 手机 显示器 电脑 耳机 键盘
城市
上海 0.0 0.0 7999.0 0.0 1495.0
北京 7998.0 1299.0 0.0 1797.0 0.0
广州 15996.0 0.0 0.0 0.0 0.0
深圳 0.0 0.0 0.0 2396.0 0.0# 3. 换成销量
print(pd.pivot_table(df, index="城市", columns="商品", values="销量", aggfunc="sum"))商品 手机 显示器 电脑 耳机 键盘
城市
上海 NaN NaN 1 NaN 1
北京 1 1 NaN 1 NaN
广州 2 NaN NaN NaN NaN
深圳 NaN NaN NaN 1 NaN# 4. 加合计行
print(pd.pivot_table(df, index="城市", values="金额", aggfunc="sum",
margins=True, margins_name="合计")) 金额
城市
上海 9494.0
北京 11094.0
广州 15996.0
深圳 2396.0
合计 38980.0要点: 第 4 题如果不写 aggfunc,pivot_table 的默认值是 mean(平均值),不是求和。
所以永远要显式写 aggfunc。
练习 4:使用 crosstab() 统计两个分类变量的频数#
题目: 完成下面 4 个任务:
- 统计「城市」×「商品」的交叉频数;
- 同上,但按行算出百分比(每一行加起来等于 1),保留 2 位小数;
- 统计「城市」×「商品」×「销量」的交叉表(第 3 个维度是数值,用
values=指定); - 用
crosstab统计每个城市卖出的「手机」有多少笔。
思考题: 第 2 题里北京那一行是 0.33, 0.33, 0.00, 0.33, 0.00,加起来是 0.99 而不是 1,为什么?
点击查看参考答案
# 1. 交叉频数
print(pd.crosstab(df["城市"], df["商品"]))商品 手机 显示器 电脑 耳机 键盘
城市
上海 0 0 1 0 1
北京 1 1 0 1 0
广州 2 0 0 0 0
深圳 0 0 0 1 0# 2. 按行归一化
print(pd.crosstab(df["城市"], df["商品"], normalize="index").round(2))商品 手机 显示器 电脑 耳机 键盘
城市
上海 0.00 0.00 0.50 0.00 0.50
北京 0.33 0.33 0.00 0.33 0.00
广州 1.00 0.00 0.00 0.00 0.00
深圳 0.00 0.00 0.00 1.00 0.00# 3. 三个维度:前两个做交叉表,第三个用 values 指定
print(pd.crosstab([df["城市"], df["商品"]], df["销量"]))销量 1 2 3 4 5
城市 商品
北京 手机 0 2 0 0 0
显示器 1 0 0 0 0
耳机 0 0 3 0 0
上海 电脑 1 0 0 0 0
键盘 0 0 0 0 5
广州 手机 0 2 0 0 0
深圳 耳机 0 0 0 4 0# 4. 只看「手机」这一列
phone = pd.crosstab(df["城市"], df["商品"])
print(phone["手机"])城市
上海 0
北京 1
广州 2
深圳 0
Name: 手机, dtype: int64要点: 第 2 题北京那行是 1/3 = 0.333...,保留 2 位小数后变成 0.33,三个 0.33 加起来是 0.99。
这是四舍五入的正常结果,不是算错了。如果要精确值,就别 round()。
练习 5:按照多个字段分组,并计算各组的统计指标#
题目: 完成下面 5 个任务:
- 按「城市」+「商品」分组,统计每组的订单数和总金额;
- 把第 1 题的结果用
reset_index()摊平成普通列; - 从第 1 题的结果里,取出北京这一组的所有行;
- 从第 1 题的结果里,取出北京 + 手机这一组的总金额;
- 统计**「城市 + 商品」这个组合**一共出现过多少种(提示:
reset_index()后数行数)。
思考题: 第 5 题的结果和「城市数 × 商品数」一样吗?为什么?
点击查看参考答案
# 1. 多级分组
g = df.groupby(["城市", "商品"])["金额"].agg(["count", "sum"])
print(g) count sum
城市 商品
上海 电脑 1 7999.0
键盘 1 1495.0
北京 手机 1 7998.0
显示器 1 1299.0
耳机 1 1797.0
广州 手机 2 15996.0
深圳 耳机 1 2396.0# 2. 摊平成普通列
flat = g.reset_index()
print(flat) 城市 商品 count sum
0 上海 电脑 1 7999.0
1 上海 键盘 1 1495.0
2 北京 手机 1 7998.0
3 北京 显示器 1 1299.0
4 北京 耳机 1 1797.0
5 广州 手机 2 15996.0
6 深圳 耳机 1 2396.0# 3. 取北京这一组
print(g.loc["北京"])商品
手机 7998.0
显示器 1299.0
耳机 1797.0
dtype: float64# 4. 取北京+手机这一组
# g 有 count 和 sum 两列,所以取出来是一个 Series
print(g.loc["北京", "手机"])count 1.0
sum 7998.0
Name: (北京, 手机), dtype: float64只要「总金额」那一列的话:
print(g.loc[("北京", "手机"), "sum"]) # 7998.0# 5. 一共有多少种「城市+商品」组合
print(len(flat)) # 7要点: 第 5 题答案是 7,而「4 个城市 × 5 种商品 = 20」。因为很多组合压根没出现过(比如北京没卖过键盘)。 组合数不能靠乘法算,得实际跑一遍。
加分练习:一份完整的城市销售报表#
题目: 用本节学到的所有工具,做一份「城市销售报表」,要求:
- 每个城市一行,列出:订单数、总金额、平均客单价(总金额 ÷ 总销量)、最高单笔金额;
- 按总金额从高到低排序;
- 加一行合计;
- 把「城市」这一列改名为「城市名称」;
- 打印报表。
点击查看参考答案
# 1. 汇总
report = df.groupby("城市").agg(
订单数=("订单号", "count"),
总金额=("金额", "sum"),
最高单笔=("金额", "max"),
总销量=("销量", "sum"),
)
# 客单价 = 总金额 / 总销量
report["平均客单价"] = (report["总金额"] / report["总销量"]).round(2)
# 2. 按总金额从高到低排序
report = report.sort_values("总金额", ascending=False)
# 3. 加合计行
report.loc["合计"] = report[["订单数", "总金额", "总销量"]].sum()
report.loc["合计", "平均客单价"] = round(
report.loc["合计", "总金额"] / report.loc["合计", "总销量"], 2
)
# 4. 改列名
report = report.rename(columns={"城市": "城市名称"})
# 5. 打印
print(report) 订单数 总金额 最高单笔 总销量 平均客单价
城市
广州 2.0 15996.0 7998.0 4.0 3999.00
北京 3.0 11094.0 7998.0 6.0 1849.00
上海 2.0 9494.0 7999.0 6.0 1582.33
深圳 1.0 2396.0 2396.0 4.0 599.00
合计 8.0 38980.0 NaN 20.0 1949.00要点: 这个练习把 groupby + agg + 命名聚合 + sort_values + rename 全串起来了。
两个小细节值得注意:
- 合计行的「最高单笔」是 NaN,因为「各组最大值再相加」本身没意义,所以这里干脆没算它;
- 合计行的客单价是 1949.00,这是「总金额 ÷ 总销量」,而不是各城市客单价直接相加(那样是 8029.33,没意义)。
客单价也可以用 transform() 算,但要先保证分子分母逐行对齐,写法比较绕:
d = df.copy()
d["城市总金额"] = d.groupby("城市")["金额"].transform("sum")
d["城市总销量"] = d.groupby("城市")["销量"].transform("sum")
d["客单价"] = d["城市总金额"] / d["城市总销量"]真实项目里,先算好中间结果、每一步都 print() 确认,比追求一行写完更重要。
练习小结#
| 题目 | 用到的核心语法 |
|---|---|
| 练习 1 | groupby()、size()、sort=False |
| 练习 2 | agg(["count","mean","sum","max"])、agg({列: 函数})、idxmax() |
| 练习 3 | pivot_table()、index/columns/values/aggfunc、fill_value、margins |
| 练习 4 | crosstab()、normalize="index" |
| 练习 5 | groupby(["A","B"])、reset_index()、g.loc[("北京","手机")] |
做完这 5 题,下一步 现在你有了一张「按城市汇总」的报表。但如果报表上还要显示「客户名」「会员等级」这些来自另一张表的信息怎么办?那就要把两张表「拼」起来——第 5 章的
merge()和concat()。