Bra*_*mon 12 python datetime pandas python-dateutil
熊猫的内部问题:我一直惊讶地发现了几次,明确传递调用到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)
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)
您已经确定了两个重要的函数: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 函数。
该系列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 函数,用于将字符串数组解析为给定特定格式的日期时间结构。