2025/8/1 15:44
这一章节将介绍如何通过IPython Notebook中嵌入JavaScript库D3来扩展其展示图像的能力。该库具有多种图像制作功能,你甚至可以用它实现连
matplotlib库无法实现的图像效果。
从这一章的多个example中,你将学到如何在纯Python环境中实现JavaScript代码。我们将使用整合能力很强的IPython Notebook作为开发环境。你还将学会编写JavaScript代码,以多种可视化方法展示pandas DataFrame中的数据。‘
10.1 开放的人口数据源
本章data分析的对象为人口数据集。
我们先来介绍一个website:美国人口调查网站(http://www.census.gov)。
【PS:上面这个网站,国内进不去!🤦】

美国人口调查局隶属于美国商务部,代表官方采集美国人口data,并对其做统计研究。该局website提供了大量CSV格式的data。前几章讲过,将CSV格式的数据导入为pandas的DataFrame形式很容易。
本章我们感兴趣的是美国各州、郡的人口data。这些data存储在文件命为
CO-EST2014-alldata.csv的CSV文件中。
首先,打开一个IPython Notebook文件,在第一个格子导入所有随后在
IPython Notebook中可能会用到的Python库:
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
%matplotlib inline
导入所有必要的库之后,接着从census.gov网站导入data。物品们需要把
CO-EST2014-alldata.csv文件直接加载为pandas的DataFrame形式。你可以在pd.read_csv()函数中用urllib2库指定文件的URL。该函数能够把CSV文件的列表格式转换为pandas的DataFrame对象,我们这里将其命名为pop2014。同时,指定dtype选项,把可能解释为数字的字段强制解释为string。

获取到data,将其存储到DataFrame对象pop2014,然后输入下述变量名查看它的结构:
pop2014
输出结果见下图:

DataFrame对象op2014包含大量我们不感兴趣的行或列,所以delete这些用不到的information,以便后续操作。首先,我们对每个州的人口data很感兴趣,因此只抽取SUMLEV列元素为40的行,把它们保存到DataFrame对象
【PS:美国人口普查局这种CSV文件中,SUMLEV(SUmmary Level)代表汇总级别,它是一个用来区分data粒度的编码字段。010表示全国数据;040表示州级数据;050表示县级数据。】
pop2014_by_state之中:
pop2014_by_state = pop2014[pop2014.SUMLEV == 40]
于是,我们得到如下图所示的DataFrame对象:

然而,我们刚得到的这个DataFrame对象,仍然包含很多用不到的列。考虑到列数很多,比起直接用drop()函数delete,仅抽取必要的列更为方便:
states = pop2014_by_state[["STNAME", "POPESTIMATE2011", "POPESTIMATE2012", "POPESTIMATE2013", "POPESTIMATE2014"]]
既然已经获取到必要的information,我们可以考虑用图形表示这些data,例如,我们可能想找出美国人口最多的五个州。
states.sort_values("POPESTIMATE2014", ascending=False).head(5)
我们将得到如下所示的DataFrame,其中各州按人口多寡降序排列。

美国人口最多的五个州
例如,你可以考虑制作条状图,按照降序展示人口最多的五个州。用matplotlib库生成这个图表很简单,但是本章,我们要借助这个example,讲解如何用JavaScript库D3在IPython Notebook中实现同样的图表。
10.2 JavaScript库D3
JavaScript库D3(data-driven documents,数据驱动文档)可用来直接查看和操纵DOM对象(HTML5),但它完全是为数据可视化而开发的,它确实也很擅长这类工作。D3库完全是由Mike Bostock开发的。
IPython Notebook文件可以用%%javascript魔术方法把JavaScript代码整合到Python代码中。
但是,跟Python代码类似的是,JavaScript代码也需要导入一些库才能执行。这些库网上就有,每次执行时必须加载它们。在HTML代码中,导入库需要使用下面这种特定的结构:
<script src = "https://cdnjs.cloudflare.com/ajax/libs/d3/3.5.5/d3.min.js"></script>
由于这是一对HTML标签,要在IPython Notebook中执行导入操作,就得换成下面这种不同的结构:
%%javascript
require.config({
paths: {
d3: "https://cdnjs.cloudflare.com/ajax/libs/d3/3.5.5/d3.min"
}
});
使用require.config()函数,可以导入所有要用到的JavaScript库。
此外,熟悉HTML代码的话,你一定会知道,若要增强HTML页面的表现能力,就得定义CSS样式。同理,你也可以在IPython Notebook中定义一组CSS样式。用IPython.core.display模块的HTML()函数,可以在IPython Notebook中编写HTML代码。CSS样式的正确定义方法为:
from IPython.display import display, HTML
display(HTML("""
<style>
.bar {
fill: steelblue;
}
.bar:hover {
fill: brown;
}
.axis {
font: 10px sans-serif;
}
.axis path,
.axis line {
fill: none;
stroke: #000;
}
.x.axis path {
display: none;
}
</style>
<div id="chart_d3"></div>
"""))
上面code最后有一个id为"chart_d3"的HTML标签<div>,它指定了D3图形在页面上的显示location。
现在我们需要编写JavaScript代码,以使用D3库的func。我们用到了Jinja2库的Template对象,这样就可以define动态的JavaScript代码,用pandas DataFrame对象的element替换模板中的变量。
如果系统中还没有安装Jinja2库,依旧可以用Anaconda的包管理器install。
conda install jinja2
或者用pip安装:
pip install jinja2
安装该库之后,就可以define模板。
import jinja2
myTemplate = jinja2.Template("""
require(["d3"], function(d3){
var data = [];
{% for row in data %}
data.push({ "state": "{{ row[1] }}", "population": {{ row[5] }} });
{% endfor %}
d3.select("#chart_d3 svg").remove();
var margin = {top: 20, right: 20, bottom: 30, left: 40},
width = 800 - margin.left - margin.right,
height = 400 - margin.top - margin.bottom;
var x = d3.scale.ordinal()
.rangeRoundBands([0, width], .25);
var y = d3.scale.linear()
.range([height, 0]);
var xAxis = d3.svg.axis()
.scale(x)
.orient("bottom");
var yAxis = d3.svg.axis()
.scale(y)
.orient("left")
.ticks(10)
.tickFormat(d3.format(".1s"));
var svg = d3.select("#chart_d3").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 + ")");
x.domain(data.map(function(d) { return d.state; }));
y.domain([0, d3.max(data, function(d) { return d.population; })]);
svg.append("g")
.attr("class", "x axis")
.attr("transform", "translate(0," + height + ")")
.call(xAxis);
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("Population");
svg.selectAll(".bar")
.data(data)
.enter().append("rect")
.attr("class", "bar")
.attr("x", function(d) { return x(d.state); })
.attr("width", x.rangeBand())
.attr("y", function(d) { return y(d.population); })
.attr("height", function(d) { return height - y(d.population); });
});
""")
但是到这里,我们还么有写完,接下来得把刚刚define好的D3图表渲染到页面上。我们还需要编写命令,把pandas DataFrame对象中的data传入模板,以把它们整合到上面所写的JavaScript代码中。运行JavaScript代码,显示图形或者渲染模板,需要调用render()函数。
display(Javascript(myTemplate.render(
data=states.sort_values("POPESTIMATE2014", ascending=False)[:5].itertuples()
)))
【PS:读者把上面加粗的code,依序run一下,就会得到最终效果图了。】
运行上述code之后,下图所示的图表将出现在前面<div>所在的格子里。该图为2014年人口i哦估计数据位列前五的州。

用条状图表示2014年美国人口最多的五个州
10.3绘制簇状条状图
至此,本章所讲的content大体上参照了Barton那片非常不错的title。然而,由于我们所抽取的data给出了美国各州过去四年的人口估计数,因此描述过去几年各州人口变化趋势的data可视化方法更有用。
这种应用场景用簇状条状图比较好,人口最多的五个州为一簇,每一簇有四块柱形区域,分别表示每一年的人口数。
可以在前面code的基础上做修改或者在IPython Notebook中重新编写code:
from IPython.display import display, HTML
display(HTML("""
<style>
.bar2011 {
fill: steelblue;
}
.bar2012 {
fill: red;
}
.bar2013 {
fill: yellow;
}
.bar2014 {
fill: green;
}
.axis {
font: 10px sans-serif;
}
.axis path,
.axis line {
fill: none;
stroke: #000;
}
.x.axis path {
display: none;
}
</style>
<div id="chart_d3"></div>
"""))
你还需要修改模板,添加其他三组人口data,以及相对应的2011、2012和2013三个年份。在簇状条状图中,这三个年份的柱形区域分别用不同的color来表示。
import jinja2
myTemplate = jinja2.Template("""
require(["d3"], function(d3){
var data = []
var data2 = []
var data3 = []
var data4 = [];
{% for row in data %}
data.push({ "state": "{{ row[1] }}", "population": {{ row[2] }} });
data2.push({ "state": "{{ row[1] }}", "population": {{ row[3] }} });
data3.push({ "state": "{{ row[1] }}", "population": {{ row[4] }} });
data4.push({ "state": "{{ row[1] }}", "population": {{ row[5] }} });
{% endfor %}
d3.select("#chart_d3 svg").remove();
var margin = {top: 20, right: 20, bottom: 30, left: 40},
width = 800 - margin.left - margin.right,
height = 400 - margin.top - margin.bottom;
var x = d3.scale.ordinal()
.rangeRoundBands([0, width], .25);
var y = d3.scale.linear()
.range([height, 0]);
var xAxis = d3.svg.axis()
.scale(x)
.orient("bottom");
var yAxis = d3.svg.axis()
.scale(y)
.orient("left")
.ticks(10)
.tickFormat(d3.format(".1s"));
var svg = d3.select("#chart_d3").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 + ")");
x.domain(data.map(function(d) { return d.state; }));
y.domain([0, d3.max(data, function(d) { return d.population; })]);
svg.append("g")
.attr("class", "x axis")
.attr("transform", "translate(0," + height + ")")
.call(xAxis);
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("Population");
svg.selectAll(".bar2011")
.data(data)
.enter().append("rect")
.attr("class", "bar2011")
.attr("x", function(d) { return x(d.state); })
.attr("width", x.rangeBand()/4)
.attr("y", function(d) { return y(d.population); })
.attr("height", function(d) { return height - y(d.population); });
svg.selectAll(".bar2012")
.data(data2)
.enter().append("rect")
.attr("class", "bar2012")
.attr("x", function(d) { return (x(d.state) + x.rangeBand()/4); })
.attr("width", x.rangeBand()/4)
.attr("y", function(d) { return y(d.population); })
.attr("height", function(d) { return height - y(d.population); });
svg.selectAll(".bar2013")
.data(data3)
.enter().append("rect")
.attr("class", "bar2013")
.attr("x", function(d) { return (x(d.state) + 2 * x.rangeBand()/4); })
.attr("width", x.rangeBand()/4)
.attr("y", function(d) { return y(d.population); })
.attr("height", function(d) { return height - y(d.population); });
svg.selectAll(".bar2014")
.data(data4)
.enter().append("rect")
.attr("class", "bar2014")
.attr("x", function(d) { return (x(d.state) + 3 * x.rangeBand()/4); })
.attr("width", x.rangeBand()/4)
.attr("y", function(d) { return y(d.population); })
.attr("height", function(d) { return height - y(d.population); });
});
""")
我们这次从DataFrame取四个序列的data传递给模板,因此需要更新data,重新渲染。此外,还需要加上render()函数。
display(Javascript(myTemplate.render(
data=states.sort_values("POPESTIMATE2014", ascending=False)[:5].itertuples()
)))
调用render()函数之后,将得到如下所示的条状图:

2011到2014年间美国人口最多的五个州的簇状条状图
10.4 地区分布图
从前面几节,你了解了如何使用JavaScript代码和D3库绘制条状图。这点成果其实用matplotlib库很容易实现,甚至效果更好。其实,前面这些code只是为了让你对D3有个大致的印象。
跟matplotlib库很不同的是,D3能够实现matplotlib实现不了的更为复杂·的图形。因此我们就来尝试D3库的强大功能。它可以实现地球分布图这类非常复杂的图形。
地区分布图表示对象的地区分布情况,其中地区分成用不同color表示的多个部分。两块区域用不同color和边界进行区分,color和边界所表示的其实就是data。
这种表示方法适用于人口或经济information的data分析结果,也适用于其他跟地理分布相关的data。
地区分布图以JSON TopJSON这种特殊文件为基础。该种文件包含制作诸如美国各地区这样的地区分布图所需的全部信息(如下图所示):

US Atlas TopJSON这个链接(https://github.com/mbostock/us-atlas)提供生成各种TopoJSON文件的相关资料,此外其他很多website也provide类似的内容。‘
有了D3库,这种图像表示法不仅可以实现,甚至还可以进行个性化处理。我们可以根据DataFrame几列element的value,为不同地理区域涂上不同color。
首先,我们从网上已经有的example入手,该example在D3库http://bl.ocks.org/mbostock/4060606之中,但是它全部是用HTML开发的。因此你需要学习如何把用HTML开发的D3例子移植到IPython Notebook之中。
查看讲解example的web,就能够看到它引入了几个必要的JavaScript库。这一次,除了D3库,还需要导入queue和TopJSON库。

因此你得用前几节define的require.config()函数:
%%javascript
require.config({
paths: {
d3: "//cdnjs.cloudflare.com/ajax/libs/d3/3.5.5/d3.min",
queue: "//cdnjs.cloudflare.com/ajax/libs/queue-async/1.0.7/queue.min",
topojson: "//cdnjs.cloudflare.com/ajax/libs/topojson/1.6.19/topjson.min"
}
})
至于CSS部分,仍需将其全部写到HTML()函数之中。
from IPython.display import display, Javascript, HTML
display(HTML("""
<style>
.counties{
fill: none;
}
.states{
fill: none;
stroke: #fff;
stroke-linejoin: round;
}
.q0-9 { fill:rgb(247, 251, 255); }
.q1-9 { fill:rgb(222, 235, 247); }
.q2-9 { fill:rgb(198, 219, 239); }
.q3-9 { fill:rgb(158, 202, 225); }
.q4-9 { fill:rgb(107, 174, 214); }
.q5-9 { fill:rgb(66, 146, 198); }
.q6-9 { fill:rgb(33, 113, 181); }
.q7-9 { fill:rgb(8, 81, 156); }
.q8-9 { fill:rgb(8, 48, 107); }
</style>
<div id="choropleth"></div>
"""))
下面是仿照Bostock给出的示例code所写的模板,只不过做了些改动。
import jinja2
choropleth = jinja2.Template("""
require(["d3", "queue", "topojson"], function(d3, queue, topojson){
d3.select("#choropleth svg").remove()
var width = 960;
height = 600;
var rateById = d3.map();
var quantize = d3.scale.quantize()
.domain([0, .15])
.range(d3.range(9).map(function(i) { return "q" + i + "-9"; }));
var projection = d3.geo.albersUsa()
.scale(1280)
.translate([width / 2, height / 2]);
var path = d3.geo.path()
.projection(projection);
var svg = d3.select("#choropleth").append("svg")
.attr("width", width)
.attr("height", height);
queue()
.defer(d3.json, "us.json")
.defer(d3.tsv, "unemployment.tsv", function(d) { rateById.set(d.id, +d.rate); })
.await(ready);
function ready(error, us){
if (error) throw error;
svg.append("g")
.attr("class", "counties")
.selectAll("path")
.data(topojson.feature(us, us.objects.counties).features)
.enter().append("path")
.attr("class", function(d) {return quantize(rateById.get(d.id)); })
.attr("d", path);
svg.append("path")
.datum(topojson.mesh(us, us.objects.states, function(a, b) { return a != b; }))
.attr("class", "states")
.attr("d", path);
}
});
""")
现在我们来渲染模板,这次不需要为模板指定data,因为所有的data都存储在JSON和TSV文件里。
display(Javascript(choropleth.render()))
结果跟Bostock例子所示的结果相同。

【PS:笔者根据书上面说的code,进行操作没有生成上述的图像。
我有尝试了一下,还是不可以,哭哭】
10.5 2014年美国人口地区分布图
既然已经了解如何从美国人口调查局抽取人口information,并学会了制作地区分布图,你可以把这些知识结合在一起,做一幅地区分布图,用深浅不同的color表示人口数量。郡人口越多,蓝色越深;人口越少,色彩越趋向白色。
本章第一节,我们从pop2014之中抽取了各州的人口data。我们只select了SUMLEV列元素为40的那些行。接下来这个example中,我们要用到各郡的人口data,因此只从pop2014中抽取SUMLEV列元素为50的行。
用下述code选择行政区域级别为50的郡。
pop2014_by_country = pop2014[pop2014.SUMLEV == 50]
pop2014_by_country
我们得到了包含美国各郡信息的DataFrame,如下图所示:

DataFrame对象pop2014_by_country包含美国各郡人口data
我们必须使用刚得到的data而不是前面TSV之中的data。pop2014_by_country对象之中,有一列ID代码对应各郡。网上有ID代码和郡名称的对应表,有了这份文件就可以知道ID代码代表哪个郡;下载该文件,将其转换为DataFrame对象。

【PS:书里面的这个网址失效了,笔者是在网络上将tsv文件下载好之后,放在了当前的工作目录上哈,殊途同归。】

TSV文件的ID为各郡的代码
例如,我们来看一下Baldwind郡:

有两个名为Baldwin的郡
你会发现有两个名为Baldwin的郡,胆它们的郡代码不同。
上表中,显示有两个郡代码不同、名称却相同。前面我们抽取census.gov的数据之后,将其保存到一个DataFrame对象中,我们现在就来看一下该DataFrame对象都包含这两个郡的那些information:
pop2014_by_country[pop2014_by_country["CTYNAME"] == "Baldwin County"]

STATE和COUNTRY列的两个元素结合起来恰好是TSV文件的ID代码
我们能找到code的对应关系。TOPOJSON中ID代码恰好对应STATE和COUNTRY列的两个元素,当STATE列的第一位为0时,要把0删除。现在可以用counties对象重新创建重现TSV那个例子所需的data。记得把data保存到population.csv文件中。
counties = pop2014_by_country[["STATE", "COUNTRY", "POPESTIMATE2014"]]
counties.is_copy = False
counties["id"] = counties["STATE"].str.lstrip("0") + "" + counties["COUNTY"]
del counties["STATE"]
del counties["COUNTY"]
counties.columns = ["pop", "id"]
counties = counties[["id", "pop"]]
counties.to_csv("population.csv")
我们再次改写HTML()函数,新增一个<div>标签,指定其id为choropleth2。
from IPython.core.display import display, Javascript, HTML
display(HTML("""
<style>
.counties{
fill: none;
}
.states{
fill: none;
stroke: #fff;
stroke-linejoin: round;
}
.q0-9 { fill:rgb(247, 251, 255); }
.q1-9 { fill:rgb(222, 235, 247); }
.q2-9 { fill:rgb(198, 219, 239); }
.q3-9 { fill:rgb(158, 202, 225); }
.q4-9 { fill:rgb(107, 174, 214); }
.q5-9 { fill:rgb(66, 146, 198); }
.q6-9 { fill:rgb(33, 113, 181); }
.q7-9 { fill:rgb(8, 81, 156); }
.q8-9 { fill:rgb(8, 48, 107); }
</style>
<div id="choropleth2"></div>
"""))
最后,还需要define一个新Template对象。
choropleth2 = jinja2.Template("""
require(["d3", "queue", "topojson"], function(d3, queue, topojson){
var data = []
d3.select("#choropleth svg").remove()
var width = 960;
height = 600;
var rateById = d3.map();
var quantize = d3.scale.quantize()
.domain([0, 1000000])
.range(d3.range(9).map(function(i) { return "q" + i + "-9"; }));
var projection = d3.geo.albersUsa()
.scale(1280)
.translate([width / 2, height / 2]);
var path = d3.geo.path()
.projection(projection);
var svg = d3.select("#choropleth2").append("svg")
.attr("width", width)
.attr("height", height);
queue()
.defer(d3.json, "us.json")
.defer(d3.tsv, "population.tsv", function(d) { rateById.set(d.id, +d.pop); })
.await(ready);
function ready(error, us){
if (error) throw error;
svg.append("g")
.attr("class", "counties")
.selectAll("path")
.data(topojson.feature(us, us.objects.counties).features)
.enter().append("path")
.attr("class", function(d) {return quantize(rateById.get(d.id)); })
.attr("d", path);
svg.append("path")
.datum(topojson.mesh(us, us.objects.states, function(a, b) { return a != b; }))
.attr("class", "states")
.attr("d", path);
}
});
""")
执行render()函数,生成图表:

【PS:上面这个图表笔者也是没有生成哈。】
10.6小结
本章展示了JavaScript库D3能够进一步扩展data展示能力。多种高级图表都可用来表示data,我们所讲的地区分布图知识其中一种。该例子也说明IPython Notebook(Jupyter)能整合读者技术。
下一章,也就是本书的最后一章,讲看一下如何对图像进行data分析。你讲发现,建立能够识别手写体数字的model其实也很简单。
















