Cython解析数字的字符串行

时间:2018-10-01 22:03:26

标签: python pandas cython

我已经有一个表作为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不会处理文本,因此对此没有帮助。

感谢任何可以提供帮助的人!

1 个答案:

答案 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]