核密度估计

一个常见的统计学问题是从一个样本中估计随机变量的概率密度分布函数(PDF)
这个问题被称为密度估计,对此最著名的工具是直方图。直方图是一个很好的可视化工具
(主要是因为每个人都理解它)。但是对于对于数据特征的利用却并不是非常有效率。

核密度估计(KDE对于这个问题)是一个更有效的工具。这个gaussian_kde估计方法可以被用来估计
单元或多元数据的PDF。它在数据呈单峰的时候工作的最好,但也可以在多峰情况下工作。

单元估计

我们以一个最小数据集来观察gaussian_kde是如何工作的,以及带宽(bandwidth)的不同选择方式。
PDF对应的数据被以蓝线的形式显示在图像的底端(被称为毯图(rug plot))

  1. >>> from scipy import stats
  2. >>> import matplotlib.pyplot as plt
  3. >>> x1 = np.array([-7, -5, 1, 4, 5], dtype=np.float)
  4. >>> kde1 = stats.gaussian_kde(x1)
  5. >>> kde2 = stats.gaussian_kde(x1, bw_method='silverman')
  6. >>> fig = plt.figure()
  7. >>> ax = fig.add_subplot(111)
  8. >>> ax.plot(x1, np.zeros(x1.shape), 'b+', ms=20) # rug plot
  9. >>> x_eval = np.linspace(-10, 10, num=200)
  10. >>> ax.plot(x_eval, kde1(x_eval), 'k-', label="Scott's Rule")
  11. >>> ax.plot(x_eval, kde1(x_eval), 'r-', label="Silverman's Rule")
  12. >>> plt.show()

核密度估计 - 图1

我们看到在Scott规则以及Silverman规则下的结果几乎没有差异。以及带宽的选择相比较于
数据的稀少显得太宽。我们可以定义我们的带宽函数以获得一个更少平滑的结果。

  1. >>> def my_kde_bandwidth(obj, fac=1./5):
  2. ... """We use Scott's Rule, multiplied by a constant factor."""
  3. ... return np.power(obj.n, -1./(obj.d+4)) * fac
  4. >>> fig = plt.figure()
  5. >>> ax = fig.add_subplot(111)
  6. >>> ax.plot(x1, np.zeros(x1.shape), 'b+', ms=20) # rug plot
  7. >>> kde3 = stats.gaussian_kde(x1, bw_method=my_kde_bandwidth)
  8. >>> ax.plot(x_eval, kde3(x_eval), 'g-', label="With smaller BW")
  9. >>> plt.show()

核密度估计 - 图2

我们看到如果我们设置带宽为非常狭窄,则获得PDF的估计退化为围绕在数据点的简单的高斯和。

我们现在使用更真实的例子,并且看看在两种带宽选择规则中的差异。这些规则被认为在
正态分布上很好用,但即使是偏离正态的单峰分布上它也工作的很好。作为一个非正态分布,
我们采用5自由度的t分布。

  1. import numpy as np
  2. import matplotlib.pyplot as plt
  3. from scipy import stats
  4. np.random.seed(12456)
  5. x1 = np.random.normal(size=200) # random data, normal distribution
  6. xs = np.linspace(x1.min()-1, x1.max()+1, 200)
  7. kde1 = stats.gaussian_kde(x1)
  8. kde2 = stats.gaussian_kde(x1, bw_method='silverman')
  9. fig = plt.figure(figsize=(8, 6))
  10. ax1 = fig.add_subplot(211)
  11. ax1.plot(x1, np.zeros(x1.shape), 'b+', ms=12) # rug plot
  12. ax1.plot(xs, kde1(xs), 'k-', label="Scott's Rule")
  13. ax1.plot(xs, kde2(xs), 'b-', label="Silverman's Rule")
  14. ax1.plot(xs, stats.norm.pdf(xs), 'r--', label="True PDF")
  15. ax1.set_xlabel('x')
  16. ax1.set_ylabel('Density')
  17. ax1.set_title("Normal (top) and Student's T$_{df=5}$ (bottom) distributions")
  18. ax1.legend(loc=1)
  19. x2 = stats.t.rvs(5, size=200) # random data, T distribution
  20. xs = np.linspace(x2.min() - 1, x2.max() + 1, 200)
  21. kde3 = stats.gaussian_kde(x2)
  22. kde4 = stats.gaussian_kde(x2, bw_method='silverman')
  23. ax2 = fig.add_subplot(212)
  24. ax2.plot(x2, np.zeros(x2.shape), 'b+', ms=12) # rug plot
  25. ax2.plot(xs, kde3(xs), 'k-', label="Scott's Rule")
  26. ax2.plot(xs, kde4(xs), 'b-', label="Silverman's Rule")
  27. ax2.plot(xs, stats.t.pdf(xs, 5), 'r--', label="True PDF")
  28. ax2.set_xlabel('x')
  29. ax2.set_ylabel('Density')
  30. plt.show()

核密度估计 - 图3

下面我们看到这个一个宽一个窄的双峰分布。可以想到结果将难达到以十分近似,
因为每个峰需要不同的带宽去拟合。

  1. >>> from functools import partial
  2. >>> loc1, scale1, size1 = (-2, 1, 175)
  3. >>> loc2, scale2, size2 = (2, 0.2, 50)
  4. >>> x2 = np.concatenate([np.random.normal(loc=loc1, scale=scale1, size=size1),
  5. ... np.random.normal(loc=loc2, scale=scale2, size=size2)])
  6. >>> x_eval = np.linspace(x2.min() - 1, x2.max() + 1, 500)
  7. >>> kde = stats.gaussian_kde(x2)
  8. >>> kde2 = stats.gaussian_kde(x2, bw_method='silverman')
  9. >>> kde3 = stats.gaussian_kde(x2, bw_method=partial(my_kde_bandwidth, fac=0.2))
  10. >>> kde4 = stats.gaussian_kde(x2, bw_method=partial(my_kde_bandwidth, fac=0.5))
  11. >>> pdf = stats.norm.pdf
  12. >>> bimodal_pdf = pdf(x_eval, loc=loc1, scale=scale1) * float(size1) / x2.size + \
  13. ... pdf(x_eval, loc=loc2, scale=scale2) * float(size2) / x2.size
  14. >>> fig = plt.figure(figsize=(8, 6))
  15. >>> ax = fig.add_subplot(111)
  16. >>> ax.plot(x2, np.zeros(x2.shape), 'b+', ms=12)
  17. >>> ax.plot(x_eval, kde(x_eval), 'k-', label="Scott's Rule")
  18. >>> ax.plot(x_eval, kde2(x_eval), 'b-', label="Silverman's Rule")
  19. >>> ax.plot(x_eval, kde3(x_eval), 'g-', label="Scott * 0.2")
  20. >>> ax.plot(x_eval, kde4(x_eval), 'c-', label="Scott * 0.5")
  21. >>> ax.plot(x_eval, bimodal_pdf, 'r--', label="Actual PDF")
  22. >>> ax.set_xlim([x_eval.min(), x_eval.max()])
  23. >>> ax.legend(loc=2)
  24. >>> ax.set_xlabel('x')
  25. >>> ax.set_ylabel('Density')
  26. >>> plt.show()

核密度估计 - 图4

正如预想的,KDE并没有很好的趋近正确的PDF,因为双峰分布的形状不同。通过使用默认带宽
(Scott*0.5)我们可以做得更好,再使用更小的带宽将使平滑性受到影响。这里我们真正需要
的是非均匀(自适应)带宽。

多元估计

通过gaussian_kde我们可以像单元估计那样进行多元估计。我们现在来解决二元情况,
首先我们产生一些随机的二元数据。

  1. >>> def measure(n):
  2. ... """Measurement model, return two coupled measurements."""
  3. ... m1 = np.random.normal(size=n)
  4. ... m2 = np.random.normal(scale=0.5, size=n)
  5. ... return m1+m2, m1-m2
  6. >>> m1, m2 = measure(2000)
  7. >>> xmin = m1.min()
  8. >>> xmax = m1.max()
  9. >>> ymin = m2.min()
  10. >>> ymax = m2.max()

然后我们对这些数据使用KDE:

  1. >>> X, Y = np.mgrid[xmin:xmax:100j, ymin:ymax:100j]
  2. >>> positions = np.vstack([X.ravel(), Y.ravel()])
  3. >>> values = np.vstack([m1, m2])
  4. >>> kernel = stats.gaussian_kde(values)
  5. >>> Z = np.reshape(kernel.evaluate(positions).T, X.shape)

最后我们把估计的双峰分布以colormap形式显示出来,并且在上面画出每个数据点。

  1. >>> fig = plt.figure(figsize=(8, 6))
  2. >>> ax = fig.add_subplot(111)
  3. >>> ax.imshow(np.rot90(Z), cmap=plt.cm.gist_earth_r,
  4. ... extent=[xmin, xmax, ymin, ymax])
  5. >>> ax.plot(m1, m2, 'k.', markersize=2)
  6. >>> ax.set_xlim([xmin, xmax])
  7. >>> ax.set_ylim([ymin, ymax])
  8. >>> plt.show()

核密度估计 - 图5