如何绘制经验累积分布函数(ECDF)

73
如何在Python中使用Matplotlib绘制一组数字的经验CDF?我想要的是Pylab的hist函数的CDF类似物。
我能想到的一种方法是:
from scipy.stats import cumfreq
a = array([...]) # my array of numbers
num_bins =  20
b = cumfreq(a, num_bins)
plt.plot(b)
19个回答

131

如果你喜欢使用 linspace,并且更倾向于使用一行代码,那么可以这样写:

plt.plot(np.sort(a), np.linspace(0, 1, len(a), endpoint=False))

根据我的口味,我几乎总是这样做:

# a is the data array
x = np.sort(a)
y = np.arange(len(x))/float(len(x))
plt.plot(x, y)

即使有超过 >O(1e6) 的数据值,这对我也有效。 如果你真的需要下采样,我会设置

x = np.sort(a)[::down_sampling_step]

编辑以回应评论/编辑关于为什么我使用endpoint=False 或上方定义的 y。以下是一些技术细节。

经验分布函数通常被正式定义为

CDF(x) = "number of samples <= x"/"number of samples"
为了完全匹配这个形式定义,你需要使用y = np.arange(1,len(x)+1)/float(len(x)),以便我们得到y = [1/N, 2/N ... 1]。这个估计器是一个无偏估计器,在样本数量趋于无穷时会收敛到真实的CDF 维基百科参考资料
我倾向于使用y = [0, 1/N, 2/N ... (N-1)/N],因为: (a) 这更容易编码/更符合惯例, (b) 但仍然在形式上是合理的,因为可以在收敛证明中始终交换CDF(x)1-CDF(x), (c) 并且适用于上述简单的下采样方法。
在某些特定情况下,定义如下有用:
y = (arange(len(x))+0.5)/len(x)

这种方法在这两种习惯用法之间处于中间。实际上,它表明“在我的样本中,有1/(2N)的机会出现比我看到的最小值更小的值,以及1/(2N)的机会出现比我目前看到的最大值更大的值。”

请注意,选择这种约定与plt.step中使用的where参数相互作用,如果将CDF显示为分段常数函数似乎更有用。为了完全匹配上面提到的正式定义,需要使用建议的y=[0,1/N..., 1-1/N]约定和where=pre或者y=[1/N, 2/N ... 1]约定和where=post,但不能反过来。

然而,对于大样本和合理的分布,主体答案中给出的约定易于编写,是真实CDF的无偏估计,并且可以使用下采样方法。


9
这个回答应该得到更多的赞,因为它是迄今为止唯一没有强制进行分箱的回答。我只是简化了代码,使用了linspace。 - hans_meine
2
@hans_meine 您的编辑,即 yvals=linspace(0,1,len(sorted)),会产生不是真实 CDF 的无偏估计的 yvals - Dave
那么,我们应该使用endpoint = False的linspace,对吗? - hans_meine
1
@Dave 也许使用 plt.step 比 plt.plot 更好,这样做会有什么问题吗? - Ezequiel Castaño
1
@EzequielCastaño 大多数情况下,我认为这是一种风格问题,但您需要注意where参数的选择与y参数的定义之间的关系。对我来说最有意义的是使用where=pre和建议的y=np.arange(0,len(x))/len(x),或者您可以使用y=np.arange(1,len(x)+1)/len(x)并使用where=post,但在它们之间切换“where”会(稍微)错误地表示CDF。 - Dave
显示剩余6条评论

85
你可以使用scikits.statsmodels库中的ECDF函数:
import numpy as np
import scikits.statsmodels as sm
import matplotlib.pyplot as plt

sample = np.random.uniform(0, 1, 50)
ecdf = sm.tools.ECDF(sample)

x = np.linspace(min(sample), max(sample))
y = ecdf(x)
plt.step(x, y)

在版本0.4中,scicits.statsmodels被重命名为statsmodelsECDF现在位于distributions模块中(而statsmodels.tools.tools.ECDF已过时)。

import numpy as np
import statsmodels.api as sm # recommended import according to the docs
import matplotlib.pyplot as plt

sample = np.random.uniform(0, 1, 50)
ecdf = sm.distributions.ECDF(sample)

x = np.linspace(min(sample), max(sample))
y = ecdf(x)
plt.step(x, y)
plt.show()

2
@bmu(和@Luca):太棒了,感谢您们慷慨地将代码与当前的statsmodel保持一致! - ars
对于scikits.statsmodels v0.3.1,需要import scikits.statsmodels.tools as smtoolsecdf = smtools.tools.EDCF(...) - alexei
2
这仍然通过 x = np.linspace(…) 强制进行分箱处理。您可以通过使用 plt.step(ecdf.x, ecdf.y) 来规避这个问题。 - Wrzlprmft
1
在 statsmodels v12.2 中,您可以通过 from statsmodels.distributions.empirical_distribution import ECDF 获取 ECDF(经验累积分布函数)(https://www.statsmodels.org/stable/generated/statsmodels.distributions.empirical_distribution.ECDF.html)。 - eagle33322

18

看起来这几乎正是你想要的。有两点需要注意:

首先,结果是一个包含四个项目的元组。第三个是bin的大小,第二个是最小bin的起始点,第一个是每个bin中的点数(或以下的点数)。 (最后一个是超出限制之外的点数,但由于您没有设置任何限制,因此所有点都将被分组成bin)。

其次,你需要重新调整结果使得最终值为1,以遵循CDF的通常约定,但其他方面是正确的。

这是它在内部执行的操作:

def cumfreq(a, numbins=10, defaultreallimits=None):
    # docstring omitted
    h,l,b,e = histogram(a,numbins,defaultreallimits)
    cumhist = np.cumsum(h*1, axis=0)
    return cumhist,l,b,e

该函数实现了直方图,并对每个bin中的计数进行累加。因此,结果的第i个值就是小于或等于第i个bin的最大值的数组值的数量。因此,最终的值只是初始数组的大小。

最后,为了绘制它,您需要使用bin的初始值和bin的大小来确定需要的x轴值。

另一种选择是使用numpy.histogram函数进行规范化并返回bin的边缘。您需要自己对生成的计数进行累加。

a = array([...]) # your array of numbers
num_bins = 20
counts, bin_edges = numpy.histogram(a, bins=num_bins, normed=True)
cdf = numpy.cumsum(counts)
pylab.plot(bin_edges[1:], cdf)

(bin_edges[1:] 是每个箱子的上边缘。)


26
简短说明:这份代码实际上并没有给出经验累积分布函数(一种在n个数据点处以1/n递增的阶梯函数)。相反,这份代码基于一个基于直方图的概率密度函数估计来给出CDF的估计值。这个基于直方图的估计可以通过仔细/不当地选择箱子进行操作/偏差,因此它不像真正的ECDF那样能够很好地表征真实的CDF。 - David B.
3
我也不喜欢这种强制分箱的做法。可以参考 Dave 的简短回答,他使用 numpy.sort 在不进行分箱的情况下绘制累积分布函数。 - hans_meine

15

您尝试过在pyplot.hist中使用cumulative=True参数吗?


1
非常好的评论。不过,这就需要进行分箱处理;请参考Dave使用np.sort的答案。 - hans_meine
不错且简便的选择,但缺点是生成的线图的自定义能力有限,例如无法添加标记。我选择了scikits.statsmodels的答案。 - alexei

6

基于戴夫的回答的一行代码:

plt.plot(np.sort(arr), np.linspace(0, 1, len(arr), endpoint=False))

编辑:评论中 hans_meine 也提出了这个建议。


4
假设 vals 保存了你的数值,那么你可以按照以下方式简单绘制 CDF 图表:
y = numpy.arange(0, 101)
x = numpy.percentile(vals, y)
plot(x, y)

要将其缩放在0和1之间,只需将y除以100。


3
我们可以使用来自matplotlibstep函数,它生成阶梯状图,这就是经验CDF的定义:
import numpy as np
from matplotlib import pyplot as plt

data = np.random.randn(11)

levels = np.linspace(0, 1, len(data) + 1)  # endpoint 1 is included by default
plt.step(sorted(list(data) + [max(data)]), levels)

最后一根竖线在max(data)处是手动添加的,否则绘图只会停留在水平线1 - 1/len(data)。另外,我们可以使用step()函数的where='post'选项来实现同样的效果。
levels = np.linspace(1. / len(data), 1, len(data))
plt.step(sorted(data), levels, where='post')

如果是这种情况,则不会绘制从零开始的初始垂直线。


3

虽然这里有很多好的答案,但我会包括一个更加定制化的ECDF图

生成经验累积分布函数的值

import matplotlib.pyplot as plt

def ecdf_values(x):
    """
    Generate values for empirical cumulative distribution function
    
    Params
    --------
        x (array or list of numeric values): distribution for ECDF
    
    Returns
    --------
        x (array): x values
        y (array): percentile values
    """
    
    # Sort values and find length
    x = np.sort(x)
    n = len(x)
    # Create percentiles
    y = np.arange(1, n + 1, 1) / n
    return x, y

def ecdf_plot(x, name = 'Value', plot_normal = True, log_scale=False, save=False, save_name='Default'):
    """
    ECDF plot of x

    Params
    --------
        x (array or list of numerics): distribution for ECDF
        name (str): name of the distribution, used for labeling
        plot_normal (bool): plot the normal distribution (from mean and std of data)
        log_scale (bool): transform the scale to logarithmic
        save (bool) : save/export plot
        save_name (str) : filename to save the plot
    
    Returns
    --------
        none, displays plot
    
    """
    xs, ys = ecdf_values(x)
    fig = plt.figure(figsize = (10, 6))
    ax = plt.subplot(1, 1, 1)
    plt.step(xs, ys, linewidth = 2.5, c= 'b');
    
    plot_range = ax.get_xlim()[1] - ax.get_xlim()[0]
    fig_sizex = fig.get_size_inches()[0]
    data_inch = plot_range / fig_sizex
    right = 0.6 * data_inch + max(xs)
    gap = right - max(xs)
    left = min(xs) - gap
    
    if log_scale:
        ax.set_xscale('log')
        
    if plot_normal:
        gxs, gys = ecdf_values(np.random.normal(loc = xs.mean(), 
                                                scale = xs.std(), 
                                                size = 100000))
        plt.plot(gxs, gys, 'g');

    plt.vlines(x=min(xs), 
               ymin=0, 
               ymax=min(ys), 
               color = 'b', 
               linewidth = 2.5)
    
    # Add ticks
    plt.xticks(size = 16)
    plt.yticks(size = 16)
    # Add Labels
    plt.xlabel(f'{name}', size = 18)
    plt.ylabel('Percentile', size = 18)

    plt.vlines(x=min(xs), 
               ymin = min(ys), 
               ymax=0.065, 
               color = 'r', 
               linestyle = '-', 
               alpha = 0.8, 
               linewidth = 1.7)
    
    plt.vlines(x=max(xs), 
               ymin=0.935, 
               ymax=max(ys), 
               color = 'r', 
               linestyle = '-', 
               alpha = 0.8, 
               linewidth = 1.7)

    # Add Annotations
    plt.annotate(s = f'{min(xs):.2f}', 
                 xy = (min(xs), 
                       0.065),
                horizontalalignment = 'center',
                verticalalignment = 'bottom',
                size = 15)
    plt.annotate(s = f'{max(xs):.2f}', 
                 xy = (max(xs), 
                       0.935),
                horizontalalignment = 'center',
                verticalalignment = 'top',
                size = 15)
    
    ps = [0.25, 0.5, 0.75]

    for p in ps:

        ax.set_xlim(left = left, right = right)
        ax.set_ylim(bottom = 0)

        value = xs[np.where(ys > p)[0][0] - 1]
        pvalue = ys[np.where(ys > p)[0][0] - 1]

        plt.hlines(y=p, xmin=left, xmax = value,
                    linestyles = ':', colors = 'r', linewidth = 1.4);

        plt.vlines(x=value, ymin=0, ymax = pvalue, 
                   linestyles = ':', colors = 'r', linewidth = 1.4)
        
        plt.text(x = p / 3, y = p - 0.01, 
                 transform = ax.transAxes,
                 s = f'{int(100*p)}%', size = 15,
                 color = 'r', alpha = 0.7)

        plt.text(x = value, y = 0.01, size = 15,
                 horizontalalignment = 'left',
                 s = f'{value:.2f}', color = 'r', alpha = 0.8);

    # fit the labels into the figure
    plt.title(f'ECDF of {name}', size = 20)
    plt.tight_layout()
    

    if save:
        plt.savefig(save_name + '.png')

    

ecdf_plot(np.random.randn(100), name='Normal Distribution', save=True, save_name="ecdf")

在这里输入图片描述

额外资源:


3

如果您想显示实际真实的ECDF(正如David B所指出的那样,它是一个阶梯函数,每个n数据点增加1/n),我的建议是编写代码为每个数据点生成两个“绘图”点:

a = array([...]) # your array of numbers
sorted=np.sort(a)
x2 = []
y2 = []
y = 0
for x in sorted: 
    x2.extend([x,x])
    y2.append(y)
    y += 1.0 / len(a)
    y2.append(y)
plt.plot(x2,y2)

这样做可以得到具有ECDF特征的n步图,对于数据集足够小以至于步骤可见而言尤为美好。此外,无需使用直方图进行任何分箱(这可能会在绘制ECDF时引入偏差)。


3
我有一个对AFoglia方法的细微补充,可以规范累积分布函数。
n_counts,bin_edges = np.histogram(myarray,bins=11,normed=True) 
cdf = np.cumsum(n_counts)  # cdf not normalized, despite above
scale = 1.0/cdf[-1]
ncdf = scale * cdf

将直方图归一化可以使其积分为1,这意味着累积分布函数不会被归一化。你需要自己进行缩放。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接