Matplotlib散点图进阶:颜色映射+大小映射实现多维相关性分析 📅 发布时间:2026/9/10 0:06:18 👁 浏览次数: 1. 先把散点图这件事想透它到底能帮我们看清什么我做了这么多年数据可视化说句实在话Matplotlib 里最被低估的图就是散点图。柱状图、折线图大家都在用但散点图往往被当成“先画个点看看”的草稿工具很少有人真的把它当主力分析图去用。实际上散点图是探索两个变量关系时信息密度最高的一种图形没有之一。举个最直白的例子假设你有两组数据一组是广告投放金额一组是销售额。用柱状图你只能看到“每个月的投入和产出分别长什么样”但这两者之间到底有没有关系、是正相关还是负相关、是线性还是非线性、有没有异常点柱状图根本回答不了。而一张散点图把每个月的投放金额, 销售额作为一个点画在二维平面上你一眼就能看出趋势方向、密集程度、离群点甚至还能看出数据是否分层、是否有聚类结构。这就是散点图的核心价值它不在“描述”数据而在“暴露关系”。这篇文章我想完整拆解一下如何用 Matplotlib 把散点图从“能看”做到“能分析”重点讲清楚相关性分析、颜色映射和大小映射这三件事以及它们背后真正解决的是什么问题。适合谁来读如果你是刚入门数据分析、正在学 pandas Matplotlib 的初学者这篇文章能帮你少走很多弯路如果你已经会画基础散点图但总觉得自己的图“缺点信息量”那这篇文章能告诉你散点图的上限比你想象的高得多。2. 散点图的分析价值它天生就是为相关性分析服务的2.1 为什么相关性分析离不开散点图先聊个基础概念。统计学里的相关性分析最常用的指标是皮尔逊相关系数Pearson correlation coefficient取值范围在 -1 到 1 之间。大于 0 表示正相关小于 0 表示负相关绝对值越接近 1 说明线性关系越强。但这里有个很多初学者容易忽略的坑相关系数只是一个数值它无法告诉你数据的“形状”。我举个例子你就明白了。著名的 Anscombes quartet安斯库姆四重奏里有四组数据它们的均值、方差、相关系数、回归线几乎完全一样但把散点图画出来四组数据的分布形态截然不同——一组是正常的线性关系一组是明显的抛物线关系一组是只有一个离群点拉出来的伪线性还有一组是垂直线分布。如果只算相关系数你会以为这四组数据没区别但散点图一出来真相一目了然。这就是散点图在相关性分析里不可替代的原因它把“关系”变得可感知。相关系数告诉你“有关系”散点图告诉你“是什么关系”。那么 Matplotlib 里怎么画一张最基础的散点图代码非常简单import matplotlib.pyplot as plt plt.scatter(x, y) plt.show()但如果你只是把数据丢进去就完事那确实浪费了散点图的潜力。接下来我要讲的颜色映射和大小映射才是让散点图真正“解锁数据隐藏关系”的关键。2.2 数据准备先用一个真实的例子把全场串起来为了让你看得更直观我先造一份模拟数据后面所有的图都基于这份数据展开。场景我们定为一个“电商平台用户行为分析”假设我们想研究用户的“每日使用时长”和“累计消费金额”之间的关系同时还想看看不同用户群体的差异。import numpy as np import pandas as pd np.random.seed(42) n 300 usage_minutes np.random.normal(30, 10, n).clip(5, 60) spending 20 usage_minutes * 1.5 np.random.normal(0, 20, n) spending spending.clip(0, 150) membership_level np.random.choice([A, B, C], sizen, p[0.2, 0.5, 0.3]) # 这里把用户等级和消费稍微关联一下让数据更有分析价值 for i in range(n): if membership_level[i] A: spending[i] np.random.normal(20, 10) elif membership_level[i] B: spending[i] np.random.normal(5, 10) else: spending[i] - np.random.normal(10, 10) df pd.DataFrame({ usage_minutes: usage_minutes, spending: spending, membership_level: membership_level }) print(df.head())给数据里人为加入一些分层差异。这样到后面做颜色映射的时候你能明显看到“不同会员等级在散点图中的分布差异”而不是像很多教程那样颜色映射只是“为了好看”而存在。3. 颜色映射把第三个维度的信息“画”进图里3.1 单色散点图的局限性以及从单色到渐变的跨越我们先画一个最普通的散点图看看为什么它不够用。plt.figure(figsize(8, 6)) plt.scatter(df[usage_minutes], df[spending]) plt.xlabel(Daily Usage Minutes) plt.ylabel(Total Spending) plt.title(Usage vs Spending (Single Color)) plt.show()这张图能看出什么大概能看出“使用时长和消费金额似乎有点正相关”点分布得比较散但也就这样了。如果这是一份真实业务数据你盯着这张图很难继续深挖出更多信息——你不知道哪些点属于高价值用户不知道使用时长长的人是不是都集中在某个消费区间更看不出有什么隐藏的分群结构。这时候颜色映射就该上场了。颜色映射colormap的本质是把“第三个维度”的数值信息编码到点的颜色上。散点图天生只有两个坐标轴所以只能表达两个变量但通过颜色、大小、形状、透明度这些视觉通道我们可以在一张图里塞进更多维度的信息。这就好比你把一个三维立体模型压到一张二维照片上颜色就是那个“高度信息”。3.2 类别颜色映射用颜色区分不同分组最常用的颜色映射之一是用颜色区分“类别”。回到我们的数据里每个用户有会员等级A/B/C我们自然想看不同等级的用户在使用时长和消费金额上的表现差异。实现方法有两种一种是直接循环分组分别用不同颜色画另一种是利用 seaborn 的 hue 参数但这里我们专注用 Matplotlib 实现这样你能更清楚底层逻辑。colors {A: #d62728, B: #1f77b4, C: #2ca02c} plt.figure(figsize(8, 6)) for level in [A, B, C]: subset df[df[membership_level] level] plt.scatter(subset[usage_minutes], subset[spending], ccolors[level], labelfLevel {level}, alpha0.6) plt.xlabel(Daily Usage Minutes) plt.ylabel(Total Spending) plt.title(Usage vs Spending by Membership Level) plt.legend() plt.show()这里我加入了alpha0.6也就是透明度调到 60%。这个细节很重要——当散点数量比较多时点与点会互相重叠如果不加透明度后面的点完全被挡住你就看不出密度分布了。加了透明度之后重叠越多颜色越深等于自带了一点点“密度图”的效果。这是实战中非常实用的小技巧。从这张图里你能看到什么A 类用户明显处于较高消费区间B 类用户集中在中间C 类用户普遍偏低而且 C 类用户的分布更分散。这就是颜色映射带来的第一层“隐藏关系”把类别属性可视化以后你不再只是看“使用时长和消费的关系”而是看“这种关系在不同用户群体之间有什么差异”。3.3 连续颜色映射用渐变色表达连续数值变量类别颜色映射之外更常用的是连续数值颜色映射。比如我们想在这个散点图里再加入一个维度“最近一次登录时间间隔days since last login”。这个值越小说明用户越活跃。还是刚才的例子给数据加一列df[days_since_login] np.random.randint(0, 30, sizen)现在我们有四个维度了x 轴是使用时长y 轴是消费金额颜色可以表示“最近登录间隔”点的大小可以留给下一个环节。连续颜色映射的写法是plt.figure(figsize(8, 6)) sc plt.scatter(df[usage_minutes], df[spending], cdf[days_since_login], cmapviridis, s30) plt.colorbar(sc, labelDays Since Last Login) plt.xlabel(Daily Usage Minutes) plt.ylabel(Total Spending) plt.title(Usage vs Spending (Colored by Recent Activity)) plt.show()c参数接受一个数值序列cmap指定颜色映射方案然后plt.colorbar(sc)把颜色条画出来作为图例。这样一来读者不仅能看到点的位置还能通过颜色深浅判断这个点对应的第三维数值。选cmap有个讲究。色盲友好且顺序感强的颜色映射推荐viridis、plasma、inferno这三个是 Matplotlib 内置的感知均匀perceptually uniform颜色映射。什么叫感知均匀就是颜色深浅的变化和数值大小的变化在视觉上是线性对应的不会出现“某个数值区间的颜色变化特别剧烈另一个区间又几乎看不出变化”的问题。而像rainbow、jet这类彩虹色映射虽然颜色丰富但中间有大量非单调的亮度跳跃一是不专业二是对色盲读者不友好建议直接弃用。注意一点连续颜色映射适合“顺序型数值变量”如果第三维是类别还是用互不混淆的离散颜色更合适。混淆了这两种场景图会变得很难解读。3.4 颜色映射的配色方案选择指南关于颜色我再多聊几句。很多初学者会问“我应该选什么颜色”其实这不只是审美问题更是个信息传达问题。我把常见场景和推荐方案整理成一张表方便你直接参考场景推荐方案说明连续数值顺序型如金额、时长viridis,plasma,inferno,cividis感知均匀适合表达数值高低连续数值发散型如相关系数、差值coolwarm,RdBu_r,PiYG两端对比明显中间有中性色离散类别少量分类2~6类自定义颜色列表或tab10色相差异大不易混淆离散类别大量分类10类以上tab20但更建议用其他视觉通道辅助分类一多纯靠颜色很难分辨用颜色做映射核心原则是“颜色的差异要能被正常人眼快速理解”。如果你把连续变量画成彩虹色读者第一反应不是“深色代表大、浅色代表小”而是“这图花里胡哨到底想表达什么”。所以克制比丰富更重要。4. 大小映射把“权重”和“规模”变成可见的信息4.1 大小映射的适用场景与实现方法颜色能表达第三个维度大小则可以表达第四个维度。还是回到电商场景假设除了使用时长、消费金额、最近登录间隔我们还关心每个用户“累计下单次数”。下单次数能反映用户的购买频率是衡量价值的重要指标。这个信息能不能画进图里当然能——把点的大小映射到下单次数上。点越大说明下单次数越多。这样一张散点图里x 轴、y 轴、颜色、大小四个维度同时展示信息量远高于普通散点图。实现方式在plt.scatter()里s参数接收一个数值序列表示每个点的面积单位是点的像素面积不是直径。注意这里有个非常关键的细节Matplotlib 中s接收的是点的面积而不是直径。很多人直接传原始数据比如下单次数 1~50结果画出来点的大小差距不大或者小点几乎看不见就因为默认面积映射是线性的。我的建议是对原始数值做一次归一化或者指数变换让点的大小差异“肉眼可见但不过分夸张”。例如sizes 20 (df[order_count] / df[order_count].max()) * 180 plt.figure(figsize(8, 6)) sc plt.scatter(df[usage_minutes], df[spending], cdf[days_since_login], cmapviridis, ssizes, alpha0.6) plt.colorbar(sc, labelDays Since Last Login) plt.xlabel(Daily Usage Minutes) plt.ylabel(Total Spending) plt.title(Usage vs Spending (Size Order Count)) plt.show()把最大下单次数映射到面积 200最小映射到 20这样点的大小差距明显同时小点也不会小到看不见。如果你希望视觉上“面积”与数值严格成比例可以直接传原始值但实践中由于人眼对面积差异的感知并非线性适当用平方根或对数变换会更友好。4.2 大小映射的避坑指南别把点画成“一坨”大小映射最容易出的问题就是“点太大、重叠太严重”导致整张图变成一团墨什么信息都看不出来。我踩过不少次这个坑总结出三条经验第一体量大的数据务必加透明度。不加透明度时重叠区域后面的点完全被遮挡颜色映射的信息就丢失了。加alpha0.5左右可以让重叠区域颜色加深保留密度信息。第二面积范围要克制。不要让最大点和最小点的面积差超过 50 倍否则大点会“吃掉”小点。优先用s 20 ~ 200这个区间视觉上比较舒服。第三大小映射的数据要理解清楚含义。如果原始数据是订单数 1 到 5那你映射出来的点差异很小如果原始数据从 1 到 5000直接用原始值会让小点消失。所以通常建议先取对数np.log1p再映射能有效压缩极端值的影响。4.3 综合实战把颜色 大小 相关性一次画到位现在我们把所有技术点串起来画一张“综合信息散点图”。这张图包含x 轴使用时长、y 轴消费金额、颜色最近登录间隔、大小下单次数。同时我们还要在图上叠加相关性分析的文本信息。from scipy.stats import pearsonr corr_coef, p_value pearsonr(df[usage_minutes], df[spending]) sizes 20 (df[order_count] / df[order_count].max()) * 180 plt.figure(figsize(10, 7)) sc plt.scatter(df[usage_minutes], df[spending], cdf[days_since_login], cmapviridis, ssizes, alpha0.6) plt.colorbar(sc, labelDays Since Last Login) # 把相关系数直接写在标题里 plt.xlabel(Daily Usage Minutes) plt.ylabel(Total Spending) plt.title(fUsage vs Spending (r {corr_coef:.2f}, p {p_value:.2e})) # 画一条趋势线辅助观察 z np.polyfit(df[usage_minutes], df[spending], 1) p np.poly1d(z) plt.plot(df[usage_minutes], p(df[usage_minutes]), colorred, linestyle--, linewidth2, labelTrend Line) plt.legend() plt.show()这一步把前面所有内容整合起来了。读者能一眼看到总体趋势是正相关颜色深的点活跃度低分布偏右下方点大的用户下单多更多集中在消费金额较高的区域。一张图四个维度的信息信息密度和洞察效率完全不是普通散点图能比的。这里顺便提一下scipy.stats.pearsonr它返回两个值相关系数r和 p 值。p 值用于判断相关性是否统计显著一般小于 0.05 认为显著。我习惯把r和p都放到标题里既方便自己看也方便别人读图时快速掌握统计结论。5. 散点图与相关性的深层绑定Matplotlib 里如何配套分析5.1 散点图矩阵多变量同时看避免“只见一隅”前面我们一直聚焦在两个变量的关系上但现实中数据往往有多个维度。这时候散点图矩阵scatter matrix就很有用了。它可以把数据里所有数值型变量两两组合统统展示在一张图里让你快速浏览哪些变量之间可能存在相关性。Seaborn 提供了pairplot但不依赖 seaborn 的话用 pandas 的scatter_matrix也能做。这里我还是给你一套纯 Matplotlib 的实现思路理解这个原理后你就能灵活控制每张子图了。import matplotlib.pyplot as plt from pandas.plotting import scatter_matrix # 挑选几个数值列 cols [usage_minutes, spending, days_since_login, order_count] scatter_matrix(df[cols], figsize(10, 10), diagonalhist, alpha0.5) plt.show()对角线画的是每个变量的分布直方图非对角线就是两两变量的散点图。用scatter_matrix你可以一次性发现“哪两个变量可能存在强线性关系”。这是相关性分析前的快速侦察手段非常实用。5.2 快速计算相关性矩阵与散点图互相印证散点图是“定性”相关系数是“定量”两者结合才能得到可靠结论。我通常在画图之前先跑一遍相关性矩阵心里有个预期再去画图验证或者先画图再用相关系数确认自己看到的趋势不是错觉。corr_matrix df[cols].corr() print(corr_matrix)这里面usage_minutes与spending的相关系数应该会在 0.7 左右因为数据本身是线性构造的。这时候你再看散点图就会确信“这不是错觉确实有正相关”。反过来如果相关系数只有 0.1但散点图里你总觉得有点规律那很可能不是线性关系而是抛物线、指数或周期性关系——这时候可以试试取对数、平方等变换再看看相关系数是否提高。5.3 Spearman 相关性分析什么时候应该放弃皮尔逊很多教程只讲了皮尔逊相关系数但实际业务数据经常不满足线性假设。这时候Spearman 秩相关系数就有用武之地了。皮尔逊相关系数衡量的是“线性相关”它要求两个变量大致呈直线关系且对异常值非常敏感。而 Spearman 相关系数计算的是“变量的等级相关”——它把原始数据排序再计算排序后的皮尔逊相关。换句话说它衡量的是两个变量的单调关系无论是直线、对数还是指数而且对异常值的鲁棒性更强。用 SciPy 计算 Spearman 很简单from scipy.stats import spearmanr rho, p_val spearmanr(df[usage_minutes], df[spending]) print(fSpearman rho {rho:.3f}, p {p_val:.2e})什么时候用 Spearman我的经验是当散点图呈现明显的非线性单调趋势数据点沿一条曲线而不是直线分布时当数据有明显异常值且你不想让异常值过度影响结论时当变量是序数型数据如满意度评分 1~5时。实战中我常常把皮尔逊和 Spearman 都算出来对比一下。如果两者差异很大说明数据中存在异常值或强非线性关系需要进一步检查散点图。6. 画布、比例、进阶呈现散点图的“面子工程”也不能拉胯6.1 画布大小和分辨率怎么设置才合适散点图的信息密度高画布太小会显得拥挤点与点之间糊成一团。设置画布大小和分辨率是基本功但也经常被忽略。常用的两种方式# 方式一用 plt.figure 设置 plt.figure(figsize(10, 6), dpi120) plt.scatter(x, y) plt.show() # 方式二用 rcParams 设置全局默认 plt.rcParams[figure.figsize] (10, 6) plt.rcParams[figure.dpi] 120我个人的建议在写分析脚本时用plt.figure(figsize(8, 6))起步如果点很多超过 1000 个就加大到(10, 7)或更大。dpi也就是分辨率在屏幕上显示用 100~150 即可如果是要保存成论文配图或报告配图建议 300。6.2 x 轴和 y 轴比例统一什么场景下必须等比例这是一个很有迷惑性的细节微博热搜词里也有“matplotlib 中 x 轴和 y 轴比例统一”这个说法。它的意思是当 x 轴和 y 轴的数据单位相同、且数值范围差异不大时你应该让两个轴的单位长度保持一致否则图形会拉伸变形误导读者。典型场景是地理坐标、形状分析或角度分析。比如你想看两个点在平面上的真实距离关系如果 x 轴拉得比 y 轴宽正方形看起来就是长方形圆看起来就是椭圆。Matplotlib 里设置等比例有两种方式# 方式一axis(equal) plt.axis(equal) # 方式二set_aspect(equal) ax plt.gca() ax.set_aspect(equal)如果两个轴的数值范围差异很大比如 x 范围是 0~1000y 范围是 0~10强制等比例会让散点图被压成一条极扁的带子。这种情况下等比例反而不合适。到底要不要等比例取决于你的分析目标。如果是看相关性趋势不用等比例如果是要比较几何形状和距离关系应该等比例。6.3 柱状图 散点图 置信区间三合一的“论文级”组合图热搜词里还有一个高频需求“柱状图加散点图置信区间图如何绘制”。这其实是很多论文和汇报里的常见组合图用柱状图显示各组的均值用误差线显示标准差或置信区间再把原始数据点叠加上去展示数据分布和样本量。这种图比单纯的柱状图信息量大得多因为它同时展现了“总体统计量”和“个体分布”。下面用代码演示这个组合。我们按会员等级分组每组画一根柱子和误差线再把每个用户的数据点以散点的形式叠加到对应分组上。import numpy as np import matplotlib.pyplot as plt grouped df.groupby(membership_level)[spending] means grouped.mean() sems grouped.sem() # 标准误 counts grouped.count() ci95 sems * 1.96 # 95% 置信区间近似 plt.figure(figsize(8, 6)) x_pos np.arange(len(means)) # 柱状图 置信区间误差线 plt.bar(x_pos, means, yerrci95, width0.5, capsize8, color[#d62728, #1f77b4, #2ca02c], alpha0.6, labelMean ± 95% CI) # 叠加散点图原始数据点注意用 jitter 避免完全重叠 for i, level in enumerate([A, B, C]): data df[df[membership_level] level][spending] jitter np.random.normal(0, 0.08, sizelen(data)) plt.scatter(np.full(len(data), x_pos[i]) jitter, data, s12, colorblack, alpha0.4) plt.xticks(x_pos, [Level A, Level B, Level C]) plt.ylabel(Spending) plt.title(Spending by Membership Level (Bar Scatter CI)) plt.legend() plt.show()柱状图给出各组均值和置信区间散点则展示了原始数据的分布形态和离散程度。注意我给散点加了 jitter随机抖动——如果不加同一组的点会垂直重叠在一条线上看不出密度。抖动的幅度不能太大否则点会“跑”到别的组去这里控制在 0.08 左右刚好让点分布开又不会超出组别范围。这种组合图在学术论文、分析报告里非常常见值得你掌握。7. 常见问题与排查技巧实录7.1 画出来的散点图是空的怎么办最常见的原因有两个一是数据里有大量NaN被 Matplotlib 自动跳过二是 x 和 y 没对应上比如一个是一维数组一个是 pandas DataFrame 的列但索引不一致。排查方法很简单在scatter之前先打印数据形状和缺失值数量print(df[[usage_minutes, spending]].isna().sum()) print(len(x), len(y))如果有 NaN先df df.dropna(subset[usage_minutes, spending])。如果长度对不上检查索引是否对齐必要时用.reset_index(dropTrue)。7.2 颜色映射的图例不出来plt.colorbar()只在scatter调用后且c参数传入数值序列时有效。如果你的scatter在循环里被多次调用并且每次只画了一个子集那colorbar可能只认最后一次调用。还有一种情况是你用了c参数但传的是字符串比如传了颜色名字符串列表Matplotlib 也会出问题。我的建议是连续颜色映射尽量一次scatter画完不要分成多次循环画。如果必须分组画又想统一颜色映射可以用vmin和vmax固定色标范围sc plt.scatter(x, y, cz, cmapviridis, vmin0, vmax30) plt.colorbar(sc)vmin和vmax分别设置颜色条的下限和上限。这样即使多次画颜色范围也是一致的不会出现同一种颜色在不同子图里代表不同数值的情况。7.3 点的大小和期望不一致前面提到过s参数是面积不是直径。如果你希望视觉上的“点的大小”与数值成正比建议对数据做平方根变换s np.sqrt(raw_value) * factor此外如果点的大小差异在图中完全看不出来很可能是原始数据量级太小或太大可以用np.log1p()压缩后映射。7.4 散点图太密集看不出密度当数据量超过几千个点时纯散点图会变成一个实心块看不出密度变化。有两个解决方案一是调小点的大小并增加透明度让重叠区域颜色加深形成伪密度效果。plt.scatter(x, y, s5, alpha0.3)二是改用plt.hexbin()它直接把平面分成六边形格子用颜色深浅表达格子内点的数量适合大数据集。plt.hexbin(x, y, gridsize30, cmapviridis) plt.colorbar(labelCounts)7.5 相关性分析中碰到的“伪相关”陷阱这是一条必须唠叨的经验。散点图里看到明显的正相关不代表两件事存在因果关系。比如用户使用时长和消费金额正相关可能是因为更活跃的用户本来就对产品更满意也可能是因为高消费用户有更多空闲时间。作为数据分析师你能做的是结合业务背景合理假设再通过 A/B 测试或更严格的因果推断方法去验证。散点图只能帮你发现“相关”不能替你判断“因果”。这个边界一定要守住。8. 我的一些实操心得最后分享几点我自己的经验。第一散点图是我做探索性分析时的“第一张图”。任何数据拿过来我都会先画几个关键变量两两之间的散点图然后再决定下一步建模方向。相比于一上来就训练模型、算相关系数散点图能让我快速发现数据里的异常点、聚类结构和非线性关系避免后期走弯路。第二颜色和大小映射不要贪多。一张图塞 5 个以上维度读者很难消化。我的原则是单张图最多 4 个维度x、y、颜色、大小再多就拆成多张子图或者用交互式图表工具。信息密度高是好事但过了头就会变成“读取困难”。第三保存图片时用bbox_inchestight可以裁掉多余白边让图片在报告里更紧凑plt.savefig(scatter_summary.png, dpi300, bbox_inchestight)另外把相关系数直接写进图片或标题里是我觉得最值回票价的一个小习惯。别人看你图的时候一眼就知道统计结论而不需要再去翻代码。散点图就是这么个工具——看似简单但只要你把颜色映射、大小映射和相关性分析用透它就能从“画个点看看”升级成“数据关系侦查仪”。如果你以前只把散点图当草图用希望这篇分享能让你重新认识它。接下来你完全可以拿自己手上的数据试一试从单色散点图开始逐步叠加颜色、大小和相关分析你会慢慢体会到数据里那些“藏起来的关系”是怎么浮出水面的。