用几个数说清一列
完整讲解 · 8 段教学·配 3 道练习题·预计 45 分钟
本页是本章的通读版,可直接读完全部讲解。想动手写代码、跑判分,去 闯关模式。
前言
上一章学会了从表里挑出想要的数据。可是挑出来以后,还得把它说清楚才行。比如记账本里一千多笔花销,一笔一笔地看,看到眼花,也说不出个所以然。要是有人问你「你平时一笔花多少钱」,总不能把整本账递过去吧?
这时候,就得用几个数,把一整列说清楚:一般是多少?最多的是哪种?大的有多大?波动又有多大?这几个数,在统计学里叫描述统计。
其实第 2 章用 describe() ,已经一下子算出了好几个,还发现了一件怪事:平均每笔 51 元,可一半的花销都不超过 21 元。当时说这一点第 4 章还会细讲,指的就是这一章。所以这一章,就把这几个数一个一个拆开,弄清楚它们各自在说什么,什么时候该信哪一个。
学这一章之前,需要先学完 数据分析课第 3 章 。
目录
这一章会学到:
- 一、平均数和中位数——「一般是多少」该信哪个
- 二、数一数——
value_counts(),以及占多大比例 - 三、分位数——大的有多大,
quantile()和nlargest() - 四、标准差——波动有多大
- 五、常见坑
- 六、自己查一次文档——用
value_counts()把金额分成几段 - 七、小结与回看
学完这一章,别人问起「你平时一笔花多少钱」,你能用几个数答得又准又清楚。
一、平均数和中位数
先来解开第 2 章留下的那个疑问:平均数和中位数,为什么会差那么多?别急着看记账本,先用四个数做个小实验。这里要用到 pd.Series() ,它能直接把一个列表变成一列:
import pandas as pd
s = pd.Series([10, 20, 30, 1000])
print(s.mean())
print(s.median())输出的结果:
265.0
25.0两个数差了一大截。它们各是怎么算出来的呢?
mean()算的是平均数:把所有数加起来,再除以个数。(10 + 20 + 30 + 1000) ÷ 4 = 265。median()算的是中位数:先从小到大排好,再取正中间的那个数。个数是双数的时候,就取中间两个的平均,(20 + 30) ÷ 2 = 25。
各位童鞋发现没有?四个数里有三个都不超过 30,平均数却是 265,没有一个数接近它。为什么会这样?就是因为那一个 1000,把平均数拉了上去。中位数就不一样了,它只看排在中间的是谁。最大的那个数是 1000 也好,100 万也好,中位数都不受影响。
小实验看明白了,再回到记账本。这回分两次算:先算全部的花销,再把房租去掉,算一遍。
import pandas as pd
ledger = pd.read_csv('ledger_2026.csv')
print(round(ledger['cost'].mean(), 2))
print(ledger['cost'].median())
no_rent = ledger[ledger['item'] != '房租']
print(round(no_rent['cost'].mean(), 2))
print(no_rent['cost'].median())输出的结果:
51.1
21.0
29.21
21.0对比一下:去掉一年的 12 笔房租以后,平均数从 51 元一下子掉到了 29 元;中位数却纹丝不动,还是 21 元。
所以,「平时一笔花多少」这个问题,中位数答得更准:21 元左右。那平均数是不是就没用了呢?也不是。平均数乘上笔数,就是总额,所以算总账离不开它。而且,两个数差得越远,说明数据里「特别大」或「特别小」的值越多,这本身就是一条值得注意的信息。
把两者放在一起,比一比:
平均数 mean() | 中位数 median() | |
|---|---|---|
| 怎么算 | 加起来除以个数 | 排好序取中间 |
| 受特别大、特别小的值影响吗 | 受,影响很大 | 几乎不受 |
| 适合回答 | 一共多少、平均分到每一个是多少 | 一般情况是多少 |
先做一道练习:晚饭一般花多少?用平均数和中位数各算一遍。
读进记账本,挑出名称(item)是「晚饭」的那些金额,分两行打印:平均数(用 round() 保留两位小数)和中位数。
运行后的输出应该是:
39.58
40.0💡 提示(3 条)
先挑行再取列:ledger[ledger['item'] == '晚饭']['cost']
平均数 round(dinner.mean(), 2),中位数 dinner.median()
完整代码:
import pandas as pd ledger = pd.read_csv('ledger_2026.csv') dinner = ledger[ledger['item'] == '晚饭']['cost'] print(round(dinner.mean(), 2)) print(dinner.median())
二、数一数
平均数和中位数,都是拿数字来算的。可是类别、付款方式这些列是文字,没法求平均,那该怎么说清楚呢?办法就是数一数:每一种各出现了几次。
import pandas as pd
ledger = pd.read_csv('ledger_2026.csv')
print(ledger['category'].value_counts())输出的结果:
category
餐饮 984
交通 241
娱乐 46
居住 36
购物 29
学习 11
医疗 7
Name: count, dtype: int64value_counts() 会数出每个值出现了几次,还会自动按从多到少排好。还记得第 2 章 describe() 里的 top 和 freq 吗?它们其实就是这张表的第一行:餐饮,984 次。
不过,光看次数,不太容易感觉出到底占了多少。这时候加上 normalize=True ,次数就变成了比例:
import pandas as pd
ledger = pd.read_csv('ledger_2026.csv')
print(ledger['category'].value_counts(normalize=True).round(3))输出的结果:
category
餐饮 0.727
交通 0.178
娱乐 0.034
居住 0.027
购物 0.021
学习 0.008
医疗 0.005
Name: proportion, dtype: float64这些比例加起来是 1。代码和输出里,还有两个小地方要说一下:
round(3)是 Series 的方法,会把每个值都保留三位小数。- 最下面的名字,也从
count变成了proportion,就是「比例」的意思。
回到餐饮那一行,0.727 的意思是:一年里每 100 笔花销,大约有 73 笔是吃饭。可这样的小数,读起来总不如百分数直观。想写成百分数,可以用 f-string 的 % 格式:
import pandas as pd
ledger = pd.read_csv('ledger_2026.csv')
shares = ledger['category'].value_counts(normalize=True)
print(f"餐饮占 {shares['餐饮']:.1%}")输出的结果:
餐饮占 72.7%shares['餐饮'] 是按名字取出一个值。后面的 :.1% 会把它乘以 100,保留一位小数,再加上百分号。
先做一道练习,用同样的办法,看看各种付款方式各占几成。
三、分位数
中位数,说的是「排在正中间的那个」。把这个想法推广一下:排在四分之一处的、排在十分之九处的,又各是多少呢?这些数叫分位数,用 quantile() 来算:
import pandas as pd
ledger = pd.read_csv('ledger_2026.csv')
print(ledger['cost'].quantile(0.5))
print(ledger['cost'].quantile([0.25, 0.75, 0.9]))输出的结果:
21.0
0.25 9.625
0.75 32.000
0.90 55.500
Name: cost, dtype: float64先看第一行:quantile(0.5) 算出来是 21,它就是中位数。括号里要是放一个列表,就能一次算出好几个。下面这几个数,读法是这样的:
0.25对应 9.625:四分之一的花销不超过 9.6 元左右,像早饭、地铁这类小钱。0.75对应 32:四分之三的花销不超过 32 元。0.9对应 55.5:十笔里有九笔不超过 55.5 元。
有的童鞋可能会问:账本里真有 9.625 元这笔花销吗?其实并没有。排在四分之一处的那个位置,正好落在两笔花销之间,pandas 就按比例,在两者之间取了一个值。
分位数告诉你的,是「大的有多大」。那最大的几笔,具体都是什么呢?这时候用 nlargest() :第一个参数是要几行,第二个参数是按哪一列来比。
import pandas as pd
ledger = pd.read_csv('ledger_2026.csv')
no_rent = ledger[ledger['item'] != '房租']
print(no_rent.nlargest(5, 'cost'))输出的结果:
date item category cost payment
1140 2026-11-06 衣服 购物 391.0 支付宝
1317 2026-12-23 衣服 购物 388.5 微信
695 2026-07-09 衣服 购物 383.5 支付宝
1256 2026-12-08 衣服 购物 364.5 支付宝
566 2026-06-02 衣服 购物 317.0 微信注意,这里先去掉了房租。为什么呢?因为不去掉的话,前 12 名全是房租。去掉以后就能看出:除了房租,最大的几笔都是买衣服,而且都在三四百元。
反过来,要找最小的几笔,就换成 nsmallest() 。
四、标准差
前面三节,回答的是「一般是多少」「最多的是哪种」「大的有多大」。前言里列的几个问题,还剩最后一个:波动有多大?先来想一想:平均数一样,情况就一样吗?
想象有两个人:一个每顿饭都花 30 元;另一个有时花 10 元,有时花 50 元,平均下来也是 30 元。这两个人的平均数一样,可第二个人花钱忽多忽少,就更难做预算。
衡量这种「忽多忽少」的数,叫标准差,用 std() 来算。刚开始,可以大致这样理解它:每个值离平均数,一般差多少。差得越多,标准差就越大。
拿一日三餐来比一比:
import pandas as pd
ledger = pd.read_csv('ledger_2026.csv')
for meal in ['早饭', '午饭', '晚饭']:
cost = ledger[ledger['item'] == meal]['cost']
print(meal, round(cost.mean(), 2), round(cost.std(), 2), cost.min(), cost.max())输出的结果:
早饭 10.65 2.63 6.0 15.0
午饭 26.4 4.98 18.0 35.0
晚饭 39.58 11.69 20.0 59.5每一行依次是:平均数、标准差、最少、最多。挑早饭和晚饭来对比一下:
- 早饭平均 10.65 元,标准差 2.63,一般就在 8 到 13 元上下,最少 6 元,最多 15 元,很稳定。
- 晚饭平均 39.58 元,标准差 11.69,有时二十出头,有时将近 60 元,忽多忽少。
光看平均数,只知道晚饭比早饭贵;加上标准差,才知道晚饭的钱最不好预算。所以要省钱,从晚饭下手最有空间。
最后再说一句这段代码。上面用 for 循环,把同样的事情对三种饭各做了一遍。其实「按某一列分成几组,每组各算一遍」这种事,pandas 有更省事的办法,第 6 章会讲。
五、常见坑
这几个统计量,算起来都只要一行代码。可正因为算得太容易,一不留神,算出来的数就会误导你。下面这三个坑,都不会报错,所以更要留心。
1、空着的值被悄悄跳过了
books.csv 里,有的书还没有人打分,rating 这一列就空着。那平均评分会怎么算呢?试一下:
import pandas as pd
books = pd.read_csv('books.csv')
print(len(books))
print(books['rating'].count())
print(round(books['rating'].mean(), 2))输出的结果:
120
109
3.55120 本书,有评分的只有 109 本。可 mean() 并没有报错,因为它跳过了空值,算的是这 109 本书的平均。
这通常正是我们想要的,因为没人打分的书,不该当成 0 分来算。可你得知道:这个平均数背后,是 109 本书,而不是 120 本。
不光是 mean() ,value_counts() 也一样,默认不数空值。想把空值也数进去,就加上 dropna=False :
import pandas as pd
books = pd.read_csv('books.csv')
print(books['rating'].value_counts(dropna=False).head(3))输出的结果:
rating
4.0 13
NaN 11
2.8 8
Name: count, dtype: int64这里的 NaN ,就是 pandas 里的空值,意思是「不是一个数」。可以看到,空着的有 11 本,排在第二,只比 4.0 分的 13 本少,比其他任何一个分数的书都多。这一点,要是不把空值数进来,是看不出来的。空值该怎么处理,第 5 章清洗数据时会专门讲。
2、按笔数和按金额,结论不一样
再来看一个问题:「我最常用什么付钱?」分别按笔数和按金额,各算一遍:
import pandas as pd
ledger = pd.read_csv('ledger_2026.csv')
print(ledger['payment'].value_counts())
print(ledger.groupby('payment')['cost'].sum().sort_values(ascending=False))输出的结果:
payment
支付宝 582
微信 575
银行卡 185
现金 12
Name: count, dtype: int64
payment
银行卡 34442.5
支付宝 19207.0
微信 15259.5
现金 286.5
Name: cost, dtype: float64按笔数,支付宝第一;按金额,却是银行卡第一。为什么呢?因为房租这种大钱,都是用银行卡交的。
两个结论都对,只是回答的是不同的问题。所以说「最常用」之前,先想清楚:说的是「用的次数最多」,还是「花的钱最多」?
3、四舍五入以后,比例加起来不是 1
第二节说过,比例加起来是 1。可要是先四舍五入了呢?
import pandas as pd
ledger = pd.read_csv('ledger_2026.csv')
shares = ledger['payment'].value_counts(normalize=True).round(3)
print(shares)
print(shares.sum())输出的结果:
payment
支付宝 0.430
微信 0.425
银行卡 0.137
现金 0.009
Name: proportion, dtype: float64
1.001每个比例都往上或者往下凑了一点,加起来就多出了 0.001。所以,要是还要拿比例接着算,就用没有四舍五入的原始值;round() 只在最后给人看的时候才用。
六、自己查一次文档
value_counts() 能数文字,那能不能拿来数金额呢?问题是,金额几乎每一笔都不一样,直接数的话,会数出几百种来。更有用的做法,是把金额分成几段:10 元以下的有几笔,10 到 20 元的有几笔……这能不能做到呢?去看看文档:Series.value_counts 。
它的参数一共有五个:
normalize、dropna,前面已经用过了。sort默认是True,按次数排序;设成False,就按原来的顺序。ascending管的是从小到大排,还是从大到小排。- 还有一个
bins。文档说,用了它,就不再数每个值,而是把它们分成几个半开区间来数,这是pd.cut的简便写法,只能用在数字上。它的类型写的是int,也就是分成几段。
看起来,bins 就是咱们要找的。那就试一试,分成 4 段:
import pandas as pd
ledger = pd.read_csv('ledger_2026.csv')
print(ledger['cost'].value_counts(bins=4))输出的结果:
(0.502, 627.25] 1342
(1875.75, 2500.0] 12
(627.25, 1251.5] 0
(1251.5, 1875.75] 0
Name: count, dtype: int64分是分了,可没什么用。为什么呢?因为它是把最小值到最大值平均切成 4 段,每段六百多元宽,房租以外的花销,全挤在了第一段里。
咱们想要的,是自己定边界,比如 10、20、50、100。这又该怎么写呢?文档里提到,它是 pd.cut 的简便写法。那就顺着点进 pandas.cut ,看看它的 bins 参数。原来,除了写一个整数,还可以写一串数。这串数就是每一段的边界,各段可以不一样宽。
那就试试把边界传进去:
import pandas as pd
ledger = pd.read_csv('ledger_2026.csv')
print(ledger['cost'].value_counts(bins=[0, 10, 20, 50, 100, 3000], sort=False))输出的结果:
(-0.001, 10.0] 363
(10.0, 20.0] 262
(20.0, 50.0] 557
(50.0, 100.0] 108
(100.0, 3000.0] 64
Name: count, dtype: int64这下清楚了:20 到 50 元的最多,有 557 笔;超过 100 元的只有 64 笔。这里还加了一个 sort=False ,让各段按金额从小到大排,读起来更顺。
再说一下输出里的写法。(10.0, 20.0] 是半开区间:圆括号表示不包括 10,方括号表示包括 20。第一段写成了 (-0.001, 10.0] ,是 pandas 为了把正好等于 0 的值也算进去,往左挪了一点点,不用在意。
从这次查文档也能看出来:文档里一个参数的说明,有时只写了最常用的用法。顺着它提到的另一个函数看过去,往往能找到更多的门道。
七、小结与回看
这一章,咱们用几个数,就把一整列说清楚了。回头梳理一下,学到了这几件事:
- 平均数
mean()是加起来除以个数,容易被特别大、特别小的值带偏;中位数median()取的是排在中间的值,几乎不受影响。说「一般情况」,中位数更靠谱;算总账,离不开平均数。 value_counts()能数出每个值出现了几次。加上normalize=True就变成比例,round()能保留几位小数,:.1%能写成百分数。quantile()算分位数,比如0.9表示十个里有九个不超过它;nlargest()、nsmallest()能找出最大、最小的几行。- 标准差
std()衡量的是忽多忽少:每个值离平均数,一般差多少。 - 常见的坑:空值会被悄悄跳过,要留意背后的个数;按笔数和按金额,结论不一样;四舍五入只在最后做。
- 查文档学会了用
value_counts(bins=...)把金额分段,还顺着pd.cut的文档,学会了自己定边界。
哪里卡住了,就回去看一看:
- f-string:第 2 章
for循环:第 5 章
到这里,咱们用的都是一份干干净净的账本。可现实里的数据,往往没有这么整齐。所以下一章换一份数据:一份还没整理过的账单。它的日期格式不统一,金额后面带着「元」,还有空着的、重复的、明显记错的。把它收拾干净,是数据分析里最花时间、也最见功夫的一步。
读完了?动手练一遍才算真会。
去闯关模式练习 →