# 34 | 数据处理(二):如何处理多元变量? 你好,我是月影。 数据处理是一门很深的学问,想要学好它,我们不仅需要掌握很复杂的理论,还需要不断地积累经验。不过,其中也有一些基础的数据处理技巧,掌握它们,我们就能更好地入门可视化了。 比如我们上节课重点讲解的数据分类,就是其中一种非常基础的数据处理技巧,也是数据处理的第一步。这一节课,我会以处理2014年北京市的天气历史数据为例,来和你进一步讨论数据处理的基础技巧,包括从数据到图表的展现以及处理多元变量的方法。 ## 从数据到图表展现 一般来说,我们拿到的原始数据通常可以组织成表格的形式,表格中会有很多列,每一列都代表一个变量。比如,我们拿到的这份天气历史数据,它看起来可能是下面这样的: ![](https://static001.geekbang.org/resource/image/24/48/24b87b9076ab5df4c9c8ed486a6dc948.jpg) 这里有许多变量,比如时间、最高气温、平均气温、最低气温、最高湿度、平均湿度、最低湿度、露点等等。一般的情况下,我们会将其中我们最关心的一个变量平均气温,用一个图表展现出来。具体怎么做呢?我们可以来动手操作一下。 这份数据是csv格式的,是一张表,我们先用D3.js将数据读取出来,然后结构化成JSON对象。 ``` const rawData = await (await fetch('beijing_2014.csv')).text(); const data = d3.csvParse(rawData); const dataset = data.filter(d => new Date(d.Date).getMonth() < 3) .map(d => {return {temperature: Number(d['Temperature(Celsius)(avg)']), date: d.Date, category: '平均气温'}}); console.log(dataset); ``` 如上面代码所示,我们通过fetch读取csv的数据。CSV文件格式是用逗号和回车分隔的文本,所以我们用.text()读取内容。然后我们使用d3的csvParse方法,将数据解析成JSON数组。最后,我们再通过数组的filter和map,将我们感兴趣的数据取出来。这里,我们截取了1月到3月的平均气温数据。 取到了想要的数据,接下来我们就可以将它展示出来了,这一步我们可以使用数据驱动框架。在预习篇我们讲过,数据驱动框架是一种特殊的库,它们更专注于处理数据的组织形式,将数据呈现交给更底层的图形系统(DOM、SVG、Canvas)或通用图形库(SpriteJS、ThreeJS)去完成。 但是,为了方便你理解,这里我就不使用数据驱动框架了,而是直接采用一个图表库[QCharts](https://www.qcharts.cn/),它是一个基于SpriteJS设计的图表库。与数据驱动框架相比,图表库虽然减少了灵活性,但是使用上更加方便,通过一些简单的配置,我们就可以完成图表的渲染。 用来展示平均气温最常见的图表就是折线图,展示折线图的过程可以简单分为4步:第一步是创建图表(Chart)并传入数据;第二步是创建图形(Visual),这里我们创建的是折线图,所以使用Line对象;第三步是创建横、纵两个坐标轴(Axis)、提示(ToolTip)和一个图例(Legend);最后一步是将图形、坐标轴、提示和图例都添加到图表上。具体的代码如下: ``` const { Chart, Line, Legend, Tooltip, Axis } = qcharts; const chart = new Chart({ container: '#app' }); let clientRect={bottom:50}; chart.source(dataset, { row: 'category', value: 'temperature', text: 'date' }); const line = new Line({clientRect}); const axisBottom = new Axis({clientRect}).style('grid', false); axisBottom.attr('formatter', d => ''); const toolTip = new Tooltip({ title: arr => { return arr.category } }); const legend = new Legend(); const axisLeft = new Axis({ orient: 'left',clientRect }).style('axis', false).style('scale', false); chart.append([line, axisBottom, axisLeft, toolTip, legend]); ``` 这样,我们就将图表渲染到画布上了。 ![](https://static001.geekbang.org/resource/image/3c/fd/3c35aed4df29c2617d6d877fed588ffd.jpg) ## 处理多元变量 刚才我们已经成功将平均气温这个变量用折线图展示出来了,但在很多数据可视化场景里,我们不只会关心一个变量,还会关注多个变量,比如,我们需要同时关注温度和湿度数据。那怎么才能把多个变量绘制在同一张图表上呢?换句话说,同一张图表怎么展示多元变量呢? ### 在一张图表上绘制多元变量 最简单的方式是直接在图表上同时绘制多个变量,每个变量对应一个图形,这样一张图表上就同时显示多个图形。 我们直接以刚才的代码为例,现在,我们修改例子中的代码,直接添加平均湿度数据,代码如下: ``` const rawData = await (await fetch('beijing_2014.csv')).text(); const data = d3.csvParse(rawData).filter(d => new Date(d.Date).getMonth() < 3); const dataset1 = data .map(d => { return { value: Number(d['Temperature(Celsius)(avg)']), date: d.Date, category: '平均气温'} }); const dataset2 = data .map(d => { return { value: Number(d['Humidity(%)(avg)']), date: d.Date, category: '平均湿度'} }); ``` 然后,我们修改图表的数据,将温度(dataset1)和湿度(dataset2)数据都传入图表,代码如下: ``` chart.source([...dataset1, ...dataset2], { row: 'category', value: 'value', text: 'date' }); ``` 这样,我们就得到了同时显示温度和湿度数据的折线图。 ![](https://static001.geekbang.org/resource/image/8a/f5/8a988aff89201fabea2d63629663ebf5.jpg) ### 用散点图分析变量的相关性 不过,你应该也发现了,把温度和深度同时绘制到一张折线图之后,我们很难直观地看出温度与湿度的相关性。所以,如果我们希望了解2014年全年,北京市温度和湿度之间的关联性,我们还得用另外的方式。那都有哪些方式呢? 一般来说,要分析两个变量的相关性,我们可以使用散点图,散点图有两个坐标轴,其中一个坐标轴表示变量A,另一个坐标轴表示变量B。这里,我们将平均温度、相对湿度数据获取出来,然后用QCharts的散点图(Scatter)来渲染。具体的代码和示意图如下: ``` const rawData = await (await fetch('beijing_2014.csv')).text(); const data = d3.csvParse(rawData); console.log(data); const dataset = data .map(d => { return { temperature: Number(d['Temperature(Celsius)(avg)']), humdity: Number(d['Humidity(%)(avg)']), category: '平均气温与湿度'} }); const { Chart, Scatter, Legend, Tooltip, Axis } = qcharts; const chart = new Chart({ container: '#app' }); let clientRect={bottom:50}; chart.source(dataset, { row: 'category', value: 'temperature', text: 'humdity' }); const scatter = new Scatter({ clientRect, showGuideLine: true, }); const toolTip = new Tooltip({ title: (data) => '温度与湿度:', formatter: (data) => { return `温度:${data.temperature}C 湿度:${data.humdity}% ` } }); const legend = new Legend(); const axisLeft = new Axis({ orient: 'left',clientRect }).style('axis', false).style('scale', false); const axisBottom = new Axis(); chart.append([scatter, axisBottom, axisLeft, toolTip, legend]); ``` ![](https://static001.geekbang.org/resource/image/de/01/dec1994d0a99794182f581924d17ef01.jpg) 从这个图表我们可以看出,平均温度和相对湿度并没有相关性,所以点的空间分布比较随机。事实上也是如此,气温和绝对湿度有关,但相对湿度因为已经考虑过了温度因素,所以就和气温没有相关性了。 那你可能会有疑问,相关的图形长什么样呢?我们可以用另外两个变量,比如露点和平均温度,来试试看能不能画出相关的散点图。 我们先来说说什么是露点。在空气中水汽含量不变, 并且气压一定的情况下, 空气能够冷却达到饱和时的温度就叫做露点温度, 简称露点, 它的单位与气温相同。 从定义里我们知道,露点和温度与湿度都有相关性。接下来,我们来看一下露点和温度的相关性在散点图中是怎么体现的。很简单,我们只要修改一下上面代码里的数据,把平均湿度换成平均露点温度就行了。 ``` const dataset = data .map(d => { return { temperature: Number(d['Temperature(Celsius)(avg)']), tdp: Number(d['Dew Point(Celsius)(avg)']), category: '平均气温与露点'} }); ``` 这样,我们最终呈现出来的散点图具有典型的数据正相关性,也就是说图形的点更集中在对角线附近的区域。 ![](https://static001.geekbang.org/resource/image/48/1a/483712c909fcfd962072a2c19caaeb1a.jpg) 我们还可以把湿度数据也加上。 ``` const dataset = data .map(d => { return { value: Number(d['Temperature(Celsius)(avg)']), tdp: Number(d['Dew Point(Celsius)(avg)']), category: '平均气温与露点'} }); const dataset2 = data .map(d => { return { value: Number(d['Humidity(%)(avg)']), tdp: Number(d['Dew Point(Celsius)(avg)']), category: '平均湿度与露点'} }); ... chart.source([...dataset, ...dataset2], { row: 'category', value: 'value', text: 'tdp' }); ``` 我们发现,平均湿度和露点也是成正相关的,不过露点与温度的相关性更强,因为散点更集中一些。 ![](https://static001.geekbang.org/resource/image/6d/86/6dcd043da6c8338865313105733f5286.jpg) 为了再强化理解,我们还可以看一组强相关的数据,比如平均温度和最低温度,你会发现,图上的散点基本上就在对角线上。 ![](https://static001.geekbang.org/resource/image/2b/07/2b2c8f25ed11aeb7a00aac436a2f4e07.jpg) 总的来说,两个数据的散点越集中在对角线,说明这两个数据的相关性越强,当然这么说还不够严谨只是方便我们记忆而已。这里,我找到了一张散点图和相关性之间关系的总结图,你可以多了解一下。 ![](https://static001.geekbang.org/resource/image/44/61/44ac95977e2143d67c079ec10a9c7661.jpg) ### 散点图的扩展 通过前面的例子,我们可以看到,用散点图可以分析出数据的二元变量之间的相关性,这对数据可视化场景的信息处理非常有用。不过,散点图也有明显的局限性,那就是它的维度只有二维,所以它一般只能处理二元变量,超过二维的多元变量的相关性,它处理起来就有些力不从心了。 不过,我们还不想放弃散点图在相关性上的优异表现。所以在处理高维度数据时,我们可以对散点图进行扩展,比如引入颜色、透明度、大小等信息来表示额外的数据维度,这样就可以处理多维数据了。 举个例子,我在下面给出了一张根据加州房产数据集制作的散点图。其中,点的大小代表街区人口数量、透明度代表人口密度,而颜色代表房价高低,并且加上经纬度代表点的位置。这个散点图一共表示了五维的变量(经度、纬度、人口总数、人口密度、房价高低),将它们都呈现在了一张图上,这在一定程度上表达了这些变量的相关信息。 [![](https://static001.geekbang.org/resource/image/bc/61/bcbf60ff83cf90a4d5e05d4f78b2b161.jpeg "图片来源:知乎")](https://zhuanlan.zhihu.com/p/141118125) 这里,我带你做个简单的分析。从这张图上,我们很容易就可以得出两个结论,第一个是,房价比较高的区域集中于两个中心,并且都靠近海湾。第二个是房价高的地方对应的人口密集度也高。 这就是用散点图处理多维数据的方法了。 ### 其他图表形式 事实上,处理多维信息,我们还可以用其他的图表展现形式,比如用晴雨表来表示数据变化的趋势就比较合适。北大可视化实验室在疫情期间就制作了一张疫情数据晴雨表,你能明显看出每个省份每天的疫情变化。如下所示: [![](https://static001.geekbang.org/resource/image/48/93/4837642324296791cd00230938bc2e93.jpg "图片来源:mp.weixin.qq.com")](https://mp.weixin.qq.com/s/Nq0-p6z1GO869XaS0zliiw) 再比如,还有[平行坐标图](https://ww2.mathworks.cn/help/stats/examples/visualizing-multivariate-data.html)。平行坐标图也有横纵两个坐标轴,并且把要进行对比的五个不同参数都放在了水平方向的坐标上。在下面的示意图中,绘制了所有4缸、6缸或8缸汽车在五个不同参数(变量)上的对比。 ![](https://static001.geekbang.org/resource/image/23/26/23948ddb9761907d3fd1316c506d3726.jpeg "左为平行坐标图,右为热力图") 此外,我们还可以用[热力图](https://www.jianshu.com/p/a575e53bcaa9)、[三维直方图](https://www.jianshu.com/p/a575e53bcaa9)、[三维气泡图](http://https://zhuanlan.zhihu.com/p/147243101)等等其他的可视化形式来展现多维度的信息。 ![](https://static001.geekbang.org/resource/image/d8/1e/d805e7f932c3403c9732fec8bae1141e.jpeg "左为三维直方图,右为三维气泡图") 总之,这些数据展现形式的基本实现原理,我们都在前面的视觉篇中讲过了。在掌握了视觉基础知识之后,我们就可以用自己想要的呈现形式,自由发挥,设计出各种直观的、形式有趣的图表了。 ## 要点总结 这一节课,我们主要讨论数据到图表的展现以及如何处理多元变量。 在数据到图表的展现中,我们首先用d3.js把原始数据从csv中读取出来,然后选择我们需要的数据,用简单的图表库,比如,使用QCharts图表库进行渲染。渲染过程可以分为4步,分别是:创建图表对象 Chart并传入数据,创建图形Visual,创建坐标轴、提示和图例,把图形、坐标轴、提示和图例添加到图表对象中完成渲染。 在处理多元变量的时候,我们可以用散点图表示变量的相关性。对于超过二维的数据,我们可以扩展散点图,调整颜色、大小、透明度等等手段表达额外的信息。除了散点图之外,我们还可以用晴雨表、平行坐标图、热力图、三维直方图、气泡图等等图表,来表示多维数据的相关性。 到这里,我们用两节课的时间讲完了可视化的数据处理的基础部分。这部分内容如果再深入下去,就触达了数据分析的领域了,这也是一个和可视化密切相关的学科。那我也说过,可视化的重点,一是数据、二是视觉,视觉往深入研究,就进入渲染引擎、游戏等等领域,数据往深入研究,就进入数据分析的领域。所以,在可视化的入门或者说是基础阶段,掌握我现在讲的这些基础知识就够了。当然,如果你想深入研究也是好事,你可以参考我在课后给出的文章好好阅读一下。 ## 小试牛刀 1. 我在GitHub代码仓库里放了两份数据,一份是我们今天讲课用到的,另一份是[2013到2018年的全国各地空气质量数据](https://github.com/akira-cn/graphics/blob/master/data/weather/2013-2018.csv)。你能把2014年北京每日PM2.5的数据用折线图表示出来吗?你还能结合这两份数据,用散点图分析平均气温和PM2.5指数的相关性吗? 2. 你可以模仿我北大可视化实验室的疫情晴雨表,实现一个2018年全国各城市空气质量晴雨表吗? 欢迎在留言区和我讨论,分享你的答案和思考,也欢迎你把这节课分享给你的朋友,我们下节课见! * * * ## 源码 [课程中完整示例代码见GitHub仓库](https://github.com/akira-cn/graphics/tree/master/data/weather) ## 推荐阅读 \[1\] [从1维到6维-多维数据可视化策略](https://zhuanlan.zhihu.com/p/147243101) \[2\] [QCharts](https://www.qcharts.cn/)