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

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

数据收集

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

莫斯科的两大机场,谢列梅捷沃国际机场(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

时间: 2024-09-10 22:26:23

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

手把手教你绘制超逼真的积雪场景

  Step 1 在图片上新建图层.选择地面区域并用带点灰蓝色填充(#d6d8e3) Step 2 使用图层蒙版(如果懒的话可以直接用橡皮擦)把砖柱露出来,我们只需要积雪的区域. Step 3 大致调整下砖柱跟远方的山的形状,让场景更自然. Step 4 创建一个新图层并按住Shift键使用椭圆工具画出一个正圆. Step 5 创建新图层并用灰蓝色(#6d85ad)填充,剪贴蒙版至之前的圆内(按住ALT在两个图层中间点击或者使用CTRL+ALT+G). Step 6 使用柔圆画笔,用比之前更亮的

PS手把手教你绘制超逼真的湖面冰层

  Step 1 按照透视定义水的区域. Step 2 新建图层,使用矩形选框工具(M)选择水的区域,使用任意颜色填充. Step 3 使用图层蒙版或橡皮擦工具露出砖柱部分.我们将使用这一图层作为剪贴图层. Step 4 复制(CTRL+J)背景图层,并将其剪贴至上一图层(CTRL+ALT+G).使用滤镜>模糊>高斯模糊--这能创建冰层厚度效果. Step 5 冰会有反射效果,背景的反射效果很容易,我们要花费更多工夫在砖柱的反射上.反射需要符合透视效果!使用钢笔工具(P)选择前面的砖柱,将路径

PS手把手教你绘制逼真的霓虹字效

  编者按:霓虹字效是经典款教程,从很久之前到现在都没过时,非常值得学起来.今天@AverageJoseph 给同学们一个特别容易上手的方法,帮大家迅速掌握这类效果的绘制操作 >>> 霓虹灯效极富酷感深邃之美,用PS创建霓虹字效其实超简单.本期教程,选用粉色作为灯光色,如果你偏好蓝色或者绿色,也可稍作尝试. 第一步:创建文字图层 首先,PS新建大小为1400*525像素的文件,当然咯,创建文件大小并无严格要求,合适即可.背景填充深灰蓝(#2b3036).输入黑色文字:NEON LIGHT

PS新手教程!手把手教你绘制亚麻质地的复古风铅印

  Step 1 新建一个1920 * 1280px大小的画布,背景色填充为#C7C2B8.双击背景图层,将其解锁,然后再次双击背景图层,打开图层样式对话框,设置如下: "图案叠加"中所用的图案是Photoshop图案库中"艺术表面"的"纱布"图案. 这一步的效果图: Step 2 在背景图层上复制一个"污渍纹理"图层,图层混合模式设置为"叠加",不透明度为25%. 污渍纹理在之前的教程<PS教程!教

PS手把手教你绘制扁平风行星与雷达图标

  行星图标 第1步 首先绘制一个淡黄色的圆形作为行星的轮廓.使用矩形工具,星球上方绘制很多不同颜色的方框,让它们的颜色和宽度都不尽相同.你可以将这些方框编组并进行旋转,这样会显得更真实. 第2步 现在我们来去掉方框不需要的部分.选择圆形所在的图层,选择椭圆工具并在图标上右击,在弹出的菜单中选择"创建选区",然后在弹出的对话框中点击确定,选项都保持默认即可. 然后,选择这些矩形所在的组并单击图层面板下方的图层蒙版按钮,将不需要的部分隐藏起来. 第3步 让我们给我们的星球增加一点光照感.

PS手把手教你绘制人体特效海报

  很多电影海报都会用的效果,简单易学还特别有视觉冲击力!今天这篇教程是小麦丫临摹的海报效果,教程不难,10分钟就可以学会,一起来试试. 素材 导入PS,调整颜色.方向(使之接近临摹海报的黑白色调): 复制人物层(图层2),并绘制出分割皮肤的两条红色参考线,主要是为了让大家看清,我大致会在哪里分割皮肤,后续切割皮肤的时候,也可以用来参考): 来到人物层 拷贝层,用多边形套索工具,沿着红色参考线,把此图层切割成四块,并重新调整好四块皮肤的位置,再把参考线删除: 接下来就是给四块皮肤变形,为了方便观

PS手把手教你绘制一枚拟物化水晶纽扣

  如何绘制逼真的高光?如何创造立体的感觉?今天分享一个简单水晶纽扣的教程,仅仅利用图层样式跟画笔就可以绘制出精美的高光+3D感,最适合UI设计刚入门的同学了,练习的时候要不断去思考高光和阴影的关系,以及实物的状态,这样才能短期内快速进步呦! 或许入门的PS学徒们都曾经试图制作过诸如一滴水.一个气球或一只苹果这样的拟物玩意儿的设计.通常让人感到迷惑的是,为什么我们要画一个苹果而不是用照片拍下一个苹果再把它抠出来?我的答案是,学习是没有捷径可走的.比较快速的做法当然同样可以达到效果,但它带来的收获

PS手把手教你绘制大气的墨迹字体效果

  墨迹字体经常出现在一些与传统,民族等元素挂钩的设计中,是很受欢迎的一种字体效果.因为之前有人问过这种效果要怎么做.今天就来带给大家一个简单的教程,十分钟包你搞定,学起! 由于根据不同的需要,这种东西的制作的步骤是可以随机应变来删减的. 这里为了能够把流程说得尽量清楚,就把每个步骤都走了走,最后的成品就显得有点画蛇添足的感觉.Whatever,亲们能理解其中的方法,就够了~ 俗话说,不打没有准备的仗.为了得到墨迹的效果和撕裂的笔触和纹理,该类型的笔刷当然是不可以少的了.所以,我们新建一层背景之

PS手把手教你绘制逼真的粽子文字特效

  编者按:端午节将近,@DearSalt 直接给同学们来一发应景教程.教程难度不大,非常适合新手,素材已打包,练手走起 >>> 哈喽!大家好,我是桂桂!端午节就要到了,想必大家都吃了粽子吧,但对一个设计师而言,怎么能仅仅光想着吃呢!所以桂桂一边吃着粽子一边创作了这件作品,哈哈,本例教程非常适合初学PS的新手哦,喂!快别吃辣!教程开始了! 一.搭建背景 首先把"台子"搭建好,Ctrl+N快捷键新建一个1920×1080的白色画布,然后将"木质背景"