[译] 使用 python 分析 14 亿条数据

使用 python 分析 14 亿条数据

使用 pytubes,numpy 和 matplotlib

Google Ngram viewer是一个有趣和有用的工具,它使用谷歌从书本中扫描来的海量的数据宝藏,绘制出单词使用量随时间的变化。举个例子,单词 Python (区分大小写)html

这幅图来自:books.google.com/ngrams/grap…,描绘了单词 ‘Python’ 的使用量随时间的变化。前端

它是由谷歌的 n-gram 数据集驱动的,根据书本印刷的每个年份,记录了一个特定单词或词组在谷歌图书的使用量。然而这并不完整(它并无包含每一本已经发布的书!),数据集中有成千上百万的书,时间上涵盖了从 16 世纪到 2008 年。数据集能够免费从这里下载python

我决定使用 Python 和我新的数据加载库 PyTubes 来看看从新生成上面的图有多容易。android

挑战

1-gram 的数据集在硬盘上能够展开成为 27 Gb 的数据,这在读入 python 时是一个很大的数据量级。Python能够轻易地一次性地处理千兆的数据,可是当数据是损坏的和已加工的,速度就会变慢并且内存效率也会变低。ios

总的来讲,这 14 亿条数据(1,430,727,243)分散在 38 个源文件中,一共有 2 千 4 百万个(24,359,460)单词(和词性标注,见下方),计算自 1505 年至 2008 年。git

当处理 10 亿行数据时,速度会很快变慢。而且原生 Python 并无处理这方面数据的优化。幸运的是,numpy 真的很擅长处理大致量数据。 使用一些简单的技巧,咱们可使用 numpy 让这个分析变得可行。github

在 python/numpy 中处理字符串很复杂。字符串在 python 中的内存开销是很显著的,而且 numpy 只可以处理长度已知并且固定的字符串。基于这种状况,大多数的单词有不一样的长度,所以这并不理想。数据库

Loading the data

下面全部的代码/例子都是运行在 8 GB 内存 的 2016 年的 Macbook Pro。 若是硬件或云实例有更好的 ram 配置,表现会更好。编程

1-gram 的数据是以 tab 键分割的形式储存在文件中,看起来以下:后端

Python 1587 4 2
Python 1621 1 1
Python 1651 2 2
Python 1659 1 1
复制代码

每一条数据包含下面几个字段:

1. Word
2. Year of Publication
3. Total number of times the word was seen
4. Total number of books containing the word
复制代码

为了按照要求生成图表,咱们只须要知道这些信息,也就是:

1. 这个单词是咱们感兴趣的?
2. 发布的年份
3. 单词使用的总次数
复制代码

经过提取这些信息,处理不一样长度的字符串数据的额外消耗被忽略掉了,可是咱们仍然须要对比不一样字符串的数值来区分哪些行数据是有咱们感兴趣的字段的。这就是 pytubes 能够作的工做:

import tubes

FILES = glob.glob(path.expanduser("~/src/data/ngrams/1gram/googlebooks*"))
WORD = "Python"

# Set up the data load pipeline
one_grams_tube = (tubes.Each(FILES)
    .read_files()
    .split()
    .tsv(headers=False)
    .multi(lambda row: (
        row.get(0).equals(WORD.encode('utf-8')),
        row.get(1).to(int),
        row.get(2).to(int)
    ))
)

# 将数据读入一个 numpy 数组。经过设置一个大概的精准度
# 预估行数,pytubes 优化分配模式 
# fields=True 这里是冗余的,可是确保了返回的 ndarray
# 使用字段,而不是一个单独的多维数组 one_grams = one_grams_tube.ndarray(estimated_rows=500_000_000, fields=True)
复制代码

差很少 170 秒(3 分钟)以后, one_grams 是一个 numpy 数组,里面包含差很少 14 亿行数据,看起来像这样(添加表头部为了说明):

╒═══════════╤════════╤═════════╕
│   Is_Word │   Year │   Count │
╞═══════════╪════════╪═════════╡
│         0 │   1799 │       2 │
├───────────┼────────┼─────────┤
│         0 │   1804 │       1 │
├───────────┼────────┼─────────┤
│         0 │   1805 │       1 │
├───────────┼────────┼─────────┤
│         0 │   1811 │       1 │
├───────────┼────────┼─────────┤
│         0 │   1820 │     ... │
╘═══════════╧════════╧═════════╛
复制代码

从这开始,就只是一个用 numpy 方法来计算一些东西的问题了:

每年的单词总使用量

谷歌展现了每个单词出现的百分比(某个单词在这一年出现的次数/全部单词在这一年出现的总数),这比仅仅计算原单词更有用。为了计算这个百分比,咱们须要知道单词总量的数目是多少。

幸运的是,numpy让这个变得十分简单:

last_year = 2008
YEAR_COL = '1'
COUNT_COL = '2'

year_totals, bins = np.histogram(
    one_grams[YEAR_COL], 
    density=False, 
    range=(0, last_year+1),
    bins=last_year + 1, 
    weights=one_grams[COUNT_COL]
)
复制代码

绘制出这个图来展现谷歌每一年收集了多少单词:

很清楚的是在 1800 年以前,数据总量降低很迅速,所以这回曲解最终结果,而且会隐藏掉咱们感兴趣的模式。为了不这个问题,咱们只导入 1800 年之后的数据:

one_grams_tube = (tubes.Each(FILES)
    .read_files()
    .split()
    .tsv(headers=False)
    .skip_unless(lambda row: row.get(1).to(int).gt(1799))
    .multi(lambda row: (
        row.get(0).equals(word.encode('utf-8')),
        row.get(1).to(int),
        row.get(2).to(int)
    ))
)
复制代码

这返回了 13 亿行数据(1800 年之前只有 3.7% 的的占比)

Python 在每一年的占比百分数

得到 python 在每一年的占比百分数如今就特别的简单了。

使用一个简单的技巧,建立基于年份的数组,2008 个元素长度意味着每年的索引等于年份的数字,所以,举个例子,1995 就只是获取 1995 年的元素的问题了。

这都不值得使用 numpy 来操做:

# 找到匹配的行 (column 是 Ture)
word_rows = one_grams[IS_WORD_COL]
# 建立一个空数组来保存每一年占比百分数的值 
word_counts = np.zeros(last_year+1)
# 迭代至每条匹配的数据 (匹配一个单词时,应该只有几千行数据)
for _, year, count in one_grams[word_rows]:
    # 设置相关的 word_counts 行为计算后的数值
    word_counts[year] += (100*count) / year_totals[year]
复制代码

绘制出 word_counts 的结果:

形状看起来和谷歌的版本差很少

实际的占比百分数并不匹配,我认为是由于下载的数据集,它包含的用词方式不同(好比:Python_VERB)。这个数据集在 google page 中解释的并非很好,而且引发了几个问题:

  • 人们是如何将 Python 当作动词使用的?
  • ‘Python’ 的计算总量是否包含 ‘Python_VERB’?等

幸运的是,咱们都清楚我使用的方法生成了一个与谷歌很像的图标,相关的趋势都没有被影响,所以对于这个探索,我并不打算尝试去修复。

性能

谷歌生成图片在 1 秒钟左右,相较于这个脚本的 8 分钟,这也是合理的。谷歌的单词计算的后台会从明显的准备好的数据集视图中产生做用。

举个例子,提早计算好前一年的单词使用总量而且把它存在一个单独的查找表会显著的节省时间。一样的,将单词使用量保存在单独的数据库/文件中,而后创建第一列的索引,会消减掉几乎全部的处理时间。

此次探索 确实 展现了,使用 numpy 和 初出茅庐的 pytubes 以及标准的商用硬件和 Python,在合理的时间内从十亿行数据的数据集中加载,处理和提取任意的统计信息是可行的,

语言战争

为了用一个稍微更复杂的例子来证实这个概念,我决定比较一下三个相关说起的编程语言:Python,Pascal,Perl.

源数据比较嘈杂(它包含了全部使用过的英文单词,不只仅是编程语言的说起,而且,好比,python 也有非技术方面的含义!),为了这方面的调整, 咱们作了两个事情:

  1. 只有首字母大写的名字形式能被匹配(Python,不是 python)
  2. 每个语言的说起总数已经被转换到了从 1800 年到 1960 年的百分比平均数,考虑到 Pascal 在 1970 年第一次被说起,这应该有一个合理的基准线。

结果:

对比谷歌 (没有任何的基准线调整):

运行时间: 只有 10 分钟多一点

代码: gist.github.com/stestagg/91…

之后的 PyTubes 提高

在这个阶段,pytubes 只有单独一个整数的概念,它是 64 比特的。这意味着 pytubes 生成的 numpy 数组对全部整数都使用 i8 dtypes。在某些地方(像 ngrams 数据),8 比特的整型就有点过分,而且浪费内存(总的 ndarray 有 38Gb,dtypes 能够轻易的减小其 60%)。 我计划增长一些等级 1,2 和 4 比特的整型支持(github.com/stestagg/py…)

更多的过滤逻辑 - Tube.skip_unless() 是一个比较简单的过滤行的方法,可是缺乏组合条件(AND/OR/NOT)的能力。这能够在一些用例下更快地减小加载数据的体积。

更好的字符串匹配 —— 简单的测试以下:startswith, endswith, contains, 和 is_one_of 能够轻易的添加,来明显地提高加载字符串数据是的有效性。

一如既往,很是欢迎你们 patches


掘金翻译计划 是一个翻译优质互联网技术文章的社区,文章来源为 掘金 上的英文分享文章。内容覆盖 AndroidiOS前端后端区块链产品设计人工智能等领域,想要查看更多优质译文请持续关注 掘金翻译计划官方微博知乎专栏

相关文章
相关标签/搜索