我已经有一个表作为python字符串,其中包含300000+行,如下所示:
123 1 2.263E-04 2.024E+00 8.943E+03 9.030E+02 2.692E+03 5.448E+03 3.816E-01 1.232E-01 0.000E+00 4.389E+02 1.950E+02
如果有帮助,则使用以下Fortran FORMAT语句生成该表:
FORMAT (2I5,1P11E11.3)
我想看看是否可以比pandas.read_csv(...,delim_whitespace = True)加载得更快,这对我来说耗时540毫秒。
text = r''' 372 1 0.000E+00 0.000E+00 0.000E+00 9.150E+02 3.236E+03 0.000E+00 0.000E+00 0.000E+00 0.000E+00 0.000E+00 3.623E+02\n'''*300000
%timeit df = pd.read_csv(StringIO(text), delim_whitespace=True, header=None)
产量:
549 ms ± 3.42 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)
我认为知道行长和列宽会使read_fwf
更快,但显然优化程度较低:
widths = [5]*2 + [11]*11
%timeit df = pd.read_fwf(StringIO(text), widths=widths, header=None)
产量:
2.95 s ± 29 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)
使用Cython可以使速度更快吗?我对C或Cython的经验很少,所以很遗憾,我不知道从最初的示例开始。我也喜欢f2py之类的东西,但前提是值得在Cython上解决麻烦。我的依赖项中已经有一些numba和Cython的东西,因此我对Cython解决方案更加开放。我意识到numba不会处理文本,因此对此没有帮助。
感谢任何可以提供帮助的人!
答案 0 :(得分:0)
我想出了可以满足我的需要的Cython解决方案。这是针对Jupyter笔记本使用Cython单元魔术来进行编译的。我选择2000000进行数组初始化,因为这是我数据的合理上限。该函数仅返回实际填充的numpy数组的行。然后将该numpy数组传递给pandas数据帧是相当便宜的。
由于我实际上也抛出了一些我认为排除了内存映射的垃圾行,因此我不确定可以做更多的优化。我可能可以利用an answer to another question I had中的指针,但是如果我要移动文件中的数据,则很难在文件中找到数据并检测不良行(有关读取数据页的更大问题,请参见下文,以获取更多信息)。指针而不是迭代线。
%%cython
import numpy as np
cimport numpy as np
np.import_array()
from libc.stdlib cimport atof
from cpython cimport bool
def read_with_cython(filename):
cdef float[:, ::1] data = np.zeros((2000000, 13), np.float32)
cdef int i = 0
with open(filename, 'rb') as f:
for line in f:
if len(line) == 133:
data[i, 0] = atof(line[0:5])
data[i, 1] = atof(line[5:10])
data[i, 2] = atof(line[12:21])
data[i, 3] = atof(line[23:32])
data[i, 4] = atof(line[34:43])
data[i, 5] = atof(line[45:54])
data[i, 6] = atof(line[56:65])
data[i, 7] = atof(line[67:76])
data[i, 8] = atof(line[78:87])
data[i, 9] = atof(line[89:98])
data[i, 10] = atof(line[100:109])
data[i, 11] = atof(line[111:120])
data[i, 12] = atof(line[122:131])
i += 1
return data.base[:i]
有了这个,我能够运行以下内容:
text = ''' 372 1 0.000E+00 0.000E+00 0.000E+00 9.150E+02 3.236E+03 0.000E+00 0.000E+00 0.000E+00 0.000E+00 0.000E+00 3.623E+02\n'''*300000
with open('demo_file.txt', 'w') as f:
f.write(text)
%timeit result = read_with_cython('demo_file.txt')
并获得以下结果:
473 ms ± 6.63 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)
为了比较和完整起见,我还编写了一个快速的纯python版本:
def read_python(text):
data = np.zeros((300000, 13), dtype=np.float)
for i, line in enumerate(text.splitlines()):
data[i, 0] = float(line[:5])
data[i, 1] = float(line[5:10])
for j in range(11):
a = 10+j*11
b = a + 11
data[i, j+2] = float(line[a:b])
return data
哪个跑了1.15秒:
1.15 s ± 8.14 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)
然后我尝试将其调整为一个非常简单的Cython示例,运行时间为717ms:
%%cython
def read_python_cy(text):
text.replace('\r\n', '')
i = 0
while True:
float(line[i:i+5])
float(line[i+5:i+10])
for j in range(11):
a = i+10+j*11
b = i+a + 11
float(line[a:b])
i += 131
return 0
717 ms ± 5.66 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)
然后,我分解并找出了上面优化的Cython版本。
到那时,我意识到Cython可以更有效地解决此问题以及缓慢出现的正则表达式问题。我使用正则表达式查找并捕获了约5000页数据,然后将这些数据连接到我要在此处读取的表中。下图更接近我的实际Cython函数。这用于查找数据页面,捕获页面级别的详细信息(时间),然后读取实际数据行,直到检测到停止标志(以0或1开头的行)为止。我的正则表达式接管了1s只是为了提取我想要的数据,因此总体上为我节省了很多时间。
%%cython
import numpy as np
cimport numpy as np
np.import_array()
from libc.stdlib cimport atof
import cython
from cpython cimport bool
def read_pages_cython(filename):
cdef int n_pages = 0
cdef bool reading_page = False
cdef float[:, ::1] data = np.zeros((2000000, 14), np.float32)
cdef int i = 0
cdef float time
with open(filename, 'rb') as f:
for line in f:
if not reading_page:
if b'SUMMARY' in line:
time = atof(line[73:80])
reading_page = True
else:
if len(line) == 133:
data[i, 0] = atof(line[0:5])
# data[i, 1] = atof(line[5:10])
data[i, 2] = atof(line[12:21])
data[i, 3] = atof(line[23:32])
data[i, 4] = atof(line[34:43])
data[i, 5] = atof(line[45:54])
data[i, 6] = atof(line[56:65])
data[i, 7] = atof(line[67:76])
data[i, 8] = atof(line[78:87])
data[i, 9] = atof(line[89:98])
data[i, 10] = atof(line[100:109])
data[i, 11] = atof(line[111:120])
data[i, 12] = atof(line[122:131])
data[i, 13] = time
if len(line) > 6:
if line[:1] == b'1':
if b'SUMMARY' in line:
time = atof(line[73:80])
reading_page = True
else:
reading_page = False
i += 1
continue
elif line[:1] == b'0':
reading_page = False
i += 1
continue
i += 1
return data.base[:i]