Python中的核心外数据帧:Dask和OpenStreetMap

274 阅读11分钟

在这篇文章中,我将看看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]: image.png

从下往上看,这张图显示了我们的计算将发生什么:首先,数组xdd.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);

image.png 同样,这些数据还远远不够完整,但我们还是可以看到许多人之前注意到的总体趋势。星巴克倾向于西海岸,而邓肯甜甜圈则倾向于东海岸。对于任何在西雅图和波士顿呆过很长时间的人来说,这种总体趋势不应该是那么令人惊讶的!

快餐国家

让我们通过 "便利性 "一栏来看看这组数据的另一面。我们可以调用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);

仅仅是酒馆的位置就足以使大部分岛屿的边界和轮廓清晰可见


以上的可视化是很有趣的,但仅仅是对这些数据的表面处理--利用这些数据和工具,你能想出什么有趣的地理可视化方法?

谢谢你的阅读!