Skip to content
On this page

复杂数据图表 · 箱线图


第 13 节 复杂数据图表 · 箱线图

箱线图是一种结合了散点图和柱状图特性的复合数据图表,它主要用于展示一组或多组离散型数值数据的多个特征值及离散程度。

箱线图可以非常好地表现一组数据中大致的整体状况,从而配合统计学方法对数据进行分析和评价。

13.1 准备数据

13.1.1 箱线图统计量

箱线图是利用离散数据中的 5 个统计量进行绘制的:最小值、第一四分位数 Q_1、中位数、第三四分位数 Q_3 以及最大值,并利用 Q_1Q_3 两个四分位数计算得到四分位距 IQR

IQR 是一种用于表示离散数据离散程度的统计量,其定义为一组离散数据中的第一四分位数与第三四分位数的差值。

IQR=Q_3-Q_1

得到了 IQR 之后便可以继续推导出离散数据箱线图的内限 [Q_1-1.5 imes IQR,Q_3+1.5 imes IQR],超出这个内限的值便为该组数据中的离群值(outlier),需要被单独标记。

虽然在 ECharts 中已经提供了这些数据的计算工具,但是为了能够更好地理解其中的统计学含义,这里将会一步步地计算这些我们需要使用到的数据。

13.1.2 计算统计量

跟前几节不一样的是,使用 Math.random() 所生成的数据在不加以处理的情况下都会呈均匀分布,而像 IQR 这些用于表示数据离散程度的统计量在这里便失去了意义。因此,这一节会使用一组真实的数据来作为将要使用的数据。

这份数据来自非常著名的物理实验 —— 迈克耳孙-莫雷实验,它是一项用于验证“以太”物质是否存在的实验。

const data = [ 850, 740, 900, 1070, 930, 850, 950, 980, 980, 880, 1000, 980, 930, 650, 760, 810, 1000, 1000, 960, 960 ]

最小值 & 最大值

最小值和最大值可以说是我们非常熟悉的一对数据统计量,在 JavaScript 中计算这两个值的方法非常多,可以先使用排序然后取头尾两值。当然在 JavaScript 中我们有更好的方法,可以分别使用 Math.min()Math.max() 来进行计算。

const min = Math.min(...data) //=> 650
const max = Math.max(...data) //=> 1070

中位数

中位数的定义为离散数据 S 在数轴上的中间值:

  • 如果离散数据的个数 n 为奇数,则中位数就为第 rac{n+1}{2} 个数值,median=S_{rac{n+1}{2}}
  • 若数据的个数 n 为偶数,则中位数为最中间两个数值的平均值,median=rac{1}{2}(S_{rac{n}{2}}+S_{rac{n}{2}+1})
let median = 0
const n = data.length
const sortedData = data.sort(function(a, b) {
  return a - b
})

if (n % 2 == 1) {
  median = sortedData[((n + 1) / 2) - 1]
} else {
  median = (sortedData[(n / 2 + 1) - 1] + sortedData[(n / 2) - 1]) / 2
}

console.log(median) //=> 940

四分位数

中位数实际上就是一个四分位数,将离散数据画在数轴上,然后以最小值和最大值作为范围,将数轴切分成四份。第一和第二份的边界点为第一四分位数,第二和第三份的边界点为中位数,而第三和第四份的边界点则为第三四分位数。

四分位数

当中位数的位置上不存在某一个特定的数值时,则取最中间两个数值的平均数。而第一和第三四分位数则除了需要合并两个相邻的数值以外,还需要根据各自的位置进行相应的计算。

比如当第一四分位数的位置上并不是特定的一个数值时,则取前一个数乘以 rac{1}{4} 的乘积加上后一个数乘以 rac{3}{4} 的乘积,而并不是两者的平均数,因为这样才更符合第一四分位数的定义。第三四分位数同理。

egin{equation}
eft
egin{array}{lr}
Q_1=S_{rac{n}{4}} & n=2k(k e 0) 
Q_1=rac{1}{4}S_{eft floor rac{n}{4} ight floor} + rac{3}{4}S_{eft floor rac{n}{4} ight floor + 1} & n=2k+1(k e 0)
nd{array}
ight.
nd{equation}

egin{equation}
eft
egin{array}{lr}
Q_3=S_{rac{3n}{4}} & n=2k(k e 0) 
Q_3=rac{3}{4}S_{eft floor rac{3n}{4} ight floor} + rac{1}{4}S_{eft floor rac{3n}{4} ight floor + 1} & n=2k+1(k e 0)
nd{array}
ight.
nd{equation}

function quantile4(data, pos) {
  if (pos < 1 || pos > 3 || pos % 1 !== 0) {
    throw 'the second argument pos should be an interger and should be 1, 2 or 3'
  }

  const sortedData = data.sort(function(a, b) {
    return a - b
  })

  const n = sortedData.length

  if ((pos * n / 4) % 1 !== 0) {
    // pos * n / 4 不为整数时
    return sortedData[pos * n / 4 - 1]
  } else {
    return (pos / 4) * (sortedData[Math.floor(pos * n / 4) - 1]) + ((4 - pos) / 4) * (sortedData[Math.floor(pos * n / 4)])
  }
}

// 使用中位数作为验证
console.log(quantile4(data, 2)) //=> 940

const Q1 = quantile4(data, 1)
const Q3 = quantile4(data, 3)

IQR

IQR 即四分位距,定义为第一四分位数和第三四分位数的差值。

const IQR = Q3 - Q1 //=> 130

内限 & 离群值

若需要判断一个数据点是否为离群值,就需要先通过四分位数和 IQR 计算出内限,再通过对比该数据点与内限来判断它是否为离群值。

const limit = [
  Q1 - 1.5 * IQR,
  Q3 + 1.5 * IQR
]

const outliers = data.filter(function(k) {
  return k < limit[0] || k > limit[1]
})

console.log(outliers) //=> [ 650 ]

13.2 绘制箱线图

凑齐了这些需要用到的数据之后,我们便可以将它们放进图表上了。与前面我们学习使用过的数据图表不一样的是,箱线图一个数据系列就要使用到五个维度的数据。所以我们在做数据绑定的时候也需要分别为这五个维度的数据进行绑定。

13.2.1 准备数据集

跟其他数据图表一样,箱线图的数据同样可以使用 dataset 来提供数据支持。分别需要将 6 个不同的维度数据传入:箱线图标识、最小值、第一四分位数、中位数、第三四分位数、最大值。

const option = {
  dataset: {
    source: [
      [ 1 /* 第一个箱线图 */, min, Q1, median, Q3, max ]
    ]
  }
}

13.2.2 准备笛卡尔坐标系

因为箱线图的数据类型是计量数据,所以我们所使用的依然是最熟悉的笛卡尔坐标系。而由于这里暂时只有一个箱线图,为了能够更好地表达数轴的概念,我们将 X 轴作为数据轴,Y 轴作为系列轴。

const option = {
  xAxis: {
    type: 'value',
    scale: true
  },
  yAxis: {
    type: 'category'
  },
}

13.2.3 绑定数据系列

绑定数据系列是使用 ECharts 绘制数据图表中最重要的一环,因为这一步直接关系到如何将数据展示在图表上。而箱线图的特殊性在这一步中则显得格外突出,它需要绑定 5 个不同维度的数据。

const option = {
  series: {
    type: 'boxplot',
    encode: {
      y: 0,
      x: [ 1, 2, 3, 4, 5 ]
    }
  }
}

完成了上面的工作以后,我们便可以将图表配置应用到图表实例中查看效果了。

boxplot-chart-1

非常好,我们已经在这个图表上看到了一个很好的效果,箱线图非常好地表现了数据的数值范围、离散程度以及中位数特征值。

13.2.4 绘制离群值

除了箱线图以外,我们还知道箱线图有一个内限,用于判断数值是否离群。如果数据中出现了离群值,我们可以将其单绘制到图表上表示出来。离群值可以使用散点 scatter 绘制在图表上。

我们可以多增加一个数据集来存储离群值的数据,以绑定 scatter 数据系列。

const option = {
  dataset: [
    {
      source: [
        [ 1, min, Q1, median, Q3, max ]
      ]
    },
    {
      source: outliers.map(function(outlier) {
        return [ 1, outlier ]
      })
    }
  ]
}

然后我们需要进一步修改数据系列,包括前面的箱线图系列。

const option = {
  series: [
    {
      type: 'boxplot',
      datasetIndex: 0,
      encode: {
        y: 0,
        x: [ 1, 2, 3, 4, 5 ]
      }
    },
    {
      type: 'scatter',
      datasetIndex: 1,
      encode: {
        y: 0,
        x: 1
      }
    }
  ]
}

boxplot-chart-2

13.3 多系列箱线图

前面我使用一组数据绘制了一个更偏向于一维数轴的箱线图,但是在实际开发和应用中,我们往往需要在一张图表上绘制多组不同数据的箱线图。就比如在进行统计试验的时候,不同的测试水平需要进行多次试验得到数据并进行分析。而多次试验的数据结果需要进行可视化,便可以使用到箱线图进行表达。

比如上面的迈克耳孙-莫雷实验,真正记录的数据肯定不止这一次,我们可以引入多组试验的数据。

const data = [
    [850, 740, 900, 1070, 930, 850, 950, 980, 980, 880, 1000, 980, 930, 650, 760, 810, 1000, 1000, 960, 960],
    [960, 940, 960, 940, 880, 800, 850, 880, 900, 840, 830, 790, 810, 880, 880, 830, 800, 790, 760, 800],
    [880, 880, 880, 860, 720, 720, 620, 860, 970, 950, 880, 910, 850, 870, 840, 840, 850, 840, 840, 840],
    [890, 810, 810, 820, 800, 770, 760, 740, 750, 760, 910, 920, 890, 860, 880, 720, 840, 850, 850, 780],
    [890, 840, 780, 810, 760, 810, 790, 810, 820, 850, 870, 870, 810, 740, 810, 940, 950, 800, 810, 870]
]

13.3.1 整合统计量计算

前面我们为一组数据计算了多个统计量以展示在箱线图上,那么在多组数据中我们可以将前面的计算过程进行封装。

function boxplotDatas(data) {
  const min = Math.min(...data)
  const max = Math.max(...data)

  let median = 0
  const n = data.length
  const sortedData = data.sort(function(a, b) {
    return a - b
  })

  if (n % 2 == 1) {
    median = sortedData[((n + 1) / 2) - 1]
  } else {
    median = (sortedData[(n / 2 + 1) - 1] + sortedData[(n / 2) - 1]) / 2
  }

  const Q1 = quantile4(data, 1)
  const Q3 = quantile4(data, 3)

  const IQR = Q3 - Q1

  const limit = [
    Q1 - 1.5 * IQR,
    Q3 + 1.5 * IQR
  ]

  const outliers = data.filter(function(k) {
    return k < limit[0] || k > limit[1]
  })

  return {
    min, max, median, Q1, Q3, outliers,
  }
}

const boxplotData = data.map(function(exprData, i) {
  return Object.assign({ id: i }, boxplotDatas(exprData))
})
//=> [
//   {id: 0, min: 650, max: 1070, median: 940, Q1: 850, …},
//   {id: 1, min: 760, max: 960, median: 845, Q1: 800, …},
//   ...
// ]

const outliers = boxplotData
  .map(function({ id, outliers }) {
    return outliers.map(function(outlier) {
      return [ id, outlier ]
    })
  })
  .reduce(function(left, right) {
    return left.concat(right)
  })
//=> [
//   [0, 650], [2, 620], ...
// ]

得到数据之后,我们就可以进行数据图表绘制了。

const option = {
  dataset: [
    {
      source: boxplotData
    },
    {
      source: outliers
    }
  ],
  xAxis: {
    type: 'category'
  },
  yAxis: {
    type: 'value',
    scale: true
  },
  series: [
    {
      type: 'boxplot',
      datasetIndex: 0,
      encode: {
        x: 'id',
        y: [ 'min', 'Q1', 'median', 'Q3', 'max' ]
      }
    },
    {
      type: 'scatter',
      datasetIndex: 1,
      encode: {
        x: 0,
        y: 1
      }
    }
  ]
}

boxplot-chart-3

小结

这一节中我们学习了一个比较复杂的数据图表,它相较于前面学习和使用过的数据图表使用到了更多维度的数值数据,其自身所具有的统计分析意义也能更直观地表达。

习题

思考箱线图、散点图、折线图、柱状图这几种用于表达计量数据的不同数据图表的异同。