8.2 具体位置
一份地点清单是我们能得到的最好处理的空间数据了。我们有了一堆地点的经纬度,只需将它们标注在地图上即可。也许你还想展现事件(比如犯罪)在哪些地方发生,或者想找到事件发生最密集的是哪个区域,这些都很容易办到,而且有很多方法。
在网上,调用地图最常用的方法是借助Google或者Microsoft地图。通过他们的地图API,我们就能立刻得到可以缩放和平移的可交互地图,而这只需要几行JavaScript代码就能实现。对于如何运用这些API,有海量的在线教程,而且说明文档也很详细,所以这里我就不再赘述了。
说明 Google和Microsoft为刚开始使用他们地图API的新手们提供了超级浅显易懂的教程,如果你打算用他们的产品实现一些基本的地图功能,不妨先过一遍这些教程。
不过这种地图也有不足。它们的可定制程度很低,到最后得到的结果看上去还是像Google或Microsoft地图。我不是说它们难看,只是当我们开发应用或设计图表时,地图在视觉上还是要与设计主题相吻合。有些办法能解决这个问题,但是为此花上太多精力又不值得,尤其是还有其他工具能做得更好的时候。
8.2.1 找到纬度和经度
在绘制地图之前,先考虑一下能拿到哪些数据,又需要哪些数据。如果你连需要的数据都没有,自然也就没法可视化了,不是吗?在大多数实际应用中,你需要经纬度才能确定地点的位置,而大部分数据集都不太可能会提供这些信息,我们能拿到的更有可能只是一份地址列表而已。
自然,你不能把街道名称和邮编直接硬插到地图里面,那样不可能会有好效果。我们必须首先拿到经纬度,而要想做到这一点,可以试试地理编码(geocode)。其原理是我们把地址传给服务提供商,服务提供商会请求数据库与地址进行匹配,然后判断这个地址应该是在地球的哪个位置,之后我们就能得到该位置的经纬度。
至于有哪些服务提供商可用,嗯,倒是有不少。如果需要地理编码的地点不多,我们可以去某个网站然后手工输入查询。Geocoder.us是一个很好的选择,而且免费。如果地点不需要特别精确,也可以试试Pierre Gorissen开发的基于Google地图的Latitude Longitude Popup。该应用有一个简单的Google地图界面,在地图上点击任何位置,它都会弹出该地点的经纬度信息。
但如果你有很多地点需要地理编码,就应该通过程序来解决了,否则手工的复制粘贴太花时间。Google、Yahoo!、Geocoder.us和Mediawiki都提供地理编码的API,而Python中有一个叫做Geopy的地理编码工具箱则把它们都囊括进来了。
有用的地理编码工具
□ Geocoder.us(http://geocoder.us)——提供简单的界面,将地址复制并粘贴进去后会得到经纬度信息。还提供API。
□ Latitude Longitude Popup(www.gorissen.info/Pierre/maps/)——Google地图的混搭模式。在地图上点击某个位置,它就能告诉你该地的经纬度。
□ Geopy(http://code.google.com/p/geopy/)——Python的地理编码工具箱。在一个工具包中囊括了多个地理编码API。
访问Geopy的网页可以浏览该工具包的安装指南,还能找到很多简单的应用实例。我们下面的例子假设你已经在计算机上安装了该工具包。
打开一个新文件,将其存储为geocode-locations.py。和往常一样,首先导入你要用到的工具包。
- from geopy import gedocoders
- import csv
我们还需要一个API开发密匙(API Key)才能获得各种服务。对于本例来说,我们只用从Google那里获取一个即可。
说明 访问http://code.google.com/apis/maps/signup.html注册一个Google地图API的免费开发密匙。过程非常简单,只需几分钟就能完成。
把开发密匙存入一个名为g_api_key的变量,在初始化地理编码时使用它。
- g_api_key = 'INSERT_YOUR_API_KEY_HERE'
- g = geocoders.Google(g_api_key)
载入costcos-limited.csv数据文件,然后开始循环处理。针对每一行数据,我们把所有的地址信息拼成一个整体,然后进行地理编码。
- costcos = csv.reader(open('costcos-limited.csv'), delimiter=',')
- next(costcos) # 跳过标题
- # 打印标题
- print "Address,City,State,Zip Code,Latitude,Longitude"
- for row in costcos:
- full_addy = row[1] + "," + row[2] "," + row [3] + "," + row[4]
- place, (lat, lng) = list(g.geocode(full_addy, exactly_one=False))[0]
- print full_addy + "," + str(lat) + "," + str(lng)
这就行了。运行Python脚本,并把输出结果存储为costcos-geocoded.csv。以下是所得数据的头几行:
- Address,City,State,Zip Code,Latitude,Longitude
- 1205 N. Memorial Parkway,Huntsville,Alabama,35801-5930,34.7430949,-86
- .6009553
- 3650 Galleria Circle,Hoover,Alabama,35244-2346,33.377649,-86.81242
- 8251 Eastchase Parkway,Montgomery,Alabama,36117,32.363889,-86.150884
- 5225 Commercial Boulevard,Juneau,Alaska,99801-7210,58.3592,-134.483
- 330 West Dimond Blvd,Anchorage,Alaska,99515-1950,61.143266,-149.884217
- ...
相当酷。如果幸运的话,每一个地址都应该能找到对应的经纬度坐标。但这种情况通常不会出现。如果遇到了这种问题,你应该在之前代码的倒数第二行加入错误验证。
- try:
- place, (lat, lng) = list(g.geocode(full_addy, exactly_one=False))
- [0]
- print full_addy + "," + str(lat) + "," + str(lng)
- except:
- print full_addy + ",NULL,NULL"
这段代码会尝试寻找对应的经纬度坐标,如果寻找失败,就会输出该行数据的地址以及空的坐标值。运行Python脚本并存储输出结果,我们就能从中找到那些空值。此时我们可以用Geopy试试其他服务商,或者在Geocoder.us里手工输入这些空值的地址进行查询。
8.2.2 单纯的点
现在我们有了带经纬度信息的各个地点,可以绘制地图了。最直接的办法就是为各个地点添加标记,和真实世界中往纸板上摁图钉很相似,只不过是让计算机来完成。基本框架如图8-1所示。
图8-1 地图标识地点的基本框架
虽然这个概念很简单,但我们仍然能在数据中看到集群、扩展和异常值等特征。
1.带有点的地图
虽然R在绘制地图方面的功能比较有限,但在地图上放置点却很轻松。这一任务主要是由maps工具包来完成的。在R中输入install.packages( ),或者通过Package Installer安装maps工具包。安装好后将其载入到工作区。
- library(maps)
下一步:载入数据。你可以使用之前地理编码过的好市多店铺的地址数据,或者也可以直接通过URL载入我加工好的数据集。
- costcos <-
- read.csv('http://book.flowingdata.com/ch08/geocode/costcos-geocoded
- .csv", sep=",")
现在开始绘图。在创建地图时,把它们看做是层会比较好理解(不管你用的是什么软件)。最底层一般就是显示地域界限的基础地图,它的上面再是那些数据的层。在本例中,最底层是美国地图,而第二层就是好市多店铺的地址。以下代码可以创建第一层,结果如图8-2所示。
- map(database="state")
图8-2 美国的平面地图
第二层,或者说好市多店铺层,则要通过symbols( )函数来绘制。它也就是我们在第6章中绘制气泡图所用的函数,使用方法也完全一样,只不过这次传递的是经纬度,而非之前的x轴和y轴坐标。此外设置add为TRUE,表示我们是希望在地图上添加其他标记符号,而非创建一个新图表。
- symbols(costcos$Longitude, costcos$Latitude,
- circles=rep(1, length(costcos$Longitude)), inches=0.05, add=TRUE)
图8-3显示了代码的结果。所有的圆圈大小都相同,因为我们将circles设置为一个数组,它的长度等于地点的总数,同时我们将inches设为了0.05,它规定了各个圆圈的尺寸。如果你希望这些标记再小一点,只需要减小这个值即可。
图8-3 好市多全美店址地图
和之前一样,我们可以调整地图和圆圈的颜色,以便各个地点的显示能更突出,同时让边界线隐入到背景中去,如图8-4所示。现在让我们把这些圆点改为好市多的标准红色,同时把州界线改为浅灰色。
- map(database="state", col="#cccccc")
- symbols(costcos$Longitude, costcos$Latitude, bg="#e2373f", fg="#ffffff",
- lwd=0.5, circles=rep(1, length(costcos$Longitude)),
- inches=0.05, add=TRUE)
图8-4 为地图上的地点着色
在图8-3中,未填色的圆圈和地图都是同一种颜色,线条的粗细也一样,所以全都混杂在了一起。但如果正确运用了颜色,我们就能让数据占据视线的焦点。
只用几行代码就能达到这种效果,看起来不坏。很明显,好市多比较倾向于在沿海地带开店,圆点都聚集在加利福尼亚的南北部、华盛顿的西北部以及美国东北部。
不过,这张图还是有一个遗漏,确切地说应该是两个:阿拉斯加和夏威夷在哪里?它们也是美国的一部分,但在图中没法找到,即使map( )里已经调用了"state"数据库。实际上,这两个州是在"world"数据库里面,所以如果想看好市多在阿拉斯加和夏威夷开的店,就需要绘制整个世界地图,如图8-5所示。
- map(database="world", col="#cccccc")
- symbols(costcos$Longitude, costcos$Latitude, bg="#e2373f", fg="#ffffff",
- lwd=0.3, circles=rep(1, length(costcos$Longitude)),
- inches=0.03, add=TRUE)
图8-5 好市多店址的世界地图
我知道这有点浪费空间。有很多方法可以解决这个问题,在技术文档里面都可以找到,但最简单的办法就是在Illustrator里面把其他国家的地图删掉,然后把美国放大。
提示 在使用R时,如果遇到了问题,随时可以查阅技术文档。方法是在不熟悉的函数或工具包前面加上一个问号。
现在换一个角度,假设我们只希望显示少数几个州的好市多店址,那么可以用region参数来实现这一点。
- map(database="state", region=c("California", "Nevada", "Oregon",
- "Washington"), col="#cccccc")
- symbols(costcos$Longitude, costcos$Latitude, bg="#e2373f", fg=#ffffff",
- lwd=0.5, circles=rep(1, length(costcos$Longitude)), inches=0.05,
- add=TRUE)
效果如图8-6所示,我们创建了包括加利福尼亚、内华达、俄勒冈和墨西哥州的地图作为底层,然后在它上面再创建数据层。有一些点并不在这些州里,但它们处于绘图区域中,所以依然会显示。同样地,我们可以在自己喜欢的矢量绘图软件中删除这些点。
图8-6 选定州中的好市多店址
2.带有线的地图
有时候,如果地图上点的顺序存在关联,那么可能需要将点连接起来。随着Foursquare等在线定位服务的流行,位置追踪已经并不少见。比较简单的画线方法自然是利用lines( )函数。为了方便演示,我们来看一下我作为乌有国政府的特工人员,在7天7夜间的旅行轨迹吧。和往常一样,以载入数据开始,然后先绘制基础的世界地图。
- faketrace <-
- read.scv("http://book.flowingdata.com/ch08/points/fake-trace.txt",
- sep="\t")
- map(database="world", col="#cccccc")
在R的控制台中输入faketrace,先看一眼数据帧。你会发现它只显示了两列,分别是纬度和经度,而且只有8个数据点。我们假设这些地点的顺序正是我在这漫长7天中旅行的顺序。
- latitude longitude
- 1 46.31658 3.515625
- 2 61.27023 69.609375
- 3 34.30714 105.468750
- 4 -26.11599 122.695313
- 5 -30.14513 22.851563
- 6 -35.17381 -63.632813
- 7 21.28937 -99.492188
- 8 36.17336 -115.180664
将这两列插入lines( )函数,就可以绘制出线条了。同时指定线条的颜色(col)和宽度(lwd)。
- lines(faketrace$longitude, faketrace$latitude, col="#bb4cd4", lwd=2)
现在再加入圆点。过程和我们之前在好市多一例中所做的一样,效果如图8-7所示。
- symbols(faketrace$longitude, faketrace$latitude, lwd=1, bg="#bb4cd4",
- fg="#ffffff", circles=rep(1, length(faketrace$longitude)), inches=0.05,
- add=TRUE)
图8-7 绘制位置追踪轨迹
作为特工为乌有国政府奔波了7天7夜之后,我决定洗手不干了。这工作并不像007电影里面表现的那么迷人。不过,我还是为那些我拜访过的国家创建了连接线。从我居住的地方到所有其他国家之间画上线,可能会很有趣,效果如图8-8所示。
- map(database="world", col="#cccccc")
- for (i in 2:length(faketrace$longitude)-1) {
- lngs <- c(faketrace$longitude[8], faketrace$longitude[i])
- lats <- c(faketrace$latitude[8], faketrace$latitude[i])
- lines(lngs, lats, col="#bb4cd4", lwd=2)
- }
图8-8 绘制与世界各地的连接线
在创建好底层地图之后,我们对数据帧的每个点循环处理,让它们和数据帧最后的那个点之间用线条连接起来。这张图可能信息含量并不高,但也许你会找到合适的机会用上它。举这个例子的目的是希望大家了解,在绘制好地图之后,只要有经纬度坐标,就可以利用R的各种图形函数来随心所欲地绘制你想要的任何东西。
哦,对了,我并没有真的当过乌有国的特工。我只是在开玩笑而已。
8.2.3 有大有小的点
让我们回到真实的数据上来,进入一个比特工恶作剧更有意思的话题。有些时候,我们手上并不只有位置数据,可能还会有其他的数值,例如销售数据或者城市人口等。我们依然是在地图上绘制圆点,但这次可以用上气泡图的原则。
我应该不必再次解释气泡的尺寸是根据面积而非半径来的,对吧?很好。
带有气泡的地图
在本例中,让我们看一下《2008年联合国人类发展报告》中的未成年人生育率,也就是每1000名15~19岁年龄段女性中的生育数量。其中的地理坐标由GeoCommons提供。我们希望根据不同国家间生育率的比例来指定各个气泡的大小。
所用到的代码和之前绘制好市多店址地图时所用的差不多,但要记住当时我们只向symbols( )函数传递了一个向量来控制圆圈的尺寸。而这次,我们会用到生育率的sqrt( )函数来指定圆点的大小。
- fertility <-
- read.csv("http://book.flowingdata.com/ch08/points/adol-fertility.csv")
- map('world', fill = FALSE, col = "#cccccc")
- symbols(fertility$longitude, fertility$latitude,
- circles=sqrt(fertility$ad_fert_rate), add=TRUE,
- inches=0.15, bg="#93ceef", fg="#ffffff")
图8-9显示了输出的结果。我们很快就能发现非洲国家的未成年人生育率比较高,而欧洲国家相对较低。单独只看图表,并不清楚每个圆代表多少数值,因为尚未给出图例。在R中用summary( )可以得到一份概览。
- summary(fertility$ad_fert_rate)
- Min. 1st Qu. Median Mean 3rd Qu. Max. NA's
- 3.20 16.20 39.00 52.89 78.20 201.40 1.00
图8-9 全球未成年人生育率
这个数据对我们来说已经不错了,但我们只是受众之一。如果希望其他没有看过此数据的人也能理解图表,就需要更多的解释。我们可以为表现突出(生育率最高和最低)的国家添加注释,可以专门标识出读者最多的国家(在本例中也就是美国),同时还可以提供一段引文,帮助读者理解如何阅读图表。图8-10显示了这些改变。
图8-10 为更广泛的读者群对生育率提供清晰的解释