数据分析进阶:数据可视化
一、实验简介
图片来自pixabay.com
学习数据可视化,能够让你面对任何数据时,都能很快明白它代表了什么。本节我们将学习如何对空间数据进行可视化,并且利用 D3.js 绘制条形图。
1.1 知识点
- 百度地图开放服务的使用
- d3.js 数据可视化 API 的使用
- 简单网页的编写
1.2 准备工作
本节实验需要用到上一实验中保存的数据,请在完成上一实验后再进行本节实验。
二、在地图上显示聚类的簇中心
图片来自pixabay.com
2.1 注册百度地图开放平台和创建应用
为了访问方便,本节实验使用的是百度地图,需要注册相关的账号和创建应用。
在浏览器中打开百度地图开放平台(http://lbsyun.baidu.com/
),使用百度账户登录后,进入 API 控制台(http://lbsyun.baidu.com/apiconsole/key
)。
如果你还没有百度账户,请自行注册和验证。
在 API 控制台下的 “查看应用” 页面,点击 “创建应用” 按钮。
在创建应用相关的表单内,依次填写应用名称(请自行决定)、选择应用类型为浏览器端、启用所有服务(默认是全选),以及设置 Referer白名单 为 *
。
我们目前是通过调试的方式去使用百度地图的相关开放服务,因此设置为 * 以便于减少开发步骤。如果你要在即将上线的正式应用中使用上述服务,请设置合理的白名单。
应用创建成功后,你就可以在应用列表中看到刚刚创建的这个应用的 AK ( Authentication Key ,认证密钥)。
在稍后的步骤中,我们会用到该应用的 AK 去调用百度地图相关的 API 服务。
注意:为了保证应用安全,本课程中均以字符串
piqXXXXXXXXXXXXXXXXXXXXXXXXXXBue
代表应用的 AK,在学习过程中,请将其替换为你自己创建的应用的 AK。
2.2 静态图 API
百度地图静态图API,可实现将百度地图以图片形式嵌入到网页中。作为开发者,我们只需要发送HTTP请求,访问百度地图静态图服务,就可以在网页上以图片形式显示指定区域和大小地图。
静态图API是百度地图Web服务API中的一种,它根据所设定的参数,通过标准HTTP协议,返回PNG格式的地图图片。我们还可以通过高级模式向静态图中添加标注(Marker),将聚类结果中的各个簇的中心标注在地图中。
静态图的 API 文档地址为:
http://lbsyun.baidu.com/index.php?title=static
我们需要向服务地址 http://api.map.baidu.com/staticimage/v2
发起请求。以下几个参数是必需的:
ak
:用户的访问密钥 AK。width
:图片宽度。取值范围:(0, 1024]。height
:图片高度。取值范围:(0, 1024]。center
:地图中心点位置,参数可以为经纬度坐标或名称。在本例中我们将其设置为103.939612,30.806829
。zoom
:地图级别。高清图范围[3, 18];低清图范围[3,19]markers
:标注,可通过经纬度或地址/地名描述;多个标注之间用竖线分隔。在本例中我们需要将 CSV 文件中的结果按照该参数的格式进行转换。
2.3 准备数据
在上一节实验中,我们保存了聚类的结果。结果位于 /home/shiyanlou/kmResult
目录中。
请双击打开桌面上的终端。这里要将分散在各个文件中的结果合并在单个文件中,同时要去除首尾的括号。
第一步是去除行首的括号。我们用 sed 命令来进行指定字符的删除操作,正则表达式 s/^.//
匹配了行首的第一个字符(左括号)。
请在终端输入下面的代码。
cd ~/kmResult
sed 's/^.//' kmresult.csv >> tmp1.csv
同样地,去除行尾的括号(正则表达式 s/.$//
)匹配了行尾的倒数第一个字符(右括号)。
请在终端输入下面的代码。
sed 's/.$//' tmp1.csv >> tmp2.csv
最后,我们需要将所有的换行符替换为分隔符 |
。
在正则表达式 :a;N;$!ba;s/\n/|/g
中: a
用于创建一个标记,通过 N
追加当前行和下一行到模式区域。 $!ba
的意思是如果处于最后一行前,则跳转到之前的标记处。最后定义好的置换操作,把模式区域(即整个文件)的每一个换行符 \n
换成一个分隔符 |
。
请在终端输入下面的代码。
sed ':a;N;$!ba;s/\n/|/g' tmp2.csv >> markers.txt
用 gedit 编辑器打开 markers.txt 文件,复制所有的 marker 参数,组合成最终的请求参数即可。
2.4 绘制地图
数据准备完成后,就可以向 API 发起请求。
首先检查一下设置好的各项参数。
- 应用授权码 AK (请替换为自己申请得到的 AK)。
ak=piqXXXXXXXXXXXXXXXXXXXXXXXXXXBue
- 图片的宽高,分别设置为 800 和 600 像素。
width=800
height=600
- 地图中心点的经纬度(请使用下方提供的值)。
center=103.939612,30.806829
- 标注的经纬度列表(请复制 markers.txt 中的内容)。
markers=104.06726384996709,30.609450493548763|103.62864092873662,30.927454678591776|104.01666698914191,30.649518166062673|103.87194641489599,30.72336475457009|104.11963245741659,30.63820183981919|104.14202993072495,30.751837570337692|103.98880611865235,30.556266759235072|104.30711381230006,30.567120027017427|104.03249349460725,30.69568173519963|104.07578233852502,30.664482433742762
- 地图的缩放级别(请使用下方提供的值)
zoom=11
最后,我们将上述各项参数组合起来,向 API 地址发起请求。最终组合而成的 URL 为(请替换自己申请的 AK):
http://api.map.baidu.com/staticimage/v2?width=800&height=600¢er=103.939612,30.806829&markers=104.06726384996709,30.609450493548763|103.62864092873662,30.927454678591776|104.01666698914191,30.649518166062673|103.87194641489599,30.72336475457009|104.11963245741659,30.63820183981919|104.14202993072495,30.751837570337692|103.98880611865235,30.556266759235072|104.30711381230006,30.567120027017427|104.03249349460725,30.69568173519963|104.07578233852502,30.664482433742762&zoom=11&ak=piqXXXXXXXXXXXXXXXXXXXXXXXXXXBue
在浏览器中打开这个 URL, 即可得到聚类结果的 10 个簇中心在地图上的位置,如下图所示。
2.5 进阶:通过逆地址解析获取各中心位置的区域和道路名称
百度地图开放服务还提供了 Geocoding API ,用于提供从地址到经纬度坐标或者从经纬度坐标到地址的转换服务。
我们可以使用该项 API 中的逆地理编码功能来获取指定经纬度的结构化地址信息。逆地址编码即逆地址解析,例如“lat:31.325152,lng:120.558957”的逆地址解析结果是“江苏省苏州市虎丘区塔园路318号”。
作为进阶内容,本小节不提供实现细节,但你可以参考我们提供的思路,使用 Scala 或 Python 语言,甚至是手动地逐条完成上述簇中心的逆地址解析。
一种可供实现的思路提示为:
- 逐条读取 tmp2.csv 中的记录。
- 设置逆地址解析服务所需的各项参数。
- 为每一条记录向服务地址
http://api.map.baidu.com/geocoder/v2/
发起请求。 - 获取 API 返回的 JSON 或 XML 格式的解析结果。
- 从解析结果中提取区县名(district)、商圈名(business)街道名(street)并保存。
以下是可供参考的辅助信息:
例如,请求的地址为(请将 AK 进行替换):
http://api.map.baidu.com/geocoder/v2/?location=30.609450493548763,104.06726384996709&output=json&ak=piqXXXXXXXXXXXXXXXXXXXXXXXXXXBue
返回的信息为:
我们可以据此推断火车南站及桐梓林商圈附近是出租车较为活跃的地区,如果我们想打车的话,应尽量向这些商圈靠近。
三、通过绘制柱状图比较不同的数据
图片来自pixabay.com
作为最基本的图表形式之一,柱状图(bar chart)是一种以长方形的长度为变量的表达图形的统计报告图。它由一系列高度不等的纵向条纹表示数据分布的情况,用来比较两个或以上的价值(不同时间或者不同条件)。柱状图可以纵向或者横向排列,或者是用多维方式表达。
3.1 创建项目
绘制柱状图时,我们仍然使用 D3.js 。关于数据可视化,已在课程《大数据带你看穿航班晚点的套路》中作了详细介绍,此处不再赘述。
项目最终的效果是一个网页,用于表现聚类结果中不同簇中心之间的出租车服务记录的差异。我们利用 D3.js 提供的 API 加载要绘制的 CSV 数据,并在网页中通过 javascript 代码和 HTML 标签来绘制柱状图。
在桌面上双击打开终端,下载课程所需要的 D3.js 和其他 javascript 脚本文件。
wget http://labfile.oss.aliyuncs.com/courses/736/js.tar.gz
然后对其进行解压缩。
tar zxvf js.tar.gz
下面我们来为创建项目所需要的目录和文件。首先新建一个名为 DataVisualization
的项目目录。所有的网页、js 文件和数据我们都存放于该目录中。
mkdir DataVisualization
进入到新创建的目录中,再分别创建两个名为 data
和 js
的目录用于存放 CSV 数据和 js 文件,最后再新建一个名为 index.html
的网页文件。
cd DataVisualization
mkdir data
mkdir js
touch index.html
数据可视化的数据来自于上一实验中我们保存在 /home/shiyanlou/busyZones/
目录下的各个文件。
首先将它合并到一个文件中。
cd ~/busyZones
cat part-* >> data.csv
在 CSV 文件的首行添加字段名称。
sed -i "1i zone,numOfServices" data.csv
把 CSV 文件复制到当前的项目目录下的 data
文件夹中。
cd ~/DataVisualization
cp ~/busyZones/data.csv ./data
就整个项目而言,我们需要在 index.html 网页中调用 D3.js 的相关 API ,来编写主要的数据可视化逻辑。并且会添加相应的 html 元素来显示数据可视化结果。因此我们还需要将之前解压的两个 js 文件复制到项目目录的 js 文件夹下。
cp ~/*.js ./js
最后我们使用 tree 命令来查看项目目录的文件结构。
tree .
3.2 数据可视化设计
3.2.1 导入项目目录
双击打开桌面上的 Brackets ,在左侧的工具栏中点击 Getting Started 按钮,选择 Open Folder... 选项。
在弹出的目录选择对话框中,选择 shiyanlou 家目录下,我们之前创建的 DataVisualization 文件夹。
导入项目目录之后的界面如下图所示。
稍后我们将编辑该项目。
3.2.2 编辑 index.html
在 index.html 中,我们需要插入一些基本的 HTML 元素。
请在 index.html 中加入以下内容。
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Chengdu Taxi Services by Zones</title>
</head>
<body>
</body>
</html>
因为我们会用 d3.js 的相关 API 来进行绘图,所以这里需要在 HTML 代码中引用 d3.min.js 文件。此外,我们还会利用 d3.js 附加的 Tips 组件来设计浮动的提示框,因此需要引用 d3.tip.v0.6.3.js 文件。
请在 <head>
标签之间插入以下语句来引用相应的 js 文件。
插入的代码为:
<script src="js/d3.min.js"></script>
<script src="js/d3.tip.v0.6.3.js"></script>
我们在 src 属性中设置了 js 文件的来源为网页根目录下的 js 目录中的对应 js 文件。
之后,我们需要在 <body>
标签之间插入一对 <script>
标签用于自定义绘图的逻辑。绘图需要 svg 元素,但我们可以通过代码动态创建它。
<script>
// TODO: add text here.
</script>
需要在该标签中填充的 JavaScript 代码已在下方给出,我们在注释中予以讲解。
// 设置画布大小和边距
var margin = {
top: 40,
right: 20,
bottom: 30,
left: 100
},
width = 960 - margin.left - margin.right,
height = 500 - margin.top - margin.bottom;
// 设置 X 方向的范围和序数
var x = d3.scale.ordinal()
.rangeRoundBands([0, width], .1);
// 设置 Y 方向的范围
var y = d3.scale.linear()
.range([height, 0]);
// 设置 X 轴的大小和位置
var xAxis = d3.svg.axis()
.scale(x)
.orient("bottom");
// 设置 Y 轴的大小和位置
var yAxis = d3.svg.axis()
.scale(y)
.orient("left")
.tickFormat(d3.format("1"));
// 设置鼠标指向矩形时显示的提示框组件的属性、文字位置偏移量和文字内容
var tip = d3.tip()
.attr('class', 'd3-tip')
.offset([-10, 0])
.html(function(d) {
return "<strong>Services:</strong> <span style='color:red'>" + d.numOfServices + "</span>";
})
/*
* 设置画布,在 body 标签内动态添加 svg 标签
* 同时设置画布的宽高和边距
*/
var svg = d3.select("body").append("svg")
.attr("width", width + margin.left + margin.right)
.attr("height", height + margin.top + margin.bottom)
.append("g")
.attr("transform", "translate(" + margin.left + "," + margin.top + ")");
// 调用提示框组件
svg.call(tip);
// 读取 data 目录下的 data.csv 文件
d3.csv("data/data.csv", type, function(error, data) {
// 设置 X、Y 方向上要显示的数据
x.domain(data.map(function(d) {
return d.zone;
}));
y.domain([0, d3.max(data, function(d) {
return d.numOfServices;
})]);
// 在画布上绘制 X 轴
svg.append("g")
.attr("class", "x axis")
.attr("transform", "translate(0," + height + ")")
.call(xAxis);
// 在画布上绘制 Y 轴,设置轴标签及属性
svg.append("g")
.attr("class", "y axis")
.call(yAxis)
.append("text")
.attr("transform", "rotate(-90)")
.attr("y", 6)
.attr("dy", ".71em")
.style("text-anchor", "end")
.text("Number Of Services");
// 在画布上绘制矩形,按照每项数据占总量的比例来绘制高度
// 为每个矩形设置事件监听器,在鼠标进入和移出当前元素时触发
svg.selectAll(".bar")
.data(data)
.enter().append("rect")
.attr("class", "bar")
.attr("x", function(d) {
return x(d.zone);
})
.attr("width", x.rangeBand())
.attr("y", function(d) {
return y(d.numOfServices);
})
.attr("height", function(d) {
return height - y(d.numOfServices);
})
.on('mouseover', tip.show)
.on('mouseout', tip.hide)
});
// 用于计算数据总量的函数
function type(d) {
d.numOfServices = +d.numOfServices;
return d;
}
绘制的逻辑完成后,我们需要在 <head>
标签中插入以下内容,用于美化图形。
<style>
/* 设置坐标轴的颜色和边缘属性 */
.axis path,
.axis line {
fill: none;
stroke: #000;
shape-rendering: crispEdges;
}
/* 设置矩形的填充颜色 */
.bar {
fill: #08bf91;
}
/* 设置矩形被选中时的填充颜色 */
.bar:hover {
fill: #003cff;
}
/* 设置 Tip 提示框的大小、间距和颜色等属性 */
.d3-tip {
line-height: 1;
font-weight: bold;
padding: 12px;
background: rgba(0, 0, 0, 0.8);
color: #fff;
border-radius: 2px;
}
/* 在 Tip 提示框所在的圆角矩形下方添加一个小的三角形,并设置它的大小和颜色 */
.d3-tip:after {
box-sizing: border-box;
display: inline;
font-size: 10px;
width: 100%;
line-height: 1;
color: rgba(0, 0, 0, 0.8);
content: "\25BC";
position: absolute;
text-align: center;
}
/* 单独设置三角形的位置 */
.d3-tip.n:after {
margin: -1px 0 0 0;
top: 100%;
left: 0;
}
</style>
上述代码添加的位置如下图所示:
为了便于校对,我们为你提供了 index.html 文件的所有代码,如下所示。
<!DOCTYPE html>
<!DOCTYPE html>
<meta charset="utf-8">
<style>
/* 设置坐标轴的颜色和边缘属性 */
.axis path,
.axis line {
fill: none;
stroke: #000;
shape-rendering: crispEdges;
}
/* 设置矩形的填充颜色 */
.bar {
fill: #08bf91;
}
/* 设置矩形被选中时的填充颜色 */
.bar:hover {
fill: #003cff;
}
/* 设置 Tip 提示框的大小、间距和颜色等属性 */
.d3-tip {
line-height: 1;
font-weight: bold;
padding: 12px;
background: rgba(0, 0, 0, 0.8);
color: #fff;
border-radius: 2px;
}
/* 在 Tip 提示框所在的圆角矩形下方添加一个小的三角形,并设置它的大小和颜色 */
.d3-tip:after {
box-sizing: border-box;
display: inline;
font-size: 10px;
width: 100%;
line-height: 1;
color: rgba(0, 0, 0, 0.8);
content: "\25BC";
position: absolute;
text-align: center;
}
/* 单独设置三角形的位置 */
.d3-tip.n:after {
margin: -1px 0 0 0;
top: 100%;
left: 0;
}
</style>
<body>
<script src="js/d3.min.js"></script>
<script src="js/d3.tip.v0.6.3.js"></script>
<script>
// 设置画布大小和边距
var margin = {top: 40, right: 20, bottom: 30, left: 100},
width = 960 - margin.left - margin.right,
height = 500 - margin.top - margin.bottom;
// 设置 X 方向的范围和序数
var x = d3.scale.ordinal()
.rangeRoundBands([0, width], .1);
// 设置 Y 方向的范围
var y = d3.scale.linear()
.range([height, 0]);
// 设置 X 轴的大小和位置
var xAxis = d3.svg.axis()
.scale(x)
.orient("bottom");
// 设置 Y 轴的大小和位置
var yAxis = d3.svg.axis()
.scale(y)
.orient("left")
.tickFormat(d3.format("1"));
// 设置鼠标指向矩形时显示的提示框组件的属性、文字位置偏移量和文字内容
var tip = d3.tip()
.attr('class', 'd3-tip')
.offset([-10, 0])
.html(function(d) {
return "<strong>Services:</strong> <span style='color:red'>" + d.numOfServices + "</span>";
})
/*
* 设置画布,在 body 标签内动态添加 svg 标签
* 同时设置画布的宽高和边距
*/
var svg = d3.select("body").append("svg")
.attr("width", width + margin.left + margin.right)
.attr("height", height + margin.top + margin.bottom)
.append("g")
.attr("transform", "translate(" + margin.left + "," + margin.top + ")");
// 调用提示框组件
svg.call(tip);
// 读取 data 目录下的 data.csv 文件
d3.csv("data/data.csv", type, function(error, data) {
// 设置 X、Y 方向上要显示的数据
x.domain(data.map(function(d) { return d.zone; }));
y.domain([0, d3.max(data, function(d) { return d.numOfServices; })]);
// 在画布上绘制 X 轴
svg.append("g")
.attr("class", "x axis")
.attr("transform", "translate(0," + height + ")")
.call(xAxis);
// 在画布上绘制 Y 轴,设置轴标签及属性
svg.append("g")
.attr("class", "y axis")
.call(yAxis)
.append("text")
.attr("transform", "rotate(-90)")
.attr("y", 6)
.attr("dy", ".71em")
.style("text-anchor", "end")
.text("Number Of Services");
// 在画布上绘制矩形,按照每项数据占总量的比例来绘制高度
// 为每个矩形设置事件监听器,在鼠标进入和移出当前元素时触发
svg.selectAll(".bar")
.data(data)
.enter().append("rect")
.attr("class", "bar")
.attr("x", function(d) { return x(d.zone); })
.attr("width", x.rangeBand())
.attr("y", function(d) { return y(d.numOfServices); })
.attr("height", function(d) { return height - y(d.numOfServices); })
.on('mouseover', tip.show)
.on('mouseout', tip.hide)
});
// 用于计算数据总量的函数
function type(d) {
d.numOfServices =+ d.numOfServices;
return d;
}
</script>
3.2.3 数据可视化效果预览
输入完成之后,检查是否有误,然后保存。在浏览器中打开这个 index.html 文件,即可看到最终的效果,如下图所示。
可以看到,编号为 9 的区域远比其他区域繁忙。同时,你可以结合百度地图的逆地址编码服务来获得它的地址结构化信息。
四、实验总结
在本实验中,我们使用百度地图的开放服务将空间数据(经纬度)的聚类结果在地图上进行了显示。同时,还利用了 D3.js ,将上个实验中得到的聚类结果中的各个簇的出租车服务次数进行了数据可视化操作。
总而言之,针对不同的数据,都需要我们去思考如何选择合适的数据可视化表现形式,而这也是这个系列课程所想要达到的目的。
在本课程学习结束之后,欢迎继续学习实验楼的其他课程。如果你在本课程学习过程中有任何疑问或者建议,都欢迎到讨论区与我们交流。