综合项目二:给书店写一份分析报告
3 道练习题·预计 60 分钟·做对一题解锁下一段
前言
上一个项目,分析的是自己的钱。这一个项目,咱们换个身份:假如你是两点书店的店长,手上有两份数据,一份是书目 books.csv ,一份是读者评分 book_reviews.csv 。你想从里面看出点什么,好决定下个月该进什么书、补什么货。
和上一章不一样的是,这一次,问题要你自己来提。
学过爬虫课的童鞋,应该认得 books.csv :它就是爬虫课第 9 章抓下来的那一份。你可以换成自己抓的文件,结果应该一模一样。没学过也不要紧,网页里已经准备好了。
那具体怎么做呢?步骤还是那六步,节奏也还是上一章的节奏:先提问,再检查数据,然后一个问题一个问题地回答,最后写成一份给店长看的报告。不过,这一章会在「检查」上多花一些力气。你会看到,哪怕是一个只差了 0.1 的数,背后也藏着值得弄清楚的东西。
学这一章之前,需要先学完 数据分析课第 9 章 。
目录
这一章会做:
- 一、提问——站在店长的角度
- 二、获取和检查——两张表对得上吗
- 三、书店里有些什么书,缺什么货
- 四、哪些书最受好评
- 五、评分的人是多了还是少了
- 六、写一份给店长的报告
- 七、常见坑
- 八、自己查一次文档——用
pd.cut()给书分价位 - 九、小结与回看
做完这一章,你就独立完成了一次从提问到报告的分析,数据分析课也就学完了。
一、提问
当店长,最关心的是什么呢?说来说去,无非是:店里的书对不对路,该进什么、该补什么。可这句话太笼统了,没法直接算。和上一章一样,先把它拆成几个具体的问题:
| 问题 | 要算的数 | 用到的本事 |
|---|---|---|
| 1. 书店有些什么书,各是什么价位? | 各类别的本数、价格中位数 | groupby() 、agg() |
| 2. 缺货的是哪些书?是不是受欢迎的书? | 缺货书的类别;缺货和有货的书,评分、评分条数比一比 | 筛选、分组 |
| 3. 哪一类书、哪几本书最受好评? | 各类别平均分;评分最高的几本 | merge() 、排序 |
| 4. 贵的书,评分是不是更高? | 不同价位的平均分 | 分组,第八节用 pd.cut() |
| 5. 来评分的人是多了还是少了? | 每个月的评分条数 | 字符串、画图 |
拆完了,还要反过来想一想:有哪些问题,是这两份数据答不了的?
- 「哪本书卖得最好?」这两份数据里没有销量。评分条数多,也许说明买的人多,可不是每个买书的人都会去打分,所以它只能算一个参考。
- 「哪类书最赚钱?」没有进价,只有售价,算不出利润。
答不了的,就不在报告里硬答。真要回答,得先去找更多的数据。
当然,这张表只是一种拆法。你当店长,也许还关心别的:老书和新书哪个评分高、作者写了好几本书的是不是更受欢迎……可以先跟着把这一章做完,再回过头来,用同样的办法回答你自己的问题。
问题有了。不过和上一章一样,动手回答之前,得先检查一下数据。
二、获取和检查
这一次有两张表,后面要合在一起用。所以第一件事,是检查它们对不对得上:
import pandas as pd
books = pd.read_csv('books.csv')
reviews = pd.read_csv('book_reviews.csv')
print(len(books), books['id'].is_unique)
print(len(reviews), reviews['book_id'].isin(books['id']).all())
print(reviews['date'].min(), reviews['date'].max())输出的结果:
120 True
834 True
2025-01-01 2026-11-01三行输出,分别检查了三件事:
is_unique:检查这一列的值是不是都不一样。结果是True,说明书的编号没有重复,每个编号只对应一本书,可以放心地当键。isin(books['id']).all():检查评分表里的每个书号,是不是都在书目表里。all()的意思是「全部都是True吗」。结果也是True,说明全都对得上,没有给不存在的书打的分。- 最后一行是评分的日期范围:从 2025 年 1 月 1 日,一直记到 2026 年 11 月 1 日。
两张表对得上号,这只是第一步。书目表里还有一列 rating ,是书店算好的平均分;而评分表里,是一条条原始的评分。那书店算的平均分,和原始的评分对得上吗?咱们用评分表自己算一遍,再和 rating 对一对,把不一样的行挑出来:
import pandas as pd
books = pd.read_csv('books.csv')
reviews = pd.read_csv('book_reviews.csv')
stats = reviews.groupby('book_id')['score'].agg(['count', 'mean']).reset_index()
table = books.merge(stats, left_on='id', right_on='book_id', how='left')
table['my_rating'] = table['mean'].round(1)
diff = table[table['rating'] != table['my_rating']]
print(diff[['id', 'title', 'rating', 'count', 'mean', 'my_rating']])输出的结果:
id title rating count mean my_rating
2 3 早起养花 NaN NaN NaN NaN
17 18 驿站笔记 3.3 4.0 3.25 3.2
21 22 轻松学网页开发 NaN NaN NaN NaN
22 23 光影的秘密 NaN NaN NaN NaN
41 42 月亮找朋友 NaN NaN NaN NaN
45 46 看懂经济学 NaN NaN NaN NaN
56 57 小熊找朋友 NaN NaN NaN NaN
60 61 旧城的钟声 NaN NaN NaN NaN
72 73 细胞的故事 4.3 4.0 4.25 4.2
75 76 长街旧影 NaN NaN NaN NaN
87 88 人人都懂的储蓄 4.3 4.0 4.25 4.2
97 98 人人都懂的价格 3.3 12.0 3.25 3.2
113 114 版式的力量 NaN NaN NaN NaN
114 115 气候的故事 NaN NaN NaN NaN
117 118 光里的秘密 NaN NaN NaN NaN一共挑出来 15 行。仔细看,它们分成两种:
- 11 行两边都是
NaN:这些是没人打过分的书。奇怪的是,NaN和NaN用!=比,结果竟然是「不相等」,所以它们被挑了出来。这是NaN的一个怪脾气:它和谁都不相等,连和自己也不相等。所以这 11 本,其实是对得上的。 - 另外 4 行,是真的差了 0.1:比如书店说 3.3,我们算的是 3.2。
这 0.1 是怎么差出来的呢?看 mean 这一列,全是 3.25、4.25 这样的数,正好落在两个数的正中间。
问题就出在「正中间」该怎么舍入上。书店的系统用的是平常的四舍五入,3.25 就进成了 3.3。而 pandas 的 round() 背后用的是 numpy,numpy 的文档里写着:正好在中间的值,要舍入到最近的偶数,所以 3.25 变成了 3.2。numpy.round 的说明里举的例子是:1.5 和 2.5,都变成 2.0。
所以,两边都没算错,只是舍入的规则不同。检查到这里,就可以放心了:两张表说的是同一回事。
你看,一个只差了 0.1 的数,追下去,就学到了 NaN 的怪脾气和两种舍入规则。分析之前做检查,就是为了把这种「差一点」的地方都弄清楚,免得后面的结论,建在说不清的数上。
数据检查完了,下一节就开始回答问题。
三、书店里有些什么书,缺什么货
先回答问题 1:书店有些什么书,各是什么价位?对每个类别,咱们想同时看几个数:有几本书、价格的中位数是多少、有几本缺货。可麻烦的是,这几个数来自不同的列,而第 6 章的 agg(['count', 'sum']) 只能对同一列来算。
那怎么办呢?其实 agg() 还有一种写法,可以给每个结果起个名字,同时说明拿哪一列、怎么算。格式是这样的:新列名=('哪一列', '怎么算') 。
import pandas as pd
books = pd.read_csv('books.csv')
books['out'] = books['stock'] == 0
summary = books.groupby('category').agg(
books=('id', 'count'),
median_price=('price', 'median'),
out_of_stock=('out', 'sum'),
)
print(summary.sort_values('median_price', ascending=False))输出的结果:
books median_price out_of_stock
category
设计 15 82.5 3
编程 15 69.3 4
科普 15 65.7 1
历史 15 60.2 3
经济 15 58.4 1
小说 15 48.6 0
生活 15 47.0 1
童书 15 38.7 1代码里有两处要说一下:
books['out']:库存是 0,就是缺货。比较的结果是一串True和False,sum()一下,就是缺货的本数。books=('id', 'count'):结果里叫books的这一列,数的是id列有几个值。
再看结果:8 个类别,每类各 15 本。设计类最贵,价格中位数 82.5 元;童书最便宜,38.7 元。缺货的一共 14 本,其中编程类最多,有 4 本。
知道了哪些类别缺货,接着就是问题 2:缺货的书,是不是更受欢迎?做法是把缺货和有货的书分成两组,比一比评分和评分条数:
import pandas as pd
books = pd.read_csv('books.csv')
books['out'] = books['stock'] == 0
print(books.groupby('out')[['rating', 'reviews']].mean().round(2))
print(books['out'].sum())输出的结果:
rating reviews
out
False 3.53 6.68
True 3.70 9.00
14这里要说一下的只有一处:
[['rating', 'reviews']]:分组以后一次取出两列,两列一起求平均。
结果是:缺货的 14 本,平均评分 3.70,比有货的 3.53 高一点;平均有 9 条评分,而有货的只有 6.68 条。看上去,缺货的确实是更受欢迎、被买得更多的书。
所以结论是:缺货的书偏向受欢迎的书,值得优先补货。不过缺货的只有 14 本,这个差别有多可靠,第七节还会再说。
先做一道练习:数一数缺货的书都属于哪个类别。
读进 books.csv ,挑出库存(stock)是 0 的书,用 value_counts() 数一数它们各属于哪个类别,打印出来。
运行后的输出应该是:
category
编程 4
历史 3
设计 3
童书 1
经济 1
科普 1
生活 1
Name: count, dtype: int64推荐一个,全网最全最友好的中文 AI 编程指南: coding.stormzhang.ai