有效地将数据从 CSV 读取到具有多个分隔符的数据框中
Posted
技术标签:
【中文标题】有效地将数据从 CSV 读取到具有多个分隔符的数据框中【英文标题】:Reading data from CSV into dataframe with multiple delimiters efficiently 【发布时间】:2019-05-31 07:48:40 【问题描述】:我有一个尴尬的 CSV 文件,它有多个分隔符:非数字部分的分隔符是 ','
,数字部分的分隔符是 ';'
。我想尽可能高效地仅从数字部分构建数据框。
我进行了 5 次尝试:其中,使用 pd.read_csv
的 converters
参数,使用带有 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)中,最快的方法是(原型1)csv_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 读取到具有多个分隔符的数据框中的主要内容,如果未能解决你的问题,请参考以下文章
如何有效地将 Postgres 数据从 Query 传输到 S3