MEP28: 从 Axes.boxplot 中移除复杂性#

状态#

讨论

分支和拉取请求#

以下列出了与此 MEP 相关的任何开放 PR 或分支:

  1. Axes.boxplot 中弃用冗余的统计关键字参数: phobson/matplotlib

  2. Axes.boxplot 中弃用冗余的样式选项: phobson/matplotlib

  3. 弃用将2D NumPy数组作为输入:无

  4. cbook.boxplot_stats 添加预处理和后处理选项:phobson/matplotlib

  5. 通过 Axes.boxplot kwargs 暴露 cbook.boxplot_stats:无

  6. 移除 Axes.boxplot 中冗余的统计参数: None

  7. 移除 Axes.boxplot 中的冗余样式选项: None

  8. 通过讨论产生的剩余项目:无

摘要#

在过去的几个版本中,Axes.boxplot 方法的复杂性有所增加,以支持完全可定制的艺术家样式和统计计算。这导致 Axes.boxplot 被拆分为多个部分。绘制箱线图所需的统计数据在 cbook.boxplot_stats 中计算,而实际的艺术家则由 Axes.bxp 绘制。原始方法 Axes.boxplot 仍然是处理将用户提供的数据传递给 cbook.boxplot_stats,将结果传递给 Axes.bxp,并为箱线图的每个方面预处理样式信息的最公开的API。

本 MEP 将概述一条回滚增加的复杂性并简化 API 的路径,同时保持合理的向后兼容性。

详细描述#

目前,Axes.boxplot 方法接受允许用户为图中绘制的每个箱线图指定中位数和置信区间的参数。这些参数是为了让高级用户能够提供以不同于 matplotlib 提供的简单方法计算的统计数据。然而,处理这些输入需要复杂的逻辑来确保数据结构的格式与需要绘制的内容相匹配。目前,该逻辑包含 9 个独立的 if/else 语句,嵌套深度可达 5 层,并带有一个 for 循环,可能会引发多达 2 个错误。这些参数是在创建 Axes.bxp 方法之前添加的,该方法从包含相关统计数据的字典列表中绘制箱线图。Matplotlib 还提供了一个通过 cbook.boxplot_stats 计算这些统计数据的函数。请注意,高级用户现在可以选择 a) 编写自己的函数来计算 Axes.bxp 所需的统计数据,或 b) 修改 cbook.boxplots_stats 返回的输出以完全自定义图表艺术家的位置。有了这种灵活性,手动指定中位数及其置信区间的参数仍然保留,以实现向后兼容。

大约在 Axes.boxplot 的两个角色被拆分为用于计算的 cbook.boxplot_stats 和用于绘制的 Axes.bxp 的同时,Axes.boxplotAxes.bxp 都被编写为接受参数,这些参数可以单独切换箱线图所有组件的绘制,并可以单独配置这些艺术家的样式。然而,为了保持向后兼容性,保留了 sym 参数(以前用于指定离群点的符号)。这个参数本身需要相当复杂的逻辑来协调 sym 参数与 matplotlibrc 指定的默认样式中的新 flierprops 参数。

本 MEP 旨在极大地简化新手和高级用户创建箱线图的过程。重要的是,此处提出的更改也将对下游包如 seaborn 可用,因为 seaborn 智能地允许用户通过 seaborn API 将任意参数字典传递给底层的 matplotlib 函数。

这将通过以下方式实现:

  1. cbook.boxplot_stats 将被修改以允许传入预计算和后计算的转换函数(例如,对数正态分布数据使用 np.lognp.exp

  2. Axes.boxplot 将被修改以接受并简单地传递它们到 ``cbook.boxplots_stats``(替代方案:传递统计函数及其可选参数的字典)。

  3. Axes.boxplot 中的过时参数将被弃用,并随后移除。

重要性#

由于须的极限是通过算术计算得出的,箱线图隐含了正态分布的假设。这主要影响哪些数据点被分类为异常值。

允许对数据和用于绘制箱线图的结果进行转换,将允许用户在已知数据不符合正态分布的情况下选择退出该假设。

下面是一个示例,展示了 Axes.boxplot 如何根据这些类型的变换对对数正态数据中的异常值进行不同的分类。

import numpy as np
import matplotlib.pyplot as plt
from matplotlib import cbook
np.random.seed(0)

fig, ax = plt.subplots(figsize=(4, 6))
ax.set_yscale('log')
data = np.random.lognormal(-1.75, 2.75, size=37)

stats = cbook.boxplot_stats(data, labels=['arithmetic'])
logstats = cbook.boxplot_stats(np.log(data), labels=['log-transformed'])

for lsdict in logstats:
    for key, value in lsdict.items():
        if key != 'label':
            lsdict[key] = np.exp(value)

stats.extend(logstats)
ax.bxp(stats)
fig.show()

(Source code, 2x.png, png)

实现#

将转换函数传递给 cbook.boxplots_stats#

本 MEP 提议向计算箱线图统计数据的 cookbook 函数添加两个参数(例如,transform_intransform_out)。这些将是可选的关键字参数,当用户省略时,可以轻松地设置为 lambda x: x 作为无操作。transform_in 函数将在 boxplot_stats 函数遍历传递给它的每个数据子集时应用于数据。在计算出统计字典列表后,transform_out 函数将应用于字典中的每个值。

这些转换可以被添加到 Axes.boxplot 的调用签名中,而对方法的复杂性影响很小。这是因为它们可以直接传递给 cbook.boxplot_stats。或者,可以修改 Axes.boxplot 以接受一个可选的统计函数关键字参数和一个字典参数,这些参数将直接传递给它。

在实现的这个阶段,用户和外部库(如 seaborn)可以通过 Axes.boxplot 方法完全控制。更重要的是,至少在最低限度上,seaborn 不需要对其 API 进行任何更改,就能让用户利用这些新选项。

Axes.boxplot API 及其他功能的简化#

简化箱线图方法主要包括弃用然后移除冗余参数。可选的下一步包括纠正 Axes.boxplotAxes.bxp 之间的小术语不一致。

即将弃用并移除的参数包括:

  1. usermedians - 由 10 行代码处理,包含 3 个 if 块,一个 for 循环

  2. conf_intervals - 由 15 行代码处理,包含 6 个 if 块,一个 for 循环

  3. sym - 由 12 行代码处理,包含 4 个 if

移除 sym 选项允许将处理剩余样式参数的所有代码移动到 Axes.bxp 中。这并没有减少任何复杂性,但确实强化了 Axes.bxpcbook.boxplot_statsAxes.boxplot 之间的单一职责原则。

此外,notch 参数可以重命名为 shownotches 以与 Axes.bxp 保持一致。这种清理可以进一步进行,whisbootstrapautorange 可以合并到传递给新 statfxn 参数的 kwargs 中。

向后兼容性#

此 MEP 的实施最终将导致向后不兼容的关键字参数 usermediansconf_intervalssym 的弃用和移除。在 GitHub 上的粗略搜索表明,usermediansconf_intervals 被少数用户使用,这些用户似乎对 matplotlib 有非常深入的了解。一个稳健的弃用周期应为这些用户提供足够的时间迁移到新的 API。

然而,sym 的弃用可能会对 matplotlib 用户群产生更广泛的影响。

日程安排#

一个加速的时间线可能如下所示:

  1. v2.0.1 为 cbook.boxplots_stats 添加了转换功能,并在 Axes.boxplot 中公开。

  2. v2.1.0 初始弃用,以及使用 2D NumPy 数组作为输入

    1. 使用2D NumPy数组作为输入。关于2D数组的语义通常令人困惑。

    2. usermedians, conf_intervals, sym 参数

  3. v2.2.0

    1. 移除 usermedians, conf_intervals, sym 参数

    2. 弃用 notch 以支持 shownotches ,以与其他参数和 Axes.bxp 保持一致

  4. v2.3.0

    1. 移除 notch 参数

    2. 将所有样式和艺术家的切换逻辑移至 Axes.bxp 中,使得 Axes.boxplot 更像是一个在 Axes.bxpcbook.boxplots_stats 之间的中介。

对用户的预期影响#

如上所述,弃用 usermediansconf_intervals 可能会影响少数用户。受到影响的用户几乎肯定是高级用户,他们将能够适应这一变化。

弃用 sym 选项可能会吸引更多用户,应采取措施收集社区对此的反馈。

对下游库的预期影响#

源代码(截至2016-10-17的GitHub主分支)被检查以查看这些更改是否会影响seaborn和python-ggplot的使用。在这个MEP中提名移除的参数均未被seaborn使用。使用matplotlib的boxplot函数的seaborn API允许用户传递任意的``**kwargs``到matplotlib的API。因此,拥有现代matplotlib安装的seaborn用户将能够充分利用此MEP新增的任何新功能。

Python-ggplot 已经实现了自己的函数来绘制箱线图。因此,实现此 MEP 不会对其产生任何影响。

替代方案#

主题的变奏#

这个 MEP 可以分为几个松散耦合的组件:

  1. cbook.boxplot_stats 中允许预计算和后计算的转换函数

  2. Axes.boxplot API 中公开该转换

  3. Axes.boxplot 中移除冗余的统计选项

  4. 将所有样式参数处理从 Axes.boxplot 转移到 Axes.bxp

通过这种方法,#2 依赖于 #1,而 #4 依赖于 #3。

对于#2,有两种可能的方法。第一种也是最直接的方法是将 cbook.boxplot_stats 中的新 transform_intransform_out 参数镜像到 Axes.boxplot 中,并直接传递它们。

第二种方法是将 statfxnstatfxn_args 参数添加到 Axes.boxplot 中。在这种实现方式下,statfxn 的默认值将是 cbook.boxplot_stats,但用户可以传递自己的函数。然后,transform_intransform_out 将作为 statfxn_args 参数的元素传递。

def boxplot_stats(data, ..., transform_in=None, transform_out=None):
    if transform_in is None:
        transform_in = lambda x: x

    if transform_out is None:
        transform_out = lambda x: x

    output = []
    for _d in data:
        d = transform_in(_d)
        stat_dict = do_stats(d)
        for key, value in stat_dict.item():
            if key != 'label':
                stat_dict[key] = transform_out(value)
        output.append(d)
    return output


 class Axes(...):
     def boxplot_option1(data, ..., transform_in=None, transform_out=None):
         stats = cbook.boxplot_stats(data, ...,
                                     transform_in=transform_in,
                                     transform_out=transform_out)
         return self.bxp(stats, ...)

     def boxplot_option2(data, ..., statfxn=None, **statopts):
         if statfxn is None:
             statfxn = boxplot_stats
         stats = statfxn(data, **statopts)
         return self.bxp(stats, ...)

两种情况都允许用户执行以下操作:

fig, ax1 = plt.subplots()
artists1 = ax1.boxplot_optionX(data, transform_in=np.log,
                               transform_out=np.exp)

但选项二允许用户编写一个完全自定义的统计函数(例如,my_box_stats),该函数具有花哨的BCA置信区间,并且根据数据的某些属性以不同方式设置须线。

这是当前API下可用的:

fig, ax1 = plt.subplots()
my_stats = my_box_stats(data, bootstrap_method='BCA',
                        whisker_method='dynamic')
ax1.bxp(my_stats)

并且使用选项二会更加简洁

fig, ax = plt.subplots()
statopts = dict(transform_in=np.log, transform_out=np.exp)
ax.boxplot(data, ..., **statopts)

用户还可以传递自己的函数来计算统计数据:

fig, ax1 = plt.subplots()
ax1.boxplot(data, statfxn=my_box_stats, bootstrap_method='BCA',
            whisker_method='dynamic')

从上面的例子来看,选项二似乎只有微小的优势,但在下游库如seaborn的背景下,其优势更加明显,因为以下操作无需对seaborn进行任何补丁即可实现:

import seaborn
tips = seaborn.load_data('tips')
g = seaborn.factorplot(x="day", y="total_bill", hue="sex", data=tips,
                       kind='box', palette="PRGn", shownotches=True,
                       statfxn=my_box_stats, bootstrap_method='BCA',
                       whisker_method='dynamic')

这种灵活性是将整体箱线图API拆分为当前三个函数的设计初衷。然而在实践中,像seaborn这样的下游库支持的matplotlib版本远早于拆分之前。因此,只需在 Axes.boxplot 中增加一点灵活性,就可以向使用现代matplotlib安装的下游库用户暴露所有功能,而无需下游库维护者的干预。

少做#

另一个明显的替代方案是省略 cbook.boxplot_statsAxes.boxplot 中添加的前后计算转换功能,并简单地删除如上所述的冗余统计和样式参数。

什么都不做#

就像生活中的许多事情一样,什么都不做也是一种选择。这意味着我们只是倡导用户和下游库利用 cbook.boxplot_statsAxes.bxp 之间的分离,并让他们决定如何提供一个接口来实现这一点。