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

时间:2018-02-11 04:34:50

标签: python pandas datetime python-dateutil

Pandas internals问:我很惊讶地发现,在date_parser内明确地将可调用传递给pandas.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)

我有兴趣了解infer内部发生的事情,以便为此提供优势。我之前的理解是,首先已经进行了某种类型的推断,因为如果两者都没有通过则会使用dateutil.parser.parser

更新:对此进行了一些挖掘,但未能回答这个问题。

read_csv()拨打helper function,然后拨打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)

3 个答案:

答案 0 :(得分:3)

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

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

由于您要求了解有关pandas源代码的更多详细信息,因此我将尝试在下面详细检查每个read_test调用,以找出我们在to_datetime中的最终位置,并最终确定时序的原因就像你观察到的数据一样。

read_test()

这非常快,因为在没有任何关于可能日期格式的提示的情况下,pandas将尝试解析类似列表的字符串列,就好像它们大约在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遍历字符串列,将每个字符串转换为日期时间格式。对于每一行,我们在_string_to_dts点击this line;然后我们转到另一个内联代码(_cstring_to_dts)的简短片段,这意味着调用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参数)意味着我们在to_datetime中登陆here

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。这会尝试构建日期时间格式字符串以用于将来的解析。 (My answer here在其能够识别的格式之上有更多细节。)

幸运的是,YYYY-MM-DD格式是可以被此功能识别的格式。更幸运的是,这种特殊格式有一条通过熊猫代码的快速路径!

你可以看到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

此异常会立即被捕获,并且在调用to_datetime之前,pandas会回到使用try_parse_dates

try_parse_dates意味着不是在数组上调用,而是为this loop中的数组的每个值重复调用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

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

回到to_datetime,我们现在有一个填充了datetime个对象的对象数组。我们再次点击array_to_datetime,但这次pandas sees a date object并使用另一个函数(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。处理错误here:pandas将改为使用parse_datetime_string。这意味着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来推断MM / DD / YYYY格式。然后两者都点击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函数,用于在给定特定格式的情况下将字符串数组解析为日期时间结构。

答案 1 :(得分:1)

密切关注之后 Pandas original pull request for infer format

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

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

答案 2 :(得分:1)

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

显式格式关键字参数有一个request from 2012,从理论上讲,它可以为您提供所需功能。取而代之的是,您最好的选择是在链接中使用Wes的建议方法,只需在字符串中读取日期,在事后调用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)