推断日期格式与传递解析器

Inferring date format versus passing a parser

Pandas 内部问题:我很惊讶地发现有几次在 pandas.read_csv 中明确地将可调用对象传递给 date_parser 会导致 much 比简单地使用 infer_datetime_format=True.

更慢的读取时间

这是为什么?这两个选项之间的时间差异是否会因日期格式而异,或者其他哪些因素会影响它们的相对时间?

在下面的例子中,infer_datetime_format=True 花费十分之一的时间来传递具有指定格式的日期解析器。我天真地认为后者会更快,因为它是明确的。

文档确实注意到了,

[if True,] pandas will attempt to infer the format of the datetime strings in the columns, and if it can be inferred, switch to a faster method of parsing them. In some cases this can increase the parsing speed by 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() 调用 helper function which in turn calls pd.core.tools.datetimes.to_datetime()。该函数(仅可作为 pd.to_datetime() 访问)具有 infer_datetime_formatformat 参数。

但是,在这种情况下,相对时间非常不同,不能反映上述情况:

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's 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)

仔细观察后 Pandas original pull request for infer format

我认为直接传递解析器与 pandas' ._convert_listlike 方法不兼容。尽管 dateutil 的解析器本身并不是可并行化的,但 pandas 的转换列表类操作可以并行执行解析,如果它们可以推断格式的话。 post 提到格式是从第一项推断出来的,然后所有其他项都得到相同的处理。作为一名数学家,我可能会建议随机取 10 个条目,并选择大多数解析格式。

更新 正如评论中提到的,在拉取请求中,也有传递格式或解析器之间的比较:test gist。让解析器成为 np.vectorize 对象或类似对象可能值得一个功能请求。

infer_datetime_format 将它推导的格式应用于单次传递中的所有元素(即以矢量化方式)。相比之下,您的 lambda 函数会为每个元素调用,从而带来更多的调用开销和性能下降。

有一个 request from 2012 用于显式格式关键字参数,理论上它可以为您提供所需的功能和所需的性能。代替实施,最好的选择是采用 Wes 在 link 中建议的方法,只需将日期作为字符串读取,在事后调用 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)

您已经确定了两个重要的函数:read_csv 准备了一个函数来使用 _make_date_converter 解析日期列,这总是会调用 to_datetime (pandas'初级string-to-date转换工具).

@WillAyd 和@bmbigbang 的回答对我来说似乎都是正确的,因为他们将缓慢的原因确定为重复调用 lambda 函数。

由于您要求提供有关 pandas 源代码的更多详细信息,我将尝试在下面更详细地检查每个 read_test 调用,以找出我们在 [=18= 中的最终位置] 以及最终为什么时间与您观察到的数据相同。


read_test()

这非常快,因为在没有任何关于可能的日期格式的提示的情况下,pandas 将尝试解析字符串的 list-like 列,就好像它们大约在 ISO8601 format(这是一个很常见的情况)。

进入 to_datetime,我们很快到达 this code branch

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

从这里开始,一路compiled Cython code

array_to_datetime iterates through the column of strings to convert each one to datetime format. For each row, we hit _string_to_dts at this line; then we go to another short snippet of inlined code (_cstring_to_dts) which means parse_iso_8601_datetime 被调用以将字符串实际解析为日期时间对象。

这个函数不仅能够解析 YYYY-MM-DD 格式的日期,所以只需要一些内务处理就可以完成这项工作(由 parse_iso_8601_datetime 填充的 C 结构成为一个合适的日期时间对象,检查了一些边界)。

如您所见,dateutil.parser.parser 根本没有 被调用。


read_test(infer_datetime_format=True)

让我们看看为什么这几乎read_test()一样快。

要求 pandas 推断日期时间格式(并且不传递 format 参数)意味着我们在 here 中登陆 to_datetime:

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

这个调用 _guess_datetime_format_for_array, which takes the first non-null value in the column and gives it to _guess_datetime_format. This tries to build a datetime format string to use for future parsing. ( 在它能够识别的格式之上有更多的细节。)

幸运的是,YYYY-MM-DD格式是这个函数可以识别的格式。更幸运的是,这种特殊格式有一个 fast-path 通过 pandas 代码!

您可以看到 pandas 将 infer_datetime_format 设置回 False here:

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

这允许代码将 same path as above 带到 parse_iso_8601_datetime 函数。


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

我们提供了一个函数来解析日期,因此 pandas 执行 this code block.

但是,这会在内部引发异常:

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

此异常会立即被捕获,并且 pandas 在调用 to_datetime.

之前回退到使用 try_parse_dates

try_parse_dates 表示不是在数组上调用,而是为数组中的每个值重复调用 lambda 函数 this loop:

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, but this time pandas sees a date object and uses another function (pydate_to_dt64) 使其成为 datetime64 对象。

速度变慢的真正原因是重复调用 lambda 函数。


关于您的更新和 MM/DD/YYYY 格式

系列 s 具有 MM/DD/YYYY 格式的日期字符串。

这是不是 ISO8601 格式。 pd.to_datetime(s, infer_datetime_format=False) 尝试使用 parse_iso_8601_datetime but this fails with a ValueError. The error is handled here: pandas is going to use parse_datetime_string instead. This means that dateutil.parser.parse is used 解析字符串以将字符串转换为日期时间。这就是为什么在这种情况下它很慢:在循环中重复使用 Python 函数。

pd.to_datetime(s, format='%m/%d/%Y')pd.to_datetime(s, infer_datetime_format=True)在速度上没有太大区别。后者使用_guess_datetime_format_for_array again to infer the MM/DD/YYYY format. Both then hit array_strptime here

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

array_strptime 是一个快速的 Cython 函数,用于将字符串数组解析为给定特定格式的日期时间结构。