一、标准库:装好就有的一整套工具
把 Python 装好,除了语言本身,还附送了几百个标准库(standard library)。它们的最大好处是:不用装、不用 pip、直接 import 就能用。
第 11 篇《导入库的方法》已经介绍过「标准库 vs 第三方库」的区别。本篇把最常用的一批挑出来,讲清楚它们各自解决什么问题:
| 模块 | 管什么 | 一句话记住 |
|---|---|---|
os | 文件、文件夹、路径、环境变量 | 和老式操作系统打交道 |
pathlib | 路径(更现代的写法) | os.path 的升级版 |
json | 读写 JSON 格式的数据 | 数据交换的通用格式 |
csv | 读写逗号分隔的表格文件 | Excel 能直接打开的纯文本表 |
datetime | 日期与时间 | 算日期、格式化时间 |
statistics | 平均数、中位数、标准差 | 统计指标一句话算完 |
random | 随机数、抽样、打乱 | 抽几个来检查、造模拟数据 |
time | 时间戳、计时、让程序等等 | 给数据盖时间、量代码快不快 |
collections | 会数数的字典、自动分类的字典 | 数谁最多、按类别累加 |
为什么是这几个?因为它们正好凑齐了一条「数据活儿」的完整链路:把数据存下来 → 读回来 → 算清楚 → 看得见。第 26 篇《文件处理》讲的是纯文本怎么读写,本篇讲的是「数据用什么格式存、用什么工具算」。《12-time库与random库》里讲过这两个库的基础用法,本篇补的是做数据分析时才会用到的那几个用法。
pathlib + json + csv(最常用),把 datetime、statistics、random、time 和第十节那几个第三方库,留到真正要做数据分析时再回来查。课程思政|科技自立自强 · 现成的轮子,也得看懂
Python 装好,就自带了几百个标准库,import 一下就能用,这是无数人替我们攒下的家底。但用得顺手,不代表真懂了:会用别人造好的轮子,和知道车为什么能跑,是两回事。os、json、csv 背后无非是「读写文件、整理数据」这些硬功夫,把原理弄明白,将来自带库里没有的需求,你才有底气自己动手。
二、pathlib:拼路径、拆路径
先说 pathlib,因为后面几个库都要处理「文件放在哪儿」。它是现在推荐的写法:把路径当成一个对象来看待。
2.1 造一个路径
from pathlib import Path
p = Path('/Users/x/data/scores.csv')
print('完整路径:', p)
print('文件名:', p.name)
print('去掉后缀:', p.stem)
print('后缀:', p.suffix)
print('所在文件夹:', p.parent)
输出:
完整路径: /Users/x/data/scores.csv
文件名: scores.csv
去掉后缀: scores
后缀: .csv
所在文件夹: /Users/x/data
注意这几个属性不用记拼写规则,看一眼就知道是什么意思——这正是 pathlib 比老写法好用的地方。
2.2 拼路径:用 / 就行
from pathlib import Path
folder = Path('data')
print(folder / 'scores.csv')
print(folder / '2026' / '10月' / 'scores.csv')
输出:
data/scores.csv
data/2026/10月/scores.csv
写 folder / '名字' 就够了,不用自己判断该用斜杠还是反斜杠——这一点很重要,因为 Windows 和 macOS 的路径分隔符不一样,手写字符串很容易在换电脑后失效。
| 写法 | 拼出来的路径 | 换到 Windows 上 |
|---|---|---|
Path('data') / 'a.csv' | data/a.csv | 自动变成 data\a.csv |
'data' + '/' + 'a.csv' | data/a.csv | 还是斜杠,可能失效 |
2.3 常用的四个动作
import pathlib, tempfile
from pathlib import Path
work = Path(tempfile.mkdtemp())
f = work / 'score.txt'
print('存在吗(写之前):', f.exists())
f.write_text('小满 92', encoding='utf-8') # 一行写文件
print('存在吗(写之后):', f.exists())
print('内容:', f.read_text(encoding='utf-8')) # 一行读文件
f.unlink() # 删除
print('删除后还在吗:', f.exists())
输出:
存在吗(写之前): False
存在吗(写之后): True
内容: 小满 92
删除后还在吗: False
这几个方法都是「一个动作一行代码」,不用再写 open() 加 with 那一套:
| 方法 | 作用 | 对应老写法 |
|---|---|---|
p.exists() | 判断存在吗 | os.path.exists(p) |
p.read_text() | 一次读出全部文字 | open(p).read() |
p.write_text(s) | 一次写入文字(覆盖原内容) | open(p, 'w').write(s) |
p.unlink() | 删除这个文件 | os.remove(p) |
p.mkdir(parents=True, exist_ok=True) | 建文件夹 | os.makedirs(p) |
2.4 老写法 os.path 也认一下
老教程和老代码里全是 os.path,看得懂就行,新代码建议用 pathlib。
import os
print('拼路径:', os.path.join('data', 'scores.csv'))
print('取文件名:', os.path.basename('/Users/x/data/scores.csv'))
print('拆后缀:', os.path.splitext('scores.csv'))
print('存在吗:', os.path.exists('这个文件不存在.txt'))
输出:
拼路径: data/scores.csv
取文件名: scores.csv
拆后缀: ('scores', '.csv')
存在吗: False
os.path.join() 拼出来的分隔符跟你的系统走:macOS / Linux 上是 /,Windows 上是 \。所以页面上这里显示 data/scores.csv,你在 Windows 上跑会看到 data\scores.csv——这是对的,不是错。要跨系统稳定,就用 pathlib 的 /。三、json:数据交换的通用格式
要把数据存进文件,得先定一个格式。自己发明格式的问题很明显:今天用逗号分,明天想加一列就崩了。
JSON 是一个大家都认的格式:它长得像 Python 的字典和列表,几乎所有编程语言都能读写。它的规则就四条:
| JSON 里的东西 | 写法 | 对应 Python |
|---|---|---|
| 对象 | {"name": "小满"} | 字典 dict |
| 数组 | [1, 2, 3] | 列表 list |
| 字符串 | "abc"(必须双引号) | 字符串 str |
| 数字 / 真值 / 空值 | 12 3.5 true false null | 小写是 True / False / None |
3.1 四个函数,两两配对
| 函数 | 作用 | 记法 |
|---|---|---|
json.dumps(对象) | Python 对象 → JSON 字符串 | dump string:转成字符串 |
json.loads(字符串) | JSON 字符串 → Python 对象 | load string:从字符串读 |
json.dump(对象, 文件) | Python 对象 → 写进文件 | 直接落盘 |
json.load(文件) | 从文件 → Python 对象 | 从文件读 |
import json
data = {'name': '小满', 'score': 92, 'courses': ['数学', '英语']}
text = json.dumps(data, ensure_ascii=False)
print(text)
back = json.loads(text)
print(back)
print('读回来还是字典吗:', type(back).__name__)
print('取出成绩:', back['score'])
输出:
{"name": "小满", "score": 92, "courses": ["数学", "英语"]}
{'name': '小满', 'score': 92, 'courses': ['数学', '英语']}
读回来还是字典吗: dict
取出成绩: 92
3.2 ensure_ascii=False 是关键
上面第二个参数如果省掉,中文会变成一堆 \u… 编码,人能看但很难读:
import json
data = {'name': '小满'}
print('默认:', json.dumps(data))
print('加参数:', json.dumps(data, ensure_ascii=False))
输出:
默认: {"name": "\u5c0f\u6ee1"}
加参数: {"name": "小满"}
两种都能被 loads() 正确读回来,功能没问题。但存给学生看的文件、或者要拿给人检查的文件,一定加上 ensure_ascii=False。
3.3 存在文件里
import json, pathlib, tempfile
work = pathlib.Path(tempfile.mkdtemp())
f = work / 'scores.json'
data = {'班级': '统计一班', '成绩': [92, 78, 85]}
# 存:dump(obj, 文件) —— 注意别写成 dumps 再自己写字符串
f.write_text(json.dumps(data, ensure_ascii=False, indent=2), encoding='utf-8')
print(f.read_text(encoding='utf-8'))
# 读:load(文件)
back = json.loads(f.read_text(encoding='utf-8'))
print('读回来的成绩:', back['成绩'])
输出:
{
"班级": "统计一班",
"成绩": [
92,
78,
85
]
}
读回来的成绩: [92, 78, 85]
indent=2 让 JSON 带缩进换行,人看着舒服、改起来方便;机器读不需要缩进。给人和给机器,选一个就行——数据量大时缩进会让文件明显变大。3.4 不是所有东西都能转成 JSON
import json
print(json.dumps({1, 2, 3})) # 集合
运行后会报错(逐字照抄解释器):
TypeError: Object of type set is not JSON serializable
能转的只有这几种:字典、列表、字符串、整数、小数、布尔、None。像集合、函数、自定义对象都转不了。元组会被转成列表(读回来时是列表,不再是元组)——这一点写程序时要留意。
课程思政|工匠精神与严谨求实 · 少一个引号,别人就读不了
JSON 只认双引号、不允许多余的逗号,规矩看着苛刻,可正是这份严格,换来了它在几乎所有语言之间都能通用。写数据也是这个道理:把一份结果存成 JSON 交给别人,格式上每一处将就,最后都得别人替你还。养成存完就用 json.loads() 试读一遍的习惯,比事后回头翻找省事得多。
四、表格格式:csv、tsv 与 xlsx
如果数据本来就是「一张表」,每行一条记录、每列一个字段,那 CSV(逗号分隔值)最合适。它的好处是:纯文本、体积小、Excel 双击就能打开、几乎所有数据分析工具都认。
一个 CSV 文件用记事本打开,长这样(三个字段、两条记录):
| 姓名 | 分数 | 班级 |
|---|---|---|
| 小满 | 92 | 统计一班 |
| 阿禾 | 78 | 统计一班 |
它在磁盘上的原始文字其实是这样一行行的,字段之间真的就是一个英文逗号:
姓名,分数,班级小满,92,统计一班阿禾,78,统计一班
4.1 读:csv.reader() 用下标取
import csv, io
text = '姓名,分数\n小满,92\n阿禾,78\n'
for row in csv.reader(io.StringIO(text)):
print(row)
输出:
['姓名', '分数']
['小满', '92']
['阿禾', '78']
每一行是一个列表,按位置取(row[0] 是姓名、row[1] 是分数)。列一多,这种写法就容易数错。
4.2 读:csv.DictReader() 用字段名取
import csv, io
text = '姓名,分数\n小满,92\n阿禾,78\n'
for row in csv.DictReader(io.StringIO(text)):
print(row)
print('这位同学是:', row['姓名'])
输出:
{'姓名': '小满', '分数': '92'}
这位同学是: 小满
{'姓名': '阿禾', '分数': '78'}
这位同学是: 阿禾
每一行是一个字典,用第一行当字段名。推荐用这种:代码可读性好,以后列顺序变了也不受影响。
4.3 写:csv.writer()
import csv, io
buf = io.StringIO()
w = csv.writer(buf)
w.writerow(['姓名', '分数'])
w.writerow(['小满', 92])
w.writerow(['阿禾', 78])
print('磁盘上真正的文字:')
print(buf.getvalue())
输出:
磁盘上真正的文字:
姓名,分数
小满,92
阿禾,78
open() 配合 csv 读写文件时,必须写 newline='':open('a.csv', 'w', encoding='utf-8', newline='')漏掉它,Windows 上每写一行会多出一个空行(因为 csv 自己写的换行和 open 的换行叠加了)。这是 csv 最经典的坑。
4.4 真正读写文件的标准写法
import csv, pathlib, tempfile
work = pathlib.Path(tempfile.mkdtemp())
f = work / 'scores.csv'
rows = [['姓名', '分数'], ['小满', 92], ['阿禾', 78]]
# 写:newline='' 一定不能少
with open(f, 'w', encoding='utf-8', newline='') as fp:
csv.writer(fp).writerows(rows)
# 读:用 DictReader,按字段名取值
with open(f, 'r', encoding='utf-8', newline='') as fp:
for row in csv.DictReader(fp):
print(row['姓名'], '的分数是', row['分数'], '(类型:' + type(row['分数']).__name__ + ')')
输出:
小满 的分数是 92 (类型:str)
阿禾 的分数是 78 (类型:str)
注意最后那个类型:读进来的分数是字符串 '92',不是数字 92。要参与计算必须先转:int(row['分数'])。这一点和 input() 收到的一律是文字是同一个道理。
utf-8-sig(带 BOM):encoding='utf-8-sig'。这样 Excel 打开中文不会乱码。读的时候用 utf-8 或 utf-8-sig 都能读。4.5 csv 之外,Excel 还能打开哪些格式
别再以为「Excel 文件」只有一种。同样是 Excel 能双击打开的表格,后缀不同,里面的东西差得很远:
| 后缀 | 里面到底是什么 | 标准库能读写吗 | 什么时候用 |
|---|---|---|---|
.csv | 纯文本,字段之间用逗号隔开 | 能,用 csv | 数据交换、导给别的软件——最通用 |
.txt / .tsv | 纯文本,字段之间用制表符隔开 | 能,还是用 csv(换个分隔符) | 内容里本来就带逗号,或者习惯按 Tab 分列 |
.xlsx | 不是文本:一个 zip 压缩包,里面装着一堆 XML | 不能,要装第三方库 | 正式报表,要保留格式 / 公式 / 多个工作表 |
.xls | 老版本 Excel 的二进制格式 | 不能,要装第三方库 | 只有老系统还在用,新表一律别用它 |
一句话记住:能用记事本打开、看得懂的是纯文本(csv / tsv),标准库全包;用记事本打开全是乱码的是二进制(xlsx / xls),必须请第三方库出场。
.xlsx?要装 openpyxl 或 pandas,写法在第十节 10.3。这一节先把「纯文本表格」练熟——它是所有软件都认的「通用货币」。4.6 分隔符不一定是逗号
CSV 的 C 是 comma(逗号),但只要换个分隔符,同一个 csv 模块也能写出「制表符分隔」的 .tsv——Excel 照样双击能开:
import csv, io
buf = io.StringIO()
w = csv.writer(buf, dialect='excel-tab') # 用制表符代替逗号
w.writerow(['姓名', '分数'])
w.writerow(['小满', 92])
w.writerow(['阿禾', 78])
print(repr(buf.getvalue()))
输出:
'姓名\t分数\r\n小满\t92\r\n阿禾\t78\r\n'
看仔细:字段之间变成了 \t(制表符,就是键盘上那个 Tab),每行结尾是 \r\n。这两个都是「看不见的字符」,所以在 Python 里要用 repr() 打印,才能把它们看清楚。
拿到一份别人给的表,不知道是逗号分隔还是制表符分隔怎么办?让 csv 自己看:
import csv
texts = [
'姓名,分数\n小满,92\n阿禾,78\n小鹿,85\n',
'姓名\t分数\n小满\t92\n阿禾\t78\n小鹿\t85\n',
]
for t in texts:
dialect = csv.Sniffer().sniff(t)
print('这一份用的分隔符是:', repr(dialect.delimiter))
输出:
这一份用的分隔符是: ','
这一份用的分隔符是: '\t'
csv.Sniffer() 就是「探测器」:给它一段内容,它替你猜分隔符。数据来源杂的时候很好用,比自己写 if ',' in line 靠谱。
五、datetime:日期与时间
给数据加时间戳、算「距今多少天」、把时间显示成指定格式,都要用 datetime。它里面最常用的四个东西:
| 名字 | 是什么 | 例子 |
|---|---|---|
datetime.datetime | 日期 + 时间 | 2026-10-04 19:30:00 |
datetime.date | 只有日期 | 2026-10-04 |
datetime.timedelta | 一段时间(几天几小时) | timedelta(days=10) |
datetime.now() | 取当前时间(每次都不一样) | — |
5.1 造一个时间、显示成你要的样子
为了输出可重复,下面用一个固定的时间点当例子;真实代码里写 datetime.now() 就行。
from datetime import datetime
d = datetime(2026, 10, 4, 19, 30, 0)
print('原样:', d)
print('自定义格式:', d.strftime('%Y年%m月%d日 %H:%M'))
print('只要日期:', d.strftime('%Y-%m-%d'))
print('星期几:', d.strftime('%A'))
输出:
原样: 2026-10-04 19:30:00
自定义格式: 2026年10月04日 19:30
只要日期: 2026-10-04
星期几: Sunday
strftime 里的格式符号是最需要记的一小块:
| 符号 | 含义 | 例子输出 |
|---|---|---|
%Y | 四位年份 | 2026 |
%m | 两位月份 | 10 |
%d | 两位日期 | 04 |
%H / %M / %S | 时 / 分 / 秒(24 小时制) | 19 30 00 |
%A | 星期几(英文) | Sunday |
5.2 把字符串变成时间:strptime
from datetime import datetime
d = datetime.strptime('2026-10-04', '%Y-%m-%d')
print(d)
print('类型:', type(d).__name__)
print('年份:', d.year)
输出:
2026-10-04 00:00:00
类型: datetime
年份: 2026
strftime 和 strptime 只差一个字母,方向刚好相反,是新手最容易记混的一对:
| 函数 | 方向 | 记法 |
|---|---|---|
strftime | 时间对象 → 字符串 | format:去格式化 |
strptime | 字符串 → 时间对象 | parse:去解析 |
5.3 算日期:timedelta
from datetime import date, timedelta
d = date(2026, 10, 4)
print('十天后:', d + timedelta(days=10))
print('一周前:', d - timedelta(weeks=1))
print('相差几天:', (date(2026, 10, 4) - date(2026, 10, 1)).days)
输出:
十天后: 2026-10-14
一周前: 2026-09-27
相差几天: 3
两个日期相减得到 timedelta,取 .days 就是天数;日期加减 timedelta 得到新日期。月份、闰年、跨年这些麻烦事它都替你算好了,千万不要自己用 30 天当一个月。
AI 助手小贴士|标准库几十个模块,先让 AI 帮你挑最常用的三个上手。
打开任意 AI 对话工具(网页版或 App 都可以),试试这样问:从 os、datetime、json、random、math、time 里挑 3 个初学者最常用的,各给一个能直接跑的最小例子
六、statistics:描述性统计一次算全
statistics 是标准库自带的「统计小工具」,专门算平均数、中位数、众数、标准差这些指标。做数据分析,第一步就是把数字算对——这一节把描述性统计常用的指标过一遍,能算的都算给你看。
6.1 四个最常用的指标
import statistics
scores = [88, 92, 79, 95, 61]
print('平均分:', statistics.mean(scores))
print('中位数:', statistics.median(scores))
print('标准差:', round(statistics.stdev(scores), 2))
print('最高分:', max(scores))
输出:
平均分: 83
中位数: 88
标准差: 13.69
最高分: 95
四个词的区别:mean 是全部加起来再平均;median 是从小到大排在正中间的那个;stdev 是标准差,衡量分数「散不散」,越小越整齐;mode 是出现次数最多的那个。
| 函数 | 算什么 | 什么时候用 |
|---|---|---|
statistics.mean(x) | 平均值 | 大家都差不多时,最直观 |
statistics.median(x) | 中位数 | 数据里有极端值(有人考 0 分、有人收入特别高) |
statistics.mode(x) | 众数 | 分类数据:哪种最受欢迎 |
statistics.stdev(x) | 标准差 | 比较两队稳不稳定 |
6.2 描述性统计,看四类指标
「描述性统计」听着唬人,其实就是用几个数字把一堆数据的样子说清楚。这些数字分四类,各回答一个问题:
| 类别 | 回答什么问题 | 代表指标 |
|---|---|---|
| 集中趋势 | 数据的「平均水平」在哪 | 平均值、中位数、众数 |
| 离散程度 | 数据散不散、稳不稳定 | 极差、方差、标准差、变异系数 |
| 分布位置 | 数据从哪到哪、怎么分布 | 最小值、最大值、四分位数、四分位距 |
| 变量间关系 | 两组数据有没有一起变 | 相关系数、回归直线 |
下面按这个顺序一个一个试。所有例子用的都是同一组十位同学的分数,方便前后对照。
6.3 集中趋势:平均数不止一种
import statistics
scores = [88, 92, 79, 95, 61, 84, 90, 73, 100, 0]
print('平均值:', statistics.mean(scores))
print('中位数:', statistics.median(scores))
print('下中位数:', statistics.median_low(scores))
print('上中位数:', statistics.median_high(scores))
输出:
平均值: 76.2
中位数: 86.0
下中位数: 84
上中位数: 88
十个数是偶数个,「正中间」落在第 5 个(84)和第 6 个(88)之间:median 取两者的平均 86.0,median_low 取靠下的 84,median_high 取靠上的 88。要求「结果本身就在数据里」(比如「中位数同学考了多少分」),就用 low / high。
还有两种平均数专门对付特殊场景。看增长率用几何平均;看速度、单价这种「每段总量相同」的用调和平均:
import statistics
# 连续三年的增长率:1.10 表示比上一年涨 10%
growth = [1.10, 0.95, 1.20]
print('几何平均(平均增长率):', round(statistics.geometric_mean(growth), 4))
# 来回两趟路程一样,去时 60、回时 40,平均速度是多少?
print('调和平均(平均速度):', statistics.harmonic_mean([60, 40]))
输出:
几何平均(平均增长率): 1.0784
调和平均(平均速度): 48.0
平均速度是 48 不是 50——很多人第一次见到会愣一下:慢的那一趟花的时间更久,所以它拖得更狠。不能把 60 和 40 加起来除以 2。
最常见的一种是加权平均:期末总评 = 平时分 30% + 期末考 70%,权重不一样,就不能简单平均。标准库用 fmean() 的 weights 参数:
import statistics
# 总评 = 平时分 30% + 期末考 70%
usual = [85, 90, 78]
final = [80, 95, 70]
for i, (u, f) in enumerate(zip(usual, final), 1):
total = round(statistics.fmean([u, f], weights=[0.3, 0.7]), 1)
print(f'第 {i} 位同学总评:', total)
输出:
第 1 位同学总评: 81.5
第 2 位同学总评: 93.5
第 3 位同学总评: 72.4
拿第 1 位核对:85 × 0.3 + 80 × 0.7 = 81.5。权重是「乘完再除以权重之和」,所以 [0.3, 0.7] 和 [3, 7] 算出来完全一样,写小数还是写整数都不影响结果。
最后是众数——看谁出现次数最多。分类数据(奶茶口味、专业、血型)没有「平均」,就靠它:
import statistics
votes = ['珍珠奶茶', '珍珠奶茶', '柠檬茶', '柠檬茶', '椰子水']
print('众数:', statistics.mode(votes))
print('并列最多的所有选项:', statistics.multimode(votes))
print('一共有几种选择:', len(set(votes)))
输出:
众数: 珍珠奶茶
并列最多的所有选项: ['珍珠奶茶', '柠檬茶']
一共有几种选择: 3
注意 mode() 只给一个(并列时给最先进来的那个),multimode() 会把并列的都给出来。想说「哪几种口味都受欢迎」,用 multimode() 才诚实。
6.4 平均数会骗人,中位数不会
import statistics
# 全班 9 人都考得不错,只有一位同学交了白卷
scores = [88, 92, 79, 95, 61, 84, 90, 73, 100, 0]
print('平均分:', round(statistics.mean(scores), 1))
print('中位数:', statistics.median(scores))
输出:
平均分: 76.2
中位数: 86.0
平均分 76.2、中位数 86.0,差了将近 10 分。原因就是那个 0 分——它把平均值往下拽,却几乎动不了中位数。看到「平均」先别急着信,问一句「有没有极端值」,再把中位数一起算出来。
6.5 离散程度:数据散不散
两个班平均分都是 80:一个班人人 75~85,另一个班一半 100 一半 60——「水平」一样,「整齐程度」差远了。衡量「散不散」的指标有好几个:
import statistics
scores = [88, 92, 79, 95, 61, 84, 90, 73, 100, 0]
print('极差(最大减最小):', max(scores) - min(scores))
print('样本方差:', round(statistics.variance(scores), 2))
print('总体方差:', round(statistics.pvariance(scores), 2))
print('样本标准差:', round(statistics.stdev(scores), 2))
print('总体标准差:', round(statistics.pstdev(scores), 2))
print('变异系数:', round(statistics.stdev(scores) / statistics.mean(scores), 4))
输出:
极差(最大减最小): 100
样本方差: 846.18
总体方差: 761.56
样本标准差: 29.09
总体标准差: 27.6
变异系数: 0.3817
| 指标 | 怎么算 | 什么时候用 |
|---|---|---|
| 极差 | 最大值 − 最小值 | 最快看一眼「跨度多大」,但中间的人差多少完全看不出来 |
| 方差 | 每个数离平均值的差,平方后再平均 | 数学上最常用,可单位是「平方分」,不好解释 |
| 标准差 | 方差开平方 | 和原数据同单位,「平均偏离多少分」,最好解释 |
| 变异系数 | 标准差 ÷ 平均值 | 两组数据量级不同时,比谁更「散」 |
为什么会有「样本 / 总体」两套?手上这组数就是全部(比如全班就这十个人)→ 用带 p 的那套;只是从更大的一群里抽出来的样本 → 用不带 p 的那套。样本方差 846.18 比总体方差 761.56 大一点,就是因为它分母上除以 n-1 而不是 n。数据越少,两者差得越明显。
6.6 分布位置:分位数与五数概括
只看最大值最小值太粗糙——中间的人过得怎么样?把数据排好队、切成几段来看,就是分位数:
import statistics
scores = [88, 92, 79, 95, 61, 84, 90, 73, 100, 0]
q1, q2, q3 = statistics.quantiles(scores, n=4)
print('Q1 下四分位:', q1)
print('Q2 中位数:', q2)
print('Q3 上四分位:', q3)
print('四分位距 IQR:', round(q3 - q1, 2))
print('五数概括:', min(scores), '/', q1, '/', q2, '/', q3, '/', max(scores))
输出:
Q1 下四分位: 70.0
Q2 中位数: 86.0
Q3 上四分位: 92.75
四分位距 IQR: 22.75
五数概括: 0 / 70.0 / 86.0 / 92.75 / 100
n=4 表示切成四段,返回三个切点。Q2 永远等于中位数,可以拿它自检。Q1 = 70 读作「约 25% 的人低于 70 分」,Q3 = 92.75 读作「约 75% 的人低于 92.75 分」。
Q3 − Q1 叫四分位距(IQR),它只看中间那 50% 的人,所以完全不受极端值影响——上面这组的极差是 100(有个 0 分),听着吓人;四分位距只有 22.75,说明「大部分同学的差距」其实没那么夸张。最小、Q1、中位数、Q3、最大这五个数合起来叫五数概括,画箱线图用的就是它。
6.7 变量之间的关系:相关与回归
前面都在描述「一组数据」,最后看「两组数据之间有没有关系」。比如:平时练得多的同学,期末是不是考得更好?
import statistics
# 平时练习次数 vs 期末成绩
practice = [2, 3, 5, 6, 8, 9, 11, 12]
final = [55, 61, 68, 70, 78, 82, 88, 95]
print('相关系数 r:', round(statistics.correlation(practice, final), 3))
slope, intercept = statistics.linear_regression(practice, final)
print('回归直线:成绩 =', round(slope, 2), '× 练习次数 +', round(intercept, 2))
print('照这条线,练 10 次大概能考:', round(slope * 10 + intercept, 1))
输出:
相关系数 r: 0.996
回归直线:成绩 = 3.74 × 练习次数 + 48.45
照这条线,练 10 次大概能考: 85.8
correlation() 给的是相关系数 r,永远落在 -1 到 1 之间:
| r 的值 | 怎么读 |
|---|---|
| 接近 1 | 一个变大,另一个也跟着变大(正相关) |
| 接近 -1 | 一个变大,另一个反而变小(负相关) |
| 接近 0 | 看不出线性关系 |
0.996 已经非常接近「一条直线」,说明这 8 个人里,练习次数和成绩几乎同步变化。
linear_regression() 返回斜率和截距,也就是那条最贴合的直线。斜率 3.74 读作「多练一次,成绩平均提高约 3.74 分」。
① 相关不等于因果。r 高只说明「一起变」,不说明「因为练了所以考好」——也可能两者都是「学习态度好」的结果。
② 别往外推。这条线是用 2~12 次的数据拟合出来的,拿它预测「练 100 次能考 422 分」就荒唐了(满分才 100)。回归线只在数据的范围内用才靠谱。
6.8 指标速查表
把上面所有指标收成四张表,忘了就回来查。左边三个字母开头的 p,都表示「这组数就是全体」。
① 集中趋势(数据水平在哪)
| 指标 | 函数 | 一句话说明 |
|---|---|---|
| 算术平均 | mean(x) | 总和 ÷ 个数,最常用 |
| 算术平均(更快) | fmean(x) | 总是返回小数;数据多时更快 |
| 加权平均 | fmean(x, weights=w) | 「平时分 30% + 期末 70%」这种 |
| 中位数 | median(x) | 排队后正中间那个,抗极端值 |
| 下 / 上中位数 | median_low(x) / median_high(x) | 偶数个数据时,取靠下 / 靠上的那一个 |
| 分组中位数 | median_grouped(x) | 数据本来就是分组区间时用 |
| 众数 | mode(x) / multimode(x) | 出现次数最多 / 并列最多的全部 |
| 几何平均 | geometric_mean(x) | 连乘场景:增长率、比率 |
| 调和平均 | harmonic_mean(x) | 平均速度、平均单价这类「每份总量相同」的场合 |
② 离散程度(数据散不散)
| 指标 | 函数 | 一句话说明 |
|---|---|---|
| 极差 | max(x) - min(x) | 最大减最小,最粗的「散不散」 |
| 样本方差 | variance(x) | 偏离平均值的平方的平均(除以 n-1) |
| 总体方差 | pvariance(x) | 同上,但这组数就是全体(除以 n) |
| 样本标准差 | stdev(x) | 方差开方,和原数据同单位,最好解释 |
| 总体标准差 | pstdev(x) | 同上,数据就是全体时用 |
| 变异系数 | stdev(x) / mean(x) | 量级不同的两组数据比离散程度 |
③ 分布位置(数据从哪到哪)
| 指标 | 函数 | 一句话说明 |
|---|---|---|
| 最小 / 最大 | min(x) / max(x) | 数据的两个端点 |
| 分位数 | quantiles(x, n=4) | 切成 n 段的切点;n=4 是四分位,n=10 是十分位 |
| 四分位距 IQR | q3 - q1 | 中间 50% 的人跨多大,抗极端值 |
④ 变量间关系(两组数据一起变吗)
| 指标 | 函数 | 一句话说明 |
|---|---|---|
| 相关系数 | correlation(x, y) | -1 到 1,两条数据一起变的程度 |
| 回归直线 | linear_regression(x, y) | 返回(斜率, 截距),用来做预测 |
| 协方差 | covariance(x, y) | 相关的「未标准化版」,单位跟着数据走 |
| 偏度 | 标准库没有 | 数据往哪边歪 → 用 pandas 的 Series.skew() |
| 峰度 | 标准库没有 | 数据尖不尖、尾巴厚不厚 → 用 pandas 的 Series.kurt() |
statistics 里还有 NormalDist(正态分布)和 kde(核密度),已经属于概率的范畴,等用到再翻。statistics 只吃数字:拿空列表求平均会报 StatisticsError: mean requires at least one data point,只有一个数求标准差会报 StatisticsError: stdev requires at least two data points。算之前先确认「手里有几个数」。课程思政|诚实与批判性思维 · 别只挑一个对自己有利的数
同一份数据,换个指标就能讲出不一样的故事:想说「我们班考得好」就报中位数,想说「我们班退步了」就报平均分。指标本身没有对错,只报一个、不说明口径,才是问题。将来你写报告、做汇报,数字怎么算、为什么这么算,都要经得起别人追问一句「这个数是怎么来的」。
七、random:抽样与可复现
random 的基础用法在《12-time库与random库》里已经讲过。本节只补做数据分析时才会用到的两个点:抽样、造模拟数据——以及让它们可复现。
7.1 随机抽几个人来检查
import random
random.seed(42) # 固定种子:以后每次跑,结果都一模一样
names = ['小满', '阿禾', '小鹿', '乐乐', '果果', '小北']
print('抽查 2 人:', random.sample(names, 2))
print('每天卖出的杯数:', [random.randint(40, 90) for _ in range(5)])
输出:
抽查 2 人: ['小北', '小满']
每天卖出的杯数: [41, 87, 57, 55, 54]
random.seed(42) 是这一节的关键:种子一样,抽出来的名单和数字永远一样。你交上去的作业、写进报告的数字,别人照着跑能得到完全相同的结果,这才叫「可复现」。
7.2 没有数据?先造一批「看起来像真的」
import random
random.seed(7)
# gauss(平均值, 标准差):造一批符合正态分布的模拟数据
print('模拟 6 位同学的分数:', [round(random.gauss(78, 10)) for _ in range(6)])
输出:
模拟 6 位同学的分数: [75, 83, 76, 75, 69, 76]
random.gauss(平均值, 标准差) 生成的是正态分布的随机数:大部分落在平均值附近,越往两边越少。成绩、身高、销量大多是这个形状。真实数据还没拿到时,先用它造一批来练,代码就能先跑通。
seed(),每次跑的结果都不一样,「贴出来给别人看」的输出自然对不上。规矩很简单:凡是要写进报告、作业、教程的随机结果,前面都先加一行 random.seed(数字)。八、time:时间戳与给代码计时
time 在《12-time库与random库》里讲过让程序等一下(sleep)和格式化日期。做数据分析时,它还常用来干两件事:给数据盖上时间戳、给代码计时。
8.1 时间戳:给数据记上「什么时候存的」
import time
now = time.time() # 现在的时间戳:一个浮点数
stamp = 1735689600 # 一个具体的时间戳
print('时间戳的类型:', type(now))
print('这个时间戳对应:', time.strftime('%Y-%m-%d %H:%M', time.gmtime(stamp)))
输出:
时间戳的类型: <class 'float'>
这个时间戳对应: 2025-01-01 00:00
gmtime() 按 UTC(零时区)换算,比北京时间少 8 小时:这个时间戳对应的北京时间是 2025-01-01 08:00。想要手机上的本地时间,把 gmtime 换成 time.localtime() 就行,时间戳本身不变。时间戳就是「从 1970-01-01 00:00 起过了多少秒」,本质是一个数字。存数据时一律存时间戳,要给人看时再用 strftime 转成日期——这样跨时区、换电脑都不会乱。
8.2 给代码计时:用 perf_counter()
import time
t0 = time.perf_counter()
total = sum(range(1000001))
t1 = time.perf_counter()
print('1 加到 100 万:', total)
print('耗时不到 1 秒:', t1 - t0 < 1)
输出:
1 加到 100 万: 500000500000
耗时不到 1 秒: True
套路就三步:开始前记一次 t0、干完活记一次 t1、两个一减就是耗时。为什么用它而不是 time()?perf_counter() 精度更高,也不受「系统时间被人调过」的影响。数据多到跑不动时,就靠它找出到底是哪一段慢。
| 常用函数 | 管什么 |
|---|---|
time.time() | 现在的时间戳(浮点数,单位秒) |
time.perf_counter() | 高精度计时器,专门用来量「跑了多久」 |
time.sleep(秒) | 让程序停一会儿(做倒计时、控制请求频率) |
time.gmtime(时间戳) | 把时间戳拆成年月日时分秒 |
time.strftime(格式, 时间) | 格式化成能看懂的日期字符串 |
九、collections 与 itertools:数数、分组、组合
最后两个「顺手工具」。它们不是非用不可,但用上之后,很多绕来绕去的循环会短一大截。
9.1 Counter:自动数谁出现得最多
from collections import Counter, defaultdict
orders = ['珍珠奶茶', '椰果奶茶', '珍珠奶茶', '柠檬茶', '珍珠奶茶']
print('点单排行:', Counter(orders).most_common())
# defaultdict(int):分类累加时不用先问「这个键在不在」
spend = defaultdict(int)
for item, price in [('奶茶', 12), ('奶茶', 15), ('炸鸡', 25)]:
spend[item] += price
print('分类合计:', dict(spend))
输出:
点单排行: [('珍珠奶茶', 3), ('椰果奶茶', 1), ('柠檬茶', 1)]
分类合计: {'奶茶': 27, '炸鸡': 25}
Counter 是「会自动数数的字典」:塞进去一串东西,它直接告诉你每样出现了几次,most_common() 还会按次数从多到少排好——词频统计、投票、点单排行都是它。defaultdict(int) 则是「分类累加」的省事写法:不用先判断键在不在,直接 spend[item] += price。第 26 篇《文件处理》的词频案例用的就是 Counter。
9.2 itertools:组合、排列交给它
from itertools import combinations
# 三个人两两握手,一共几次?——组合枚举
people = ['小满', '阿禾', '小鹿']
print('两两一组:', list(combinations(people, 2)))
输出:
两两一组: [('小满', '阿禾'), ('小满', '小鹿'), ('阿禾', '小鹿')]
自己写两层嵌套循环算组合也能算,但容易写错、还容易漏。常用的几个:combinations(列表, k) 取组合(不管顺序)、permutations(列表, k) 取排列(管顺序)、chain(a, b) 把两串接成一串、product(a, b) 出笛卡尔积(比如「尺码 × 颜色」的所有搭配)。
十、再往前一步:numpy / pandas / openpyxl / matplotlib
前面九节用的都是「装好就有」的标准库。真做数据分析,还会碰上几个名字:numpy、pandas、matplotlib,还有专门读写 Excel 的 openpyxl。它们不是标准库,要自己装(第 11 篇《导入库的方法》讲过「标准库 vs 第三方库」的区别)。
| 库 | 管什么 | 什么时候该上它 |
|---|---|---|
numpy | 一整列数字一起算(数组、矩阵) | 数据几千几万行,逐行循环太慢 |
pandas | 带行列名的表格(DataFrame) | 要按列筛、按班级分组、做汇总 |
matplotlib | 画图:折线、柱状、散点 | 要把结果画出来给人看 |
openpyxl | 读写 Excel 的 .xlsx 文件 | 别人发来的表格是 .xlsx,导出的报表也要 .xlsx |
装它们,在终端里执行这一行(虚拟环境的做法见第 27 篇《模块与包》第八节,建议装进项目自己的环境里):
pip install numpy pandas matplotlib openpyxl
下面几段的首行都写了 # 示意——它们本身就是标准 Python 代码,只是要先装库才能跑,所以这里只讲「长什么样、好在哪」,不贴具体输出。
10.1 numpy:整个数组一起算
# 示意:要先 pip install numpy 才能跑
import numpy as np
scores = np.array([88, 92, 79, 95, 61])
print(scores.mean())
print(scores + 5) # 每个元素一起加 5
卖点是「整个数组一起算」:scores + 5 表示每个元素都加 5,不用写循环;scores.mean() 直接出平均分。数据到几万行时,「用不用 numpy」就是「跑得动」和「跑不动」的区别。
10.2 pandas:像 Excel 一样的表格
# 示意:要先 pip install pandas 才能跑
import pandas as pd
df = pd.read_csv('scores.csv')
print(df.groupby('班级')['总分'].mean())
同一件活,标准库写法和 pandas 写法差多少,放在一起就看清了:
| 要干的事 | 标准库写法 | pandas 写法 |
|---|---|---|
| 读一张 csv | csv.DictReader(文件对象) | pd.read_csv('scores.csv') |
| 某一列求平均 | statistics.mean([int(r['总分']) for r in rows]) | df['总分'].mean() |
| 按班级分组求平均 | 循环 + defaultdict(list) + mean | df.groupby('班级')['总分'].mean() |
| 某一列求和 | sum(int(r['总分']) for r in rows) | df['总分'].sum() |
| 排个名 | sorted(rows, key=..., reverse=True) | df.sort_values('总分', ascending=False) |
| 画一张柱状图 | 标准库画不了 | df['总分'].plot.bar() |
10.3 openpyxl / pandas:读写 Excel 的 .xlsx
第四节说过:.csv 是纯文本,标准库就能读写;.xlsx 不行——它本质是个 zip 压缩包,标准库解析不了。要处理它,装 openpyxl:
# 示意:要先 pip install openpyxl 才能跑
import openpyxl
wb = openpyxl.load_workbook('成绩单.xlsx') # 打开整个工作簿
ws = wb['统计一班'] # 选中一张工作表
for row in ws.iter_rows(min_row=2, values_only=True):
print(row) # 每行一个元组
三个概念对上就能用了:工作簿(整个 .xlsx 文件)→ 工作表(底下那排标签页)→ 单元格。iter_rows() 一行一行往下走,min_row=2 跳过表头,values_only=True 表示只要值、不要单元格对象。
# 示意:要先 pip install openpyxl 才能跑
import openpyxl
wb = openpyxl.Workbook() # 新建一个空工作簿
ws = wb.active # 用它默认那张工作表
ws.append(['姓名', '分数']) # append 一整行
ws.append(['小满', 92])
ws.append(['阿禾', 78])
wb.save('新成绩单.xlsx') # 存盘
装了 pandas 就更省事,读写各一行,而且直接给你 DataFrame:
# 示意:要先 pip install pandas openpyxl 才能跑
import pandas as pd
df = pd.read_excel('成绩单.xlsx', sheet_name='统计一班')
print(df.head())
df.to_excel('汇总.xlsx', index=False) # index=False:别把行号也写进表里
pandas 自己并不解析 Excel,它是在背后调 openpyxl(读 .xlsx)或 xlrd(读老的 .xls)。所以「read_excel 报缺库」,要装的是 openpyxl,不是 pandas 本身。10.4 matplotlib:把结果画出来
# 示意:要先 pip install matplotlib 才能跑
import matplotlib.pyplot as plt
plt.plot(['一', '二', '三', '四'], [12, 15, 9, 18])
plt.title('一周奶茶销量')
plt.show()
跑完会弹出一张折线图。三步记住就行:plot() 给数据点、title() 起名字、show() 显示出来。中文标题如果显示成一串方框,就指定一下中文字体(Mac 用 Heiti TC,Windows 用 SimHei)。
csv + statistics 把「读数据 → 算指标 → 出报表」整条路走通(第十一节就是这件事),再上 pandas。一上手就用 pandas,很容易连「数据到底长什么样、每个字段什么意思」都没看清。AI 助手小贴士|标准库写法 ↔ pandas 写法,最适合对照着学。
打开任意 AI 对话工具(网页版或 App 都可以),试试这样问:把下面这段用 csv 和 statistics 写的统计代码改成 pandas 写法,并逐行标注两边的对应关系:<贴上你的代码>
十一、合起来用:一份会自己记日期的成绩表
把前面学的串一条线:用 pathlib 管路径、csv 存表格、datetime 记时间、json 存汇总。
# 文件:save_scores.py
import csv, json
from datetime import datetime
from pathlib import Path
def save(folder, scores):
folder = Path(folder)
folder.mkdir(parents=True, exist_ok=True)
# 1) 明细存成 csv,Excel 能直接打开
csv_path = folder / 'scores.csv'
with open(csv_path, 'w', encoding='utf-8-sig', newline='') as fp:
w = csv.writer(fp)
w.writerow(['姓名', '分数'])
w.writerows(scores)
# 2) 汇总存成 json,带生成时间
total = sum(s for _, s in scores)
summary = {
'生成时间': datetime.now().strftime('%Y-%m-%d %H:%M'),
'人数': len(scores),
'平均分': round(total / len(scores), 1),
}
json_path = folder / 'summary.json'
json_path.write_text(
json.dumps(summary, ensure_ascii=False, indent=2), encoding='utf-8')
return csv_path, json_path
用法和效果:
import csv, json, pathlib, tempfile
from pathlib import Path
work = Path(tempfile.mkdtemp())
scores = [('小满', 92), ('阿禾', 78), ('小鹿', 85)]
# 写明细
csv_path = work / 'scores.csv'
with open(csv_path, 'w', encoding='utf-8-sig', newline='') as fp:
w = csv.writer(fp)
w.writerow(['姓名', '分数'])
w.writerows(scores)
# 写汇总
total = sum(s for _, s in scores)
summary = {'人数': len(scores), '平均分': round(total / len(scores), 1)}
(work / 'summary.json').write_text(
json.dumps(summary, ensure_ascii=False, indent=2), encoding='utf-8')
# 再读回来验证一遍
with open(csv_path, 'r', encoding='utf-8', newline='') as fp:
rows = list(csv.DictReader(fp))
print('从 csv 读回', len(rows), '条记录')
print('第一条:', rows[0])
print('从 json 读回:', json.loads((work / 'summary.json').read_text(encoding='utf-8')))
输出:
从 csv 读回 3 条记录
第一条: {'\ufeff姓名': '小满', '分数': '92'}
从 json 读回: {'人数': 3, '平均分': 85.0}
这一套「写出去能被人打开、读回来能继续算」的模式,就是实际项目里最常用的数据落地方式。
| 需求 | 选哪个 | 原因 |
|---|---|---|
| 要给人看、要能用 Excel 打开 | csv | 表格结构,双击就开 |
| 要存复杂的嵌套结构 | json | 能装字典套列表 |
| 只存一行行纯文本 | 直接读写 txt | 最简单,第 26 篇讲过 |
| 要给数据加时间标记 | datetime | 格式化 + 算差值 |
课程思政|数据安全与法治素养 · 会落地,也要会看管
这一节把 pathlib、csv、datetime、json 串成一条线,让一份成绩表既能存下来,又能读回来接着算。工具顺手了,责任也跟着来了:同学的成绩、联系方式都属于个人信息,能随手续写,不等于能随便留存和传阅。自己练手就用脱敏或假数据,将来在工作里也先问一句「这份数据我有没有权限存」。
十二、常见坑
坑 1:忘了写 encoding='utf-8',中文乱码或直接报错
import pathlib, tempfile
work = pathlib.Path(tempfile.mkdtemp())
f = work / 'gbk.txt'
f.write_bytes('中文'.encode('gbk')) # 假设这是别的软件存出来的文件
print(f.read_text(encoding='utf-8')) # 按 utf-8 去读
运行后会报错(逐字照抄解释器):
UnicodeDecodeError: 'utf-8' codec can't decode byte 0xd6 in position 0: invalid continuation byte
处理办法:读写文件一律显式写 encoding='utf-8'。读到老软件存的 GBK 文件时,试 encoding='gbk'。
坑 2:csv 写出空行
import csv
# 错的写法:漏了 newline='',Windows 上每行后面会多出一个空行
with open('a.csv', 'w', encoding='utf-8') as fp:
csv.writer(fp).writerow(['a', 'b'])
# 对的写法
with open('a.csv', 'w', encoding='utf-8', newline='') as fp:
csv.writer(fp).writerow(['a', 'b'])
坑 3:csv 读出来的数字是字符串
import csv, io
rows = list(csv.DictReader(io.StringIO('姓名,分数\n小满,92\n')))
score = rows[0]['分数']
print('值:', score, '|类型:', type(score).__name__)
print('算数前先转:', int(score) + 1)
输出:
值: 92 |类型: str
算数前先转: 93
处理办法:参与计算前先 int() 或 float() 转一下,和 input() 的处理方式一样。
坑 4:json 文件里中文变成 \u4e2d
import json
print(json.dumps({'城市': '广州'}))
print(json.dumps({'城市': '广州'}, ensure_ascii=False))
输出:
{"\u57ce\u5e02": "\u5e7f\u5dde"}
{"城市": "广州"}
处理办法:加 ensure_ascii=False。两种都能被 loads() 读回来,加参数只是为了「人看着舒服」。
坑 5:手写路径拼接
# 别这么写:换个系统就失效
path = 'data' + '/' + 'a.csv'
# 这么写:pathlib 会自动用对分隔符
from pathlib import Path
path = Path('data') / 'a.csv'
坑 6:json.dumps 和 json.dump 用混
import json
from pathlib import Path
data = {'姓名': '小满', '分数': 92}
# dumps(带 s)= 变成字符串,返回值给你,需要自己存
text = json.dumps(data, ensure_ascii=False)
Path('a.json').write_text(text, encoding='utf-8')
# dump(不带 s)= 直接写进文件对象
with open('a.json', 'w', encoding='utf-8') as fp:
json.dump(data, fp, ensure_ascii=False)
口诀:带 s 的是「转成字符串」,不带 s 的是「直接对文件操作」。其余几个也同理:loads / load。
坑 7:把字符串丢给 statistics 去算
import statistics
print(statistics.mean(['88', '92', '95']))
运行后会报错(逐字照抄解释器):
TypeError: can't convert type 'str' to numerator/denominator
处理办法:和「csv 读出来的数字是字符串」是同一个病根——算之前先转:statistics.mean([int(s) for s in data])。
坑 8:只有一个数就求标准差
import statistics
print(statistics.stdev([88]))
运行后会报错(逐字照抄解释器):
statistics.StatisticsError: stdev requires at least two data points
处理办法:「散不散」至少要两个数才比得出来。算之前先判断 if len(data) >= 2:,不够就先不算,或者直接提示「样本太少」。
坑 9:拿列表去数数
from collections import Counter
print(Counter([['奶茶'], ['炸鸡']]))
运行后会报错(逐字照抄解释器):
TypeError: unhashable type: 'list'
处理办法:Counter 数的东西必须是「能当字典键的」(字符串、数字、元组)。要数一组一组的组合,先把每组转成 tuple(单品) 或拼成一个字符串再数。
坑 10:把 .xlsx 当作 csv 去读
import csv, pathlib, tempfile, zipfile
work = pathlib.Path(tempfile.mkdtemp())
p = work / '成绩单.xlsx'
with zipfile.ZipFile(p, 'w', zipfile.ZIP_STORED) as z: # .xlsx 的本质就是一个 zip 压缩包
z.writestr('sheet1.xml', bytes(range(256)))
print('后缀明明是 .xlsx,开头两个字节却是:', p.read_bytes()[:2])
try:
with open(p, 'r', encoding='utf-8') as fp:
list(csv.reader(fp))
except UnicodeDecodeError:
print('用 csv 打开它 → 报错 UnicodeDecodeError(根本不是文本,读不了)')
输出:
后缀明明是 .xlsx,开头两个字节却是: b'PK'
用 csv 打开它 → 报错 UnicodeDecodeError(根本不是文本,读不了)
处理办法:认后缀。b'PK' 就是 zip 压缩包的标志(P / K 取自 zip 格式发明人 Phil Katz 的首字母),而 .xlsx 正是 zip。这类文件只能交给 openpyxl 或 pandas.read_excel()(第十节 10.3)——改编码、改后缀都没用,里面的字节一个都没变。
十三、报错手册
报错原文都是从本机解释器逐字抄下来的,对照着找就行。
| 报错原文(最后一行) | 什么情况 | 怎么改 |
|---|---|---|
FileNotFoundError: [Errno 2] No such file or directory: '没有的文件.txt' | 要读的文件不存在(路径写错 / 文件没放对地方) | 用 Path(...).exists() 先判断,或核对路径 |
json.decoder.JSONDecodeError: Expecting property name enclosed in double quotes: line 1 column 2 (char 1) | JSON 格式不合法(常见:用了单引号、多了逗号) | JSON 里键和字符串必须双引号;用在线工具或 json.dumps 生成 |
TypeError: Object of type set is not JSON serializable | 拿集合(或函数、自定义对象)去 json.dumps | 先转成列表:list(集合) |
UnicodeDecodeError: 'utf-8' codec can't decode byte 0xd6 in position 0: invalid continuation byte | 文件不是 UTF-8 编码(常见是老软件存的 GBK) | 试 encoding='gbk';或先把文件转成 UTF-8 |
ValueError: time data '2026/10/04' does not match format '%Y-%m-%d' | 字符串和格式串对不上(/ 对 -) | 格式串要写成 '%Y/%m/%d' 才能解析斜杠 |
AttributeError: 'str' object has no attribute 'exists' | 把字符串当路径对象用 | 先 p = Path('a.txt'),再 p.exists() |
statistics.StatisticsError: mean requires at least one data point | 拿空列表去求平均 | 先判断 if data:,空的时候给个默认值 |
statistics.StatisticsError: stdev requires at least two data points | 只有一个数就求标准差 | 标准差至少要两个数据点,先 if len(data) >= 2: |
TypeError: can't convert type 'str' to numerator/denominator | 把字符串列表丢给 statistics.mean() | 先 int() / float() 转成数字再算 |
ValueError: Sample larger than population or is negative | random.sample(名单, 10) 要的数量比名单人数还多 | 抽的数量 k 不能大于列表长度 |
TypeError: unhashable type: 'list' | 往 Counter 里塞了列表 | 先转成 tuple 或字符串 |
ModuleNotFoundError: No module named 'numpy' | 没装就 import numpy(第三方库不是自带的) | 在终端执行 pip install numpy;已装还报,多半是装到了别的 Python 环境 |
zipfile.BadZipFile: File is not a zip file | 把 .csv 改名成 .xlsx 就去打开了——改后缀 | 不会换格式,里面还是纯文本;读 .xlsx 要用 openpyxl |
ModuleNotFoundError: No module named 'openpyxl' | 用了 pandas.read_excel() 或 openpyxl 但没装 | pip install openpyxl(老式的 .xls 则是 pip install xlrd) |
| 标准差比「手算」小了一点点 | 用了 pstdev(总体),而不是 stdev(样本) | 数据是抽来的样本就用 stdev;这组数就是全体才用 pstdev |
| 相关系数很高,就断定「A 导致 B」 | 相关不等于因果,r 高只说明「一起变」 | 要下因果结论得靠实验设计,别用相关系数硬推 |
下面这几条是「不报错,但结果不对」的,比报错更值得当心:
| 现象 | 原因 | 怎么改 |
|---|---|---|
| CSV 每行后面多一个空行 | 少了 newline='' | 写和读都加上 newline='' |
| CSV 读出来的数字没法直接相加 | 读进来的是字符串 | 先 int() / float() |
JSON 文件里中文全是 \u… | 没加 ensure_ascii=False | 加上这个参数 |
| Excel 打开 CSV 中文乱码 | 编码不是 Excel 认的那一种 | 存的时候用 encoding='utf-8-sig' |
| Windows 上路径报错,macOS 上好好的 | 路径里的分隔符写死了 | 改用 pathlib 的 / 拼 |
| 平均分和中位数差很多 | 数据里有极端值(0 分、超高的个例) | 两个都算出来,报告里一并说明 |
| 每次跑出来的随机结果都不一样,输出对不上 | 忘了写 random.seed() | 需要复现时,先固定种子再生成 |
| 页面上的耗时和我不一样 | 计时结果本来就因机器而异 | 只比数量级,别当固定值写进结论 |
十四、十六个基础案例
下面十六个例子都很短,建议每个都亲手敲一遍、跑一遍。后六个是统计、抽样、计时、计数,以及表格文件的几种写法。
1. 拆一个路径的各个部分
from pathlib import Path
p = Path('data/2026/scores.csv')
print(p.name, '|', p.stem, '|', p.suffix, '|', p.parent)
输出:
scores.csv | scores | .csv | data/2026
2. 判断文件在不在,再决定读不读
import pathlib, tempfile
f = pathlib.Path(tempfile.mkdtemp()) / 'a.txt'
if f.exists():
print('能读:', f.read_text(encoding='utf-8'))
else:
print('文件不在,先跳过')
输出:
文件不在,先跳过
3. 一行写、一行读
import pathlib, tempfile
f = pathlib.Path(tempfile.mkdtemp()) / 'note.txt'
f.write_text('今天学了标准库', encoding='utf-8')
print(f.read_text(encoding='utf-8'))
输出:
今天学了标准库
4. 用 json 存一份带嵌套结构的配置
import json
cfg = {'班级': '统计一班', '人数': 30, '课程': ['Python', '统计']}
text = json.dumps(cfg, ensure_ascii=False, indent=2)
print(text)
print('人数:', json.loads(text)['人数'])
输出:
{
"班级": "统计一班",
"人数": 30,
"课程": [
"Python",
"统计"
]
}
人数: 30
5. 用 csv 写一张能被 Excel 打开的表
import csv, pathlib, tempfile
f = pathlib.Path(tempfile.mkdtemp()) / 'scores.csv'
with open(f, 'w', encoding='utf-8-sig', newline='') as fp:
w = csv.writer(fp)
w.writerow(['姓名', '分数'])
w.writerow(['小满', 92])
print('写完之后,文件里的内容是:')
print(f.read_text(encoding='utf-8-sig'))
输出:
写完之后,文件里的内容是:
姓名,分数
小满,92
6. 用 DictReader 按字段名取数据并求和
import csv, io
text = '姓名,分数\n小满,92\n阿禾,78\n小鹿,85\n'
total = sum(int(r['分数']) for r in csv.DictReader(io.StringIO(text)))
print('总分:', total)
输出:
总分: 255
7. 把当前时间格式化成一行日志
from datetime import datetime
d = datetime(2026, 10, 4, 19, 30, 0)
print('[%s] 程序开始运行' % d.strftime('%Y-%m-%d %H:%M:%S'))
输出:
[2026-10-04 19:30:00] 程序开始运行
8. 算两个日期差多少天
from datetime import date
start = date(2026, 9, 1)
end = date(2026, 10, 4)
print('开学到今天:', (end - start).days, '天')
输出:
开学到今天: 33 天
9. 把学生名单按「姓名-分数」两列存成 json
import json
scores = [('小满', 92), ('阿禾', 78)]
data = [{'姓名': n, '分数': s} for n, s in scores]
print(json.dumps(data, ensure_ascii=False))
输出:
[{"姓名": "小满", "分数": 92}, {"姓名": "阿禾", "分数": 78}]
10. 目录不存在就先建出来
import pathlib, tempfile
from pathlib import Path
base = Path(tempfile.mkdtemp())
target = base / '学期项目' / '第一周'
target.mkdir(parents=True, exist_ok=True)
print('文件夹建好了吗:', target.exists())
print('最后一级是:', target.name)
输出:
文件夹建好了吗: True
最后一级是: 第一周
11. 算一组成绩的平均分、中位数和标准差
import statistics
scores = [72, 68, 91, 85, 77, 80]
print('平均分:', round(statistics.mean(scores), 1))
print('中位数:', statistics.median(scores))
print('标准差:', round(statistics.stdev(scores), 2))
输出:
平均分: 78.8
中位数: 78.5
标准差: 8.42
12. 固定种子,随机抽查 3 个人
import random
random.seed(2026)
students = ['小满', '阿禾', '小鹿', '乐乐', '果果', '小北', '汤圆', '芋圆']
print('被抽到的 3 人:', random.sample(students, 3))
输出:
被抽到的 3 人: ['阿禾', '小鹿', '果果']
13. 给一段代码计时
import time
t0 = time.perf_counter()
total = sum(i * i for i in range(1, 100001))
t1 = time.perf_counter()
print('平方和:', total)
print('耗时不到 1 秒:', t1 - t0 < 1)
输出:
平方和: 333338333350000
耗时不到 1 秒: True
14. 数一数每样加料被点了几次
from collections import Counter
words = '珍珠 椰果 珍珠 布丁 椰果 珍珠 西米 椰果'.split()
print('每样加料被点了几次:', Counter(words).most_common())
print('加料种数:', len(set(words)))
输出:
每样加料被点了几次: [('珍珠', 3), ('椰果', 3), ('布丁', 1), ('西米', 1)]
加料种数: 4
AI 助手小贴士|标准库真正的用法是「组合拳」,出组合题练。
打开任意 AI 对话工具(网页版或 App 都可以),试试这样问:出 2 道标准库组合题:①用 datetime 和 os 把文件夹里的照片按月份归类;②用 json 和 random 做随机抽签并保存记录。只要思路提示
15. 写一个 Excel 也能打开的 tsv
import csv, io
buf = io.StringIO()
w = csv.writer(buf, delimiter='\t') # 用制表符分隔
w.writerow(['姓名', '分数', '班级'])
w.writerow(['小满', 92, '统计一班'])
w.writerow(['阿禾', 78, '统计一班'])
print(repr(buf.getvalue()))
输出:
'姓名\t分数\t班级\r\n小满\t92\t统计一班\r\n阿禾\t78\t统计一班\r\n'
16. 一次算齐一组成绩的全部指标
import statistics
scores = [88, 92, 79, 95, 61, 84, 90, 73, 100, 0]
q1, q2, q3 = statistics.quantiles(scores, n=4)
print('人数:', len(scores))
print('平均:', statistics.mean(scores), '|中位:', statistics.median(scores))
print('最低:', min(scores), '|最高:', max(scores), '|极差:', max(scores) - min(scores))
print('标准差:', round(statistics.stdev(scores), 2),
'|变异系数:', round(statistics.stdev(scores) / statistics.mean(scores), 4))
print('五数概括:', min(scores), '/', q1, '/', q2, '/', q3, '/', max(scores))
输出:
人数: 10
平均: 76.2 |中位: 86.0
最低: 0 |最高: 100 |极差: 100
标准差: 29.09 |变异系数: 0.3817
五数概括: 0 / 70.0 / 86.0 / 92.75 / 100
十五、常见问题
Q:os.path 和 pathlib 到底用哪个?
新写的代码用 pathlib:写法更短、跨系统更稳、有 / 可以拼路径。但老代码和老教程里全是 os.path,必须看得懂。两个混着用也可以,因为 Path() 能接字符串、str(路径) 也能变回字符串。
Q:数据存 csv 还是 json?
一句话判断:长得像一张表(每行同几个字段)→ csv;长得像一棵树(字典套列表、层层嵌套)→ json。要给人用 Excel 打开,选 csv。
Q:为什么我的 csv 用 Excel 打开中文乱码?
Excel 默认按本地编码猜。解决:写文件时用 encoding='utf-8-sig'(多了 BOM 标记,Excel 一看就知道是 UTF-8)。
Q:json.dumps 和 json.dump 我总是记混?
只记一点:带 s 的那个 s = string(字符串)。所以 dumps 返回字符串、loads 吃字符串;不带的就直接对着文件对象。四个函数按这个规律一拆就清楚。
Q:datetime.now() 每次结果都不一样,怎么测试?
这正是要小心的地方。写测试或写教程时,用固定时间 datetime(2026, 10, 4);真实程序里用 now()。别把 now() 的结果写进需要可重复比对的输出里。
Q:这几个库要不要背?
不用。这一篇的作用是知道「有这些东西、能解决什么问题」,具体函数名忘了就查:dir(模块) 看有什么,help(函数) 看怎么用(第 27 篇《模块与包》第四节讲过)。真正需要背下来的只有「json.dumps 带 s 是转字符串」「csv 要写 newline=''」这类会踩坑的点。
Q:算平均分,该用 statistics 还是自己写循环?
都行,但优先用 statistics:一行写完、还顺带算标准差和中位数;自己写循环容易漏掉「空列表」「字符串没转数字」这两种情况。只有一件事必须自己判断——用平均还是用中位数,这不是工具问题,是口径问题。
Q:numpy / pandas 要不要现在就学?
先把标准库这一套用顺再学。判断标准很简单:数据小到能一眼看完(几十行),标准库够用;数据大到要滚动条才能看完,或者要画图,才值得上 pandas。工具是跟着需求长的,先有需求再学,不容易忘。
Q:随机抽样的结果能写进实验报告吗?
能,但要把种子一起写上:写「用 random.seed(2026) 固定种子,随机抽了 3 人」,别人才能复现。只写「随机抽了 3 人」,别人跑出来是另一批人,这份报告就没法验证。
Q:csv 和 xlsx 到底该用哪个?
一句话:要「交换」用 csv,要「交付」用 xlsx。csv 是纯文本,谁的 Python 都能直接读,出问题用记事本当场就能看一眼;xlsx 能存格式、公式、多个工作表,但读它必须先装库,而且它本质是个压缩包,出问题肉眼看不出来。给人传数据、喂给别的程序——csv;交一份带表头样式、多张工作表的正式报表——xlsx。
Q:描述性统计要报哪几个指标?
至少三个:一个「中心」(平均数 + 中位数一起报)、一个「离散」(标准差)、一个「范围」(最小 / 最大)。只报平均数是最常见的偷懒——同一份数据换个指标就能讲出另一个故事。数据里有明显极端值,或者要和别人比「谁更整齐」,再把四分位距和变异系数补上。
Q:pandas.read_excel() 提示缺 openpyxl,怎么办?
照提示装:pip install openpyxl。pandas 自己不解析 Excel,它只负责指挥,真正干活的是 openpyxl(读 .xlsx)或 xlrd(读老的 .xls)。装完还报,多半是装到了别的 Python 环境——和 ModuleNotFoundError: No module named 'numpy' 是同一类问题。
AI 助手小贴士|标准库的问题是「不知道有什么」。把需求说给 AI,让它推荐该用哪些模块、各自负责什么——先要思路,再要代码。
打开任意 AI 对话工具(网页版或 App 都可以),试试这样问:我想写一个程序:自动整理下载文件夹,把照片按拍摄月份分类到子文件夹。应该用到哪些标准库?每个模块负责什么?先给思路再给代码
十六、随堂自测(16 题)
点击选项即可判分,答错的题会立刻显示解析。
1要把 'data/scores.csv' 里的文件名、后缀、所在文件夹分别取出来,下面哪个库最方便?
- A.
pathlib,用Path(p).name/.suffix/.parent - B.
json - C.
csv - D.
datetime
pathlib 专管路径:.name 文件名、.stem 去掉后缀、.suffix 后缀、.parent 所在文件夹。它还能用 / 拼路径,自动适配 Windows 和 macOS 的分隔符。2用 pathlib 拼出 data 文件夹下的 a.csv,写法正确的是?
- A.
Path('data') + 'a.csv' - B.
Path('data') / 'a.csv' - C.
Path('data').join('a.csv') - D.
Path('data', 'a.csv', sep='/')
Path 重载了除法运算符 / 来拼路径,这是它的标志性写法,而且会自动用对当前系统的分隔符。+ 会报类型错误,也没有 join 这个方法。3json.dumps(data, ensure_ascii=False) 里的 ensure_ascii=False 有什么作用?
- A. 让 JSON 能被 Excel 打开
- B. 让 JSON 自动按字母排序
- C. 让中文直接显示,而不是变成
\u4e2d这样的编码 - D. 让数据压缩得更小
\uXXXX 转义,功能没问题但人没法读。加 ensure_ascii=False 后中文原样保存——占的字节还更少。要和 Excel 打交道应该用 csv,不是 json。4下面哪个东西不能直接转成 JSON?
- A. 字典
{'a': 1} - B. 集合
{1, 2} - C. 列表
[1, 2] - D. 布尔
True
TypeError: Object of type set is not JSON serializable,要先 list(集合) 转成列表。另外元组虽然能转,但读回来会变成列表。5用 csv 写文件时,open() 里必须加的参数是?
- A.
encoding='gbk' - B.
errors='ignore' - C.
buffering=0 - D.
newline=''
newline='',csv 自己写的换行会和 open() 的换行叠加,Windows 上每行后面多一个空行——这是 csv 最经典的坑,读和写都建议加上。6用 csv.DictReader() 读出来的每一行是什么?
- A. 一个列表,按列的位置取值
- B. 一个字符串,整行原样
- C. 一个字典,用第一行当字段名
- D. 一个元组,不能修改
DictReader 会拿第一行当字段名,之后每行给你一个字典,用 row['姓名'] 这种写法取值,比 row[0] 清楚得多,列顺序变了也不受影响。csv.reader() 才是返回列表的那个。7用 csv 读进来的分数 row['分数'] 想直接参与计算,会怎样?
- A. 它是字符串,要先
int()或float()转换 - B. 直接能算,csv 会自动转成数字
- C. 它是分数对象,要用
.value取值 - D. 会报
TypeError
'88' 而不是 88。直接相加会得到 '88' + '95' 这种字符串拼接或者报错。先转类型再计算,和 input() 的处理一样。8想按自定义格式显示时间,该用哪个方法?
- A.
strptime - B.
strftime - C.
format_time - D.
tostring
strftime 里的 f 是 format,把时间对象变成字符串;strptime 里的 p 是 parse,把字符串变成时间对象。两者只差一个字母,方向正好相反,是新手最容易记混的一对。9datetime.strptime('2026/10/04', '%Y-%m-%d') 会怎样?
- A. 正常返回 2026 年 10 月 4 日
- B. 自动忽略符号差异,只按数字解析
- C. 报
ValueError: time data '2026/10/04' does not match format '%Y-%m-%d' - D. 返回
None
'%Y/%m/%d',是短横就写 '%Y-%m-%d'。报错信息里会同时打出数据和格式串,照着对比就能找出差在哪里。10算「两个日期相差几天」,正确的做法是?
- A. 两个
date相减,取结果的.days - B. 自己拿天数除以 30 估一个
- C. 用
date.diff() - D. 把年份月份日期分别相减再换算
timedelta,.days 就是天数。月份、闰年、跨年这些麻烦 datetime 都替你算好了,自己用 30 天一个月去估一定会错。11关于 json.dumps 和 json.dump,说法正确的是?
- A. 两个完全一样,随便用
- B. 不带 s 的只能处理列表
- C. 带 s 的用于读取,不带 s 的用于写入
- D. 带 s 的返回字符串,不带 s 的直接对文件对象操作
s 记成 string 就好:dumps 把对象转成 JSON 字符串(返回值给你);dump 直接写进文件对象。loads / load 同理——一个吃字符串,一个吃文件。12要存一份「每个学生有姓名、分数、以及多门课成绩」的嵌套数据,最合适的是?
- A. csv,因为它是表格格式
- B. 必须先转成集合再存
- C. 直接写 txt,用逗号隔开
- D. json,因为它能装字典套列表这种嵌套结构
13一组成绩里有一位同学考了 0 分,想说明「大部分同学考得不错」,最该看哪个指标?
- A. 平均分
- B. 中位数
- C. 最大值
- D. 标准差
14random.seed(42) 的作用是什么?
- A. 让随机数变得更大
- B. 让随机数变成固定值(每次都是同一个数)
- C. 把随机序列固定下来,之后每次运行都得到同一串「随机」结果
- D. 提高随机性,避免重复
15要给一段代码计时,该用 time 里的哪个函数?
- A.
time.sleep() - B.
time.strftime() - C.
time.gmtime() - D.
time.perf_counter()
perf_counter() 是高精度计时器:开始前记一次、结束后记一次,两个一减就是耗时。它比 time.time() 精度高,也不受系统时间被改动的影响。sleep 是「停一会儿」,strftime / gmtime 是处理日期显示。16关于 numpy、pandas、matplotlib,说法正确的是?
- A. 它们是第三方库,要先
pip install才能 import - B. 它们是标准库,装好 Python 就自带
- C. 它们只能在 TRAE 里运行
- D. 用了 pandas 就不需要再懂 csv 和字典了
ModuleNotFoundError。不过 pandas 读的 csv、用的字典逻辑,正是本篇讲的那些东西——底层概念懂了,换工具只是换个写法。17别人发来一份 .xlsx 表格,想用 Python 读它,下面说法正确的是?
- A. 它是 zip 压缩包,标准库
csv读不了,要装openpyxl(或pandas) - B. 把后缀名改成
.csv,标准库就能读了 - C. 用
csv.DictReader直接就能读 - D.
.xlsx和.csv一样,都是纯文本
.csv 是纯文本,标准库 csv 就能读写;.xlsx 本质是个 zip 压缩包(里面装着一堆 XML),标准库解析不了。改后缀更没用——里面的字节一个都没变,只会再报一个 BadZipFile。要读 .xlsx,得装 openpyxl 或用 pandas.read_excel()。18想说明「这个班的成绩比隔壁班更参差」,最该拿哪个指标去比?
- A. 两个班的平均分
- B. 两个班的标准差
- C. 两个班的最高分
- D. 两个班的人数
statistics.stdev() 的用武之地。最高分只反映一个人,人数跟整齐程度无关。