为什么numpy / pandas解析长行的csv文件这么慢?

时间:2015-04-16 14:42:09

标签: python parsing csv numpy pandas

我试图有效地解析一个csv文件,每行大约20,000个条目(和几千行)到一个numpy数组(或数组列表,或者其他类似的东西)。我发现了许多其他问题,以及this博文,这表明大熊猫的csv解析器非常快。然而,我已经对大熊猫,numpy和一些纯python方法进行了基准测试,看起来普通的纯python字符串拆分+列表理解比其他所有方法都要大得多。

  • 这里发生了什么?

  • 是否有更高效的csv解析器?

  • 如果我更改输入数据的格式会有帮助吗?

这里的源代码我用(sum()进行基准测试只是为了确保任何惰性迭代器都被迫评估所有内容):

#! /usr/bin/env python3

import sys

import time
import gc

import numpy as np
from pandas.io.parsers import read_csv
import csv

def python_iterator_csv():
    with open("../data/temp_fixed_l_no_initial", "r") as f:
        for line in f.readlines():
            all_data = line.strip().split(",")
            print(sum(float(x) for x in all_data))


def python_list_csv():
    with open("../data/temp_fixed_l_no_initial", "r") as f:
        for line in f.readlines():
            all_data = line.strip().split(",")
            print(sum([float(x) for x in all_data]))


def python_array_csv():
    with open("../data/temp_fixed_l_no_initial", "r") as f:
        for line in f.readlines():
            all_data = line.strip().split(",")
            print(sum(np.array([float(x) for x in all_data])))


def numpy_fromstring():
    with open("../data/temp_fixed_l_no_initial", "r") as f:
        for line in f.readlines():
            print(sum(np.fromstring(line, sep = ",")))


def numpy_csv():
    with open("../data/temp_fixed_l_no_initial", "r") as f:
        for row in np.loadtxt(f, delimiter = ",", dtype = np.float, ndmin = 2):
            print(sum(row))


def csv_loader(csvfile):
    return read_csv(csvfile,
                      header = None,
                      engine = "c",
                      na_filter = False,
                      quoting = csv.QUOTE_NONE,
                      index_col = False,
                      sep = ",")

def pandas_csv():
    with open("../data/temp_fixed_l_no_initial", "r") as f:
        for row in np.asarray(csv_loader(f).values, dtype = np.float64):
            print(sum(row))


def pandas_csv_2():
    with open("../data/temp_fixed_l_no_initial", "r") as f:
        print(csv_loader(f).sum(axis=1))


def simple_time(func, repeats = 3):
    gc.disable()

    for i in range(0, repeats):
        start = time.perf_counter()
        func()
        end = time.perf_counter()
        print(func, end - start, file = sys.stderr)
        gc.collect()

    gc.enable()
    return


if __name__ == "__main__":

    simple_time(python_iterator_csv)
    simple_time(python_list_csv)
    simple_time(python_array_csv)
    simple_time(numpy_csv)
    simple_time(pandas_csv)
    simple_time(numpy_fromstring)

    simple_time(pandas_csv_2)

输出(到stderr)是:

<function python_iterator_csv at 0x7f22302b1378> 19.754893831999652
<function python_iterator_csv at 0x7f22302b1378> 19.62786615600271
<function python_iterator_csv at 0x7f22302b1378> 19.66641107099713

<function python_list_csv at 0x7f22302b1ae8> 18.761991592000413
<function python_list_csv at 0x7f22302b1ae8> 18.722911622000538
<function python_list_csv at 0x7f22302b1ae8> 19.00348913199923

<function python_array_csv at 0x7f222baffa60> 41.8681991630001
<function python_array_csv at 0x7f222baffa60> 42.141840383999806
<function python_array_csv at 0x7f222baffa60> 41.86879085799956

<function numpy_csv at 0x7f222ba5cc80> 47.957625758001086
<function numpy_csv at 0x7f222ba5cc80> 47.245571732000826
<function numpy_csv at 0x7f222ba5cc80> 47.25457685799847

<function pandas_csv at 0x7f2228572620> 43.39656048499819
<function pandas_csv at 0x7f2228572620> 43.5016079220004
<function pandas_csv at 0x7f2228572620> 43.567352316000324

<function numpy_fromstring at 0x7f593ed3cc80> 32.490607361
<function numpy_fromstring at 0x7f593ed3cc80> 32.421125410997774
<function numpy_fromstring at 0x7f593ed3cc80> 32.37903898300283

<function pandas_csv_2 at 0x7f846d1aa730> 24.903284349999012
<function pandas_csv_2 at 0x7f846d1aa730> 25.498485038999206
<function pandas_csv_2 at 0x7f846d1aa730> 25.03262125800029

从上面链接的博客文章中可以看出,pandas可以以145/1.279502 = 113 MB / s的数据速率导入随机双打的csv矩阵。我的文件是814 MB,所以pandas只为我管理~19 MB / s!

编辑:正如@ASGM所指出的那样,这对熊猫来说并不公平,因为它不适合用于rowise迭代。我已经在基准测试中包含了建议的改进,但它仍然比纯python方法慢。 (另外:在将其简化为此基准测试之前,我已经使用了类似代码的分析,并且解析总是占据所花费的时间。)

edit2:在没有sum的情况下最好三次:

python_list_csv    17.8
python_array_csv   23.0
numpy_csv          28.6
numpy_fromstring   13.3
pandas_csv_2       24.2

所以如果没有求和numpy.fromstring,那么纯粹的python会有一点点差距(我认为fromstring是用C编写的,所以这很有意义。)

EDIT3:

我已经使用C / C ++ float解析代码here进行了一些实验,看起来我可能对pandas / numpy的期望过高。那里列出的大多数强大的解析器只需要10秒以上的时间来解析这个浮点数。唯一能够胜任numpy.fromstring的解析器是boost spirit::qi,它是C ++,所以不太可能进入任何python库。

[更精确的结果:spirit::qi〜3s,lexical_cast ~7s,atofstrtod ~10s,sscanf ~18s,{{1}并且stringstream在50秒和28秒时非常慢。 ]

4 个答案:

答案 0 :(得分:8)

您的CSV文件是否包含列标题?如果没有,那么显式传递header=Nonepandas.read_csv可以为Python解析引擎带来轻微的性能提升(但不是C引擎):

In [1]: np.savetxt('test.csv', np.random.randn(1000, 20000), delimiter=',')

In [2]: %timeit pd.read_csv('test.csv', delimiter=',', engine='python')
1 loops, best of 3: 9.19 s per loop

In [3]: %timeit pd.read_csv('test.csv', delimiter=',', engine='c')
1 loops, best of 3: 6.47 s per loop

In [4]: %timeit pd.read_csv('test.csv', delimiter=',', engine='python', header=None)
1 loops, best of 3: 6.26 s per loop

In [5]: %timeit pd.read_csv('test.csv', delimiter=',', engine='c', header=None)
1 loops, best of 3: 6.46 s per loop

更新

如果没有丢失或无效的值,那么通过传递na_filter=False(仅对C引擎有效)可以做得更好:

In [6]: %timeit pd.read_csv('test.csv', sep=',', engine='c', header=None)
1 loops, best of 3: 6.42 s per loop

In [7]: %timeit pd.read_csv('test.csv', sep=',', engine='c', header=None, na_filter=False)
1 loops, best of 3: 4.72 s per loop

通过明确指定dtype,可能还会有小的收获:

In [8]: %timeit pd.read_csv('test.csv', sep=',', engine='c', header=None, na_filter=False, dtype=np.float64)
1 loops, best of 3: 4.36 s per loop

更新2

跟进@ morningsun的评论,设置low_memory=False挤出更快的速度:

In [9]: %timeit pd.read_csv('test.csv', sep=',', engine='c', header=None, na_filter=False, dtype=np.float64, low_memory=True)
1 loops, best of 3: 4.3 s per loop

In [10]: %timeit pd.read_csv('test.csv', sep=',', engine='c', header=None, na_filter=False, dtype=np.float64, low_memory=False)
1 loops, best of 3: 3.27 s per loop

对于它的价值,这些基准测试都是使用当前开发版的pandas(0.16.0-19-g8d2818e)完成的。

答案 1 :(得分:4)

在纯python的情况下,你可以在行中迭代并打印。在pandas的情况下,您将整个事物导入DataFrame,然后迭代行。但是熊猫&#39;力量不是在迭代行 - 它是在整个DataFrame上发生的操作中。比较速度:

def pandas_csv():
    with open("../data/temp_fixed_l_no_initial", "r") as f:
        print csv_loader(f).sum(axis=1)

这仍然比纯python方法慢一些,如果这是您的用例范围,欢迎您使用。但正如@ ali_m的评论指出的那样,如果你想做的不仅仅是打印行的总和,或者你想以任何方式转换数据,你可能会发现pandas或numpy更有效率处理时间和编程时间。

答案 2 :(得分:2)

array_csvnumpy_csv次非常相似。如果查看loadtxt代码,您会发现操作非常相似。使用array_csv,您为每一行构造一个数组并使用它,而numpy_csv将已解析(和转换)的行收集到一个列表中,最后将其转换为数组。

每行

loadtxt

        vals = split_line(line)
        ...
        # Convert each value according to its column and store
        items = [conv(val) for (conv, val) in zip(converters, vals)]
        # Then pack it according to the dtype's nesting
        items = pack_items(items, packing)
        X.append(items)

最终

X = np.array(X, dtype)

[conv(val) for ...]行只是[float(val) for val in ...]的推广。

如果普通列表执行该作业,请不要将其转换为数组。这只会增加不必要的开销。

loadtxt列包含多种数据类型时,csv等函数最有价值。它们简化了从该数据创建结构化数组的工作。对于像你这样的纯数字数据,它们不会增加太多。

我不能代表pandas,除了它在numpy之上还有另一层,并且自己做了很多硬编码。

答案 3 :(得分:1)

如果您要将dtypes作为字典(pd.read_csv(...,dtype={'x':np.float})给予pandas,它将使事情变得更快,因为pandas会尝试检查每列的数据类型。