使用Matplotlib对直方图进行归一化处理

3

我想使用Matplotlib绘制直方图,但我希望bin的值表示总观测量的百分比。一个最小工作示例可以像这样:

#!/usr/bin/env python3
# -*- coding: utf-8 -*-

import matplotlib.pyplot as plt
import matplotlib.ticker as tck
import seaborn as sns
import numpy

sns.set(style='dark')

imagen2 = plt.figure(1, figsize=(5, 2))
imagen2.suptitle('StackOverflow Matplotlib histogram demo')

luminance = numpy.random.randn(1000, 1000)
# "Luminance" should range from 0.0...1.0 so we normalize it
luminance = (luminance - luminance.min())/(luminance.max() - luminance.min())

top_left = plt.subplot(121)
top_left.imshow(luminance)
bottom_left = plt.subplot(122)
sns.distplot(luminance.flatten(), kde_kws={"cumulative": True})

# plt.savefig("stackoverflow.pdf", dpi=300)
plt.tight_layout(rect=(0, 0, 1, 0.95))
plt.show()

这里的CDF(累积分布函数)是正确的(范围:[0,1]),但是得到的直方图与我的预期不符:

Histogram with values out of valid range

为什么直方图的结果在[0, 4]范围内?有没有方法可以修复这个问题?

直方图实际上已经被规范化,但是从它的密度来看。本质上,sum(bin_heights*bin_widths) == 1.0 - tel
如果你真的想让bin高度总和为1.0,你也可以使用numpy.histogram函数自己计算它们。我已经在我的答案下面添加了一个例子。 - tel
2个回答

2

你认为你想要的

以下是如何绘制直方图,使得每个区间的和为1:

import matplotlib.pyplot as plt
import matplotlib.ticker as tck
import seaborn as sns
import numpy as np

sns.set(style='dark')

imagen2 = plt.figure(1, figsize=(5, 2))
imagen2.suptitle('StackOverflow Matplotlib histogram demo')

luminance = numpy.random.randn(1000, 1000)
# "Luminance" should range from 0.0...1.0 so we normalize it
luminance = (luminance - luminance.min())/(luminance.max() - luminance.min())

# get the histogram values
heights,edges = np.histogram(luminance.flat, bins=30)
binCenters = (edges[:-1] + edges[1:])/2

# norm the heights
heights = heights/heights.sum()

# get the cdf
cdf = heights.cumsum()

left = plt.subplot(121)
left.imshow(luminance)
right = plt.subplot(122)
right.plot(binCenters, cdf, binCenters, heights)

# plt.savefig("stackoverflow.pdf", dpi=300)
plt.tight_layout(rect=(0, 0, 1, 0.95))
plt.show()

# confirm that the hist vals sum to 1
print('heights sum: %.2f' % heights.sum())

输出:

enter image description here

heights sum: 1.00

实际答案

这个其实非常简单。只需要做:

sns.distplot(luminance.flatten(), kde_kws={"cumulative": True}, norm_hist=True)

这是我运行您的脚本并进行上述修改后得到的结果:

enter image description here

“惊喜转折!”
“所以事实证明,根据正式的等式,你的直方图一直都是被归一化了的:”

enter image description here

通俗易懂地说,常见做法是将连续值直方图(即可以用浮点数表示其观察结果的直方图)按照其密度进行归一化处理。因此,在这种情况下,通过运行您脚本的简化版本,可以看到条柱宽度乘以条柱高度的总和为1.0:
import matplotlib.pyplot as plt
import matplotlib.ticker as tck
import numpy as np

imagen2 = plt.figure(1, figsize=(4,3))
imagen2.suptitle('StackOverflow Matplotlib histogram demo')

luminance = numpy.random.randn(1000, 1000)
luminance = (luminance - luminance.min())/(luminance.max() - luminance.min())

heights,edges,patches = plt.hist(luminance.ravel(), density=True, bins=30)
widths = edges[1:] - edges[:-1]

totalWeight = (heights*widths).sum()

# plt.savefig("stackoverflow.pdf", dpi=300)
plt.tight_layout(rect=(0, 0, 1, 0.95))
plt.show()
print(totalWeight)

而且totalWeight的确会准确等于1.0,可能存在一点舍入误差。

1

tel的回答很棒! 我只是想提供一种替代方法,用更少的代码来给你想要的直方图。关键思想是使用matplotlib hist函数中的weights参数来规范化计数。您可以将sns.distplot(luminance.flatten(), kde_kws={"cumulative": True})替换为以下三行代码:

lf = luminance.flatten()
sns.kdeplot(lf, cumulative=True)
sns.distplot(lf, kde=False,
             hist_kws={'weights': numpy.full(len(lf), 1/len(lf))})

enter image description here

如果您想在第二个y轴上看到直方图(视觉效果更好),请将ax=bottom_left.twinx()添加到sns.distplot中:

enter image description here


1
这正是我所需要的(直方图/CDF,值为样本的百分比)。我只需添加 yaxis.set_major_formatter(ticker.PercentFormatter(xmax=1)) 就可以了。谢谢! - amyspark
不客气。我很高兴你解决了你的问题。 - Y. Luo

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