seaborn 接受的数据结构#
作为一个数据可视化库,seaborn 要求你提供数据。本章解释了完成该任务的各种方法。Seaborn 支持几种不同的数据集格式,大多数函数接受来自 pandas 或 numpy 库的对象,以及内置的 Python 类型,如列表和字典。理解这些不同选项的使用模式将帮助你快速为几乎任何数据集创建有用的可视化。
备注
截至当前编写时(v0.13.0),此处涵盖的选项的全部范围被seaborn中的大多数函数支持,但并非所有函数都支持。具体来说,一些较旧的函数(例如,lmplot() 和 regplot())在其接受的选项上更为有限。
长格式 vs. 宽格式数据#
seaborn中的大多数绘图函数都是面向*数据向量*的。在绘制``x``与``y``的关系时,每个变量都应是一个向量。Seaborn接受以某种表格形式组织的多于一个向量的数据*集*。数据表有“长格式”和“宽格式”两种基本区别,seaborn会以不同的方式处理它们。
长格式数据#
长格式数据表具有以下特征:
每个变量是一列
每个观测值是一行
作为一个简单的例子,考虑“航班”数据集,该数据集记录了从1949年到1960年每个月飞行的航空公司乘客数量。该数据集有三个变量(年份、月份*和*乘客数量):
flights = sns.load_dataset("flights")
flights.head()
| year | month | passengers | |
|---|---|---|---|
| 0 | 1949 | Jan | 112 |
| 1 | 1949 | Feb | 118 |
| 2 | 1949 | Mar | 132 |
| 3 | 1949 | Apr | 129 |
| 4 | 1949 | May | 121 |
对于长格式数据,表格中的列通过显式地将它们分配给其中一个变量,在图中赋予角色。例如,制作每年每月乘客数量的图表如下所示:
sns.relplot(data=flights, x="year", y="passengers", hue="month", kind="line")
长格式数据的优点是它非常适合这种图形的显式规范。它可以容纳任意复杂度的数据集,只要变量和观察结果可以清晰定义。但这种格式需要一些时间来适应,因为它通常不是人们脑海中的数据模型。
宽格式数据#
对于简单的数据集,通常更直观地以在电子表格中查看数据的方式来思考数据,其中列和行包含不同变量的 层次 。例如,我们可以通过“透视”将航班数据集转换为宽表组织,以便每列包含各月份在多年期间的时间序列:
flights_wide = flights.pivot(index="year", columns="month", values="passengers")
flights_wide.head()
| month | Jan | Feb | Mar | Apr | May | Jun | Jul | Aug | Sep | Oct | Nov | Dec |
|---|---|---|---|---|---|---|---|---|---|---|---|---|
| year | ||||||||||||
| 1949 | 112 | 118 | 132 | 129 | 121 | 135 | 148 | 148 | 136 | 119 | 104 | 118 |
| 1950 | 115 | 126 | 141 | 135 | 125 | 149 | 170 | 170 | 158 | 133 | 114 | 140 |
| 1951 | 145 | 150 | 178 | 163 | 172 | 178 | 199 | 199 | 184 | 162 | 146 | 166 |
| 1952 | 171 | 180 | 193 | 181 | 183 | 218 | 230 | 242 | 209 | 191 | 172 | 194 |
| 1953 | 196 | 196 | 236 | 235 | 229 | 243 | 264 | 272 | 237 | 211 | 180 | 201 |
这里我们有相同的三个变量,但它们的组织方式不同。此数据集中的变量与表格的 维度 相关联,而不是与命名字段相关联。每个观测值由表格中单元格的值以及该单元格相对于行和列索引的坐标定义。
对于长格式数据,我们可以通过名称访问数据集中的变量。对于宽格式数据则不是这样。然而,由于表格的维度与数据集中的变量之间存在明确的关联,seaborn 能够将这些变量分配到图表中的角色。
备注
当 x 和 y 都未被赋值时,Seaborn 将 data 的参数视为宽格式。
sns.relplot(data=flights_wide, kind="line")
这个图看起来与之前的非常相似。Seaborn 将数据框的索引分配给 x,将数据框的值分配给 y,并为每个月绘制了一条单独的线。然而,这两个图之间有一个显著的区别。当数据集经过“pivot”操作,从长格式转换为宽格式时,值的含义信息丢失了。因此,没有 y 轴标签。(这里的线条也有虚线,因为 relplot() 将列变量映射到 hue 和 style 语义,以便图表更易于访问。我们在长格式的情况下没有这样做,但我们可以通过设置 style="month" 来实现)。
到目前为止,我们在使用宽格式数据时输入的内容少得多,但绘制的图表几乎相同。这看起来更容易!但长格式数据的一个巨大优势是,一旦数据格式正确,你就不再需要考虑其*结构*。你可以仅通过思考其中包含的变量来设计你的图表。例如,要绘制代表每年每月时间序列的线条,只需重新分配变量:
sns.relplot(data=flights, x="month", y="passengers", hue="year", kind="line")
要使用宽格式数据集实现相同的重新映射,我们需要对表格进行转置:
sns.relplot(data=flights_wide.transpose(), kind="line")
(这个例子还展示了另一个问题,即seaborn目前将宽格式数据集中的列变量视为分类变量,无论其数据类型如何,而由于长格式变量是数值型的,因此它被分配了一个定量的颜色调色板和图例。这在未来可能会改变).
缺少显式的变量赋值也意味着每种绘图类型都需要定义宽表数据维度与绘图角色之间的固定映射。由于这种自然映射在不同绘图类型之间可能有所不同,因此在使用宽表数据时,结果的可预测性较低。例如,分类 绘图将表格的 列 维度分配给 x,然后对行进行聚合(忽略索引):
sns.catplot(data=flights_wide, kind="box")
在使用 pandas 表示宽格式数据时,你仅限于几个变量(不超过三个)。这是因为 seaborn 不利用多索引信息,而多索引是 pandas 在表格格式中表示额外变量的方式。xarray 项目提供了带标签的 N 维数组对象,可以被视为宽格式数据向更高维度的泛化。目前,seaborn 不直接支持 xarray 的对象,但可以使用 to_pandas 方法将其转换为长格式的 pandas.DataFrame,然后在 seaborn 中像其他长格式数据集一样绘制。
总之,我们可以将长格式和宽格式的数据集想象成类似于这样的形式:
杂乱的数据#
许多数据集不能仅通过长格式或宽格式的规则来清晰解释。如果明确的长格式或宽格式的数据集是 “整洁” 的,我们可以说这些更模糊的数据集是“混乱”的。在混乱的数据集中,变量既不是由键唯一定义的,也不是由表格的维度定义的。这种情况经常出现在 重复测量 数据中,其中自然地组织表格使得每一行对应于数据收集的 单位 。考虑这个来自心理学实验的简单数据集,其中二十名受试者进行了一项记忆任务,他们在注意力分散或集中时研究了变位词:
anagrams = sns.load_dataset("anagrams")
anagrams
| subidr | attnr | num1 | num2 | num3 | |
|---|---|---|---|---|---|
| 0 | 1 | divided | 2 | 4.0 | 7 |
| 1 | 2 | divided | 3 | 4.0 | 5 |
| 2 | 3 | divided | 3 | 5.0 | 6 |
| 3 | 4 | divided | 5 | 7.0 | 5 |
| 4 | 5 | divided | 4 | 5.0 | 8 |
| 5 | 6 | divided | 5 | 5.0 | 6 |
| 6 | 7 | divided | 5 | 4.5 | 6 |
| 7 | 8 | divided | 5 | 7.0 | 8 |
| 8 | 9 | divided | 2 | 3.0 | 7 |
| 9 | 10 | divided | 6 | 5.0 | 6 |
| 10 | 11 | focused | 6 | 5.0 | 6 |
| 11 | 12 | focused | 8 | 9.0 | 8 |
| 12 | 13 | focused | 6 | 5.0 | 9 |
| 13 | 14 | focused | 8 | 8.0 | 7 |
| 14 | 15 | focused | 8 | 8.0 | 7 |
| 15 | 16 | focused | 6 | 8.0 | 7 |
| 16 | 17 | focused | 7 | 7.0 | 6 |
| 17 | 18 | focused | 7 | 8.0 | 6 |
| 18 | 19 | focused | 5 | 6.0 | 6 |
| 19 | 20 | focused | 6 | 6.0 | 5 |
注意力变量是 组间 的,但也有一个 组内 的变量:字谜的可能解决方案的数量,其范围从1到3。因变量是记忆表现的得分。这两个变量(数量和得分)在多个列中共同编码。因此,整个数据集既不是明显长格式,也不是明显宽格式。
我们如何告诉 seaborn 根据注意力和解决方案数量绘制平均分数?我们首先需要将数据转换为两种结构之一。让我们将其转换为整洁的长格式表格,使得每个变量是一列,每行是一个观察值。我们可以使用方法 pandas.DataFrame.melt() 来完成这个任务:
anagrams_long = anagrams.melt(id_vars=["subidr", "attnr"], var_name="solutions", value_name="score")
anagrams_long.head()
| subidr | attnr | solutions | score | |
|---|---|---|---|---|
| 0 | 1 | divided | num1 | 2.0 |
| 1 | 2 | divided | num1 | 3.0 |
| 2 | 3 | divided | num1 | 3.0 |
| 3 | 4 | divided | num1 | 5.0 |
| 4 | 5 | divided | num1 | 4.0 |
现在我们可以制作我们想要的图表:
sns.catplot(data=anagrams_long, x="solutions", y="score", hue="attnr", kind="point")
进一步阅读和要点#
关于表格数据结构的更详细讨论,你可以阅读 Hadley Whickham 的 “Tidy Data” 论文。请注意,seaborn 使用了一组与论文中定义略有不同的概念。虽然论文将整洁性与长格式结构联系在一起,但我们区分了“整洁的宽格式”数据,其中数据集中的变量与表格的维度之间有明确的映射,以及“混乱数据”,其中不存在这样的映射。
长格式结构有明显的优势。它允许你通过显式地将数据集中的变量分配到图中的角色来创建图形,并且你可以这样做超过三个变量。在可能的情况下,尝试在进行严肃分析时用长格式结构表示你的数据。seaborn 文档中的大多数示例将使用长格式数据。但在数据集保持宽格式更自然的情况下,记住 seaborn 仍然可以发挥作用。
可视化长格式数据的选项#
虽然长格式数据有一个精确的定义,但seaborn在数据在内存中的数据结构中的实际组织方式上相当灵活。文档其余部分的示例通常会使用 pandas.DataFrame 对象,并通过将它们列的名称分配给绘图中的变量来引用其中的变量。但也可以在Python字典或实现该接口的类中存储向量:
flights_dict = flights.to_dict()
sns.relplot(data=flights_dict, x="year", y="passengers", hue="month", kind="line")
许多 pandas 操作,例如 group-by 的拆分-应用-合并操作,会产生一个数据框,其中信息已从输入数据框的列移动到输出数据框的索引。只要名称被保留,您仍然可以像往常一样引用数据:
flights_avg = flights.groupby("year").mean(numeric_only=True)
sns.relplot(data=flights_avg, x="year", y="passengers", kind="line")
此外,可以直接将数据向量作为参数传递给 x、y 和其他绘图变量。如果这些向量是 pandas 对象,则将使用 name 属性来标记绘图:
year = flights_avg.index
passengers = flights_avg["passengers"]
sns.relplot(x=year, y=passengers, kind="line")
实现了 Python 序列接口的 Numpy 数组和其他对象也可以使用,但如果它们没有名称,在不进行进一步调整的情况下,绘图将不会那么具有信息量:
sns.relplot(x=year.to_numpy(), y=passengers.to_list(), kind="line")
可视化宽格式数据的选项#
传递宽格式数据的选项更加灵活。与长格式数据一样,pandas 对象是首选,因为可以使用名称(在某些情况下,还有索引)信息。但从本质上讲,任何可以视为单个向量或向量集合的格式都可以传递给 data,并且通常可以构建有效的图表。
我们在上面看到的例子使用了矩形的 pandas.DataFrame,可以将其视为其列的集合。字典或 pandas 对象的列表也可以工作,但我们将会失去轴标签:
flights_wide_list = [col for _, col in flights_wide.items()]
sns.relplot(data=flights_wide_list, kind="line")
集合中的向量不需要具有相同的长度。如果它们有一个 index,它将被用来对齐它们:
two_series = [flights_wide.loc[:1955, "Jan"], flights_wide.loc[1952:, "Aug"]]
sns.relplot(data=two_series, kind="line")
而对于 numpy 数组或简单的 Python 序列,将使用序数索引:
two_arrays = [s.to_numpy() for s in two_series]
sns.relplot(data=two_arrays, kind="line")
但这样一个向量的字典至少会使用以下键:
two_arrays_dict = {s.name: s.to_numpy() for s in two_series}
sns.relplot(data=two_arrays_dict, kind="line")
矩形的 numpy 数组被视为没有索引信息的数据框,因此它们被视为列向量的集合。请注意,这与 numpy 索引操作的工作方式不同,其中单个索引器将访问一行。但这与 pandas 将数组转换为数据框或 matplotlib 绘制数组的方式一致:
flights_array = flights_wide.to_numpy()
sns.relplot(data=flights_array, kind="line")