在这篇文章中,我将看看dask在查看一个大型数据集时如何发挥作用:从OpenStreetMap中提取的全部兴趣点。我们将使用Dask来操作和探索数据,同时还将看到使用matplotlib的Basemap工具包在地图上可视化结果。
数据:OpenStreetMap
我们在这里要看的数据是在Open Street Map中提取的标记地点的数据库。OpenStreetMap是一个免费的、开放的、众包的在线地图工具,这意味着它上面的所有内容都是由个人用户添加的。这导致了一个由近千万个不同兴趣点组成的汇编,包括从邮筒到餐馆到历史遗迹到公园长椅的一切。
在OSM-x-tractor网站上可以找到这些兴趣点的数据提取表,这是一个免费的项目,对这些兴趣点进行汇编,并以各种通用格式提供。在这篇文章中,我下载了世界兴趣点的CSV文件,并将压缩文件提取到"POIWorld.csv" ,一个标准的逗号分隔值文件。
这个文件只有900多万行,每一行都对应着一个特定的兴趣点。
In [1]:
nrows = sum(1 for _ in open('POIWorld.csv'))
nrows
输出[1]。
9140114
使用命令行工具,我们还可以看到,该文件的大小约为760MB。
在[2]中。
!ls -lh POIWorld.csv
-rw-r--r-- 1 jakevdp staff 761M Jul 14 14:10 POIWorld.csv
虽然这可以放入大多数现代机器的内存中,但在这里我们将采取一种更可扩展的方法,利用Dask在核心外进行数据摄取和操作。这样做的好处是,这里使用的方法可以直接扩展到在多台机器上分析的更大的数据集。
在我们开始看这些数据之前,让我们先看看Dask以及它如何帮助我们解决这个问题。
Dask基础知识
Dask,从根本上说,是一个轻量级的Python任务图生成器。任务图是一种描述操作序列的方式,以便它们可以在以后执行。通过构建这些任务图,Dask描述了你的算法所需的输入、操作和输出的确切顺序,并可以将这种描述发送到各种后端,以实现高效的并行和/或核外计算。
虽然低级别的细节很有趣,但大多数人都会使用高级别的接口。这些接口是。
dask.bag:使用函数式编程风格创建任务图dask.array:使用类似NumPy的数组接口创建任务图dask.dataframe使用类似Pandas的DataFrame接口创建任务图。
每一个都提供了一个熟悉的Python接口来对数据进行操作,不同的是个别操作是建立图而不是计算结果;结果必须通过调用compute() 方法来明确提取。作为Dask运行的一个例子,让我们看一下使用dask.array 的例子。
Dask数组¶
首先,让我们使用标准的NumPy工具做一串简单的数组操作。
在[3]中:
import numpy as np
# create an array of normally-distributed random numbers
a = np.random.randn(1000)
# multiply this array by a factor
b = a * 4
# find the minimum value
b_min = b.min()
print(b_min)
-11.4051061336
Dask允许我们使用非常相似的代码来建立一个描述这些操作的任务图。
在[4]中:
import dask.array as da
# create a dask array from the above array
a2 = da.from_array(a, chunks=200)
# multiply this array by a factor
b2 = a2 * 4
# find the minimum value
b2_min = b2.min()
print(b2_min)
dask.array<x_3, shape=(), chunks=(), dtype=float64>
在这种情况下,计算的结果不是一个值,而是一个Dask数组对象,其中包含计算该值所需的步骤序列。
如果你安装了graphviz包(pip install graphviz ),你可以在这个分块的数据上将这个步骤序列可视化。
In [5]:
from dask.dot import dot_graph
dot_graph(b2_min.dask)
输出[5]:
从下往上看,这张图显示了我们的计算将发生什么:首先,数组x 被dd.from_array ,并被分成5个200个元素的块。每个小块都被乘以4,然后找到最小值。最后,在这些块状的最小值中找到全局最小值,并返回结果。
我们可以告诉Dask使用dask对象的compute() 方法来计算这个图形所编码的结果。
In[6]:
b2_min.compute()
Out[6]:
-11.405106133564583
正如预期的那样,我们得到的结果与正常的、不费力的方法相同。
我不打算在这里讨论Dask的更多技术细节,但你应该知道,它包含了更多的选项,可以灵活地创建和评估这种任务图。要了解更多信息,我建议通读dask文档,或者观看Matthew Rocklin在最近的PyData西雅图会议上所做的精彩的Dask教程。
Dask的数据框架和OpenStreetMap
有了这个任务图加 懒惰评估的框架,让我们来看看在一个更有趣的背景下使用Dask:探索OpenStreetMap的数据。
我们可以先用Pandas看一下数据的前几行,只是为了看看我们在做什么。
In[7]:
import pandas as pd
data = pd.read_csv('POIWorld.csv', nrows=5)
data.columns
Out[7]:
Index(['"osmid"', 'name', 'amenity', 'emergency', 'historic', 'leisure', 'man_made', 'office', 'shop', 'sport', 'tourism', 'craft', 'Longitude', 'Latitude'],
dtype='object')
每个兴趣点都有纬度和经度,以及其他各种数据标签。我们将提取这些位置,并专注于与之相关的 "名称 "和 "设施 "列。这可以通过Dask的Pandas版本的read_csv 命令来完成。
In[8]:
from dask import dataframe as dd
columns = ["name", "amenity", "Longitude", "Latitude"]
data = dd.read_csv('POIWorld.csv', usecols=columns)
data
Out[8]:
dd.DataFrame<read-csv-POIWorld.csv-ea29fef3df6f73002fe27223a3749930, divisions=(None, None, None, ..., None, None)>
注意这里,read_csv 命令实际上并没有打开文件和访问数据,而只是创建了一个任务图,描述了访问数据所需的操作。
接下来,让我们使用Pandas风格的过滤器来选择这些数据的两个子集:那些指定了 "便利性 "的行,以及那些指定了 "名称 "的行。
在[9]中:
with_name = data[data.name.notnull()]
with_amenity = data[data.amenity.notnull()]
再一次,我们还没有做任何实际的计算,只是指定了如何找到我们感兴趣的那部分数据。
潜入数据中:咖啡的地理环境
我们可以用这些数据做的一件事是拉出某些感兴趣的点,在地图上比较它们的分布。在这里,我们将尝试复制最近的Reddit帖子,该帖子绘制了邓肯甜甜圈和星巴克在美国的分布。
首先,我们必须通过名称列进一步过滤数据。Dask让我们使用Pandas风格的屏蔽和矢量字符串操作,所以我们可以这样做。
在[10]中:
is_starbucks = with_name.name.str.contains('[Ss]tarbucks')
is_dunkin = with_name.name.str.contains('[Dd]unkin')
starbucks = with_name[is_starbucks]
dunkin = with_name[is_dunkin]
为了查看我们有多少个结果,我们可以使用count() ,并将其传递给dd.compute() ,以查看结果。这就是我们最终真正加载数据并从数值中计算数量的时候。
In [11]:
dd.compute(starbucks.name.count(), dunkin.name.count())
Out[11]:
(5301, 1276)
我们在全球数据集中发现了大约5300个星巴克和1300个邓肯甜甜圈的位置;这比这些连锁店的真实数量少得多,仅在美国就有大约12000个星巴克和8000个邓肯甜甜圈显而易见,OpenStreetMap的数据并不是那么完整。从我自己对数据集的传闻来看,我发现数据在密集的城市地区往往是相当完整的,而在人口稀少的地方则缺少许多细节。
尽管有这种不完整性,让我们用我们拥有的数据继续下去,看看我们能发现什么。我们可以先从我们所生成的图表中计算并提取出经纬度。我们将在一个单一的dd.compute() 调用中完成这一工作,这样数据将只被摄取一次。
在[12]:
locs = dd.compute(starbucks.Longitude,
starbucks.Latitude,
dunkin.Longitude,
dunkin.Latitude)
# extract arrays of values fro the series:
lon_s, lat_s, lon_d, lat_d = [loc.values for loc in locs]
现在我们有了这些数据,我们可以使用matplotlib的basemap工具包将结果在地图上可视化。
在[13]中:
%matplotlib inline
import matplotlib.pyplot as plt
from mpl_toolkits.basemap import Basemap
def draw_USA():
"""initialize a basemap centered on the continental USA"""
plt.figure(figsize=(14, 10))
return Basemap(projection='lcc', resolution='l',
llcrnrlon=-119, urcrnrlon=-64,
llcrnrlat=22, urcrnrlat=49,
lat_1=33, lat_2=45, lon_0=-95,
area_thresh=10000)
在[14]中:
m = draw_USA()
# Draw map background
m.fillcontinents(color='white', lake_color='#eeeeee')
m.drawstates(color='lightgray')
m.drawcoastlines(color='lightgray')
m.drawcountries(color='lightgray')
m.drawmapboundary(fill_color='#eeeeee')
# Plot the values in Starbucks Green and Dunkin Donuts Orange
style = dict(s=5, marker='o', alpha=0.5, zorder=2)
m.scatter(lon_s, lat_s, latlon=True,
label="Starbucks", color='#00592D', **style)
m.scatter(lon_d, lat_d, latlon=True,
label="Dunkin' Donuts", color='#FC772A', **style)
plt.legend(loc='lower left', frameon=False);
同样,这些数据还远远不够完整,但我们还是可以看到许多人之前注意到的总体趋势。星巴克倾向于西海岸,而邓肯甜甜圈则倾向于东海岸。对于任何在西雅图和波士顿呆过很长时间的人来说,这种总体趋势不应该是那么令人惊讶的!
快餐国家
让我们通过 "便利性 "一栏来看看这组数据的另一面。我们可以调用count() 方法来检查有多少行的数据有amenity 指定。
In[15]:
dd.compute(with_amenity.amenity.count())
Out[15]:
(5075909,)
我们看到刚刚超过500万行有安康标签。通过Pandas的value_counts() 函数,我们可以检查数据集中这些标签中最常见的。这里的head() 调用触发了一个计算。
In[16]:
with_amenity.amenity.value_counts().head(20)
输出[16]。
bench 492190
restaurant 400620
place_of_worship 389266
school 335937
parking 282460
fuel 198865
post_box 181858
cafe 156946
bank 152218
fast_food 146962
recycling 135912
pharmacy 127259
waste_basket 119881
grave_yard 118324
bicycle_parking 110657
post_office 102431
drinking_water 94594
pub 94416
toilets 93708
telephone 90894
dtype: int64
有点令人惊讶的是,有标签的长椅比其他任何单一标签的对象都要多。
再往下看,我们看到fast_food 类别,它有大约15万个全局条目。使用一个过滤器加上另一个值计数,我们可以检查哪些快餐店是最常见的。
In[17]:
is_fastfood = with_amenity.amenity.str.contains('fast_food')
fastfood = with_amenity[is_fastfood]
fastfood.name.value_counts().head(12)
Out[17]。
McDonald's 8608
Subway 6784
Burger King 3180
KFC 2812
Wendy's 1301
Taco Bell 1272
Pizza Hut 965
マクドナルド 918
Dairy Queen 744
Domino's Pizza 680
McDonalds 634
Arby's 606
dtype: int64
顺便说一句,我们看到的一个有趣的事情是,在这个列表中,有三个版本的麦当劳:当然有 "麦当劳 "和 "麦当劳",但也有マクドナルド(大致读作 "Makudonarudo"),这是对这个著名餐厅名称的日语改编。如果你试图使用这个数据集按连锁店来计算地点,那么考虑到这些多个类似的标签就很重要了
接下来,让我们看一下快餐店位置的完整集合,提取它们的经纬度坐标,并在美国地图上绘制它们的位置。
在[18]中:
lat, lon = dd.compute(fastfood.Latitude,
fastfood.Longitude)
在[19]中:
lat, lon = dd.compute(fastfood.Latitude,
fastfood.Longitude)
在这里,我特意忽略了地理边界;我们看到,仅凭快餐店的位置,我们就可以看到主要城市,甚至可以追踪到一些主要的州际路线我怀疑,像上面一样,这个数据远远不够完整,特别是在更多的农村地区。我很想看看完整的快餐国家地图会是什么样子,但经过打探,似乎大多数可用的数据都是专有的(尽管FlowingData有一个有趣的可视化,具有同样的精神)。
不列颠群岛的酒吧
让我们看一下最后一个例子,复制Ramiro Gomez的帖子,他是柏林的一个开发者,他的网站绝对值得点击一下。在这里,我们将从数据集中提取所有的酒吧位置,并使用它们来可视化一个小岛屿国家,这些场所的密度令人震惊。
我们将从过滤 "酒吧 "这个词的设施开始(注意使用标记单词边界的正则表达式,这样我们就不会匹配 "公共厕所 "这样的东西)。
In[20]:
is_pub = with_amenity.amenity.str.contains(r'\bpub\b')
pubs = with_amenity[is_pub]
pubs.amenity.count().compute()
Out[20]:
94478
我们有大约95,000个标签中含有 "pub "的世界性兴趣点。
接下来,如上所述,我们可以从我们的数据中提取经度和纬度数组。
在[21]中:
lon, lat = dd.compute(pubs.Longitude, pubs.Latitude)
最后,通过几行Basemap的代码,我们可以将结果可视化。
在[22]中:
fig, ax = plt.subplots(figsize=(10, 15))
m = Basemap(projection='mill',
lon_0=-5.23636, lat_0=53.866772,
llcrnrlon=-10.65073, llcrnrlat=49.16209,
urcrnrlon=1.76334, urcrnrlat=60.860699)
m.drawmapboundary(fill_color='#ffffff', linewidth=.0)
x, y = m(lon.values, lat.values)
m.scatter(x, y, s=1, marker=',', color="steelblue", alpha=0.8);
仅仅是酒馆的位置就足以使大部分岛屿的边界和轮廓清晰可见
以上的可视化是很有趣的,但仅仅是对这些数据的表面处理--利用这些数据和工具,你能想出什么有趣的地理可视化方法?
谢谢你的阅读!