直方图是根据给定数据进行频数分析后利用各分箱中的频数等统计量绘图,核密度估计图则是根据给定的有限的样本数据对总体进行估计,得到描述总体的概率密度函数并绘图。[大谦Excel,dqexcel点com]
一元核密度估计曲线图
直方图是直接用紧密排列的柱形表现样本数据的分布特征。核密度估计则不同,它是使用非参数的方法,用有限的样本数据对总体进行估计,得到描述总体特征的概率密度函数。一元核密度估计曲线图如图4-9所示。
4.1节绘制直方图使用的是各分箱中的数据点数,即频数,将频数换成频率,即各分箱中的数据点数除以总数据个数,得到频率直方图。采用极限思维,理论上将数据区间无限细分,频率直方图将变成一条曲线,即概率密度曲线。
用核密度估计得到的概率密度函数为:
其中,x是自变量,n表示样本数据的个数,h表示带宽,K为核函数,xi表示样本数据中的一个数据点。可见,用核密度估计得到的概率密度函数是用各点处的核函数加权平均得到的,与正态分布、指数分布等的参数表示不同。
常用的核函数有高斯核函数、Box函数、三角函数等。核函数必须是对称的,值大于0,并且曲线下的面积为1。
图4-9 核密度估计曲线图
用Python xlwings编程绘制核密度估计曲线图的关键在于核密度估计的计算。下面的KDE函数实现上面核密度估计的计算公式,可以计算任意x处的概率密度值。KDE函数使用的核函数为高斯核函数,用标准正态分布的概率密度函数进行计算。完整代码见:Samples->ch07 数值型图表->05 一元核密度估计曲线图->py.py。
Function KDE(rng As Range, dblX As Double, dblW As Double) As Double
Dim dblSum As Double
Dim intCount As Integer
Dim cell As Range
Dim dblXI As Double
Dim dblH As Double
dblSum = 0
intCount = 0
dblH = dblW
For Each cell In rng
dblXI = cell.Value
dblSum = dblSum + (1 / Sqr(2 * 3.1415926)) * _
Exp(-0.5 * ((dblX - dblXI) / dblH) * ((dblX - dblXI) / dblH))
intCount = intCount + 1
Next
KDE = dblSum / intCount / dblH
End Function
Sub CreateCharts()
Dim rng As Range
Set rng = Range("A2:A61")
Dim intI As Integer
Dim kdeX(1 To 180, 1 To 1) As Double
Dim kdeF(1 To 180, 1 To 1) As Double
Dim kdeP(1 To 183, 1 To 2) As Single
For intI = 1 To 180
kdeX(intI, 1) = (intI - 60) / 10
kdeF(intI, 1) = KDE(rng, CDbl((intI - 60) / 10), 1.5)
Next
ActiveSheet.Range("C2:C181").Value = kdeF
ActiveSheet.Range("C2:C181").Select
Dim shp As Shape
Dim cht As Chart
Set shp = ActiveSheet.Shapes.AddChart2()
Set cht = shp.Chart
cht.ChartType = xlXYScatter
cht.Axes(1).MinimumScale = -5.9
cht.Axes(1).MaximumScale = 12
cht.Axes(2).MinimumScale = 0
cht.Axes(2).MaximumScale = 0.15
Dim ser1 As Series
Set ser1 = cht.SeriesCollection(1)
ser1.Delete
cht.SeriesCollection.NewSeries
Dim ax1 As Axis
Dim ax2 As Axis
Set ax1 = cht.Axes(1)
Set ax2 = cht.Axes(2)
ax1.CrossesAt = ax1.MinimumScale
ax2.CrossesAt = ax2.MinimumScale
SetStyle cht
For intI = 1 To 180
kdeP(intI, 1) = ShapeX(cht, CDbl(kdeX(180 - intI + 1, 1)))
kdeP(intI, 2) = ShapeY(cht, CDbl(kdeF(180 - intI + 1, 1)))
Next
kdeP(181, 1) = kdeP(180, 1)
kdeP(181, 2) = ShapeY(cht, 0)
kdeP(182, 1) = kdeP(1, 1)
kdeP(182, 2) = ShapeY(cht, 0)
kdeP(183, 1) = kdeP(1, 1)
kdeP(183, 2) = kdeP(1, 2)
Dim shp2 As Shape
Set shp2 = cht.Shapes.AddPolyline(kdeP)
shp2.Fill.Visible = False
shp2.Line.ForeColor.RGB = RGB(0, 0, 255)
shp2.Line.Weight = 1.5
End Sub
运行代码生成图4-9。
单色填充核密度估计曲线图
如果觉得图4-9中的核密度估计曲线图过于单调,可以考虑将曲线和坐标系横轴围成的区域用颜色进行填充。可以用单色填充,也可以用渐变色填充。单色填充的效果如图4-10所示。
图4-10 单色填充核密度估计图
核密度估计曲线相关的计算与4.2.1小节的相同。不同的是最后绘制多边形区域,而不是多义线。多边形区域的顶点按逆时针方向排列,最后一个顶点与第一个顶点重合。完整代码见:Samples->ch07 数值型图表->06 单色填充核密度估计曲线图->py.py。
Sub CreateCharts()
'省略部分代码
Dim ser1 As Series
Set ser1 = cht.SeriesCollection(1)
ser1.Delete
cht.SeriesCollection.NewSeries
Dim ax1 As Axis
Dim ax2 As Axis
Set ax1 = cht.Axes(1)
Set ax2 = cht.Axes(2)
ax1.CrossesAt = ax1.MinimumScale
ax2.CrossesAt = ax2.MinimumScale
SetStyle cht
For intI = 1 To 180
kdeP(intI, 1) = ShapeX(cht, CDbl(kdeX(180 - intI + 1, 1)))
kdeP(intI, 2) = ShapeY(cht, CDbl(kdeF(180 - intI + 1, 1)))
Next
kdeP(181, 1) = kdeP(180, 1)
kdeP(181, 2) = ShapeY(cht, 0)
kdeP(182, 1) = kdeP(1, 1)
kdeP(182, 2) = ShapeY(cht, 0)
kdeP(183, 1) = kdeP(1, 1)
kdeP(183, 2) = kdeP(1, 2)
Dim shp3 As Shape
Set shp3 = cht.Shapes.AddPolyline(kdeP)
shp3.Fill.ForeColor.RGB = RGB(0, 0, 255)
shp3.Fill.Transparency = 0.5
shp3.Line.ForeColor.RGB = RGB(0, 0, 255)
shp3.Line.Weight = 2
End Sub
运行代码生成图4-10。
渐变色填充核密度估计曲线图
4.2.2小节用单色填充核密度估计曲线与横轴之间的区域,这里用渐变色进行填充,用到了多边形区域渐变色填充的知识。渐变色填充的效果如图4-11所示。
图4-11 渐变色填充核密度估计曲线图
核密度估计曲线相关的计算与4.2.1小节的相同。不同的是最后绘制多边形区域,而不是多义线。多边形区域的顶点按逆时针方向排列,最后一个顶点与第一个顶点重合。这里使用FillFormat对象的OneColorGradient方法实现单色渐变填充,也可以实现双色渐变填充或多色渐变填充。完整代码见:Samples->ch07 数值型图表->07 渐变色填充核密度估计曲线图->py.py。
Sub CreateCharts()
'省略部分代码
For intI = 1 To 180
kdeP(intI, 1) = ShapeX(cht, CDbl(kdeX(180 - intI + 1, 1)))
kdeP(intI, 2) = ShapeY(cht, CDbl(kdeF(180 - intI + 1, 1)))
Next
kdeP(181, 1) = kdeP(180, 1)
kdeP(181, 2) = ShapeY(cht, 0)
kdeP(182, 1) = kdeP(1, 1)
kdeP(182, 2) = ShapeY(cht, 0)
kdeP(183, 1) = kdeP(1, 1)
kdeP(183, 2) = kdeP(1, 2)
Dim shp3 As Shape
Set shp3 = cht.Shapes.AddPolyline(kdeP)
shp3.Fill.Transparency = 0.5
shp3.Fill.ForeColor.RGB = RGB(0, 0, 255)
shp3.Fill.OneColorGradient msoGradientHorizontal, 1, 1
shp3.Line.ForeColor.RGB = RGB(0, 0, 255)
shp3.Line.Weight = 1
End Sub
运行代码生成图4-11。
复合核密度估计曲线图
复合核密度估计曲线图是在同一幅图中绘制多个向量数据的核密度估计曲线图,如图4-13所示。常将曲线下的区域设置为半透明,这样可以看到区域下方的其他曲线。
图4-13 复合核密度估计曲线图
绘制复合核密度估计曲线图,一个一个地绘制即可。为了使代码更简洁,将绘制单个核密度估计曲线图地代码写成draw_kde函数,方便重复调用。完整代码见:Samples->ch07 数值型图表->08 复合一元核密度估计曲线图->py.py。
Sub CreateCharts()
'省略部分代码
For intI = cht.SeriesCollection.count To 1 Step -1
cht.SeriesCollection(intI).Delete
Next
cht.SeriesCollection.NewSeries
ax1.CrossesAt = ax1.MinimumScale
ax2.CrossesAt = ax2.MinimumScale
DrawKDE rng(1), cht, 0, 0, 0, 255, -10, 10
DrawKDE rng(2), cht, 0, 255, 128, 0, -10, 10
End Sub
运行代码生成图4-13。
山脊图:单色填充
山脊图是用核密度估计图表现多个一元数据分布特征、以及不同数据之间整体差异的常见图表,如图4-14所示。
图4-14 单色填充山脊图
与绘制复合核密度估计曲线图类似,绘制山脊图也是一个一个地绘制单个曲线图,只是图的位置不同。本例使用了颜色查找表对图表进行着色,相关内容请参见第4章。完整代码见:Samples->ch09 数值型图表->07 山脊图->py.py。
Sub CreateCharts()
'省略部分代码
For intI = cht.SeriesCollection.count To 1 Step -1
cht.SeriesCollection(intI).Delete
Next
ax1.CrossesAt = ax1.MinimumScale
ax2.CrossesAt = ax2.MinimumScale
Dim idx(1 To 8) As Double
Dim intCount As Integer
Dim intR As Integer
Dim intG As Integer
Dim intB As Integer
For intI = 1 To 8
idx(intI) = (intI - 1) / 7
Next
For intI = 8 To 1 Step -1
If Int(idx(intI) * 256) = 0 Then
intCount = 1
intR = cm(1, 1)
intG = cm(1, 2)
intB = cm(1, 3)
Else
intCount = Int(idx(intI) * 256)
intR = cm(intCount, 1)
intG = cm(intCount, 2)
intB = cm(intCount, 3)
End If
DrawKDE rng(intI), cht, 0.2 * (intI - 1), intR, intG, intB, -10, 10
Next
Dim shp6 As Shape
Dim tk1LabelPos(1 To 8) As Double
Dim tk1Labels(1 To 8) As String
For intI = 1 To 8
tk1LabelPos(intI) = (intI - 1) * 0.2
Next
tk1Labels(1) = "A"
tk1Labels(2) = "B"
tk1Labels(3) = "C"
tk1Labels(4) = "D"
tk1Labels(5) = "E"
tk1Labels(6) = "F"
tk1Labels(7) = "G"
tk1Labels(8) = "H"
For intI = 1 To 8
lf = ShapeX(cht, -11)
tp = ShapeY(cht, tk1LabelPos(intI) + 0.08)
wd = cht.PlotArea.InsideWidth / (cht.Axes(1).MaximumScale - cht.Axes(1).MinimumScale) * 1.6
ht = cht.PlotArea.InsideHeight / (cht.Axes(2).MaximumScale - cht.Axes(2).MinimumScale) * 0.1
Set shp6 = cht.Shapes.AddLabel(msoTextOrientationHorizontal, lf, tp, wd, ht)
shp6.TextFrame2.TextRange.Characters.Text = Format(CStr(tk1Labels(intI)), "0")
shp6.TextFrame2.TextRange.Characters.Font.Size = 8
shp6.TextFrame2.AutoSize = msoAutoSizeTextToFitShape
Next
Dim shp7 As Shape
Dim tk2LabelPos(1 To 11) As Double
Dim tk2Labels(1 To 11) As Double
For intI = 1 To 11
tk2LabelPos(intI) = (intI - 1) * 2 - 10
Next
For intI = 1 To 11
tk2Labels(intI) = (intI - 1) * 2 - 10
Next
For intI = 1 To 11
lf = ShapeX(cht, tk2LabelPos(intI) - 0.5)
tp = ShapeY(cht, -0.03)
wd = cht.PlotArea.InsideWidth / (cht.Axes(1).MaximumScale - cht.Axes(1).MinimumScale) * 1.6
ht = cht.PlotArea.InsideHeight / (cht.Axes(2).MaximumScale - cht.Axes(2).MinimumScale) * 0.1
Set shp7 = cht.Shapes.AddLabel(msoTextOrientationHorizontal, lf, tp, wd, ht)
shp7.TextFrame2.TextRange.Characters.Text = Format(CStr(tk2Labels(intI)), "0")
shp7.TextFrame2.TextRange.Characters.Font.Size = 8
shp7.TextFrame2.AutoSize = msoAutoSizeTextToFitShape
Next
Dim shp8 As Shape
lf = ShapeX(cht, -10)
tp = ShapeY(cht, 0)
wd = ShapeX(cht, -10)
ht = ShapeY(cht, 1.8)
Set shp8 = cht.Shapes.AddLine(lf, tp, wd, ht)
shp8.Line.Weight = 1
shp8.Line.ForeColor.RGB = RGB(0, 0, 0)
Dim shp9 As Shape
lf = ShapeX(cht, -10)
tp = ShapeY(cht, 1.8)
wd = ShapeX(cht, 10)
ht = ShapeY(cht, 1.8)
Set shp9 = cht.Shapes.AddLine(lf, tp, wd, ht)
shp9.Line.Weight = 1
shp9.Line.ForeColor.RGB = RGB(0, 0, 0)
Dim shp10 As Shape
lf = ShapeX(cht, 10)
tp = ShapeY(cht, 0)
wd = ShapeX(cht, 10)
ht = ShapeY(cht, 1.8)
Set shp10 = cht.Shapes.AddLine(lf, tp, wd, ht)
shp10.Line.Weight = 1
shp10.Line.ForeColor.RGB = RGB(0, 0, 0)
Dim shp11 As Shape
lf = ShapeX(cht, -10)
tp = ShapeY(cht, 0)
wd = ShapeX(cht, 10)
ht = ShapeY(cht, 0)
Set shp11 = cht.Shapes.AddLine(lf, tp, wd, ht)
shp11.Line.Weight = 1
shp11.Line.ForeColor.RGB = RGB(0, 0, 0)
End Sub
运行代码生成类似图4-14的山脊图。
山脊图:渐变色填充
本例用渐变色填充山脊图中的单个核密度估计曲线图,如图4-15所示。
图4-15 渐变色填充山脊图
4.2.3小节介绍了核密度估计曲线图的渐变色填充,请参阅。本例使用了颜色查找表对图表进行着色,相关内容请参见第4章。完整代码见:Samples->ch07 数值型图表->10 山脊图2->py.py。
Sub DrawKDE(rng As Range, cht As Chart, dblY As Double, intR As Integer, intG As Integer, intB As Integer, dblMinX As Double, dblMaxX As Double)
Dim intI As Integer
Dim kdeX(1 To 180, 1 To 1) As Double
Dim kdeF(1 To 180, 1 To 1) As Double
Dim kdeP(1 To 183, 1 To 2) As Single
Dim dblStep As Double
dblStep = (dblMaxX - dblMinX) / 180
For intI = 1 To 180
kdeX(intI, 1) = dblMinX + dblStep * (intI - 1)
kdeF(intI, 1) = dblY + KDE(rng, kdeX(intI, 1), 1.5)
Next
For intI = 1 To 180
kdeP(intI, 1) = ShapeX(cht, kdeX(180 - intI + 1, 1))
kdeP(intI, 2) = ShapeY(cht, kdeF(180 - intI + 1, 1))
Next
kdeP(181, 1) = kdeP(180, 1)
kdeP(181, 2) = ShapeY(cht, dblY)
kdeP(182, 1) = kdeP(1, 1)
kdeP(182, 2) = ShapeY(cht, dblY)
kdeP(183, 1) = kdeP(1, 1)
kdeP(183, 2) = kdeP(1, 2)
Dim shp3 As Shape
Set shp3 = cht.Shapes.AddPolyline(kdeP)
shp3.Fill.ForeColor.RGB = RGB(intR, intG, intB)
shp3.Fill.OneColorGradient msoGradientHorizontal, 1, 1
shp3.Line.ForeColor.RGB = RGB(intR, intG, intB)
shp3.Line.Weight = 1
End Sub
运行代码生成类似图4-15的山脊图。
二元核密度估计曲面图
前面各小节介绍了一元核密度估计,类似地,二元核密度估计也是用样本数据对总体进行估计。二元核密度估计的概率密度函数为
其中,x,y是自变量,n表示样本数据的个数,h表示带宽,K为核函数,xi,yi表示样本数据中的一个数据点。
二元核密度估计图可以用曲面图表示,也可以用等值线图表示。用Python xlwings绘制的二元核密度估计曲面图效果如图4-16所示。曲面上的环带表示用单色填充相邻三维等值线之间的曲面区域。
图4-16 二元核密度估计曲面图
用Python xlwings编程绘制二元核密度估计图的关键在于核密度估计的计算。下面的kde2函数实现上面二元核密度估计的计算公式,可以计算任意x、y处的概率密度值。kde2函数使用的核函数为二元高斯核函数,用标准二元正态分布的概率密度函数进行计算。完整代码见:Samples->ch07 数值型图表->11 二元核密度估计曲面图->py.py。
Function KDE2(rngX As Range, rngY As Range, dblX As Double, dblY As Double, dblW As Double) As Double
Dim dblSum As Double
Dim lngCount As Long
Dim cellX As Range, cellY As Range
Dim dblXI As Double, dblYI As Double
Dim dblH As Double
dblSum = 0
intcount = 0
dblH = dblW
Application.ScreenUpdating = False
For Each cellX In rngX
dblXI = cellX.Value
For Each cellY In rngY
dblYI = cellY.Value
' Gaussian Kernel (2D)
dblSum = dblSum + Exp(-((dblX - dblXI) ^ 2 + _
(dblY - dblYI) ^ 2) / (2 * dblH ^ 2)) / _
(2 * 3.1416 * dblH ^ 2)
lngCount = lngCount + 1
Next
Next
KDE2 = dblSum / lngCount
Application.ScreenUpdating = True
End Function
Sub CreateCharts()
Dim rng As Range
Dim rng2 As Range
Set rng = Range("D2:D201")
Set rng2 = Range("E2:E201")
Dim kdeX(1 To 40, 1 To 1) As Double
Dim kdeY(1 To 1, 1 To 40) As Double
Dim kdeF(1 To 40, 1 To 40) As Double
Dim intI As Integer, intJ As Integer
For intI = 1 To 40
kdeX(intI, 1) = (intI - 20) / 2
kdeY(1, intI) = (intI - 20) / 2
Next
For intI = 1 To 40
For intJ = 1 To 40
kdeF(intI, intJ) = KDE2(rng, rng2, kdeX(intI, 1), kdeY(1, intJ), 1.5)
Next intJ
Next intI
Dim sht2 As Worksheet
Set sht2 = ActiveWorkbook.Sheets.Add
For intI = 1 To 40
sht2.Cells(1, intI + 1).Value = kdeX(intI, 1)
sht2.Cells(intI + 1, 1).Value = kdeY(1, intI)
For intJ = 1 To 40
sht2.Cells(intI + 1, intJ + 1).Value = kdeF(intI, intJ)
Next
Next
Dim chartObj As ChartObject
Set chartObj = sht2.ChartObjects.Add(Left:=100, Top:=50, Width:=500, Height:=400)
With chartObj.Chart
.SetSourceData Source:=sht2.Range(sht2.Cells(2, 2), sht2.Cells(41, 41))
.ChartType = xlSurface
.HasTitle = True
.ChartTitle.Text = "Surface"
.Axes(xlCategory, xlPrimary).HasTitle = True
.Axes(xlCategory, xlPrimary).AxisTitle.Text = "X Axis"
.Axes(xlSeriesAxis, xlPrimary).HasTitle = True
.Axes(xlSeriesAxis, xlPrimary).AxisTitle.Text = "Y Axis"
.Axes(xlValue, xlPrimary).HasTitle = True
.Axes(xlValue, xlPrimary).AxisTitle.Text = "Z Axis"
End With
End Sub
运行代码生成图4-16。计算好的绘图数据输出在新建的工作表中,如图4-17所示。[大谦Excel,dqexcel点com]
图4-17 二元核密度估计计算结果