Cython разбирает строки строк чисел - PullRequest
0 голосов
/ 02 октября 2018

У меня уже есть таблица в виде строки 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.Я понимаю, что нумба не имеет отношения к тексту, поэтому для этого он бесполезен.

Спасибо всем, кто может помочь!

1 Ответ

0 голосов
/ 04 октября 2018

Я нашел решение Cython, которое отвечает моим потребностям.Это использует магию ячеек Cython для ноутбука Jupyter для обработки компиляции.Я выбрал 2000000 для инициализации массива, потому что это разумный верхний предел для моих данных.Функция возвращает только те строки массива numpy, которые были фактически заполнены.Передача этого массива в массив данных pandas обходится довольно дешево.

Я не уверен, сколько еще можно провести оптимизации, учитывая, что я также выбрасываю некоторые строки мусора, которые, как мне кажется, исключают память-отображением.Возможно, я мог бы использовать указатели, как в для ответа на другой вопрос, который у меня был , но было бы сложно найти данные в моем файле и обнаружить плохие строки (подробнее о моей большей проблеме чтения страниц см. Ниже)данных), если бы я перемещал указатель вместо итерации строк.

%%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может более эффективно решить и эту проблему, и проблему регулярных выражений с медленной скоростью.Я использовал regex для поиска и захвата ~ 5000 страниц данных, которые затем объединялись в таблицу, которую я пытаюсь прочитать здесь.Что-то ближе к моей реальной функции Cython показано ниже.Это обрабатывает поиск страницы данных, захватывает детализацию на уровне страницы (время), затем читает строки фактических данных, пока не будет обнаружен флаг остановки (строка, начинающаяся с 0 или 1).Моему регулярному выражению потребовалось 1 с, чтобы извлечь нужные данные, так что это экономит мне много времени в целом.

%%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]
...