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

Bra*_*mon 12 python datetime pandas python-dateutil

熊猫的内部问题:我一直惊讶地发现了几次,明确传递调用到date_parserpandas.read_csv导致慢读取时间比单纯使用infer_datetime_format=True.

为什么是这样?这两个选项之间的时间差异是特定于日期格式的,还是其他因素会影响它们的相对时间?

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

文档注意到,

[如果为True,] pandas将尝试推断列中日期时间字符串的格式,如果可以推断,请切换到更快的解析方法.在某些情况下,这可以将解析速度提高5-10倍.

但是没有给出太多细节,我无法完全通过源头工作.

建立:

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)
Run Code Online (Sandbox Code Playgroud)

我有兴趣知道内部发生了什么,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'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)
Run Code Online (Sandbox Code Playgroud)

Ale*_*ley 4

您已经确定了两个重要的函数:read_csv准备一个函数来使用 解析日期列_make_date_converter,并且这总是会进行调用to_datetime(pandas 的主要字符串到日期转换工具)。

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

由于您要求了解有关 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(...)
Run Code Online (Sandbox Code Playgroud)

从这里开始,就一直编译Cython代码了。

array_to_datetime迭代字符串列,将每个字符串转换为日期时间格式。对于每一行,我们都会点击_string_to_dts这一;然后我们转到内联代码的另一个简短片段(_cstring_to_dts),这意味着parse_iso_8601_datetime调用它来将字符串实际解析为日期时间对象。

该函数不仅能够解析 YYYY-MM-DD 格式的日期,因此只需要一些内务处理即可完成这项工作(由 填充的 C 结构体parse_iso_8601_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)
Run Code Online (Sandbox Code Playgroud)

这会调用_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
Run Code Online (Sandbox Code Playgroud)

这允许代码采用与上面相同的路径到达函数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
Run Code Online (Sandbox Code Playgroud)

这个异常会立即被捕获,并且 pandas 会回退到try_parse_dates调用之前的using to_datetime

try_parse_dates意味着不是在数组上调用 lambda 函数,而是在此循环中针对数组的每个值重复调用 lambda 函数:

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
Run Code Online (Sandbox Code Playgroud)

尽管是编译后的代码,我们还是要付出对 Python 代码进行函数调用的代价。与上述其他方法相比,这使得它非常慢。

回到to_datetime,我们现在有一个充满datetime对象的对象数组。我们再次点击array_to_datetime,但这次pandas 看到一个日期对象并使用另一个函数 ( 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,但失败并显示ValueError. 错误在这里处理:将使用 pandasparse_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)
Run Code Online (Sandbox Code Playgroud)

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