Вывод формата даты против прохождения парсера

Pandas Внутренний вопрос: я был удивлен, обнаружив несколько раз, когда явное пропускание вызываемого на date_parser внутри pandas.read_csv приводит к значительно более медленному времени чтения, чем просто используя infer_datetime_format=True.

Почему это? Будут ли временные различия между этими двумя параметрами соответствовать конкретным датам или другим факторам, влияющим на их относительное время?

В нижеприведенном случае infer_datetime_format=True занимает одну десятую время прохождения синтаксического анализатора даты с указанным форматом. Я бы наивно предположил, что последний будет быстрее, потому что он явный.

Документы отмечают,

[if True,] pandas будет пытаться вывести формат строк datetime в столбцах, и если это можно сделать вывод, переключитесь на более быстрый метод их разбора. В некоторых случаях это может увеличить скорость разбора на 5-10x.

но здесь не так много деталей, и я не смог полностью выполнить свой путь через источник.

Настройка:

from io import StringIO

import numpy as np
import pandas as pd

np.random.seed(444)
dates = pd.date_range('1980', '2018')
df = pd.DataFrame(np.random.randint(0, 100, (len(dates), 2)),
                  index=dates).add_prefix('col').reset_index()

# Something reproducible to be read back in
buf = StringIO()
df.to_string(buf=buf, index=False)

def read_test(**kwargs):
    # Not ideal for .seek() to eat up runtime, but alleviate
    # this with more loops than needed in timing below
    buf.seek(0)
    return pd.read_csv(buf, sep='\s+', parse_dates=['index'], **kwargs)

# dateutil.parser.parser called in this case, according to docs
%timeit -r 7 -n 100 read_test()
18.1 ms ± 217 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)

%timeit -r 7 -n 100 read_test(infer_datetime_format=True)
19.8 ms ± 516 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)

# Doesn't change with native Python datetime.strptime either
%timeit -r 7 -n 100 read_test(date_parser=lambda dt: pd.datetime.strptime(dt, '%Y-%m-%d'))
187 ms ± 4.05 ms per loop (mean ± std. dev. of 7 runs, 100 loops each)

Мне интересно узнать немного о том, что происходит внутри с помощью infer, чтобы придать этому преимущество. Мое прежнее понимание заключалось в том, что вначале уже происходит какой-то вывод, потому что dateutil.parser.parser используется, если ни один из них не передан.


Обновление: некоторые из них были сделаны, но не смогли ответить на вопрос.

read_csv() вызывает вспомогательную функцию, которая в свою очередь вызывает pd.core.tools.datetimes.to_datetime(). Эта функция (доступная только как pd.to_datetime()) имеет как аргумент infer_datetime_format, так и format.

Однако в этом случае относительные тайминги очень разные и не отражают выше:

s = pd.Series(['3/11/2000', '3/12/2000', '3/13/2000']*1000)

%timeit pd.to_datetime(s,infer_datetime_format=True)
19.8 ms ± 1.54 ms per loop (mean ± std. dev. of 7 runs, 10 loops each)

%timeit pd.to_datetime(s,infer_datetime_format=False)
1.01 s ± 65.8 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)

# This was taking the longest with i/o functions,
# now it behaving "as expected"
%timeit pd.to_datetime(s,format='%m/%d/%Y')
19 ms ± 373 µs per loop (mean ± std. dev. of 7 runs, 10 loops each)

Ответ 1

Вы определили две важные функции: read_csv готовит функцию для синтаксического анализа столбцов даты с помощью _make_date_converter, и это всегда будет делать вызов to_datetime (pandas "первичная строка-дата инструмент преобразования).

Ответы @WillAyd и @bmbigbang кажутся мне правильными, потому что они идентифицируют причину медленности как повторные вызовы лямбда-функции.

Поскольку вы запрашиваете более подробные сведения о исходном коде pandas, я попробую и рассмотрю каждый вызов read_test более подробно ниже, чтобы узнать, где мы закончили в to_datetime, и, в конечном счете, почему тайминги вы наблюдали за своими данными.


read_test()

Это очень быстро, потому что без каких-либо подсказок о возможном формате даты pandas будет пытаться проанализировать столбцы строк, похожие на список, как если бы они были примерно в ISO8601 формат (это очень распространенный случай).

Переходя в to_datetime, мы быстро достигаем этой ветки кода:

if result is None and (format is None or infer_datetime_format):
    result = tslib.array_to_datetime(...)

С этого момента он скомпилированный код Cython полностью.

array_to_datetime итерации через столбец строк для преобразования каждого в формат даты и времени. Для каждой строки мы нажимаем _string_to_dts в эта строка; затем переходим к другому короткому фрагменту встроенного кода (_cstring_to_dts), что означает parse_iso_8601_datetime вызывается для фактического синтаксического анализа строки для объекта datetime.

Эта функция более чем способна анализировать даты в формате YYYY-MM-DD, и поэтому для завершения задания требуется только некоторое обслуживание (структура C, заполненная parse_iso_8601_datetime, становится надлежащим объектом datetime, некоторые ограничения проверяются).

Как вы можете видеть, dateutil.parser.parser вообще не вызывается.


read_test(infer_datetime_format=True)

Посмотрим, почему это почти так же быстро, как read_test().

Запрос pandas на вывод формата даты и времени (и передача аргумента format) означает, что мы приземляемся здесь в to_datetime:

if infer_datetime_format and format is None:
    format = _guess_datetime_format_for_array(arg, dayfirst=dayfirst)

Это вызывает _guess_datetime_format_for_array, который принимает первое ненулевое значение в столбце и присваивает его _guess_datetime_format. Это пытается создать строку формата даты и времени, которая будет использоваться для будущего анализа. (Мой ответ здесь содержит более подробные сведения о форматах, которые он может распознать.)

К счастью, формат YYYY-MM-DD - это тот, который может быть распознан этой функцией. К счастью, этот конкретный формат имеет быстрый путь через код pandas!

Вы можете видеть pandas наборы infer_datetime_format обратно на False здесь:

if format is not None:
    # There is a special fast-path for iso8601 formatted
    # datetime strings, so in those cases don't use the inferred
    # format because this path makes process slower in this
    # special case
    format_is_iso8601 = _format_is_iso(format)
    if format_is_iso8601:
        require_iso8601 = not infer_datetime_format
        format = None

Это позволяет коду принимать тот же путь, что и выше, к функции parse_iso_8601_datetime.


read_test(date_parser=lambda dt: strptime(dt, '%Y-%m-%d'))

Мы предоставили функцию для синтаксического анализа даты, поэтому pandas выполняет этот блок кода.

Однако это возникает как исключение изнутри:

strptime() argument 1 must be str, not numpy.ndarray

Это исключение сразу же попадает, а pandas возвращается к использованию try_parse_dates перед вызовом to_datetime.

try_parse_dates означает, что вместо вызова в массиве функция лямбда вызывается повторно для каждого значения массива в этот цикл:

for i from 0 <= i < n:
    if values[i] == '':
        result[i] = np.nan
    else:
        result[i] = parse_date(values[i]) # parse_date is the lambda function

Несмотря на скомпилированный код, мы платим штраф за вызовы функций для кода Python. Это делает его очень медленным по сравнению с другими подходами выше.

Вернемся в to_datetime, теперь у нас есть массив объектов, заполненный объектами datetime. Снова мы нажали array_to_datetime, но на этот раз pandas видит объект даты и использует другую функцию (pydate_to_dt64), чтобы превратить ее в объект datetime64.

Причина замедления на самом деле связана с повторными вызовами лямбда-функции.


О вашем обновлении и формате MM/DD/YYYY

Серия s имеет строки даты в формате MM/DD/YYYY.

Это не формат ISO8601. pd.to_datetime(s, infer_datetime_format=False) пытается проанализировать строку, используя parse_iso_8601_datetime, но это не удается с помощью ValueError. Ошибка обрабатывается здесь: pandas будет использовать parse_datetime_string. Это означает, что dateutil.parser.parse используется для преобразования строки в дату и время. Поэтому в этом случае он медленный: повторное использование функции Python в цикле.

Здесь нет большой разницы между pd.to_datetime(s, format='%m/%d/%Y') и pd.to_datetime(s, infer_datetime_format=True) с точки зрения скорости. Последний использует _guess_datetime_format_for_array для вывода формата MM/DD/YYYY. И тогда нажмите array_strptime здесь:

if format is not None:
    ...
    if result is None:
        try:
            result = array_strptime(arg, format, exact=exact, errors=errors)

array_strptime - это быстрая функция Cython для синтаксического анализа массива строк в datetime-структурах, заданных в определенном формате.

Ответ 2

После более пристального внимания Pandas оригинальный запрос на извлечение для формата infer

Я считаю, что передача парсера напрямую не совместима с методом pandas '._convert_listlike. В то время как синтаксический анализатор dateutil не является неотъемлемо параллелизуемым, pandas преобразовать список, подобный операциям, может воздействовать на парсинг параллельно, если они могут вывести формат. В сообщении упоминается, что формат выводится из первого пункта, затем все остальные получают одинаковое обращение. Будучи математиком, я бы, вероятно, рекомендовал принять 10 случайных записей и принять формат синтаксического анализа в качестве избранного.

Обновление Как упоминалось в комментариях и в запросе на растяжение, также существуют сравнения между передачей формата или синтаксического анализатора: test gist. Возможно, стоит добавить запрос функции, чтобы парсер был объектом np.vectorize или аналогичным.

Ответ 3

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

Существует запрос от 2012 для аргумента ключевого слова в явном виде, который теоретически предоставит вам функциональность, которую вы ищете, с производительностью ты желаешь. Вместо того, чтобы быть реализованным, лучше всего пойти с предложенным Уэсом подходом в ссылке и просто прочитать дату в виде строки, вызвав pd.to_datetime после факта

Вот пример времени с моей машины, чтобы проиллюстрировать:

%timeit read_test()
15.4 ms ± 96.2 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)

%timeit read_test(infer_datetime_format=True)
17.2 ms ± 1.82 ms per loop (mean ± std. dev. of 7 runs, 100 loops each)

%timeit read_test(date_parser=lambda dt: pd.datetime.strptime(dt, '%Y-%m-%d'))
147 ms ± 4.65 ms per loop (mean ± std. dev. of 7 runs, 10 loops each)

%timeit df = read_test(); df['index'] = pd.to_datetime(df['index'], '%Y-%m-%d')
15.3 ms ± 239 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)