AI编程(六十五):散点图 — 看两件事有没有“默契”

上一篇的饼图把”份额”分清楚了,图表三兄弟(折线、柱状、饼图)讲的都还是”一个变量”的故事。今天迎来四弟——散点图,它第一次让两个变量同台对戏。只引入一个新概念:相关性:两个数字是不是”一伙的”——一个涨另一个跟着涨?还是一涨一跌?还是各玩各的?顺手把趋势线和相关系数也见个面,最后必须把”相关≠因果”刻在脑门上再走。

一、真实场景:一句”越……越……”到底真不真

家长会上总有这样的金句:”每天学习时间越长,成绩就越好。”听着特别顺耳,但它是真的吗?说这话的人手里有证据吗?

你想验证它,找班上 20 个同学挨个问了两件事:每天学习多长时间(小时),期末数学考了多少分。现在你手里是两列数字,一列对一列:

学习时长:0.5, 0.8, 1.0, 1.2, 1.5, ... 5.5
数学成绩:52, 55, 58, 60, 63, ... 96

盯着这两列数字看半分钟,你大概能”感觉”出点什么——学得久的那几个分数确实高。但”感觉”不算数,怎么把这种”感觉”变成一眼能看见、还能拿给全班看的证据?

这就是今天的新概念:相关性——两个数字”一起动”的方向和紧密程度。它只有三种形态:

  • 正相关:一个涨,另一个跟着涨(学习时长↑ 成绩↑);
  • 负相关:一个涨,另一个反而跌(玩手机时长↑ 成绩↓);
  • 不相关:各玩各的,谁也不搭理谁(鞋码大小↑ 成绩?没谱)。

而把相关性”画出来”的工具,就是散点图:每个同学是一个点,横坐标放他的学习时长,纵坐标放他的成绩——20 个同学就是 20 个点。这些点组成的”点云”长什么形状,相关性就长什么样。

二、散点图的”世界观”:每对数字一个点

先把选图口诀接着前三篇往下记全:

  • 数据随时间变化 → 折线图(趋势);
  • 比”谁多谁少” → 柱状图(较量);
  • 比”谁占多大份额” → 饼图(份额);
  • 两个数字有没有”关系” → 散点图(相关性)。

读散点图就看三件事,叫”点云三问”:

  1. 朝哪边斜? 点云从左下往右上爬 = 正相关;从左上往右下跌 = 负相关;躺着不动 = 不相关。
  2. 挤多紧? 点越贴近一条看不见的线,关系越”铁”;越散成一盘沙,关系越松。
  3. 有没有”叛徒”? 大部队整整齐齐、偏有个点孤零零飘在角落——那可能是异常值,后面坑点疫苗细说。

注意散点图和折线图的本质区别:折线图的横轴是时间,点和点之间有先后顺序,连线是”走过的时间”;散点图的横轴是普通数值,点和点没有顺序,每个点都是独立的一对数字。所以折线图天生带线,散点图天生不连线——给散点图连线是新手最容易犯的错之一(坑点疫苗2提前挂号)。

三、动手:把”默契”画出来

第 1 步:第一张散点图 —— 每个同学一个点

# scatter_basic.py — 第一张散点图:学习时长与数学成绩
import matplotlib.pyplot as plt

# —— 中文字体(第61篇的坑点疫苗,直接复用)——
plt.rcParams["font.sans-serif"] = ["Noto Sans CJK SC", "SimHei", "WenQuanYi Zen Hei"]
plt.rcParams["axes.unicode_minus"] = False

hours = [0.5, 0.8, 1.0, 1.2, 1.5, 1.8, 2.0, 2.2, 2.5, 2.8,
         3.0, 3.2, 3.5, 3.8, 4.0, 4.3, 4.5, 4.8, 5.0, 5.5]
scores = [52, 55, 58, 60, 63, 62, 68, 70, 69, 75,
          78, 76, 82, 84, 86, 88, 90, 92, 94, 96]

plt.scatter(hours, scores)
plt.xlabel("每天学习时长(小时)")
plt.ylabel("期末数学成绩(分)")
plt.title("学习时长与数学成绩的关系(20名同学)")
plt.grid(alpha=0.3)
plt.savefig("study_scatter.png", dpi=150, bbox_inches="tight")
plt.show()

plt.scatter(x列表, y列表) 就是散点图的全部骨架——第 i 个 x 和第 i 个 y 配成一对,画成一个点。两个列表必须等长,否则 matplotlib 直接报错;而且是按位置配对,第 3 个学习时长配第 3 个成绩,顺序乱了整个图就是谎言。

跑起来你会发现点云明显从左下往右上爬——正相关没跑了。但”明显”是多明显?斜率多少?这就需要下一课的新朋友来把话说死。

第 2 步:趋势线 —— 给点云”拉一条主线”

# scatter_trend.py — 加趋势线:把"感觉相关"变成"有一条线"
import numpy as np
import matplotlib.pyplot as plt

plt.rcParams["font.sans-serif"] = ["Noto Sans CJK SC", "SimHei", "WenQuanYi Zen Hei"]
plt.rcParams["axes.unicode_minus"] = False

hours = np.array([0.5, 0.8, 1.0, 1.2, 1.5, 1.8, 2.0, 2.2, 2.5, 2.8,
                  3.0, 3.2, 3.5, 3.8, 4.0, 4.3, 4.5, 4.8, 5.0, 5.5])
scores = np.array([52, 55, 58, 60, 63, 62, 68, 70, 69, 75,
                   78, 76, 82, 84, 86, 88, 90, 92, 94, 96])

# —— 拟合一条直线:成绩 ≈ k×时长 + b ——
k, b = np.polyfit(hours, scores, 1)   # 1 = 拟合一次函数(直线)
x_line = np.linspace(hours.min(), hours.max(), 100)

plt.scatter(hours, scores, label="同学")
plt.plot(x_line, k * x_line + b, color="#d9534f", linewidth=2,
         label=f"趋势线:成绩 ≈ {k:.1f}×时长 + {b:.1f}")
plt.xlabel("每天学习时长(小时)")
plt.ylabel("期末数学成绩(分)")
plt.title("学习时长与数学成绩:趋势线一拉,关系说话")
plt.legend()
plt.grid(alpha=0.3)
plt.savefig("study_trend.png", dpi=150, bbox_inches="tight")
plt.show()

np.polyfit(x, y, 1) 是 numpy 的”最小二乘拟合”——它自动找到那条离所有点都最近的直线,返回斜率 k 和截距 b。输出大约是 成绩 ≈ 8.6×时长 + 50.8:翻译成人话,”每天多学 1 小时,成绩平均多 8.6 分”。

趋势线是散点图的”结论线”:它把一团点压成一句话。但这条线只在数据范围内可靠——这是坑点疫苗4的重点。

第 3 步:相关系数 —— 把”默契”变成一个数

# scatter_corr.py — 相关系数:把相关性变成一个能抄进报告的数
import numpy as np

hours = np.array([0.5, 0.8, 1.0, 1.2, 1.5, 1.8, 2.0, 2.2, 2.5, 2.8,
                  3.0, 3.2, 3.5, 3.8, 4.0, 4.3, 4.5, 4.8, 5.0, 5.5])
scores = np.array([52, 55, 58, 60, 63, 62, 68, 70, 69, 75,
                   78, 76, 82, 84, 86, 88, 90, 92, 94, 96])

r = np.corrcoef(hours, scores)[0, 1]
print(f"相关系数 r = {r:.3f}")
# 输出大约:相关系数 r = 0.992

np.corrcoef(两列数字) 返回一个 2×2 的”相关系数矩阵”,我们要的是第 0 行第 1 列那个数——皮尔逊相关系数 r,它把”点云挤多紧”压缩成一个 -1 到 1 之间的数:

  • r 接近 +1:强正相关,点云紧贴一条上坡直线;
  • r 接近 -1:强负相关,紧贴一条下坡直线;
  • r 接近 0:不相关,一盘散沙。

粗略的档位感:|r| ≥ 0.7 算强,0.4~0.7 算中等,0.4 以下算弱(文科场景够用,科研要查显著性,先混脸熟)。咱们这份数据 r≈0.99,属于”强得冒烟”的正相关——但记住,r 强只说明”两列数字同步变”,不说明谁导致了谁,这个疫苗第五节第一个打。

第 4 步:第三维上色 —— 一张图再捎一个变量

# scatter_color.py — 第三维:用颜色捎上"每天睡眠时长"
import numpy as np
import matplotlib.pyplot as plt

plt.rcParams["font.sans-serif"] = ["Noto Sans CJK SC", "SimHei", "WenQuanYi Zen Hei"]
plt.rcParams["axes.unicode_minus"] = False

hours = np.array([0.5, 0.8, 1.0, 1.2, 1.5, 1.8, 2.0, 2.2, 2.5, 2.8,
                  3.0, 3.2, 3.5, 3.8, 4.0, 4.3, 4.5, 4.8, 5.0, 5.5])
scores = np.array([52, 55, 58, 60, 63, 62, 68, 70, 69, 75,
                   78, 76, 82, 84, 86, 88, 90, 92, 94, 96])
sleep = np.array([5.5, 6.0, 6.2, 6.5, 6.3, 6.8, 7.0, 6.9, 7.2, 7.1,
                  7.3, 7.5, 7.4, 7.6, 7.8, 7.7, 8.0, 8.1, 8.2, 8.0])

points = plt.scatter(hours, scores, c=sleep, cmap="coolwarm",
                     s=80, edgecolors="white", linewidths=0.5)
plt.colorbar(points, label="每天睡眠时长(小时)")
plt.xlabel("每天学习时长(小时)")
plt.ylabel("期末数学成绩(分)")
plt.title("学习时长 × 成绩 × 睡眠:一张图看三个变量")
plt.grid(alpha=0.3)
plt.savefig("study_color.png", dpi=150, bbox_inches="tight")
plt.show()

c= 参数给每个点发一个数值,cmap="coolwarm" 把数值翻译成颜色(蓝低红高),plt.colorbar() 附送一把”颜色尺”。这样一张散点图就装下了三个变量:x 学习时长、y 成绩、颜色睡眠时长。顺带还能看出一层意思——红点(睡得多)多半聚在右上角,睡眠和成绩可能也有关联。这就是散点图的探索能力:一图生一问,问出下一个假设。

四、改进:跟AI一起升级

把 scatter_trend.py 发给 AI,需求往”能放进汇报”上提:

“这是学习时长和数学成绩的散点图代码(20个点)。帮我升级成汇报版:1)趋势线方程和 r 值标在图上;2)点加透明度防止重叠看不清;3)帮我检查有没有异常值,并说明它对趋势线的影响;4)除了’学习时长导致成绩提高’,还能有什么别的解释?请先给方案再给代码。”

它的方案里会冒出几个新面孔,先混脸熟:

  • plt.text():把 r = 0.99、趋势线方程直接写在图上,结论不靠嘴说;
  • alpha=0.7:点半透明,重叠的地方颜色自然加深,密度看得见;
  • 残差思路:判断异常值就是看”点离趋势线多远”,离得离谱的就是嫌疑人;
  • 混杂变量:第 4 问是今天最值钱的一问——AI 若答出”睡眠、课堂效率、基础好坏都可能同时影响时长和成绩”,你就拿到了”相关≠因果”的完整弹药。

逐条实现、逐条跑。重点不是它给了什么代码,是你能指着图说出”这条线、这个数、这个点分别证明了什么”。

五、四个必踩的坑(先打疫苗)

  1. 相关 ≠ 因果——今天最重的一针疫苗。 经典反例:冰淇淋销量和溺水人数高度正相关(r 很高),难道吃冰淇淋导致溺水?真相是混杂变量“气温”在背后同时推动两者。放到咱们的数据里:”学习时长和成绩相关”也可能藏着第三只手(睡眠好、基础强的同学既学得久又考得好)。散点图能证明”一起动”,永远不能证明”谁导致谁”。 要证明因果,得做实验或找更强的证据——对外汇报说”相关”,别说”导致”。
  1. 给散点图乱连线 = 把独立数据伪装成过程。 点和点之间没有先后顺序,连线会让读者以为中间发生了什么”过渡”。例外只有一个:同一批对象的重复测量(比如追踪这 20 个人三个学期,每个人自己的轨迹可以连线),而且必须能一眼分清线属于谁。
  1. 异常值一个点就能”绑架”趋势线。 最小二乘拟合对离群点极其敏感——20 个点里混进一个”学 10 小时考 20 分”(多半是录入错误),整条趋势线的斜率和 r 全被拖歪。疫苗打法:画完图先肉眼抓”叛徒”,核对原始数据是不是录错了;确认无误再决定留不留,并且报告”去掉异常值前后的 r”两个数,别只挑好看的说。
  1. 趋势线外推 = 算命。 数据范围是 0.5~5.5 小时,你说”学到 12 小时就能考 150 分”——趋势线没给过你这个授权。出了数据范围,世界可能拐弯(睡眠不足成绩反而崩)。还有一针:r 只认直线关系——U 形关系(睡太少和睡太多都不行)的 r 可能接近 0,但关系明明很强。所以铁律永远是:先看图,再算数,r 是图的注脚,不是图的替代品。

六、跟AI协作的正确姿势(关系图特供版)

  1. 让 AI 当”统计顾问”,但结论自己把关。 把数据和图都发给它,问:”r=0.99 能下’学习时长决定成绩’的结论吗?”它若张嘴就说”能”,这个顾问不可信——正确答案永远是”不能,只能下’强正相关'”。
  1. 把”变量的来龙去脉”告诉它。 这两列数字怎么来的?谁测的?样本多少?有没有录错的可能?散点图的结论强度一半在图里、一半在数据来源里,AI 拿不到这些前提,给的解读就是空中楼阁。
  1. 让它专职找”别的解释”。 每次画完散点图,固定追问一句:”除了我想到的解释,还有什么混杂变量?还有什么图形形态(U形、分段)可能被 r 漏掉?”这个习惯能让你的分析从”看图说话”升级到”经得起反驳”。

七、今日小练习

练习1:画出你自己的散点图。 找一对你怀疑”有关系”的数字(睡眠时长 vs 次日专注度、咖啡杯数 vs 工作效率、通勤时间 vs 心情都行),至少 10 对,画散点图并拉趋势线(提示:np.polyfit(x, y, 1) 给斜率截距;先自问”点云朝哪斜、挤多紧、有没有叛徒”点云三问)。

练习2:给”叛徒”做实验。 在练习1的数据里故意造一个异常值(比如翻倍的一个 y),对比造入前后的 r 和趋势线斜率(提示:np.corrcoef(x, y)[0, 1] 算 r;跑两遍代码打印两个 r,直观感受”一个点的破坏力”)。

练习3:当一回”抬杠专家”。 对练习1的结论写三句话:一句”能说的”(相关性描述)、一句”不能说的”(因果)、一句”还需要什么证据”(提示:想想冰淇淋和溺水的例子;试着找出至少一个混杂变量,比如”睡眠”可能同时影响专注度和咖啡量)。

下篇预告

下一篇(第66篇):图表美化 — 颜色、标签、标题的”化妆课”。 图画对只是及格,画得让人三秒读懂、过目不忘才是本事:配色怎么选不踩坑、标签怎么排不打架、标题怎么写一句话讲清结论。从下一篇起,你的图开始”会说话、说得好听”——数据可视化最后一公里的门面工程正式开工。