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_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)
答案 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函数。
系列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)