有效地将数据从 CSV 读取到具有多个分隔符的数据框中

Posted

技术标签:

【中文标题】有效地将数据从 CSV 读取到具有多个分隔符的数据框中【英文标题】:Reading data from CSV into dataframe with multiple delimiters efficiently 【发布时间】:2019-05-31 07:48:40 【问题描述】:

我有一个尴尬的 CSV 文件,它有多个分隔符:非数字部分的分隔符是 ',',数字部分的分隔符是 ';'。我想尽可能高效地仅从数字部分构建数据框。

我进行了 5 次尝试:其中,使用 pd.read_csvconverters 参数,使用带有 engine='python' 的正则表达式,使用 str.replace。它们都比读取没有转换的整个 CSV 文件慢 2 倍以上。这对我的用例来说太慢了。

我知道这种比较不是同类比较,但它确实表明整体性能不佳不是由 I/O 驱动的。有没有更有效的方法将数据读入数字 Pandas 数据帧?还是等价的 NumPy 数组?

以下字符串可用于基准测试。

# Python 3.7.0, Pandas 0.23.4

from io import StringIO
import pandas as pd
import csv

# strings in first 3 columns are of arbitrary length
x = '''ABCD,EFGH,IJKL,34.23;562.45;213.5432
MNOP,QRST,UVWX,56.23;63.45;625.234
'''*10**6

def csv_reader_1(x):
    df = pd.read_csv(x, usecols=[3], header=None, delimiter=',',
                     converters=3: lambda x: x.split(';'))
    return df.join(pd.DataFrame(df.pop(3).values.tolist(), dtype=float))

def csv_reader_2(x):
    df = pd.read_csv(x, header=None, delimiter=';',
                     converters=0: lambda x: x.rsplit(',')[-1], dtype=float)
    return df.astype(float)

def csv_reader_3(x):
    return pd.read_csv(x, usecols=[3, 4, 5], header=None, sep=',|;', engine='python')

def csv_reader_4(x):
    with x as fin:
        reader = csv.reader(fin, delimiter=',')
        L = [i[-1].split(';') for i in reader]
        return pd.DataFrame(L, dtype=float)

def csv_reader_5(x):
    with x as fin:
        return pd.read_csv(StringIO(fin.getvalue().replace(';',',')),
                           sep=',', header=None, usecols=[3, 4, 5])

检查:

res1 = csv_reader_1(StringIO(x))
res2 = csv_reader_2(StringIO(x))
res3 = csv_reader_3(StringIO(x))
res4 = csv_reader_4(StringIO(x))
res5 = csv_reader_5(StringIO(x))

print(res1.head(3))
#        0       1         2
# 0  34.23  562.45  213.5432
# 1  56.23   63.45  625.2340
# 2  34.23  562.45  213.5432

assert all(np.array_equal(res1.values, i.values) for i in (res2, res3, res4, res5))

基准测试结果:

%timeit csv_reader_1(StringIO(x))  # 5.31 s per loop
%timeit csv_reader_2(StringIO(x))  # 6.69 s per loop
%timeit csv_reader_3(StringIO(x))  # 18.6 s per loop
%timeit csv_reader_4(StringIO(x))  # 5.68 s per loop
%timeit csv_reader_5(StringIO(x))  # 7.01 s per loop
%timeit pd.read_csv(StringIO(x))   # 1.65 s per loop

更新

我愿意将命令行工具作为最后的手段。在那个程度上,我已经包含了这样一个答案。我希望有一个效率相当的纯 Python 或 Pandas 解决方案。

【问题讨论】:

您是否考虑过对多个分隔符使用正则表达式?例如:link 1、link 2。不知道会不会更快。 @chris,现在我有了(见编辑),带有engine='python' 的正则表达式比没有转换器的pd.read_csv 慢约8 倍。 @jpp,如果你使用 engine=c 会怎样,正如文档所建议的那样 C 引擎更快,而 Python 引擎目前功能更完整。 @pygo,文档解释正则表达式仅适用于引擎 python。不行。 是什么阻止你更换所有的 ;对于 , 在 CSV 文件中并正常导入? 【参考方案1】:

使用命令行工具

到目前为止,我发现的最有效的解决方案是使用专业的命令行工具将 ";" 替换为 ",",然后然后读入 Pandas。 Pandas 或纯 Python 解决方案在效率方面并不相近。

基本上,使用 CPython 或用 C/C++ 编写的工具可能会胜过 Python 级别的操作。

例如,使用Find And Replace Text:

import os

os.chdir(r'C:\temp')                       # change directory location
os.system('fart.exe -c file.csv ";" ","')  # run FART with character to replace

df = pd.read_csv('file.csv', usecols=[3, 4, 5], header=None)  # read file into Pandas

【讨论】:

更好的是,使用流而不是覆盖文件。 顺便说一句,使用subprocess.check_call 而不是os.system,因为它会检查退出代码。 @ivan_pozdeev,您能否详细说明如何使用流而不是覆盖文件?在 SO 的其他地方有这样的例子吗?【参考方案2】:

如何使用生成器进行替换,然后将其与适当的装饰器组合以获得适合 pandas 的类文件对象?

import io
import pandas as pd

# strings in first 3 columns are of arbitrary length
x = '''ABCD,EFGH,IJKL,34.23;562.45;213.5432
MNOP,QRST,UVWX,56.23;63.45;625.234
'''*10**6

def iterstream(iterable, buffer_size=io.DEFAULT_BUFFER_SIZE):
    """
    http://***.com/a/20260030/190597 (Mechanical snail)
    Lets you use an iterable (e.g. a generator) that yields bytestrings as a
    read-only input stream.

    The stream implements Python 3's newer I/O API (available in Python 2's io
    module).

    For efficiency, the stream is buffered.
    """
    class IterStream(io.RawIOBase):
        def __init__(self):
            self.leftover = None
        def readable(self):
            return True
        def readinto(self, b):
            try:
                l = len(b)  # We're supposed to return at most this much
                chunk = self.leftover or next(iterable)
                output, self.leftover = chunk[:l], chunk[l:]
                b[:len(output)] = output
                return len(output)
            except StopIteration:
                return 0    # indicate EOF
    return io.BufferedReader(IterStream(), buffer_size=buffer_size)

def replacementgenerator(haystack, needle, replace):
    for s in haystack:
        if s == needle:
            yield str.encode(replace);
        else:
            yield str.encode(s);

csv = pd.read_csv(iterstream(replacementgenerator(x, ";", ",")), usecols=[3, 4, 5])

请注意,我们通过 str.encode 将字符串(或其组成字符)转换为字节,因为 Pandas 需要这样做。

这种方法在功能上与 Daniele 的答案相同,除了我们“即时”替换值,因为它们是一次性请求的,而不是一次性的。

【讨论】:

好主意,但是这个在2min 1s打卡!【参考方案3】:

如果这是一个选项,则将字符串中的字符 ; 替换为 , 会更快。 我已将字符串x 写入文件test.dat

def csv_reader_4(x):
    with open(x, 'r') as f:
        a = f.read()
    return pd.read_csv(StringIO(unicode(a.replace(';', ','))), usecols=[3, 4, 5])

unicode() 函数是避免 Python 2 中出现 TypeError 所必需的。

基准测试:

%timeit csv_reader_2('test.dat')  # 1.6 s per loop
%timeit csv_reader_4('test.dat')  # 1.2 s per loop

【讨论】:

这对我造成了MemoryError,大概是因为它需要有效地读取所有内容两次?一次进入a,然后进入pd.DataFrame 我认为a.replace 创建了一个副本。不幸的是,如果不使用更复杂的工具,例如cython,我看不到避免这种情况的简单方法【参考方案4】:

一个非常非常非常快的结果,3.51 是结果,只需将csv_reader_4 设为下面,它只是将StringIO 转换为str,然后将; 替换为,,并读取带有sep=','的数据框:

def csv_reader_4(x):
    with x as fin:
        reader = pd.read_csv(StringIO(fin.getvalue().replace(';',',')), sep=',',header=None)
    return reader

基准测试:

%timeit csv_reader_4(StringIO(x)) # 3.51 s per loop

【讨论】:

您是否在一致的硬件/设置上测试了相对性能?我看到这是较慢的解决方案之一,我已经通过基准测试更新了我的问题。 @jpp 呃,你的时间和我的不一样,我在 Windows 上。 @U9-Forward 我通过在read() 操作期间进行替换改进了您的方法:***.com/a/54176770/6394138【参考方案5】:

Python 具有处理数据的强大功能,但不要指望使用 python 的性能。当需要性能时,C 和 C++ 是你的朋友。 python 中的任何快速库都是用 C/C++ 编写的。在 python 中使用 C/C++ 代码非常容易,看看 swig 实用程序 (http://www.swig.org/tutorial.html) 。您可以编写一个 c++ 类,其中可能包含一些快速实用程序,您将在需要时在 Python 代码中使用这些实用程序。

【讨论】:

【参考方案6】:

在我的环境(Ubuntu 16.04、4GB RAM、Python 3.5.2)中,最快的方法是(原型1csv_reader_5(取自U9-Forward's answer),它只运行不到 25比没有转换读取整个 CSV 文件慢 %。我通过实现替换 read() 方法中的字符的过滤器/包装器改进了这种方法:

class SingleCharReplacingFilter:

    def __init__(self, reader, oldchar, newchar):
        def proxy(obj, attr):
            a = getattr(obj, attr)
            if attr in ('read'):
                def f(*args):
                    return a(*args).replace(oldchar, newchar)
                return f
            else:
                return a

        for a in dir(reader):
            if not a.startswith("_") or a == '__iter__':
                setattr(self, a, proxy(reader, a))

def csv_reader_6(x):
    with x as fin:
        return pd.read_csv(SingleCharReplacingFilter(fin, ";", ","),
                            sep=',', header=None, usecols=[3, 4, 5])

与在不进行转换的情况下读取整个 CSV 文件相比,结果稍微好一点:

In [3]: %timeit pd.read_csv(StringIO(x))
605 ms ± 3.24 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)

In [4]: %timeit csv_reader_5(StringIO(x))
733 ms ± 3.49 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)

In [5]: %timeit csv_reader_6(StringIO(x))
568 ms ± 2.98 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)

1 我称它为原型是因为它假定输入流是StringIO 类型(因为它调用了.getvalue())。

【讨论】:

在 Python 3.7、Pandas 0.23.4 上,我在 pd.read_csv 行上收到了 ValueError: Invalid file path or buffer object type: <class '__main__.SingleCharReplacingFilter'>。有什么想法吗? @jpp Pandas 0.23.4 对被视为文件的对象有一个额外的要求,它必须具有__iter__ 方法。我更新了我的答案以反映这一点。 抱歉耽搁了。我对此进行了计时,在我的设置中,它比csv_reader_1 多花了 1 秒(_1 为 4.28 秒,_6 为 5.28 秒)。根据我的问题 Python 3.7.0、Pandas 0.23.4、Windows,我正在使用输入 x = """..."""*10**6。我知道这将取决于平台/设置。

以上是关于有效地将数据从 CSV 读取到具有多个分隔符的数据框中的主要内容,如果未能解决你的问题,请参考以下文章

如何有效地将数据从 CSV 加载到数据库中?

如何有效地将 Postgres 数据从 Query 传输到 S3

使用 python 有效地将数据导出到 CSV

如何自动化从具有多个部分的 CSV 文件中读取的脚本?

是否可以以相同或不同的顺序将具有相同标题或标题子集的多个 csv 文件读取到 spark 数据帧中?

合并具有不同列名但定义相同的多个CSV