直方图
直方图是数据分析中经常出现的概念,又称为质量分布图,由一系列高度不等的细长矩形排列在横轴上来表示数据分布的情况(如下图所示)。一般来说,横轴表示数据的类别,纵轴表示数据的频率。直方图是对于连续变量的概率分布图。
(直方图示例)
要画直方图,第一步需要将所有数据分组,组的数量就是直方图中竖长方形的数量。直方图的分组要求是不重叠且相邻的,并且一般是等距的。
直方图在很多领域都有广泛的应用,除了数据分析。直方图也常常被用来分析图像的特征,甚至可以用来做一些基本的图像分类。因为图像是由不同的像素点组成的,每个像素点的取值就是颜色值 0~255, 通过将图像的所有像素点都以直方图的形式展现,比如按照像素值分成 255 个组,就能根据直方图的形状来推测图像大概是什么。
画直方图
在 matplotlib 中,可以通过 plt.hist 函数画直方图。下面生成一个符合正态分布的数组,然后将其用直方图的形式表现出来,通过这个例子来学习直方图的使用方法。
首先导入必要的三大件:
import matplotlib.pyplot as plt
import pandas as pd
import numpy as np
然后初始化 NumPy 的随机数,并生成以 5 为中心,宽度为正负 10 的 1000 个符合正态分布的随机数,作为画图的数据源。
np.random.seed(100)
hints = np.random.normal(5, 10, 1000)
这里由于数字太多,就不打印了,直接逐一执行上述两个 Cell 即可。
NumPy 的部分学习了 ndarray 支持常见的统计方法,所以通过查看数组的均值和标准差来看这些数据是否符合设定的正态分布。
print(hints.mean(), hints.std())
输出如下:
4.832278426560908 10.458427194167
可以看到,均值在设定的中心(5)附近,标准差约等于设定的宽度(10,所以数据基本没有问题。
接下来就是关键部分,画直方图,代码如下:
# 创建画布
figure1 = plt.figure(figsize = (10, 5))
figure1.add_subplot(1,1,1)
# 设置轴的属性,轴属性相关的函数都支持fontsize 参数来设置字号
# 这里都设置为 14
plt.xlabel("值",fontsize = 14)
plt.ylabel("频率", fontsize = 14)
plt.xticks(fontsize=14)
plt.yticks(fontsize=14)
plt.title("正态分布的直方图", fontsize=14)
# 设置画布基本属性:1.显示中文 2.显示负号
plt.rcParams["font.sans-serif"] = "SimHei"
plt.rcParams["axes.unicode_minus"] = False
# 画直方图,分10组,用蓝色来画
plt.hist(hints, bins=10, color=[0,0,1])
plt.show()
输出如下:
从上图中不难发现,整个数据看起来就是正态分布的形状,随机的极值点到了-30 到 40。从直方图的台阶上看,相对于中心对称区域的高度都差不多(频率基本一致)。所以从直方图的几何特征上能够真实地反映出数据的概率分布特性。
plt.hist 函数的 bins 参数非常关键,bins 的值代表了要把数据分成几组,也就代表了图上一种会有多少个长方形。bins 越大,体现得就越精确,但相应的分布特征可能就越不明显。比如把上述代码的 bins 改成 50 。
...
plt.hist(hints, bins=50, color=[0,0,1])
...
输出如下:
可以看到,分组变多后,直方图展示了更多关于原始数据的信息,但是也出现了较多的锯齿,但整体仍然是正态分布的形状。
直方图另外一个非常有用的参数就是 edgecolor,即每个长方形的边框颜色。上面的直方图中间区域是一片蓝,有时候并不利于做进一步的数据分析。这个时候设置 plt.hist 函数的 edgecolor 属性,来让每个长方形都有一个边框颜色。
修改上述代码如下:
...
# 设置长方形的边框颜色是黑色
plt.hist(hints, bins=50, color=[0,0,1], edgecolor=[0,0,0])
...
输出如下:
可以看到,设置边框颜色之后,整个直方图看着更清晰了。
展示多个直方图
在很多分析场景,同时分析 23 个数据源的频率分布,这背后的技术就是把 23 个直方图在同一个坐标系中展示。
为了模拟多个直方图,首先生成另外两个数据源。
hints_1 = np.random.normal(0, 8, 500)
hints_2 = np.random.normal(15, 10, 800)
接下来三次调用 plt.hist 即可,为了区分,使用三种不同的颜色来画。
# 创建画布
figure1 = plt.figure(figsize = (10, 5))
figure1.add_subplot(1,1,1)
# 设置轴的属性,轴属性相关的函数都支持fontsize 参数来设置字号
# 这里都设置为 14
plt.xlabel("值",fontsize = 14)
plt.ylabel("频率", fontsize = 14)
plt.xticks(fontsize=14)
plt.yticks(fontsize=14)
plt.title("正态分布的直方图", fontsize=14)
# 设置画布基本属性:1.显示中文 2.显示负号
plt.rcParams["font.sans-serif"] = "SimHei"
plt.rcParams["axes.unicode_minus"] = False
# 第一个用蓝色
plt.hist(hints, bins=50, color=[0,0,1], edgecolor=[0,0,0])
# 第二个用绿色
plt.hist(hints_1, bins=50, color=[0,1,0], edgecolor=[0,0,0])
# 第三个用红色
plt.hist(hints_2, bins=50, color=[1,0,0], edgecolor=[0,0,0])
plt.show()
输出如下:
图像虽然画出来了,但是有重叠的地方因为遮挡的关系,绿色和蓝色的分布看不见了,这个时候使用 plt.hist 函数的 alpha 属性,来让三个图都有一定的透明度,解决这个问题。
修改相关代码如下:
...
# 第一个用蓝色
plt.hist(hints, bins=50, color=[0,0,1], edgecolor=[0,0,0], alpha = 0.5)
# 第二个用绿色
plt.hist(hints_1, bins=50, color=[0,1,0], edgecolor=[0,0,0], alpha = 0.5)
# 第三个用红色
plt.hist(hints_2, bins=50, color=[1,0,0], edgecolor=[0,0,0], alpha = 0.5)
...
输出如下:
增加透明度之后,即便有所遮挡也可以看到了。不过最终的颜色会比设置的蓝、绿、红要多一些,这背后的原因就是因为设置的透明度,不同的颜色区域叠在一块的时候不会完全遮挡,而是会用重叠区域的颜色混合成一种新的颜色。所以看上去好像颜色变多了。
条形图
区别条形图和直方图
条形图,又称为柱状图,有的地方也把横版的称为条形图,竖版的称为柱状图,这里统称条形图。
条形图和直方图类似,也是通过一个个细长的长方形来表示数据的频率,所以很容易搞混这两个概念。
从下面这个表格来理解条形图和直方图的区别。
很多情况下,条形图往往能表示比直方图更多维度的数据。下面是一个常见的条形图的例子。
在上图的例子中,可以看到有两个维度的类别,首先是男和女,然后是这两个类别分别在四个年龄段的分布情况。条形图一定程度上可以表示类似这样离散的三维数据,所以在部分场景,尤其是类别不多的场景下,用条形图来表示就显得非常的直观。像这样的类别称之为二维的类别。
绘制条形图
通过 plt.bar 函数可以实现条形图的绘制。下面通过一个实际的案例来学习条形图的绘制,某学校的初中二年级举行了期中考试。五个班的平均分如下:
现在通过条形图来比对各个班的平均分的分布情况。
第一步,首先将数据导入到 notebook 中,以列的维度。
math_scores = np.array([71,65,70,96,64])
chinese_scores = np.array([84,75,68,83,57])
english_scores = np.array([55,78,76,91,64])
然后就用 plt 的常规模板进行画图。这里的 x 轴的数据源直接使用五个类别的值即可,即是一班、二班等。代码如下:
# 创建画布
figure2 = plt.figure(figsize = (10, 5))
figure2.add_subplot(1,1,1)
# 设置轴的属性
plt.xlabel("",fontsize = 14)
plt.ylabel("平均分", fontsize = 14)
plt.xticks(fontsize=14)
plt.yticks(fontsize=14)
plt.title("期中成绩条形图", fontsize=14)
# 设置画布基本属性:1.显示中文 2.显示负号
plt.rcParams["font.sans-serif"] = "SimHei"
plt.rcParams["axes.unicode_minus"] = False
category = ["一班", "二班", "三班", "四班", "五班"]
# 语文成绩用蓝色
plt.bar(category, chinese_scores, color=[0,0,1])
# 数学成绩,用绿色
plt.bar(category, math_scores, color=[0,1,0])
# 英语成绩,用红色
plt.bar(category, english_scores, color=[1,0,0])
plt.show()
输出如下:
条形图已经画出来了,但是三门课的形状都画到一起了,图像的堆叠次序是根据画图的顺序来决定的。所以当英语的成绩比前面高的时候,红色的条形就遮住了前面的。
这种情况需要根据之前对条形图的定义,将三个图形并列排放。具体的实现方式分为两步:
- x 轴的数据源切换为数字,这样才能有宽度的概念;
- 在绘制三个 bar 的时候,分别指定它们的相对位置与宽度。 修改后完成的代码如下:
figure2 = plt.figure(figsize = (10, 5))
figure2.add_subplot(1,1,1)
plt.xlabel("",fontsize = 14)
plt.ylabel("平均分", fontsize = 14)
plt.xticks(fontsize=14)
plt.yticks(fontsize=14)
plt.title("期中成绩条形图", fontsize=14)
plt.rcParams["font.sans-serif"] = "SimHei"
plt.rcParams["axes.unicode_minus"] = False
category = ["一班", "二班", "三班", "四班", "五班"]
# 新增代码,使用 index 数组来代替类别
index_category = np.arange(len(category))
# 新增代码,指定每条条形的宽度
bar_width = 0.25
# 指定宽度为 bar_width, 并且绘制在 index - bar_width 的区域,也就是每组图的左边
plt.bar(index_category - bar_width, chinese_scores, width=bar_width, color=[0,0,1])
# 指定宽度为 bar_width, 并且绘制在 index 的区域,也就是每组图的中间
plt.bar(index_category, math_scores, width=bar_width, color=[0,1,0])
# 指定宽度为 bar_width, 并且绘制在 index + bar_width 的区域,也就是每组图的右边
plt.bar(index_category + bar_width, english_scores, width=bar_width, color=[1,0,0])
plt.show()
输出如下:
这样,就能够比较明确地反映出学生期中考试的考试情况了,明显四班(序号 3)的成绩优于其他班,并且英语的考试成绩整体优于另外两科。
水平条形图
水平条形图和普通的条形图逻辑都是类似的,只是绘制的方向变成了自左向右,绘制的函数名称为 plt.barh。就相当于把普通条形图的坐标轴的 x 轴和 y 轴调换一下。不过有区别的是,水平条形图一般不用于比较二维的类别,因为没有普通的条形图清晰。水平条形图多用于一维的类别。
比如下面的例子中,单独将各个班的数学成绩用水平条形图来展示:
figure3 = plt.figure(figsize = (10, 5))
figure3.add_subplot(1,1,1)
plt.xlabel("",fontsize = 14)
plt.ylabel("平均分", fontsize = 14)
plt.xticks(fontsize=14)
plt.yticks(fontsize=14)
plt.title("数学成绩条形图", fontsize=14)
plt.rcParams["font.sans-serif"] = "SimHei"
plt.rcParams["axes.unicode_minus"] = False
category = ["一班", "二班", "三班", "四班", "五班"]
# 绘制横版条形图
plt.barh(category, math_scores, color=[1,0,1])
plt.show()
输出如下:
堆叠条形图
堆叠条形图和之前画的条形图,多个条叠在一起的图不同。堆叠条形图也是解决二维类别的数据,和普通条形图核心的区别是不同子类的图形是叠在一起的(上下叠在一起,不是重叠)。堆叠条形图的好处是可以清晰地反应每个子类数据的占比。
绘制堆叠条形图的方式和普通条形图一样,也是通过 plt.bar 函数,区别只是堆叠条形图在绘制上叠的数据时,需要额外指定 bottom 参数。通过一个例子来学习一下。
某公司销售部门统计了最近四周的签单数据,目前在职的销售一共两名,销售 A 的签单数为 [10,23, 5, 11] ,销售 B 的签单数为 [3,12,6, 5]。 通过堆叠条形图来展示两个销售对于部门总销量的占比,代码如下所示:
figure4 = plt.figure(figsize = (10, 5))
figure4.add_subplot(1,1,1)
plt.xlabel("",fontsize = 14)
plt.ylabel("签单量", fontsize = 14)
plt.xticks(fontsize=14)
plt.yticks(fontsize=14)
plt.title("签单堆叠条形图", fontsize=14)
plt.rcParams["font.sans-serif"] = "SimHei"
plt.rcParams["axes.unicode_minus"] = False
# 横轴数据
category = ["第一周", "第二周", "第三周", "第四周"]
# 两个销售各周的销量
sales_a = [10,23, 5, 11]
sales_b = [3,12,6, 5]
# 首先画销售A, 用紫色
plt.bar(category, sales_a, color=[1,0,1])
# 然后用蓝色画销售B,并指定 bottom为销售a的数据
plt.bar(category, sales_b, color=[0,0,1], bottom=sales_a)
plt.show()
输出如下:
可以看到销售 A(紫色)和销售 B(蓝色)的数据在同一个条形上展示了,事实上撇开两个销售的占比不谈,单看条形图的高度反映的就是该部门的总签单量,然后不同的颜色代表两个销售各自对总签单量的共享。所以从上图中不难看出,销售 A 的贡献明显更大一些。
饼图
本节最后一节是饼图,饼图虽然在表示内容的维度与丰富度上相比直方图和条形图会差一些。但是在反映数据占比的可理解性上是最好的,这也是为什么在很多数据分析的报告中做占比分析最常见的就是饼图。
通过 plt.pie 函数可以绘制饼图。pie 函数由以下几个关键参数实现功能。
- x:饼图每一块的占比列表,列表有几个元素就代表饼图分几块。
- explode:凸出显示,也是一个列表,和 x 一一对应,代表具体某一块是否要凸出显示。
- colors:列表,和 x 一一对应,代表每一块的颜色。
- labels:标签文本列表,和 x 一一对应,代表每一块的标题。
- autopct:代表百分比文本的格式。
- startangle: 默认饼图是从角度为 0 的位置逆时针开始画,这里可以指定初始角度。
- labeldistance:文本标签距离饼图的距离。
- pctdistance:百分比标签距离饼图中心的距离。 下面通过一个案例来学习饼图的绘制方式。
假设对一个年级的学生,统计历史上得过三好学生称号的情况,完全没得过的占比为 40%, 得过一次的占比为 30%, 二次的为 25%, 三次以上的为 5%。现在用饼图表示这个数据,并将得过三次以上的凸出显示。
代码如下:
# 创建画布
figure5 = plt.figure(figsize = (10, 5))
figure5.add_subplot(1,1,1)
plt.rcParams["font.sans-serif"] = "SimHei"
plt.rcParams["axes.unicode_minus"] = False
# 设置饼图数据源
category = ["没有得过", "得过一次", "得过二次", "得过三次以上"]
size = [40, 30, 25, 5]
# 用四种不同的颜色绘制不同的部分
color = ["r", "g", "b", "c"]
explode = [0,0,0,0.1]
# 绘制饼图
plt.pie(size,explode=explode,colors=color,labels=category,labeldistance= 1.1, autopct="%1.1f%%", startangle=90,pctdistance=0.6)
plt.show()
执行之后输出如下:
脊柱
脊柱可以理解为图像的坐标轴。之前的课程里简单学习过设置范围和名称,现在系统地学习一下坐标轴的操作。
之后导入三板斧:
import matplotlib.pyplot as plt
import pandas as pd
import numpy as np
设置坐标轴的刻度
关于坐标轴的显示,之前主要学习了使用 plt.xlabel 来设置坐标轴的标题,用 xlim 来设置坐标轴的范围(y 轴同理)。但如果要把图像画得更加专业,还可以通过 xticks 来设置坐标轴的刻度。
xticks 除了常见的 fontsize 、color 等属性之外,还支持传入一个刻度的数组以及对应的标签信息,注意区分刻度和范围。范围决定的是轴上显示的数据范围,一般是一个最大值和一个最小值。而刻度决定的是在这样一个范围中哪些地方要显示刻度,以及显示的刻度文本是什么。一般来说,刻度的信息可以用 np.arange 函数方便地生成,因为它同时制定了开始、结束和步长。
举个例子来说,设置 y 轴的范围是 10 到 20,然后指定 15 到 20 的区域,每间隔 1 就展示一个刻度。代码如下:
x_ranges = np.arange(0, 10)
figure1 = plt.figure(figsize=(10, 10)) # 改动点
ax1 = figure1.add_subplot(1,1,1)
plt.title("y = 2x +1")
plt.xlabel("X")
plt.xlim([0, 20])
plt.ylabel("Y")
plt.ylim([0, 20])
plt.yticks(np.arange(15, 20, 1))
plt.plot(x_ranges, x_ranges * 2 + 1)
plt.show()
输出如下:
下面以一个更实际一点的例子来说明设置刻度的作用。以期中成绩直方图为例,画图的代码如下:
math_scores = np.array([71,65,70,96,64])
chinese_scores = np.array([84,75,68,83,57])
english_scores = np.array([55,78,76,91,64])
figure2 = plt.figure(figsize = (10, 5))
figure2.add_subplot(1,1,1)
plt.xlabel("",fontsize = 14)
plt.ylabel("平均分", fontsize = 14)
plt.xticks(fontsize=14)
plt.yticks(fontsize=14)
plt.title("期中成绩条形图", fontsize=14)
plt.rcParams["font.sans-serif"] = "SimHei"
plt.rcParams["axes.unicode_minus"] = False
category = ["一班", "二班", "三班", "四班", "五班"]
index_category = np.arange(len(category))
bar_width = 0.25
plt.bar(index_category - bar_width, chinese_scores, width=bar_width, color=[0,0,1])
plt.bar(index_category, math_scores, width=bar_width, color=[0,1,0])
plt.bar(index_category + bar_width, english_scores, width=bar_width, color=[1,0,0])
plt.show()
输出如下:
先看横轴,因为传入 index, 所以现在刻度显示的是数字。光从数字上看很难看出不同的直方图组率属于哪个班级。另一方面,学生最后的成绩并不是看百分制,而是分为 ABCD 四档。总结下来,任务就是:
- 横轴刻度用班级名称来显示;
- 纵轴刻度用 ABCD 四挡来显示。 刚才已经用 plt.yticks 函数来指定了刻度的值和范围,这次加上标签,标签一般是一个列表,列表的元素数量和刻度数需要一致。修改上述代码,如下所示:
...
category = ["一班", "二班", "三班", "四班", "五班"]
# 新增代码
# 横轴的刻度就是 0 到 category 列表的长度,步长为默认值 1,对应的标签就是 category 列表
plt.xticks(np.arange(len(category)),category)
# 新增代码
# 纵轴的刻度显示四个,所以刻度范围是 0 到100,分为四个等级,所以步长是 25
# 对应的标签列表,从低到高的对应,就是 D 到 A
plt.yticks(np.arange(0, 100,25), ["D","C", "B", "A"])
index_category = np.arange(len(category))
...
输出如下:
可以看到,通过对轴的刻度进行定制,进一步提升了图表的专业性。
轴的显示或隐藏
在之前画的图表中,虽然关注点主要放在 x 轴和 y 轴。但每一张 matplotlib 的图像都有上、下、左、右四根轴。拿上面的图来说:
每一根轴都可以设置隐藏或显示,默认全部显示。设置轴显示或者隐藏的主要通过之前介绍的子图对象(Axes)。
仍然使用期中成绩的图表来演示如何对轴进行隐藏或者显示,为了减少每次修改代码的成本,先把画图代码单独抽成一个函数。如下所示:
def draw_midterm_scores():
math_scores = np.array([71,65,70,96,64])
chinese_scores = np.array([84,75,68,83,57])
english_scores = np.array([55,78,76,91,64])
plt.xlabel("",fontsize = 14)
plt.ylabel("平均分", fontsize = 14)
plt.xticks(fontsize=14)
plt.yticks(fontsize=14)
plt.title("期中成绩条形图", fontsize=14)
plt.rcParams["font.sans-serif"] = "SimHei"
plt.rcParams["axes.unicode_minus"] = False
category = ["一班", "二班", "三班", "四班", "五班"]
plt.xticks(np.arange(len(category)),category)
plt.yticks(np.arange(0, 100,25), ["D","C", "B", "A"])
index_category = np.arange(len(category))
bar_width = 0.25
plt.bar(index_category - bar_width, chinese_scores, width=bar_width, color=[0,0,1])
plt.bar(index_category, math_scores, width=bar_width, color=[0,1,0])
plt.bar(index_category + bar_width, english_scores, width=bar_width, color=[1,0,0])
plt.show()
这样之后创建完坐标轴,只需要调用 draw_midterm_scores 即可实现绘制期中成绩条形图。
现在尝试隐藏上方和右边的轴。代码如下所示:
figure3 = plt.figure(figsize = (10, 5))
ax1 = figure3.add_subplot(1,1,1)
# 通过子图对象的spines 字典,并传入top/right/left/bottom 来获取上轴、右轴、左轴和右轴
# 之后通过set_visible 函数来决定是否显示
ax1.spines["top"].set_visible(False)
ax1.spines["right"].set_visible(False)
ax1.spines["bottom"].set_visible(True)
ax1.spines["left"].set_visible(True)
draw_midterm_scores()
输出如下:
移动坐标轴
在上述代码中,类似 ax1.spines["left"] 返回来的就是 Spine 对象,也就是脊柱。除了设置是否显示,另一个重要的功能就是轴的移动。用 set_position 函数来移动坐标轴。
set_position 接收一个列表作为参数,列表的第一个元素是位置类型,第二个元素是具体的位置数字。位置类型的取值有:
-
axes,代表位置移动是基于相对于坐标轴的倍数;
-
data,代表位置移动是基于轴上数据点的值。 上面的说法可能比较抽象,下面举几个具体的例子:
-
set_position(["axes", 0.5]), 类型为 axes,位置数字为 0.5 , 代表将当前的轴移动到坐标轴一半的位置。
-
set_position(["axes", -0.5]),代表把轴以相反方向移动当前轴一半的距离
-
set_position(["data", 10]),代表把轴移动到轴上 10 这个数据点的位置。 这次以 sin 函数为例来学习坐标轴位置设置的方法。首先第一步,基于之前实现的 sin 函数绘制,将 sin 函数封装为一个函数:
def draw_sin():
x_ranges = np.arange(-20, 20, 0.2)
plt.title("y = sin(x)")
plt.xlabel("X")
plt.xlim([-20, 20])
plt.ylabel("Y")
plt.ylim([-2.5, 2.5])
plt.plot(x_ranges,np.sin(x_ranges) )
plt.show()
之后,先看看默认的轴的位置。代码如下:
figure3 = plt.figure(figsize = (10, 5))
ax1 = figure3.add_subplot(1,1,1)
draw_sin()
输出如下:
首先,将顶部和右边的轴隐藏,然后将 y 轴移动到 -5 这个数据点的位置。代码如下:
figure3 = plt.figure(figsize = (10, 5))
ax1 = figure3.add_subplot(1,1,1)
ax1.spines["top"].set_visible(False)
ax1.spines["right"].set_visible(False)
# 位置类型为 data,数据点为 -5
ax1.spines["left"].set_position(["data",-5])
draw_sin()
输出如下:
可以看到,成功将 y 轴,也就是 left 对应的左轴,移动到了数据点 -5 的位置。
当然,改变坐标轴的位置也不是随便改,一般都有具体的意义,比如用更数学的方式来展示函数的曲线,那将 x 轴和 y 轴都移动到 0 点的位置。因为数据是对称的,所以用 axes 的位置类型,移动坐标轴的一半也可以实现类似的效果。代码如下所示:
figure3 = plt.figure(figsize = (10, 5))
ax1 = figure3.add_subplot(1,1,1)
ax1.spines["top"].set_visible(False)
ax1.spines["right"].set_visible(False)
# x 轴以数据类型移动到数据点 0
ax1.spines["left"].set_position(["data",0])
# 有轴以axes 类型往上移动坐标轴 50%的长度(效果与移动到数据点0等价)
ax1.spines["bottom"].set_position(["axes",0.5])
draw_sin()
输出如下:
可以看到,通过移动坐标轴,根据场景的不同画出更加易于理解的图像。
坐标轴的常见样式
之前学习过,通过设置 plot 函数的参数,可以设置线条的颜色、线型和线宽。同样,matplotlib 的坐标轴也支持设置这三个属性。分别通过如下函数来调整。
- set_linewidth:设置轴的粗细。
- set_linestyle:设置轴的样式,具体样式的语法可以参考之前折线图中的 fmt 语法中,线型部分的取值。
- set_color:设置轴的颜色。 这三个函数使用相对简单,直接上例子,对上面例子中的 y 轴进行一些样式的设置。代码如下:
figure3 = plt.figure(figsize = (10, 5))
ax1 = figure3.add_subplot(1,1,1)
ax1.spines["top"].set_visible(False)
ax1.spines["right"].set_visible(False)
ax1.spines["left"].set_position(["data",0])
ax1.spines["bottom"].set_position(["axes",0.5])
# 设置左轴,也就是 y 轴,宽度为3
# 线型为虚线点间隔
# 颜色为红色
ax1.spines["left"].set_linewidth(3)
ax1.spines["left"].set_linestyle("-.")
ax1.spines["left"].set_color("r")
draw_sin()
输出如下:
图例
在完成脊柱的相关设置后,进入图像体系知识的最后一个部分:图例与注解。现在虽然图表整体已经专业了很多。但在有的场景下仍然缺乏足够的表现能力,简单地说就是没那么易懂。
举一个例子,假设在图像中同时画出 sin 和 cos 的函数曲线。首先,扩展之前的 draw_sin 函数,扩展为 draw_sin_cos。
def draw_sin_cos():
x_ranges = np.arange(-20, 20, 0.2)
plt.title("Sin & Cos")
plt.xlabel("X")
plt.xlim([-20, 20])
plt.ylabel("Y")
plt.ylim([-2.5, 2.5])
plt.plot(x_ranges,np.sin(x_ranges))
# 为了区分两个曲线,cos的使用红色加虚线点样式来画
plt.plot(x_ranges, np.cos(x_ranges), "-.r")
plt.show()
然后基于刚才设计的坐标轴,画出图形:
figure3 = plt.figure(figsize = (10, 5))
ax1 = figure3.add_subplot(1,1,1)
ax1.spines["top"].set_visible(False)
ax1.spines["right"].set_visible(False)
ax1.spines["left"].set_position(["data",0])
ax1.spines["bottom"].set_position(["axes",0.5])
draw_sin_cos()
输出如下:
图像虽然看起来没什么问题,但如果把这张图像放在数据分析报告中,看报告的人很难一眼就看出哪条是 sin,哪条是 cos,有一定的理解成本。
下面就来学习如何通过图例和注解让图像更容易理解。
添加图例
在 matplotlib 中,图例代表在图像中的一个小区域,用来专门说明图像中曲线的名称。添加图例一般有两个步骤:
- 在 plot 的时候,指定曲线的 label,通过 plot 函数的label 参数指定;
- 在 plot 结束,show 之前,调用 plt.legend ,通知 matplotlib 需要展示图例。 现在就来实操一下。
首先修改 draw_sin_cos 函数,在 plot 的时候传入函数的名称,以及添加调用 legend 函数,代码如下:
def draw_sin_cos():
x_ranges = np.arange(-20, 20, 0.2)
plt.title("Sin & Cos")
plt.xlabel("X")
plt.xlim([-20, 20])
plt.ylabel("Y")
plt.ylim([-2.5, 2.5])
#添加 label 参数
plt.plot(x_ranges,np.sin(x_ranges), label = "y = sin(x)")
plt.plot(x_ranges, np.cos(x_ranges), "-.r", label = "y = cos(x)")
#调用 legend 函数
plt.legend()
plt.show()
执行代码更新函数,并再次执行调用该函数的绘图代码。输出如下:
可以看到,在右上角新增了一个说明区,并展示了曲线样式和对应的label 的关系。这样就能一目了然地知道到底哪条线是 sin 函数,哪条线是 cos 函数。
设置图例的位置
legend 函数支持 loc 参数,用于设定图例展示的位置。有以下几种取值:
比如将图例改到左上角,修改 draw_sin_cos 函数中,legend 函数的调用:
...
plt.plot(x_ranges, np.cos(x_ranges), "-.r", label = "y = cos(x)")
plt.legend(loc="upper left")
plt.show()
重新执行关联的 Cell,输出如下:
这里也可以自行替换想放的位置来做实验。
注解
注解,顾名思义就是用来为图像标注解释的工具。就是在绘制的图表上的部分区域添加文字,来帮助浏览者来快速理解图像的含义。
简易注解
axes 对象,也就是之前说的子图对象,提供了 annotate 方法用来在图像中添加注解。最简易的用法就是指定一个数据点,以及对应的文本,然后 matplotlib 就会将文本绘制在指定的数据点附近的合适的位置。
比如在之前的 Sin&Cos 的图中更清晰的表示哪条是 sin, 哪条是 cos。annotate 函数接受两个参数,第一个是要标注的字符串,第二个是点的位置。只需要选择合适的数据点添加上文本。
修改 draw_sin_cos 函数,代码如下:
...
plt.plot(x_ranges,np.sin(x_ranges), label = "y = sin(x)")
plt.plot(x_ranges, np.cos(x_ranges), "-.r", label = "y = cos(x)")
#【新增代码】 在 0, cos(0) 的位置添加 cos 的文本注解
plt.annotate("y = cos(x)", [0, np.cos(0)])
#【新增代码】在 -1.5, sin(-1.5) 的位置,添加 sin 的注解
plt.annotate("y = sin(x)", [-1.5, np.sin(-1.5)])
plt.legend(loc="upper left")
...
添加代码后,重新执行相关的 Cell, 结果如下:
可以看到,注解已经被成功添加到了曲线指定的数据点上。使用 plt.annotate 函数,可以在曲线的任意位置添加文本。哪怕不附着在曲线上。
比如,在 (5,2)的位置添加文本"Hello",在之前的 annotate 函数调用下添加
plt.annotate("Hello", [5,2])
执行相关 Cell 后输出:
可以看到,Hello 已经被添加到了指定位置。
自定义注解
上图中注解虽然已经添加上去了,但缺点也很明显,文字和曲线有重叠导致看得有点不太清楚。plt.annotate 函数支持更加高级的用法,简单地来说就是三个参数。
- xy:指定数据点的位置,是一个包含两个元素的列表,分别代表 x 和 y。
- xytext: 注解文本的位置,是一个包含两个元素的列表,分别代表 x 和 y。
- arrowprops:指向箭头的属性,是一个字典,用来说明箭头的样式。 通过这三个属性设置文本稍微离开数据点,然后通过一个箭头指向数据点,避免文本和曲线重合的问题。对于上面的图像来说,让 cos 的文本偏上一些,sin 文本偏下一些。
修改 draw_sin_cos 的代码如下所示:
def draw_sin_cos():
x_ranges = np.arange(-20, 20, 0.2)
plt.title("Sin & Cos")
plt.xlabel("X")
plt.xlim([-20, 20])
plt.ylabel("Y")
plt.ylim([-2.5, 2.5])
plt.plot(x_ranges,np.sin(x_ranges), label = "y = sin(x)")
plt.plot(x_ranges, np.cos(x_ranges), "-.r", label = "y = cos(x)")
# 【修改代码】设置 文本在数据点的右上角一些,并设置箭头为红色
plt.annotate("y = cos(x)", xy = [0, np.cos(0)], xytext = [2, np.cos(0) + 0.5],arrowprops={"facecolor":"red", "shrink":0.05}, fontsize=13)
# 【修改代码】设置 文本在数据点的左下角一些,并设置箭头为青色
plt.annotate("y = sin(x)", xy=[-1.5, np.sin(-1.5)], xytext=[-6.5, np.sin(-1.5) - 1], arrowprops={"facecolor":"cyan", "shrink":0.05},fontsize=13)
plt.legend(loc="upper left")
plt.show()
执行该 Cell,以及下方的调用 draw_sin_cos 的 Cell 后,输出如下:
可以看到现在文本已经不会和曲线重叠了,并且会将箭头指向指定的数据点。
通过搭配使用图例与注解,让图表更有表现力,也更容易理解。
小结
本节内容总结如下:
-
脊柱 通过 plt.xticks/plt.yticks 设置图像的刻度以及刻度的标签。
-
通过子图对象可以获取到 left/right/bottom/top 四个轴的对象,通过轴对象的 set_visible 可以控制显示或者隐藏某个轴。
-
通过子图对象的 set_position 移动坐标轴。
-
通过子图对象的 set_linewidth 设置轴的线宽, set_linestyle 设置线型,以及set_color 设置轴的颜色。
-
图例 通过设置 plot 函数的label参数指定轴的标签。
-
通过 plt.legend 函数通知matplotlib绘制图例,并通过 loc 参数设置图例的位置。
-
注解 通过 plt.annotate 函数可以在坐标系的任意位置添加文本。
-
可以通过 xy 参数 xytext 参数以及 arrowprops参数来实现文本适当偏移数据点,并且添加箭头指向数据点。
版本差异(数据科学栈 → 当前版本)
| 库 | 本文编写时 | 当前稳定版 | 升级要点 |
|---|---|---|---|
| Python | 3.8-3.12 | 3.14 | 3.12+ 起性能显著提升;3.14 PEP 649/750 |
| NumPy | 1.x/2.0 | 2.3.x | np.float_ 等别名移除;NEP 50 类型提升 |
| Pandas | 1.x/2.x | 3.0.x | Copy-on-Write 默认开启;inplace 移除;字符串 dtype 变化 |
| Matplotlib | 3.x | 3.x 稳定版 | API 兼容,样式更新 |
| Seaborn | 0.12/0.13 | 0.13.x | API 稳定 |
| scikit-learn | 1.x | 1.7.x | API 稳定,新算法持续加入 |
本文讲解的数据分析流程(读取→清洗→分析→可视化)与核心 API 在最新版本中成立;升级时重点关注 Pandas 3.0 的 Copy-on-Write 与 NumPy 2.x 的类型变化。