箱形图

箱形图常用于表示一组给定数据的分布特征,或者用于发现异常值。异常值是由于某种原因造成的数据中出现的统计上过大或过小的值,将它们纳入数据分析会影响分析结果。箱形图有简单箱形图、多色简单箱形图、渐变多色简单箱形图、复合箱形图、散点箱形图等多种类型,本节将逐一实现。[大谦Excel,dqexcel点com]

箱形图简介

常见箱形图如图5-4所示,由中间的矩形箱体、箱体中的横线、箱体上下的触须和表示异常值的点标记组合而成。矩形箱体的上边缘对应给定数据的75%分位数,下边缘对应给定数据的25%分位数,中间横线表示中值,即50%分位数。图中上触须和下触须的位置分别表示75%分位数+1.5×IQR和25%分位数-1.5×IQR,其中IQR表示75%分位数和25%分位数的差,也称为内四分极差。触须以外的数据点被判断为异常值,用点标记表示。图5-4中发现了2组数据中共3个异常值。

注意,上触须和下触须的位置不能高于最大值或低于最小值,所以,绘箱形图时,如果75%分位数+1.5×IQR的计算结果大于最大值,则上触须的位置取为最大值;如果25%分位数-1.5×IQR的计算结果小于最小值,则下触须的位置取为最小值。

Document Image

图5-4 用箱形图查找异常值

根据箱形图可以探查数据的分布特征。如果箱体长度比较大,说明数据比较分散;长度小则说明数据紧凑。表示中值的横线位于箱体中间位置时,说明数据呈正态分布,反之则呈偏态分布。

简单箱形图

简单箱形图用一组箱形表示一组数据的分布特征,如图5-5所示。从图中可以查看单组数据的分布状态,以及各组数据之间的整体波动状态。

Document Image

图5-6 用自定义函数绘制简单箱形图

使用Python xlwings编程,无法用Shapes对象的AddChart2函数直接创建箱形图。考虑自己绘制箱形图。完整代码见:Samples->ch08 统计图表->06 简单箱形图-自定义->py.py。

code.python
root=os.getcwd()    #获取当前工作路径
app=xw.App(visible=True,add_book=False)    #创建Excel应用
wb=app.books.open(root+r'/data.xlsx',read_only=False)    #打开数据文件返回工作簿对象
sht=wb.sheets('Sheet1')    #获取指定工作表对象
shp=sht.api.Shapes.AddChart2()    #创建空白图表
shp.Left=20
cht=shp.Chart    #获取图表
cht.ChartType=xw.constants.ChartType.xlXYScatter    #图表类型为散点图
ax1=cht.Axes(1)    #获取横轴
ax2=cht.Axes(2)    #获取纵轴
ax1.MinimumScale=0    #横轴最小值
ax1.MaximumScale=7
ax2.MinimumScale=0    #纵轴最小值.05
ax2.MaximumScale=0.35
ax1.CrossesAt=ax1.MinimumScale    #坐标轴相交位置
ax2.CrossesAt=ax2.MinimumScale
set_style(cht)    #设置样式
cht.SeriesCollection().NewSeries()    #新建序列
data=sht.range('B2:C101').value    #获取数据
count1=0
count2=0
count3=0
count4=0
count5=0
count6=0
d1=[]
d2=[]
d3=[]
d4=[]
d5=[]
d6=[]
#根据第2列的值对第1列数据进行筛选
for i in range(100):
    if data[i][1]==1:
        count1+=1
        d1.append(data[i][0])
    elif data[i][1]== 2:
        count2+=1
        d2.append(data[i][0])
    elif data[i][1]== 3:
        count3+=1
        d3.append(data[i][0])
    elif data[i][1]== 4:
        count4+=1
        d4.append(data[i][0])
    elif data[i][1]== 5:
        count5+=1
        d5.append(data[i][0])
    elif data[i][1]== 6:
        count6+=1
        d6.append(data[i][0])
#根据分组数据创建箱形图
draw_boxplot(app,cht,d1,count1,1,0,0,255,0.5,False)
draw_boxplot(app,cht,d2,count2,2,0,0,255,0.5,False)
draw_boxplot(app,cht,d3,count3,3,0,0,255,0.5,False)
draw_boxplot(app,cht,d4,count4,4,0,0,255,0.5,False)
draw_boxplot(app,cht,d5,count5,5,0,0,255,0.5,False)
draw_boxplot(app,cht,d6,count6,6,0,0,255,0.5,False)

draw_boxplot函数根据给定数据绘制箱形图。下面是该函数的完整代码。需要给该函数指定应用对象、所属Chart对象、绘图数据、数据个数、横坐标位置、颜色分量、宽度和是否渐变色填充等。

code.python
def draw_boxplot(app,cht,data,n,x,r,g,b,w,grad):
    p25=app.api.WorksheetFunction.Percentile(data,0.25)    #25%分位数
    p50=app.api.WorksheetFunction.Percentile(data,0.5)    #50%分位数
    p75=app.api.WorksheetFunction.Percentile(data,0.75)    #75%分位数
    iqr=p75-p25    #内四分极值
    pu=p75+1.5*iqr    #触须上界
    pl=p25-1.5*iqr    #触须下界
    minv=app.api.WorksheetFunction.Min(data)    #最小值
    maxv=app.api.WorksheetFunction.Max(data)    #最大值
    if minv>pl: pl=minv    #如果下界比最小值小,则下界取最小值
    if maxv<pu: pu=maxv    #如果上界比最大值大,则上界取最大值

    #画箱体
    bx=shape_x(cht,x-w/2)
    by=shape_y(cht,p75)
    ex=cht.PlotArea.InsideWidth/(cht.Axes(1).MaximumScale-cht.Axes(1).MinimumScale)*w
    ey=cht.PlotArea.InsideHeight/(cht.Axes(2).MaximumScale-cht.Axes(2).MinimumScale)*iqr
    shp=cht.Shapes.AddShape(1,bx,by,ex,ey)
    if grad:
        shp.Fill.ForeColor.RGB=xw.utils.rgb_to_int((r,g,b))
        shp.Fill.OneColorGradient(1, 1, 1)
        shp.Line.ForeColor.RGB=xw.utils.rgb_to_int((r,g,b))
        shp.Line.Weight=1.5
    else:
        shp.Fill.ForeColor.RGB=xw.utils.rgb_to_int((r,g,b))
        shp.Fill.Transparency=0.5    #半透明
        shp.Line.ForeColor.RGB=xw.utils.rgb_to_int((r,g,b))
        shp.Line.Weight=1.5

    #绘中值线
    bx=shape_x(cht,x-w/2)
    ex=shape_x(cht,x+w/2)
    by=shape_y(cht,p50)
    ey=shape_y(cht,p50)
    shp2=cht.Shapes.AddLine(bx,by,ex,ey)
    shp2.Line.Weight=1.5
    shp2.Line.ForeColor.RGB=xw.utils.rgb_to_int((r,g,b))

    #绘触须
    bx=shape_x(cht,x)
    ex=shape_x(cht,x)
    by=shape_y(cht,p75)
    ey=shape_y(cht,pu)
    shp2=cht.Shapes.AddLine(bx,by,ex,ey)
    shp2.Line.Weight=1.5
    shp2.Line.ForeColor.RGB=xw.utils.rgb_to_int((r,g,b))

    bx=shape_x(cht,x)
    ex=shape_x(cht,x)
    by=shape_y(cht,p25)
    ey=shape_y(cht,pl)
    shp2=cht.Shapes.AddLine(bx,by,ex,ey)
    shp2.Line.Weight=1.5
    shp2.Line.ForeColor.RGB=xw.utils.rgb_to_int((r,g,b))

    bx=shape_x(cht,x-w/4)
    ex=shape_x(cht,x+w/4)
    by=shape_y(cht,pu)
    ey=shape_y(cht,pu)
    shp2=cht.Shapes.AddLine(bx,by,ex,ey)
    shp2.Line.Weight=1.5
    shp2.Line.ForeColor.RGB=xw.utils.rgb_to_int((r,g,b))

    bx=shape_x(cht,x-w/4)
    ex=shape_x(cht,x+w/4)
    by=shape_y(cht,pl)
    ey=shape_y(cht,pl)
    shp2=cht.Shapes.AddLine(bx,by,ex,ey)
    shp2.Line.Weight=1.5
    shp2.Line.ForeColor.RGB=xw.utils.rgb_to_int((r,g,b))

    #绘异常点
    for i in range(n):
        xx=shape_x(cht,x-w/20)
        yy=shape_y(cht,data[i])
        ww=cht.PlotArea.InsideWidth/(cht.Axes(1).MaximumScale-\
cht.Axes(1).MinimumScale)*w/10
        hh=ww
        if data[i]>pu or data[i]<pl:
            shp2=cht.Shapes.AddShape(92,xx,yy,ww,hh)

运行代码生成类似图5-6的简单箱形图。

多色简单箱形图

多色简单箱形图中每个箱形的颜色都不一样,如图5-7所示。这种箱形图在各种学术期刊中也很常见。

Document Image

图5-7 多色简单箱形图

5.2.2小节最后介绍了自定义箱形图的方法,通过自定义,可以对箱形图有更多的控制。如下面代码所示,调用draw_boxplot函数绘制自定义箱形图时,为每个箱形指定不同的颜色。完整代码见:Samples->ch08 统计图表->07 多色简单箱形图->py.py。

code.python
#…
draw_boxplot(app,cht,d1,count1,1,102,188,152,0.5,False)
draw_boxplot(app,cht,d2,count2,2,170,208,157,0.5,False)
draw_boxplot(app,cht,d3,count3,3,227,234,150,0.5,False)
draw_boxplot(app,cht,d4,count4,4,252,220,137,0.5,False)
draw_boxplot(app,cht,d5,count5,5,224,106,68,0.5,False)
draw_boxplot(app,cht,d6,count6,6,138,35,63,0.5,False)
#…

运行代码,生成类似图5-7的多色简单箱形图。

水平方向多色简单箱形图

常需要绘制水平方向的箱形图,如图5-8所示。

Document Image

图5-8 水平多色简单箱形图

编写draw_boxplot_h函数绘制水平多色简单箱形图。完整代码见:Samples->ch08 统计图表->08 多色简单箱形图-水平->py.py。

code.python
def draw_boxplot_h(app,cht,data,n,x,r,g,b,w,grad):
    '''
    绘箱形图
    data为一维数组,r,g,b为颜色,[255 0 0]
    x为中心横坐标,w为宽度,grad为是否渐变色填充,n为数据个数
    '''
    p25=app.api.WorksheetFunction.Percentile(data,0.25)
    p50=app.api.WorksheetFunction.Percentile(data,0.5)
    p75=app.api.WorksheetFunction.Percentile(data,0.75)
    iqr=p75-p25
    pu=p75+1.5*iqr
    pl=p25-1.5*iqr
    minv=app.api.WorksheetFunction.Min(data)
    maxv=app.api.WorksheetFunction.Max(data)
    if minv>pl: pl=minv
    if maxv<pu: pu=maxv

    #绘中值线
    by=shape_y(cht,x-w/2)
    ey=shape_y(cht,x+w/2)
    bx=shape_x(cht,p50)
    ex=shape_x(cht,p50)
    shp2=cht.Shapes.AddLine(bx,by,ex,ey)
    shp2.Line.Weight=1.5
    shp2.Line.ForeColor.RGB=xw.utils.rgb_to_int((r,g,b))

    #画箱体
    bx=shape_x(cht,p25)
    by=shape_y(cht,x+w/2)
    ex=cht.PlotArea.InsideWidth/(cht.Axes(1).MaximumScale-cht.Axes(1).MinimumScale)*iqr
    ey=cht.PlotArea.InsideHeight/(cht.Axes(2).MaximumScale-cht.Axes(2).MinimumScale)*w
    shp=cht.Shapes.AddShape(1,bx,by,ex,ey)
    if grad:
        shp.Fill.ForeColor.RGB=xw.utils.rgb_to_int((r,g,b))
        shp.Fill.OneColorGradient(1, 1, 1)
        shp.Line.ForeColor.RGB=xw.utils.rgb_to_int((r,g,b))
        shp.Line.Weight=1.5
    else:
        shp.Fill.ForeColor.RGB=xw.utils.rgb_to_int((r,g,b))
        shp.Fill.Transparency=0.5    #半透明
        shp.Line.ForeColor.RGB=xw.utils.rgb_to_int((r,g,b))
        shp.Line.Weight=1.5

    #绘触须
    by=shape_y(cht,x)
    ey=shape_y(cht,x)
    bx=shape_x(cht,p75)
    ex=shape_x(cht,pu)
    shp2=cht.Shapes.AddLine(bx,by,ex,ey)
    shp2.Line.Weight=1.5
    shp2.Line.ForeColor.RGB=xw.utils.rgb_to_int((r,g,b))

    by=shape_y(cht,x)
    ey=shape_y(cht,x)
    bx=shape_x(cht,p25)
    ex=shape_x(cht,pl)
    shp2=cht.Shapes.AddLine(bx,by,ex,ey)
    shp2.Line.Weight=1.5
    shp2.Line.ForeColor.RGB=xw.utils.rgb_to_int((r,g,b))

    by=shape_y(cht,x-w/4)
    ey=shape_y(cht,x+w/4)
    bx=shape_x(cht,pu)
    ex=shape_x(cht,pu)
    shp2=cht.Shapes.AddLine(bx,by,ex,ey)
    shp2.Line.Weight=1.5
    shp2.Line.ForeColor.RGB=xw.utils.rgb_to_int((r,g,b))

    by=shape_y(cht,x-w/4)
    ey=shape_y(cht,x+w/4)
    bx=shape_x(cht,pl)
    ex=shape_x(cht,pl)
    shp2=cht.Shapes.AddLine(bx,by,ex,ey)
    shp2.Line.Weight=1.5
    shp2.Line.ForeColor.RGB=xw.utils.rgb_to_int((r,g,b))

    #绘异常点
    for i in range(n):
        yy=shape_y(cht,x-w/20)
        xx=shape_x(cht,data[i])
        ww=cht.PlotArea.InsideWidth/(cht.Axes(1).MaximumScale-\
cht.Axes(1).MinimumScale)*w/10
        hh=ww
        if data[i]>pu or data[i]<pl:
            shp2=cht.Shapes.AddShape(92,xx,yy,ww,hh)
#... 省略部分代码
#创建箱形图
draw_boxplot_h(app,cht,d1,count1,1,102,188,152,0.5,False)
draw_boxplot_h(app,cht,d2,count2,2,170,208,157,0.5,False)
draw_boxplot_h(app,cht,d3,count3,3,227,234,150,0.5,False)
draw_boxplot_h(app,cht,d4,count4,4,252,220,137,0.5,False)
draw_boxplot_h(app,cht,d5,count5,5,224,106,68,0.5,False)
draw_boxplot_h(app,cht,d6,count6,6,138,35,63,0.5,False)
#... 省略部分代码

单渐变色简单箱形图

多色简单箱形图中,虽然每个箱形的颜色不同,但每个箱形是单色的。本小节开始介绍如何绘制用渐变色填充箱体的箱形图。首先介绍比较简单的情况,即用相同的渐变色填充各箱体的情况。

Document Image

图5-10 自定义单渐变色简单箱形图

draw_boxplot函数中对箱体进行渐变色填充的代码如下所示,使用了FillFormat对象的TwoColorGradient方法。用grad参数指定是否使用渐变色进行填充。完整代码见:Samples->ch08 统计图表->09 单渐变色简单箱形图->py.py。

code.python
def draw_boxplot(app,cht,data,n,x,r,g,b,w,grad):
    #... 省略部分代码
#画箱体
    bx=shape_x(cht,x-w/2)
    by=shape_y(cht,p75)
    ex=cht.PlotArea.InsideWidth/(cht.Axes(1).MaximumScale-cht.Axes(1).MinimumScale)*w
    ey=cht.PlotArea.InsideHeight/(cht.Axes(2).MaximumScale-cht.Axes(2).MinimumScale)*iqr
    shp=cht.Shapes.AddShape(1,bx,by,ex,ey)
    if grad:
        shp.Fill.ForeColor.RGB=xw.utils.rgb_to_int((r,g,b))
        shp.Fill.OneColorGradient(1, 1, 1)
        shp.Line.ForeColor.RGB=xw.utils.rgb_to_int((r,g,b))
        shp.Line.Weight=1.5
    else:
        #... 省略部分代码

调用该函数时给每个箱形指定相同的颜色,设置最后一个参数的值为True,表示用渐变色进行填充。

code.python
#... 省略部分代码
#创建箱形图
draw_boxplot(app,cht,d1,count1,1,0,0,255,0.5,True)
draw_boxplot(app,cht,d2,count2,2,0,0,255,0.5,True)
draw_boxplot(app,cht,d3,count3,3,0,0,255,0.5,True)
draw_boxplot(app,cht,d4,count4,4,0,0,255,0.5,True)
draw_boxplot(app,cht,d5,count5,5,0,0,255,0.5,True)
draw_boxplot(app,cht,d6,count6,6,0,0,255,0.5,True)

运行代码,生成类似图5-10的单渐变色简单箱形图。

多渐变色简单箱形图

多渐变色简单箱形图用不同的渐变色填充各箱形的箱体,如图5-11所示。

Document Image

图5-11 多渐变色简单箱形图

用Python xlwings绘制多渐变色简单箱形图时,用draw_boxplot函数绘制渐变色填充的箱形,并给每个箱形指定不同的颜色即可。完整代码见:Samples->ch08 统计图表->10 多渐变色简单箱形图->py.py。

code.python
#... 省略部分代码
#创建箱形图
draw_boxplot(app,cht,d1,count1,1,102,188,152,0.5,True)
draw_boxplot(app,cht,d2,count2,2,170,208,157,0.5,True)
draw_boxplot(app,cht,d3,count3,3,227,234,150,0.5,True)
draw_boxplot(app,cht,d4,count4,4,252,220,137,0.5,True)
draw_boxplot(app,cht,d5,count5,5,224,106,68,0.5,True)
draw_boxplot(app,cht,d6,count6,6,138,35,63,0.5,True)

简单箱形图叠加均值连线

箱形图上没有均值信息,所以,常常在一组箱形图上叠加连接均值得到的折线。

Document Image

图5-13 箱形图叠加均值连线

使用Python xlwings实现时先计算各组数据的均值,然后利用这些均值绘制折线。完整代码见:Samples->ch08 统计图表->11 简单箱形图叠加均值连线->py.py。

code.python
#绘制均值连线
meanv=[0 for _ in range(6)]
meanv[0]=app.api.WorksheetFunction.Average(d1)    #计算均值
meanv[1]=app.api.WorksheetFunction.Average(d2)
meanv[2]=app.api.WorksheetFunction.Average(d3)
meanv[3]=app.api.WorksheetFunction.Average(d4)
meanv[4]=app.api.WorksheetFunction.Average(d5)
meanv[5]=app.api.WorksheetFunction.Average(d6)
#均值连线
for i in range(5):
    bx=shape_x(cht,i+1)
    by=shape_y(cht,meanv[i])
    ex=shape_x(cht,i+2)
    ey=shape_y(cht,meanv[i+1])
    shp2=cht.Shapes.AddLine(bx,by,ex,ey)
    shp2.Line.ForeColor.RGB=xw.utils.rgb_to_int((100,100,100))
    shp2.Line.Weight=1.5

运行代码,生成的效果类似图5-13所示。

复合箱形图

复合箱形图用多组箱形序列表示多个分组的多组数据。如图5-14所示,颜色相同的箱形构成一个序列,相邻的不同颜色的箱形构成一个分组,图中有2个序列,6个分组。

Document Image

图5-14 复合箱形图

用Python xlwings无法直接绘制复合箱形图,需要按照5.2.2小节介绍的方法自己创建。按照该方法自己绘制两组简单箱形图即可,注意计算好它们的显示位置。