[手把手]教你绘制全球热门航线和客流分布图

简介:

摘要:这张地图描绘了一些目前最热门的民航线路,每条线路都用不同的颜色和宽度表示出了最近一年有多少乘客往返于这两个机场之间。


数据收集


当我开始收集用于这张地图的数据时,我知道并不是所有的机场都在它的维基百科页面上公布了它和不同目的地之间往返的乘客数目。但我也不确定是否可以根据其他机场给出的乘客数目来填补这些空缺。


莫斯科的两大机场,谢列梅捷沃国际机场(Sheremetyevo)和多莫杰多沃国际机场(Domodedovo)都没有在维基百科中公布在它们和一些热门目的地之间往返的乘客数目。但是曼谷(Bangkok)的素万那普机场(Suvarnabhumi Airport)在维基上公布了2013年它与谢列梅捷沃机场以及多莫杰多沃机场之间往返的乘客数目分别是266,889和316,055人。新西伯利亚(Novosibirsk)的托尔马切沃机场(Tolmachevo)也公布2013年它与素万那普机场之间往返的乘客数目为215,408,这与素万那普机场在维基上公布的数字212,715很接近。


捷克的布拉格机场(Prague Airport)和法国巴黎的戴高乐机场(Charles de Gaulle Airport)分别公布了它们和谢列梅捷沃机场之间往返的乘客数目是637,566和790,922人。但其它有些机场,例如乌克兰基辅(Kiev)的鲍里斯波尔机场(Boryspil)则是将谢列梅捷沃机场(Sheremetyevo)和多莫杰多沃机场(Domodedovo)的数字合并在一起以城市为单位来统计乘客数目。


谢列梅捷沃机场(Sheremetyevo)&埃及的沙姆沙伊赫机场(Sharm el-Sheikh),谢列梅捷沃机场(Sheremetyevo)&俄罗斯的克拉斯诺达尔以及谢列梅捷沃机场(Sheremetyevo)&加里宁格勒机场这三条航线都没有公布相应的乘客数目。同时,中国、印度、巴西和南美的大部分客流也没有按照出发/抵达的机场进行分类统计。


我从28,731个标题中包含“机场”的维基百科条目中提取出5,958个机场,其中343个包含了按照目的地分类的乘客数目信息。这343个机场中的绝大部分至少列出了本年度与其往返最频繁的十大机场以及相应的乘客数目,很多列出了前二十位,有些明星机场(大多在西欧和东南亚地区)列出了50个以上。


实际数字可能更高,但这是我的分析器所能找到的所有结果。


搭建环境


我在我的Ubuntu 14.04系统中装了一些用来收集并展示数据的工具。

$ sudo apt-get update $ sudo apt-get install python-mpltoolkits.basemap \ pandoc \ libxml2-dev \ libxslt1-dev \ redis-server $ sudo pip install docopt

在这个项目的数据收集阶段,我几乎完全是在虚拟环境中工作的。但是当我想通过pip安装Matplotlib中的Basemap时我碰到了一些困难,所以我还是使用Ubuntu系统。


我将绘制地图的任务转移到plot.py中来完成。为了完成我在app.py中做的所有工作,我在虚拟环境中安装了11个软件包:

$ virtualenv passengers $ source passengers/bin/activate $ pip install -r requirements.txt

在完成数据收集,即将进入数据展示阶段时,你可按照以下方法退出虚拟环境:

$ deactivate

下载维基百科中的内容


如果可以不用向远程服务器发送千万条网络请求,即使是通过队列的形式,我将会竭尽全力实现这个目标。因此,我下载了大约11G大小的维基百科中所有英语条目。你可以将其作为一个单独的文件进行下载,也可以分块进行。

$ wget -c https://dumps.wikimedia.org/enwiki/20150702/enwiki-20150702-pages-articles1.xml-p000000010p000010000.bz2 $ wget -c https://dumps.wikimedia.org/enwiki/20150702/enwiki-20150702-pages-articles2.xml-p000010002p000025001.bz2 $ wget -c https://dumps.wikimedia.org/enwiki/20150702/enwiki-20150702-pages-articles3.xml-p000025001p000055000.bz2 $ wget -c https://dumps.wikimedia.org/enwiki/20150702/enwiki-20150702-pages-articles4.xml-p000055002p000104998.bz2 $ wget -c https://dumps.wikimedia.org/enwiki/20150702/enwiki-20150702-pages-articles5.xml-p000105002p000184999.bz2 $ wget -c https://dumps.wikimedia.org/enwiki/20150702/enwiki-20150702-pages-articles6.xml-p000185003p000305000.bz2 $ wget -c https://dumps.wikimedia.org/enwiki/20150702/enwiki-20150702-pages-articles7.xml-p000305002p000465001.bz2 $ wget -c https://dumps.wikimedia.org/enwiki/20150702/enwiki-20150702-pages-articles8.xml-p000465001p000665001.bz2 $ wget -c https://dumps.wikimedia.org/enwiki/20150702/enwiki-20150702-pages-articles9.xml-p000665001p000925001.bz2 $ wget -c https://dumps.wikimedia.org/enwiki/20150702/enwiki-20150702-pages-articles10.xml-p000925001p001325001.bz2 $ wget -c https://dumps.wikimedia.org/enwiki/20150702/enwiki-20150702-pages-articles11.xml-p001325001p001825001.bz2 $ wget -c https://dumps.wikimedia.org/enwiki/20150702/enwiki-20150702-pages-articles12.xml-p001825001p002425000.bz2 $ wget -c https://dumps.wikimedia.org/enwiki/20150702/enwiki-20150702-pages-articles13.xml-p002425002p003125001.bz2 $ wget -c https://dumps.wikimedia.org/enwiki/20150702/enwiki-20150702-pages-articles14.xml-p003125001p003925001.bz2 $ wget -c https://dumps.wikimedia.org/enwiki/20150702/enwiki-20150702-pages-articles15.xml-p003925001p004824998.bz2 $ wget -c https://dumps.wikimedia.org/enwiki/20150702/enwiki-20150702-pages-articles16.xml-p004825005p006025001.bz2 $ wget -c https://dumps.wikimedia.org/enwiki/20150702/enwiki-20150702-pages-articles17.xml-p006025001p007524997.bz2 $ wget -c https://dumps.wikimedia.org/enwiki/20150702/enwiki-20150702-pages-articles18.xml-p007525004p009225000.bz2 $ wget -c https://dumps.wikimedia.org/enwiki/20150702/enwiki-20150702-pages-articles19.xml-p009225002p011124997.bz2 $ wget -c https://dumps.wikimedia.org/enwiki/20150702/enwiki-20150702-pages-articles20.xml-p011125004p013324998.bz2 $ wget -c https://dumps.wikimedia.org/enwiki/20150702/enwiki-20150702-pages-articles21.xml-p013325003p015724999.bz2 $ wget -c https://dumps.wikimedia.org/enwiki/20150702/enwiki-20150702-pages-articles22.xml-p015725013p018225000.bz2 $ wget -c https://dumps.wikimedia.org/enwiki/20150702/enwiki-20150702-pages-articles23.xml-p018225004p020925000.bz2 $ wget -c https://dumps.wikimedia.org/enwiki/20150702/enwiki-20150702-pages-articles24.xml-p020925002p023725001.bz2 $ wget -c https://dumps.wikimedia.org/enwiki/20150702/enwiki-20150702-pages-articles25.xml-p023725001p026624997.bz2 $ wget -c https://dumps.wikimedia.org/enwiki/20150702/enwiki-20150702-pages-articles26.xml-p026625004p029624976.bz2 $ wget -c https://dumps.wikimedia.org/enwiki/20150702/enwiki-20150702-pages-articles27.xml-p029625017p047137381.bz2


缩减数据大小


我希望能一步就将所有标题含有“机场”的条目的标题和正文都从相应的XML文件中提取出来。这个过程在我的电脑上需要运行一个小时以上,但好处是即使后面的步骤失败了,我也不需要再重复这个步骤了。

$ python app.py get_wikipedia_content title_article_extract.json

这样,约11G的压缩条目文件就变成了只有68 MB的非压缩JSON文件。我很开心Python的标准库几乎可以完成我所需要的所有事情。

import bz2 import codecs from glob import glob from lxml import etree def get_parser(filename): ns_token = '{http://www.mediawiki.org/xml/export-0.10/}ns' title_token = '{http://www.mediawiki.org/xml/export-0.10/}title' revision_token = '{http://www.mediawiki.org/xml/export-0.10/}revision' text_token = '{http://www.mediawiki.org/xml/export-0.10/}text' with bz2.BZ2File(filename, 'r+b') as bz2_file: for event, element in etree.iterparse(bz2_file, events=('end',)): if element.tag.endswith('page'): namespace_tag = element.find(ns_token) if namespace_tag.text == '0': title_tag = element.find(title_token) text_tag = element.find(revision_token).find(text_token) yield title_tag.text, text_tag.text element.clear() def pluck_wikipedia_titles_text(pattern='enwiki-*-pages-articles*.xml-*.bz2', out_file='title_article_extract.json'): with codecs.open(out_file, 'a+b', 'utf8') as out_file: for bz2_filename in sorted(glob(pattern), key=lambda a: int( a.split('articles')[1].split('.')[0]), reverse=True): print bz2_filename parser = get_parser(bz2_filename) for title, text in parser: if 'airport' in title.lower(): out_file.write(json.dumps([title, text], ensure_ascii=False)) out_file.write('\n')

这些文件是按照由大到小的顺序来处理的,因为我希望能及早发现任何内存和循环方面的问题。


转换维基百科文件格式(Markdown变为HTML )


在我最初开始尝试制作这个地图时,我曾经从维基百科下载了少量机场条目的HTML文件,并用Beautiful Soup来分析这些机场的特点以及乘客数目。这个程序很简单并且在我测试的一些实例中也运行地很好。但是我从维基百科下载的文件是它们特有的Markdown格式,我需要将其转化为HTML格式。我试着用creole和pandoc来进行转化,都不能得到统一的结果。这些机场条目文件没有完全被转化成相同的格式,因此,即使数据都在,但是它们可能会以非常不同的形式存在于两个不同的页面上。信息框和表格在转化为HTML时也不能正确显示。有时,表格中的每一小格都会单独显示为一行。


为了能够按时完成任务,我决定尝试三种不同的方法来提取数据:先用creole,如果不行就换用pandoc。如果pandoc也不行,我就手工连接到维基百科网站,下载HTML文件。我不想向维基的服务器发送过多的请求,因此我设置每10秒发送3次,同时将文件暂存在Redis中。

import ratelim import redis import requests @ratelim.greedy(3, 10) # 3 calls / 10 seconds def get_wikipedia_page_from_internet(url_suffix): url = 'https://en.wikipedia.org%s' % url_suffix resp = requests.get(url) assert resp.status_code == 200, (resp, url) return resp.content def get_wikipedia_page(url_suffix): redis_con = redis.StrictRedis() redis_key = 'wikipedia_%s' % sha1(url_suffix).hexdigest()[:6] resp = redis_con.get(redis_key) if resp is not None: return resp html = get_wikipedia_page_from_internet(url_suffix) redis_con.set(redis_key, html) return html

如果某个链接连接失败或者该页面不存在,程序会自动进行下一个:

try: html = get_wikipedia_page(url_key) except (AssertionError, requests.exceptions.ConnectionError): pass # Some pages link to 404s, just move on... else: soup = BeautifulSoup(html, "html5lib")

我发现有些HTML文件非常混乱以至于Beautiful Soup会到达CPython中设置的循环次数极限。如果碰到这种情况,我会直接进行下一条,因为少量缺失数据对我影响并不是很大,不完美并不代表不好。

try: soup = BeautifulSoup(html, "html5lib") passenger_numbers = pluck_passenger_numbers(soup) except RuntimeError as exc: if 'maximum recursion depth exceeded' in exc.message: passenger_numbers = {} else: raise exc

采集机场的各项指标及中转情况的指令如下:

$ python app.py pluck_airport_meta_data \ title_article_extract.json \ stats.json


选择地图样式


我不想用柱状图来呈现这些数据,但是直接在一张主要由蓝色和绿色构成的地图上直接画线也会让我觉得图片背景很嘈杂。


幸运的是,我刚好看到James Cheshire发表的一篇博客。James Cheshire是伦敦大学学院(University College London,UCL)地理系的讲师,他在这篇博客中评论了Michael Markieta绘制的一幅地图。Michael在这副地图中画出了伦敦的四个机场和它们在全世界的各个目的地之间的航线,用作背景的地图是美国宇航局(NASA)制作的灯光夜景图(Night lights map)。


我一直非常努力地寻找一种配色方案可以从视觉上区分这些航线,Michael使用的这种从粉红到大红色的方案看上去不错。我在Color Brewer 2.0中找到了类似的配色。除了颜色之外,我还使用了不同的透明度和线的宽度来表示在某一年度中有多少乘客使用了该航线。

0.3, 250000), ('#ed8d75', 0.5, 0.4, 500000), ('#ef684b', 0.6, 0.6, 1000000), ('#e93a27', 0.7, 0.8, 2000000), ) for iata_pair, passenger_count in pairs.iteritems(): colour, alpha, linewidth, _ = display_params[0] for _colour, _alpha, _linewidth, _threshold in display_params: if _threshold > passenger_count: break colour, alpha, linewidth = _colour, _alpha, _linewidth

绘制地图


接下来很简单,我们可以直接用Basemap软件包来绘制地图。唯一的问题是一些跨越太平洋的航线会终止于图片的边缘,画一条直线到达图片的另一侧,然后一直延伸到目的地。这是画大圆周方法中的一个文件记录类问题。

line, = m.drawgreatcircle(long1, lat1, long2, lat2, linewidth=linewidth, color=colour, alpha=alpha, solid_capstyle='round') p = line.get_path() # Find the index which crosses the dateline (the delta is large) cut_point = np.where(np.abs(np.diff(p.vertices[:, 0])) > 200)[0] if cut_point: cut_point = cut_point[0] # Create new vertices with a nan in between and set # those as the path's vertices new_verts = np.concatenate([p.vertices[:cut_point, :], [[np.nan, np.nan]], p.vertices[cut_point+1:, :]]) p.codes = None p.vertices = new_verts

绘制PNG和SVG地图的命令如下:

# Exit the virtual environment in order to use the system-wide # Basemap package: $ deactivate $ python plot.py render stats.json out.png $ python plot.py render stats.json out.svg



原文发布时间为:2015-12-03

本文来自云栖社区合作伙伴“大数据文摘”,了解相关信息可以关注“BigDataDigest”微信公众号

相关文章
|
数据采集 Python
使用Python采集京东商品评论并保存至本地
使用Python采集京东商品评论并保存至本地
|
2月前
|
数据采集 安全 数据可视化
2025年能源管理平台厂商综合推荐:哪些方案真正帮助企业实现能耗精细管控与成本下降?
在“双碳”与数字化转型背景下,树根科技能源管理平台凭借工业互联网底座、千种协议兼容、多系统融合能力,实现能耗精准监测与智能优化。覆盖钢铁、工程机械等行业,助力企业降本增效,实证节能显著,入选Gartner魔力象限,成为制造企业绿色智控优选方案。
286 0
|
9月前
|
机器学习/深度学习 存储 Kubernetes
【重磅发布】AllData数据中台核心功能:机器学习算法平台
杭州奥零数据科技有限公司成立于2023年,专注于数据中台业务,维护开源项目AllData并提供商业版解决方案。AllData提供数据集成、存储、开发、治理及BI展示等一站式服务,支持AI大模型应用,助力企业高效利用数据价值。
|
消息中间件 NoSQL Java
Redis系列学习文章分享---第六篇(Redis实战篇--Redis分布式锁+实现思路+误删问题+原子性+lua脚本+Redisson功能介绍+可重入锁+WatchDog机制+multiLock)
Redis系列学习文章分享---第六篇(Redis实战篇--Redis分布式锁+实现思路+误删问题+原子性+lua脚本+Redisson功能介绍+可重入锁+WatchDog机制+multiLock)
651 0
|
开发者
鸿蒙next版开发:ArkTS组件通用属性(Popup控制)
在HarmonyOS 5.0中,ArkTS提供了灵活的Popup控制属性,允许开发者创建和管理弹出窗口,用于显示额外信息、提示、表单等,增强用户交互体验。本文详解了Popup控制的通用属性,并提供了示例代码。通过bindPopup方法,可以将弹出窗口绑定到组件上,支持多种用途,如显示额外信息、表单提交和交互反馈。
752 1
|
运维 Devops jenkins
自动化运维:打造高效DevOps流水线
【8月更文挑战第44天】本文将通过深入浅出的方式,带你构建一个自动化的DevOps流水线,提升开发和部署效率。从基础概念到实际操作,我们一步步剖析如何实现代码提交、自动测试、构建、部署的全过程自动化。你将学会使用Jenkins、Git、Docker等工具,并结合Shell脚本编写,完成一个完整的自动化流程。文章末尾附有完整的示例代码,助你快速上手实践。
|
机器学习/深度学习 算法
【机器学习】梯度消失和梯度爆炸的原因分析、表现及解决方案
本文分析了深度神经网络中梯度消失和梯度爆炸的原因、表现形式及解决方案,包括梯度不稳定的根本原因以及如何通过网络结构设计、激活函数选择和权重初始化等方法来解决这些问题。
3271 0
|
编解码
ENVI: 如何添加控制点并基于控制点进行几何校正?
ENVI: 如何添加控制点并基于控制点进行几何校正?
1696 0
|
资源调度 JavaScript 前端开发
使用Vite重构Vue3项目(上)
使用Vite重构Vue3项目(上)

热门文章

最新文章