推断日期格式与传递解析器
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_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)
仔细观察后
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 函数,用于将字符串数组解析为给定特定格式的日期时间结构。
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_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)
仔细观察后 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 函数,用于将字符串数组解析为给定特定格式的日期时间结构。