APP下载

使用 Python 分析 14 亿条资料

消息来源:baojiabao.com 作者: 发布时间:2024-05-20

报价宝综合消息使用 Python 分析 14 亿条资料

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

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

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

我决定使用 Python 和我新的资料载入库 PyTubes 来看看重新生成上面的图有多容易。

挑战

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

总的来说,这 14 亿条资料(1,430,727,243)分散在 38 个原始档中,一共有 2 千 4 百万个(24,359,460)单词(和词性标注,见下方),计算自 1505 年至 2008 年。

当处理 10 亿行资料时,速度会很快变慢。并且原生 Python 并没有处理这方面资料的优化。幸运的是,numpy 真的很擅长处理大体量资料。 使用一些简单的技巧,我们可以使用 numpy 让这个分析变得可行。

在 python/numpy 中处理字串很复杂。字串在 python 中的内存开销是很显著的,并且 numpy 只能够处理长度已知而且固定的字串。基于这种情况,大多数的单词有不同的长度,因此这并不理想。

Loading the data

下面所有的程式码/例子都是执行在 8 GB 内存的 2016 年的 Macbook Pro。 如果硬件或云实例有更好的 ram 配置,表现会更好。

1-gram 的资料是以 tab 键分割的形式储存在档案中,看起来如下:

Python 1587 4 2Python 1621 1 1Python 1651 2 2Python 1659 1 1

每一条资料包含下面几个字段:

1\. Word2\. Year of Publication3\. Total number of times the word was seen4\. Total number of books containing the word

为了按照要求生成图表,我们只需要知道这些资讯,也就是:

1\. 这个单词是我们感兴趣的?2\. 释出的年份3\. 单词使用的总次数

通过提取这些资讯,处理不同长度的字串资料的额外消耗被忽略掉了,但是我们仍然需要对比不同字串的数值来区分哪些行资料是有我们感兴趣的字段的。这就是 pytubes 可以做的工作:

import tubesFILES = glob.glob(path.expanduser("~/src/data/ngrams/1gram/googlebooks*"))WORD = "Python"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))))

差不多 170 秒(3 分钟)之后, onegrams_ 是一个 numpy 阵列,里面包含差不多 14 亿行资料,看起来像这样(新增表头部为了说明):

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

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

每一年的单词总使用量

Google展示了每一个单词出现的百分比(某个单词在这一年出现的次数/所有单词在这一年出现的总数),这比仅仅计算原单词更有用。为了计算这个百分比,我们需要知道单词总量的数目是多少。

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

last_year = 2008YEAR_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])

绘制出这个图来展示Google每年收集了多少单词:

很清楚的是在 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 来操作:

word_rows = one_grams[IS_WORD_COL]word_counts = np.zeros(last_year+1)for _, year, count in one_grams[word_rows]:word_counts[year] += (100*count) / year_totals[year]

绘制出 word_counts 的结果:

形状看起来和Google的版本差不多

实际的占比百分数并不匹配,我认为是因为下载的资料集,它包含的用词方式不一样(比如:Python_VERB)。这个资料集在 google page 中解释的并不是很好,并且引起了几个问题:

人们是如何将 Python 当做动词使用的?‘Python‘ 的计算总量是否包含 ‘Python_VERB‘?等幸运的是,我们都清楚我使用的方法生成了一个与Google很像的图示,相关的趋势都没有被影响,因此对于这个探索,我并不打算尝试去修复。

效能

Google生成图片在 1 秒钟左右,相较于这个指令码的 8 分钟,这也是合理的。Google的单词计算的后台会从明显的准备好的资料集检视中产生作用。

举个例子,提前计算好前一年的单词使用总量并且把它存在一个单独的查询表会显著的节省时间。同样的,将单词使用量储存在单独的数据库/档案中,然后建立第一列的索引,会消减掉几乎所有的处理时间。

这次探索 确实展示了,使用 numpy 和 初出茅庐的 pytubes 以及标准的商用硬件和 Python,在合理的时间内从十亿行资料的资料集中载入,处理和提取任意的统计资讯是可行的,

语言战争

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

源资料比较嘈杂(它包含了所有使用过的英文单词,不仅仅是程式语言的提及,并且,比如,python 也有非技术方面的含义!),为了这方面的调整, 我们做了两个事情:

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

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

执行时间: 只有 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, 和 isoneof 可以轻易的新增,来明显地提升载入字串资料是的有效性。

2019-01-21 07:38:00

相关文章