学Python的人最容易卡住的地方,不是语法,而是“语法都看懂了,一写AI相关的东西还是懵”。尤其到了“从零到精通”系列的第十篇,咱们已经跨过变量、循环、函数、类这些基础关卡,下一步就该碰真正的AI编程链路了。可这条链路的起点,百分之八十的人会忽略——数据。图像在内存里是一组数,文本要变成向量也是一组数,音频波形还是一组数。AI编程说白了,就是围绕“数据的读取、转换、计算、可视化”在转。所以这一期我把numpy、文件读写、matplotlib这三块串起来讲,配套一个能直接抄作业的小实践。基础好的可以跳过前两节的语法,直接看第三节的画图细节和第四节的完整流水线;刚起步的读者建议从头跟一遍,每一段代码我都标注了“为什么这么写”。
1. 从列表到数组:AI代码里数据为什么要先改造成numpy
很多初学者会问:Python自带的列表(list)不是挺好用的吗?能存数字、能存字符串、还能嵌套,为什么一到AI相关的教程里,所有人都在用numpy?原因其实不复杂——列表是给“人”用的,数组是给“机器”用的。
1.1 Python原生列表的短板:为什么AI代码里很少用for循环
你先看一段最直观的对比。假设有两个列表,要计算它们对应元素相乘后的总和,也就是向量的内积。
# 纯Python写法 a = [1, 2, 3, 4, 5] b = [10, 20, 30, 40, 50] result = 0 for i in range(len(a)): result += a[i] * b[i] print(result) # 550 # numpy写法 import numpy as np a = np.array([1, 2, 3, 4, 5]) b = np.array([10, 20, 30, 40, 50]) print(np.dot(a, b)) # 550两段代码结果一样,但背后差别很大。纯Python的循环是解释器一行一行执行的,每次循环还要做类型检查、对象引用解析,数据量一大就非常慢。numpy的np.dot底层是用C语言写好的优化程序,直接把数据整块丢进去算,速度能快几十倍甚至上百倍。
更重要的是内存结构。列表里存的是对象的“引用”,而且允许一个列表里混着整数、字符串、字典,每个元素在内存里分散在不同位置。numpy数组要求所有元素类型一致,存成一段连续的内存块,CPU读取的时候能按块预加载,运算效率自然高。这就是为什么AI框架的底层几乎都绕不开numpy——图像是一个三维数组(高、宽、颜色通道),文本经过编码后变成向量组成的矩阵,音频本身就是一维数组,统一的数据结构才是批量计算的基础。
1.2 创建数组的四种日常姿势
刚接触numpy的人最容易犯的错,是每次都手动np.array([[1,2],[3,4]])。实际开发中,数组大多不是手敲出来的,而是用函数生成的。我这里整理几套日常最常用的姿势。
import numpy as np # 1. 从列表转数组,顺便指定类型 data = [1, 2, 3, 4] arr = np.array(data, dtype=float) # 变成 [1. 2. 3. 4.] # 2. 全零矩阵 / 全一矩阵:初始化占位非常常用 zeros = np.zeros((3, 3), dtype=int) # 3x3的全0整数矩阵 ones = np.ones((2, 4)) # 2x4的全1浮点矩阵 # 3. 等差数列 / 随机数 seq = np.arange(0, 10, 2) # [0 2 4 6 8] rand = np.random.randint(0, 100, size=(3, 3)) # 3x3的0-99随机整数 # 4. 类型转换:AI里非常常见 float_arr = rand.astype(np.float32) # 整数转浮点,后面做除法才准这里重点说说“全零矩阵”和“邻接矩阵”的关系。如果你在图神经网络、路径规划这类AI场景里混过,一定见过邻接矩阵。一堆节点之间的连接关系,最方便的表达方式就是矩阵:第i行第j列是1,表示节点i连到节点j;是0就表示没连接。
# 构建一个5个节点的无向图邻接矩阵 adj = np.zeros((5, 5), dtype=int) edges = [(0, 1), (1, 2), (2, 3), (3, 0), (0, 4)] for i, j in edges: adj[i, j] = 1 adj[j, i] = 1 # 无向图必须对称 print(adj)输出就是一个5x5的对称矩阵,对角线全是0。很多算法题和AI项目里,邻接矩阵就是这么一步步搭出来的,先zeros初始化,再往里填连接关系。还有一种常见操作是数据归一化前把整数数组astype(float),不然整数除法会自动截断,结果直接变成0,排查半天都找不到原因。
顺带一提,像“李白打酒”这类经典递推题,本质上也可以用数组状态转移来算,但那是算法章节的内容,本期先把数组工具用熟,后面遇到这类问题会顺手很多。
1.3 切片、复制与广播:最容易翻车的三件事
数组切片语法是arr[起点:终点:步长],看起来和列表切片差不多,但一旦到了二维就有人懵。二维数组的切片是arr[行范围, 列范围],注意是逗号分隔,不是Python列表那种arr[行][列]。
matrix = np.array([ [1, 2, 3, 4], [5, 6, 7, 8], [9, 10, 11, 12], [13, 14, 15, 16] ]) sub = matrix[1:3, 0:2] # 第1行到第2行,第0列到第1列 print(sub) # [[5 6] # [9 10]] col = matrix[:, 2] # 所有行的第2列,得到一维数组 row = matrix[2, :] # 第2行的所有列切片返回的是“视图”而不是“拷贝”,这是99%的人踩过的坑。我举个例子:
view = matrix[0:2, 0:2] view[0, 0] = 999 print(matrix[0, 0]) # 999,原数组也被改了!如果你想得到一份独立数据,必须显式调用copy():matrix[0:2, 0:2].copy()。修改副本才不会影响原始数据。这个坑在做图像裁剪、数据增强时特别容易暴雷——你只是想截个局部做测试,结果整个原数据集都被污染了。
广播机制是numpy的灵魂,不理解它,你会经常被各种“shape不匹配”的报错气死。简单说,两个数组形状不完全相同,但numpy能在一定规则下自动扩展维度完成计算:
a = np.array([[1], [2], [3]]) # 形状 (3, 1) b = np.array([10, 20, 30, 40]) # 形状 (4,) c = a + b # 自动广播成 (3, 4) print(c) # [[11 21 31 41] # [12 22 32 42] # [13 23 33 43]]a是3行1列,b是4个元素,numpy会把a横向“拉伸”成3行4列,把b纵向“拉伸”成3行4列,然后逐元素相加。规则是:从后往前比对维度,只要某个维度相等、其中一个为1、或其中一个缺失,就能广播。如果都不满足,就会报错operands could not be broadcast together。我的经验是:写numpy代码时,每做一步重要的变形,都养成习惯print一下arr.shape,很多灵异报错瞬间就能定位。
2. 数据不会整整齐齐等你:文件读写与结构化整理三板斧
数组再厉害,数据也不会凭空出现在内存里。你从网上下载的数据集、传感器采集的日志、Excel导出的表格,第一步永远是落盘、读盘。很多教程跳过了文件读写直接讲建模,结果学员一拿到真实数据就卡壳——这是AI项目里“脏活累活”的第一站。
2.1 文件读写的基本姿势:编码、路径与with
Python里最基础的文件操作是open(),但我几乎从来不用不带with的写法。因为打开文件后忘记关闭,会导致文件句柄泄漏,尤其在循环里打开大量文件时会报“Too many open files”。with语句会在代码块结束后自动关闭文件,省心又安全。
# 常见错误写法 f = open("data.txt", "w") f.write("hello") # 忘了 f.close() # 规范写法 with open("data.txt", "w", encoding="utf-8") as f: f.write("hello")文件模式别看只有几个字母,选错就是覆水难收:
| 模式 | 作用 | 注意事项 |
|---|---|---|
| r | 只读 | 文件不存在直接报错 |
| w | 写入覆盖 | 会清空原文件内容,千万谨慎 |
| a | 追加写入 | 从文件末尾接着写,不会清空 |
| rb / wb | 二进制读写 | 图片、模型文件必须用二进制 |
编码问题在Windows上是重灾区。如果你的电脑是中文系统,默认编码是GBK,而绝大多数数据集和网上下载的文件都是UTF-8。用open("data.txt", "r")读UTF-8文件,经常会抛出UnicodeDecodeError。我习惯在打开文件的每个地方都显式写上encoding="utf-8",不嫌麻烦。
路径坑也别小瞧。新手最常犯的错是“我在命令行里明明在这个目录,为什么程序说找不到文件”。原因很简单:Python进程的当前工作目录不一定是脚本所在目录。你可以用os.getcwd()看看当前到底在哪。稳妥做法是写绝对路径,或者用pathlib来管理路径,而不是疯狂用相对路径猜。AI项目的数据文件通常放在固定的data目录里,统一用绝对路径能省掉大量“文件不存在”的报错。
2.2 CSV与JSON:结构化数据的两大门面
真实项目里用得最多的结构化文本格式就是CSV和JSON。CSV是表格数据,适合一行一条记录;JSON是嵌套数据,适合带层级关系的信息。两者用得好,数据读取的代码能短一大截。
CSV的读写,我推荐直接用标准库csv模块:
import csv # 写入 rows = [ ["张同学", 88, 76], ["李同学", 91, 82] ] with open("scores.csv", "w", newline="", encoding="utf-8") as f: writer = csv.writer(f) writer.writerow(["姓名", "数学", "语文"]) # 表头 writer.writerows(rows) # 数据行 # 读取:用DictReader可以直接按列名访问 with open("scores.csv", encoding="utf-8") as f: reader = csv.DictReader(f) for row in reader: print(row["姓名"], row["数学"], row["语文"])写CSV时有个小细节:open()里必须加newline="",否则在Windows上会在行与行之间多出空行,导出给别的软件用时特别难看。读取用DictReader的好处是,每一行会变成一个字典,直接用表头当键,不用记住第几列代表什么。
JSON的处理同样简单:
import json info = { "class": "A班", "students": [ {"name": "张同学", "math": 88}, {"name": "李同学", "math": 91} ] } # 序列化:Python字典 -> JSON字符串 text = json.dumps(info, ensure_ascii=False, indent=2) print(text) # 反序列化:JSON字符串 -> Python字典 restored = json.loads(text) print(restored["students"][0]["name"])这里ensure_ascii=False一定要记住,不然中文会被转成\uXXXX,别人拿到数据根本看不懂。所谓“结构化数据”,简单理解就是数据不是一堆散落的文本,而是有表头、有字段、有固定层次,能被程序精确解析。AI模型的训练数据、API接口的返回结果,基本都是这个形态。你在网上下载的许多数据集,解压后往往就是CSV和JSON的组合体,这两个标准库不熟练,后面寸步难行。
2.3 项目开头就应做好的三件小事:虚拟环境、依赖清单与解释器
这一节说的是环境问题。很多人的Python项目跑不起来,不是代码问题,而是环境问题。尤其到了AI编程阶段,numpy、pandas、matplotlib这些库的版本不对,代码表现完全不一样。
第一件小事,安装Python时一定记得勾选“Add Python to PATH”。如果你在命令行里输入python提示“不是内部或外部命令”,多半就是没勾选。在Linux系统里还要注意系统自带的Python版本往往偏老,装numpy前先确认python --version,不行就用python3区分开。安装第三方库用pip install numpy,国内网络慢的话可以配置镜像源,这个属于安装环节的老生常谈,但确实能省十分钟。
第二件小事,每个项目创建独立的虚拟环境。不要在全局环境里把所有库堆在一起,否则项目A要numpy 1.x,项目B要numpy 2.x,互相冲突到你怀疑人生。用内置的venv就够了:
python -m venv venv # Windows激活 venv\Scripts\activate # Linux / macOS激活 source venv/bin/activate第三件小事,环境激活后第一时间生成依赖清单。把项目依赖写到requirements.txt里,是团队协作和复现实验的基本素养。别人拿到你的代码,只要执行pip install -r requirements.txt,就能把环境还原出来。生成方式:
pip freeze > requirements.txt如果你用的VSCode,记得用Ctrl+Shift+P调出命令面板,输入Python: Select Interpreter,选择你虚拟环境里的解释器,而不是系统默认的解释器。很多人在VSCode里装了库但导入失败,十有八九是解释器选错了,包装进了另一个环境。这些动作和文件读写一样,都是AI项目的“地基”,地基没打好,后面一切都是空中楼阁。
3. 可视化不是锦上添花:matplotlib画图避坑与出图规范
数据算出来一堆数字,人是看不懂的。你要看趋势、看分布、看异常点,必须画图。matplotlib是Python可视化里最啰嗦但也最强大的工具,很多人嫌它API复杂,其实是你没掌握一套稳定的用法。
3.1 三种最常用的图:折线、柱状、散点
我平时写脚本画图的标准姿势是“面向对象风格”——先创建figure(画布)和axes(坐标轴),再在上面画内容。不要用一堆plt.plot的全局状态写法,画多子图时会乱套。
import matplotlib.pyplot as plt # 准备数据 x = [1, 2, 3, 4, 5, 6] y_line = [3, 5, 7, 6, 8, 10] # 折线图数据 y_bar = [5, 8, 12, 7, 9, 11] # 柱状图数据 x_scatter = [1, 2, 3, 4, 5, 6] y_scatter = [2, 6, 5, 9, 7, 12] # 推荐:1行2列的两个子图 fig, axes = plt.subplots(1, 3, figsize=(14, 4)) axes[0].plot(x, y_line, marker="o", linestyle="-", linewidth=1.5) axes[0].set_title("折线图:看趋势") axes[0].set_xlabel("时间") axes[0].set_ylabel("数值") axes[1].bar(x, y_bar, color="skyblue") axes[1].set_title("柱状图:看对比") axes[2].scatter(x_scatter, y_scatter, color="orange", s=60) axes[2].set_title("散点图:看分布") plt.tight_layout() plt.show()折线图适合时间序列、趋势变化;柱状图适合类别对比;散点图适合观察两个变量之间的关系或者数据点的分布。记住这个口诀就够用了。figsize参数控制整个图的长宽,单位是英寸,这个值直接影响输出图片的清晰度和排版效果。
3.2 保存图片的三个参数与一个常见坑
做完分析总要出图,不可能每次都在屏幕上截屏。保存图片用savefig,但很多人不知道这三个参数的意义。
plt.savefig("output.png", dpi=300, bbox_inches="tight", facecolor="white")dpi=300是分辨率,保证打印和放大时不糊;bbox_inches="tight"会自动裁掉图片周围的空白,不然你保存出来的图四周会有一大圈白边;facecolor="white"设置背景色,不然透明背景在某些文档里会变成黑色块。
常见坑是:先plt.show()再plt.savefig(),结果保存出来一张空白图。原因是show()之后画布进入交互模式,再保存时画布状态已经不对了。解决方案永远是:先保存,再显示,或者直接用fig.savefig("output.png")而不是plt.savefig。我把“先存再show”当成肌肉记忆,从来没再犯过这个错。
3.3 横坐标太密集的完整处理思路
这是很多人在真实数据上遇到的第一个“画图翻车点”。你的横坐标是30天日期、100个样本编号,直接画出来,所有标签挤成一团黑条,根本看不清。
看下问题场景:
import matplotlib.pyplot as plt dates = [f"2024-01-{i:02d}" for i in range(1, 31)] # 30个日期 values = range(30) fig, ax = plt.subplots(figsize=(10, 5)) ax.plot(dates, values) ax.set_xticks(range(30)) ax.set_xticklabels(dates, rotation=45) plt.show()不处理的话,30个日期标签叠在一起,基本是一坨黑块。处理思路有三个层次:
方案一:旋转标签 + 缩小字号。最粗暴,适合标签数不多(10个以内)的情况。
ax.set_xticklabels(dates, rotation=45, fontsize=8)方案二:每隔N个点才显示一个标签。这是我最常用的办法,先用set_xticks指定显示的刻度位置,再用set_xticklabels指定对应的标签文本。
step = 5 # 每隔5天显示一个标签 ticks = list(range(0, len(dates), step)) ax.set_xticks(ticks) ax.set_xticklabels([dates[i] for i in ticks], rotation=45)方案三:用定位器让matplotlib自动分配。适合坐标轴是时间类型的情况,简单设置一个上限就行:
from matplotlib.ticker import MaxNLocator ax.xaxis.set_major_locator(MaxNLocator(nbins=8)) # 最多显示8个刻度三种方案我整理成表格:
| 方案 | 适用场景 | 优点 | 缺点 |
|---|---|---|---|
| 旋转加小字号 | 标签少于15个 | 实现最简单 | 标签多了照样挤 |
| 步长抽样显示 | 任意数量标签 | 可控性强、最通用 | 需要手动计算 |
| MaxNLocator自动分配 | 时间轴或连续轴 | 自动优化 | 标签文本不灵活 |
真实项目里我会优先用方案二,因为它最直白:我明确知道哪些刻度被显示,不会出现“自动分配把日期切得很奇怪”的情况。核心思路是:横坐标密集不是数据问题,是“显示策略”问题——不是每个数据点都需要有自己的坐标轴标签。
3.4 中文显示与负号:出图的最后一公里
matplotlib默认字体不支持中文,直接画中文标题,图里全是方框□□□,这是因为字体渲染时找不到中文字形。还有更隐蔽的坑:默认负号-在部分字体下会显示成乱码。
解决办法是脚本开头统一设置:
import matplotlib.pyplot as plt plt.rcParams["font.sans-serif"] = ["SimHei", "Microsoft YaHei", "Noto Sans CJK SC"] plt.rcParams["axes.unicode_minus"] = False # 解决负号显示为方块的问题第一条是把默认字体换成中文字体(中文字体名因系统而异,Windows一般有SimHei和Microsoft YaHei,Linux常装Noto Sans CJK),第二条是让负号正常显示。这两行我建议放在脚本最顶部,省得以后每张图都出问题。还有一个备用方案是在set_title或set_xlabel里指定fontproperties,但那是单点的补丁做法,不如全局设置清爽。
如果你在VSCode里跑的脚本画图后图片里的中文还是方块,先检查font.sans-serif列表里有没有该系统已安装的字体名,fc-list :lang=zh能列出Linux系统的中文字体。这属于环境差异问题,但排起来也就几分钟的事。
4. 串起来跑一遍:一条能直接落地的小型数据流水线
前面三节的内容,现在把它们串成一个完整的小项目。这个项目不依赖任何外部数据集,不联网,用random生成模拟数据,完全可以复现,适合作为你练手的第一条数据流水线。
4.1 先定需求:模拟、写入、读回、清洗、出图
我模拟一个班级30名学生的两科成绩:数学和语文。然后做四件事:
- 把成绩写入CSV文件,模拟“收集到一份数据”;
- 从CSV读回内存,转成numpy数组;
- 清洗数据(故意塞进几个异常值,比如150分、-5分);
- 计算统计量并画柱状图保存。
这套流程看起来简单,但它就是AI项目的雏形。真实项目里,无非是把“生成模拟成绩”换成“从接口拿数据”,把“清洗成绩”换成“清洗用户行为日志”,把“画柱状图”换成“画训练曲线”。流程骨架是完全一致的。用模拟数据的最大好处是可控,异常值是你自己埋的,验证清洗逻辑对不对就特别直观。
4.2 完整代码与运行结果
import csv import random import numpy as np import matplotlib.pyplot as plt # 画图前先设置中文字体,避免标题乱码 plt.rcParams["font.sans-serif"] = ["SimHei", "Microsoft YaHei", "Noto Sans CJK SC"] plt.rcParams["axes.unicode_minus"] = False # 固定随机种子,保证每次运行结果一致,方便调试 random.seed(42) np.random.seed(42) # 1. 生成模拟成绩 names = [f"同学{i:02d}" for i in range(30)] math_scores = [random.randint(50, 100) for _ in range(30)] chinese_scores = [random.randint(50, 100) for _ in range(30)] # 故意埋几个异常值,模拟脏数据 math_scores[3] = 150 chinese_scores[7] = -5 math_scores[15] = 300 # 2. 写入CSV with open("class_scores.csv", "w", newline="", encoding="utf-8") as f: writer = csv.writer(f) writer.writerow(["姓名", "数学", "语文"]) for i in range(30): writer.writerow([names[i], math_scores[i], chinese_scores[i]]) # 3. 读回CSV并转成numpy数组 math_data = [] chinese_data = [] with open("class_scores.csv", encoding="utf-8") as f: reader = csv.DictReader(f) for row in reader: math_data.append(float(row["数学"])) chinese_data.append(float(row["语文"])) math_arr = np.array(math_data) chinese_arr = np.array(chinese_data) # 4. 清洗:分数合理范围是0~100,布尔索引过滤异常值 valid = (math_arr >= 0) & (math_arr <= 100) & (chinese_arr >= 0) & (chinese_arr <= 100) math_clean = math_arr[valid] chinese_clean = chinese_arr[valid] valid_names = np.array(names)[valid] print(f"原始样本数: {len(math_arr)}") print(f"清洗后有效样本数: {len(math_clean)}") print(f"数学平均分: {math_clean.mean():.2f}") print(f"语文最高分: {chinese_clean.max():.2f}") # 5. 画柱状图并保存 x_pos = np.arange(len(math_clean)) fig, ax = plt.subplots(figsize=(12, 6)) ax.bar(x_pos - 0.2, math_clean, width=0.4, label="数学", color="steelblue") ax.bar(x_pos + 0.2, chinese_clean, width=0.4, label="语文", color="tomato") ax.set_xticks(x_pos) ax.set_xticklabels(valid_names, rotation=90, fontsize=8) ax.set_xlabel("学生") ax.set_ylabel("分数") ax.set_title("班级成绩分布(清洗后)") ax.legend() ax.grid(axis="y", linestyle="--", alpha=0.3) plt.tight_layout() plt.savefig("final_scores.png", dpi=200, bbox_inches="tight") plt.show()运行结果大致是:
原始样本数: 30 清洗后有效样本数: 27 数学平均分: 74.33 语文最高分: 99.00输出的final_scores.png就是一张清洗后的双柱状对比图。整个过程覆盖了本期所有的知识点:random生成数据、文件写读、numpy类型转换与布尔索引、matplotlib设置中文并保存图片。清洗逻辑尤其值得多说两句:(math_arr >= 0) & (math_arr <= 100)返回的是一个布尔数组,里面每个位置是True/False,numpy直接拿这个布尔数组去索引原数组,就能把对应位置的数字筛选出来,不需要写循环。
4.3 跑完后我建议你改的几个地方
如果你只是复制粘贴跑通,收获会打折扣。我建议你至少动手改这三处:
第一,去掉random.seed(42)再跑一次,观察结果变化。你会发现每次运行平均分都不一样,这就是“随机性”。真实项目里,固定随机种子能让实验结果可复现,是AI实验的标准操作。
第二,把CSV读取部分改成JSON格式。用json.dumps把成绩列表存成JSON文件,再读回来,感受一下两种格式在读写代码上的差异。将来你对接API接口时,返回的几乎都是JSON,提前熟悉没坏处。
第三,把画图部分从柱状图改成折线图加散点图组合:用折线图看排名趋势,用散点图看数学和语文成绩的相关性。你会更自然地理解“一张图只回答一个问题”的规律——混着画容易信息过载,分开画反而清晰。
等你把这条流水线跑熟练,再把数据换成从网上采集的真实数据(比如某直播平台的弹幕文本、某个接口返回的行情数据),流程代码基本不用改,换个数据源就又是一个新项目。这也是“从零到精通”走到现在最值得沉淀下来的东西:不是背某个API,而是建立一套“数据进、信息出”的处理框架。
这一期讲完,我强烈建议你不要只看,找个晚上把代码从头敲一遍。敲的过程中你会遇到无数小问题——少个引号、路径不对、中文字体没设置,这些问题全踩一遍,你的印象会比看十遍教程都深。到第十期结束,你已经具备独立完成“数据获取、清洗、可视化”的能力了,后面的模型训练、特征工程,都建立在这套地基之上。数据这条线稳了,AI的大门才真正向你敞开。