在不手动计算子图数量的情况下创建 matplotlib 子图?

Posted

技术标签:

【中文标题】在不手动计算子图数量的情况下创建 matplotlib 子图?【英文标题】:Create matplotlib subplots without manually counting number of subplots? 【发布时间】:2020-05-22 23:58:41 【问题描述】:

在 Jupyter Notebook 中进行临时分析时,我经常希望将某些 Pandas DataFrame 的转换序列视为垂直堆叠的子图。我常用的快速而肮脏的方法是根本不使用子图,而是为每个图创建一个新图形:

import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
%matplotlib inline

df = pd.DataFrame("a": range(100))  # Some arbitrary DataFrame
df.plot(title="0 to 100")
plt.show()

df = df * -1  # Some transformation
df.plot(title="0 to -100")
plt.show()

df = df * 2  # Some other transformation
df.plot(title="0 to -200")
plt.show()

此方法有局限性。即使索引相同,x 轴刻度也未对齐(因为 x 轴宽度取决于 y 轴标签)并且 Jupyter 单元格输出包含几个单独的内联图像,而不是我可以保存或复制和粘贴的单个图像.

据我所知,正确的解决方案是使用plt.subplots()

fig, axes = plt.subplots(3, figsize=(20, 9))

df = pd.DataFrame("a": range(100)) # Arbitrary DataFrame
df.plot(ax=axes[0], title="0 to 100")

df = df * -1 # Some transformation
df.plot(ax=axes[1], title="0 to -100")

df = df * 2 # Some other transformation
df.plot(ax=axes[2], title="0 to -200")

plt.tight_layout()
plt.show()

这正是我想要的输出。然而,它也引入了一个烦恼,使我默认使用第一种方法:我必须手动计算我创建的子图的数量,并随着代码的变化在几个不同的地方更新这个计数。

在多图的情况下,添加第四个图就像第四次调用df.plot()plt.show() 一样简单。对于子图,等效更改需要更新子图计数,加上调整输出图形大小的算法,将plt.subplots(3, figsize=(20, 9)) 替换为plt.subplots(4, figsize=(20, 12))。每个新添加的子图都需要知道已经存在多少其他子图(ax=axes[0]ax=axes[1]ax=axes[2] 等),因此任何添加或删除都需要对下面的图进行级联更改。

似乎自动化应该是微不足道的——它只是计数和乘法——但我发现用 matplotlib/pyplot API 实现是不可能的。我能得到的最接近的是以下部分解决方案,它足够简洁,但仍需要显式计数:

n_subplots = 3  # Must still be updated manually as code changes

fig, axes = plt.subplots(n_subplots, figsize=(20, 3 * n_subplots))
i = 0  # Counts how many subplots have been added so far 

df = pd.DataFrame("a": range(100)) # Arbitrary DataFrame
df.plot(ax=axes[i], title="0 to 100")
i += 1

df = df * -1 # Arbitrary transformation
df.plot(ax=axes[i], title="0 to -100")
i += 1

df = df * 2 # Arbitrary transformation
df.plot(ax=axes[i], title="0 to -200")
i += 1

plt.tight_layout()
plt.show()

根本问题是,任何时候调用df.plot(),都必须存在一个已知大小的axes 列表。我考虑过以某种方式延迟df.plot() 的执行,例如通过附加到一个可以在按顺序调用之前计算的 lambda 函数列表,但这似乎是一种极端的仪式,只是为了避免手动更新整数。

有没有更方便的方法来做到这一点?具体来说,有没有办法创建一个具有“可扩展”数量的子图的图形,适用于预先不知道计数的临时/交互式上下文?

(注意:此问题可能与this question 或this one 重复,但接受的两个问题的答案恰好包含我的问题我正在尝试解决 - plt.subplots()nrows= 参数必须在添加子图之前声明。)

【问题讨论】:

您如何跟踪任意转换的数量...?和数据框? 当使用单独的数字时(上面的第一个代码示例有多个 plt.show() 调用),不需要计算它们。在第二种情况下(使用plt.subplots())我必须自己手动计算它们并在每个 Jupyter 单元格的顶部声明。我想避免自己手动计算它们,就像在第一种情况下一样,但仍然得到一个子图而不是多个图。 【参考方案1】:

首先创建一个空图,然后使用add_subplot 添加子图。使用新几何图形的新GridSpec 更新图中现有子图的subplotspecs(仅当您使用constrained 布局而不是tight 布局时才需要figure 关键字)。

import matplotlib.pyplot as plt
import matplotlib as mpl
import pandas as pd


def append_axes(fig, as_cols=False):
    """Append new Axes to Figure."""
    n = len(fig.axes) + 1
    nrows, ncols = (1, n) if as_cols else (n, 1)
    gs = mpl.gridspec.GridSpec(nrows, ncols, figure=fig)
    for i,ax in enumerate(fig.axes):
        ax.set_subplotspec(mpl.gridspec.SubplotSpec(gs, i))
    return fig.add_subplot(nrows, ncols, n)


fig = plt.figure(layout='tight')

df = pd.DataFrame("a": range(100)) # Arbitrary DataFrame
df.plot(ax=append_axes(fig), title="0 to 100")

df = df * -1 # Some transformation
df.plot(ax=append_axes(fig), title="0 to -100")

df = df * 2 # Some other transformation
df.plot(ax=append_axes(fig), title="0 to -200")

将新子图添加为列的示例(并使用约束布局进行更改):

fig = plt.figure(layout='constrained')

df = pd.DataFrame("a": range(100)) # Arbitrary DataFrame
df.plot(ax=append_axes(fig, True), title="0 to 100")

df = df + 10 # Some transformation
df.plot(ax=append_axes(fig, True), title="10 to 110")

【讨论】:

【参考方案2】:

您可以创建一个存储数据的对象,并且只有在您告诉它这样做时才创建图形。

import pandas as pd
import matplotlib.pyplot as plt

class AxesStacker():
    def __init__(self):
        self.data = []
        self.titles = []

    def append(self, data, title=""):
        self.data.append(data)
        self.titles.append(title)

    def create(self):
        nrows = len(self.data)
        self.fig, self.axs = plt.subplots(nrows=nrows)
        for d, t, ax in zip(self.data, self.titles, self.axs.flat):
            d.plot(ax=ax, title=t)



stacker = AxesStacker()

df = pd.DataFrame("a": range(100))  # Some arbitrary DataFrame
stacker.append(df, title="0 to 100")

df = df * -1  # Some transformation
stacker.append(df, title="0 to -100")

df = df * 2  # Some other transformation
stacker.append(df, title="0 to -200")

stacker.create()
plt.show()

【讨论】:

【参考方案3】:

IIUC 您需要一些容器来进行转换以实现这一目标 - 例如 list。比如:

arbitrary_trx = [
    lambda x: x,         # No transformation
    lambda x: x * -1,    # Arbitrary transformation
    lambda x: x * 2]     # Arbitrary transformation

fig, axes = plt.subplots(nrows=len(arbitrary_trx))

for ax, f in zip(axes, arbitrary_trx):
    df = df.apply(f)
    df.plot(ax=ax)

【讨论】:

对——这是我在“根本问题是……”开头的段落中提到的解决方法。对每个子图使用 lambdas / function defs 有点限制,但如果没有其他方法,我会接受这个答案。 是的,如果不知道前期有多少转换,就无法动态执行此操作 - 特别是因为您在每次新转换时都在原地更改 DataFrame

以上是关于在不手动计算子图数量的情况下创建 matplotlib 子图?的主要内容,如果未能解决你的问题,请参考以下文章

Python pandas 计算子字符串的唯一字符串源的数量

实时即未来,大数据项目车联网之创建Flink实时计算子工程

实时即未来,大数据项目车联网之创建Flink实时计算子工程

Swift 4和Firebase如何计算子值

在不修改数据的情况下创建非线性 MATLAB 颜色图

Altair - 如何在不操作 DataFrame 的情况下创建分组条形图