将字符串日期转换为纪元时间不适用于 Cython 和 POSIX C 库

Converting string date to epoch time not working with Cython and POSIX C libraries

我有一个非常大的 pandas 数据框,我想创建一个包含自 ISO-8601 格式日期字符串纪元以来的秒数的列。

我最初为此使用了标准 Python 库,但结果非常慢。我试图通过直接使用 POSIX c 库函数 strptimemktime 来替换它,但未能获得时间转换的正确答案。

这是代码(在 IPython window 中 运行)

%load_ext cythonmagic

%%cython
from posix.types cimport time_t
cimport numpy as np
import numpy as np
import time
cdef extern from "sys/time.h" nogil:
    struct tm:
        int tm_sec
        int tm_min
        int tm_hour
        int tm_mday
        int tm_mon
        int tm_year
        int tm_wday
        int tm_yday
        int tm_isdst
    time_t mktime(tm *timeptr)
    char *strptime(const char *s, const char *format, tm *tm)
cdef to_epoch_c(const char *date_text):
    cdef tm time_val
    strptime(date_text, "%Y-%m-%d", &time_val)
    return <unsigned int>mktime(&time_val)
cdef to_epoch_py(const char *date_text):
    return np.uint32(time.mktime(time.strptime(date_text, "%Y-%m-%d")))
cpdef np.ndarray[unsigned int] apply_epoch_date_c(np.ndarray col_date):
    cdef Py_ssize_t i, n = len(col_date)
    cdef np.ndarray[unsigned int] res = np.empty(n, dtype=np.uint32)
    for i in range(len(col_date)):
        res[i] = to_epoch_c(col_date[i])
    return res
cpdef np.ndarray[unsigned int] apply_epoch_date_py(np.ndarray col_date):
    cdef Py_ssize_t i, n = len(col_date)
    cdef np.ndarray[unsigned int] res = np.empty(n, dtype=np.uint32)
    for i in range(len(col_date)):
        res[i] = to_epoch_py(col_date[i])
    return res

strptime 创建的结构在我看来不正确小时、分钟和秒值太大,删除它们或将它们设置为 0 似乎没有得到我正在寻找的答案对于.

这是一个小测试 df,它显示了 c 方法的值不正确:

from pandas import DataFrame
test = DataFrame({'date_text':["2015-05-18" for i in range(3)]}, dtype=np.uint32)

apply_epoch_date_py(test['date_text'].values)
Output: array([1431903600, 1431903600, 1431903600], dtype=uint32)
apply_epoch_date_c(test['date_text'].values)
Output: array([4182545380, 4182617380, 4182602980], dtype=uint32)

我不明白为什么 c 版本的值并不总是相同,而且与它们应该的值相差甚远。我希望这个错误相当小,因为这两个在大型数据帧上的时间差异很大(我不确定 c 版本现在做的工作少了多少,因为它没有按预期工作)

test_large = DataFrame({'date_text':["2015-05-18" for i in range(int(10e6))]}, dtype=np.uint32)
%timeit -n 1 -r 1 apply_epoch_date_py(test_large['date_text'].values)
Output: 1 loops, best of 1: 1min 58s per loop
%timeit apply_epoch_date_c(test_large['date_text'].values)
Output: 1 loops, best of 3: 5.59 s per loop

我查过这个 cython time.h post and a general c unix time from string creation post 可能对回答的人有用。

因此,我的主要问题是关于函数 to_epoch_c 为什么这个函数会产生不正确的值?谢谢

更新:

@Jeff 的方法确实是使用 pandas.

解决此问题的最快最简单的方法

与其他方法相比,Python 中 strptime/mktime 的性能较差。这里提到的另一种基于 Python 的方法要快得多。 运行 此 post 中提到的所有方法的转换(加上 pd.to_datetime 给定的字符串格式)提供了有趣的结果。 Pandas 和 infer_datetime_format 无疑是最快的,扩展性非常好。有点不直观,如果你告诉 pandas 日期格式是什么,它会慢得多。

两种pandas方法的配置文件比较:

%prun -l 3 pd.to_datetime(df['date_text'],infer_datetime_format=True, box=False).values.view('i8')/10**9
352 function calls (350 primitive calls) in 0.021 seconds
Ordered by: internal time
List reduced from 96 to 3 due to restriction <3>

ncalls  tottime  percall  cumtime  percall filename:lineno(function)
    1    0.013    0.013    0.013    0.013 {pandas.tslib.array_to_datetime}
    1    0.005    0.005    0.005    0.005 {pandas.lib.isnullobj}
    1    0.001    0.001    0.021    0.021 <string>:1(<module>)

%prun -l 3 pd.to_datetime(df['date_text'],format="%Y-%m-%d", box=False).values.view('i8')/10**9
109 function calls (107 primitive calls) in 0.253 seconds

Ordered by: internal time
List reduced from 55 to 3 due to restriction <3>

ncalls  tottime  percall  cumtime  percall filename:lineno(function)
    1    0.251    0.251    0.251    0.251 {pandas.tslib.array_strptime}
    1    0.001    0.001    0.253    0.253 <string>:1(<module>)
    1    0.000    0.000    0.252    0.252 tools.py:176(to_datetime)

似乎如果你不传入 time_val.tm_hour, time_val.tm_mintime_val.tm_sec 日期解析不正确,将值设置为 0 将 return 正确的时间戳:

cdef extern from "sys/time.h" nogil:
    struct tm:
        int    tm_sec   #Seconds [0,60].
        int    tm_min   #Minutes [0,59].
        int    tm_hour  #Hour [0,23].
        int    tm_mday  #Day of month [1,31].
        int    tm_mon   #Month of year [0,11].
        int    tm_year  #Years since 1900.
        int    tm_wday  #Day of week [0,6] (Sunday =0).
        int    tm_yday  #Day of year [0,365].
        int    tm_isdst #Daylight Savings
    time_t mktime(tm *timeptr)
    char *strptime(const char *s, const char *format, tm *tm)
cdef to_epoch_c(const char *date_text):
    cdef tm time_val
    time_val.tm_hour,  time_val.tm_min,  time_val.tm_sec= 0, 0, 0
    strptime(date_text, "%Y-%m-%d", &time_val)
    return  <unsigned int>mktime(&time_val)

如果你 print(time.strptime(date_text, "%Y-%m-%d")) 你看到 python 的值设置为 0 如果你不将它们传递给 strptime:

 time.struct_time(tm_year=2015, tm_mon=5, tm_mday=18, tm_hour=12, tm_min=0, tm_sec=0, tm_wday=0, tm_yday=138, tm_isdst=-1)

to_epoch_c 中将值设置为默认值 0 也 returns 0:

{'tm_sec': 0, 'tm_hour': 0, 'tm_mday': 18, 'tm_isdst': 1, 'tm_year': 115, 'tm_mon': 4, 'tm_yday': 137, 'tm_wday': 1, 'tm_min': 0}

如果您不在 return 的随机时间戳中设置它们,因为 tm_sec 等似乎有各种值..:[=​​30=]

 {'tm_sec': -1437999996, 'tm_hour': 0, 'tm_mday': 0, 'tm_isdst': -1438000080, 'tm_year': 32671, 'tm_mon': -1412460224, 'tm_yday': 0, 'tm_wday': 5038405, 'tm_min': 32671}
{'tm_sec': -1437999996, 'tm_hour': 4, 'tm_mday': 14, 'tm_isdst': 0, 'tm_year': 69, 'tm_mon': 10, 'tm_yday': 317, 'tm_wday': 5, 'tm_min': 32671}
{'tm_sec': -1437999996, 'tm_hour': 9, 'tm_mday': 14, 'tm_isdst': 0, 'tm_year': 69, 'tm_mon': 10, 'tm_yday': 317, 'tm_wday': 5, 'tm_min': 32671}

我想也许 python 处理当你不传递它们时有点相似,但我还没有查看源代码所以也许在 c 方面更有经验的人会确认。

如果您尝试将少于 9 个元素传递给 time.time_struct,您将收到一个错误,这在一定程度上证实了我的想法:

In [60]: import time  
In [61]: struct = time.struct_time((2015, 6, 18))
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
<ipython-input-61-ee40483c37d4> in <module>()
----> 1 struct = time.struct_time((2015, 6, 18))

TypeError: time.struct_time() takes a 9-sequence (3-sequence given)

您必须传递 9 个元素的序列:

In [63]: struct = time.struct_time((2015, 6, 18, 0, 0, 0, 0, 0, 0))    
In [64]: struct
Out[65]: time.struct_time(tm_year=2015, tm_mon=6, tm_mday=18, tm_hour=0, tm_min=0, tm_sec=0, tm_wday=0, tm_yday=0, tm_isdst=0)

无论如何,您在两者中都会得到相同的行为:

In [16]: import pandas as pd

In [17]: import numpy as np

In [18]: test = pd.DataFrame({'date_text' : ["2015-05-18" for i in range(3)]}, dtype=np.uint32)

In [19]: apply_epoch_date_c(test['date_text'].values)
Out[19]: array([1431903600, 1431903600, 1431903600], dtype=uint32)

In [20]: apply_epoch_date_py(test['date_text'].values)
Out[20]: array([1431903600, 1431903600, 1431903600], dtype=uint32)

自 1970-1-1 以来每个日期的一些测试都显示 return 相同的时间戳:

In [55]: from datetime import datetime, timedelta

In [56]: tests = np.array([(datetime.strptime("1970-1-1","%Y-%m-%d")+timedelta(i)).strftime("%Y-%m-%d") for i in range(16604)])

In [57]: a = apply_epoch_date_c( tests)

In [58]: b = apply_epoch_date_py( tests)

In [59]: for d1,d2 in zip(a,b):
             assert d1 == d1
   ....:     

In [60]: 

对两种实现进行计时,cython 代码似乎确实效率更高:

In [21]: timeit apply_epoch_date_py(test['date_text'].values)
10000 loops, best of 3: 73 µs per loop

In [22]: timeit apply_epoch_date_c(test['date_text'].values)
100000 loops, best of 3: 10.8 µs per loop

简单纯粹的pandas方法。日期本机存储为 i8(自纪元以来以 ns 为单位)。

In [30]: df = DataFrame({'date_text':["2015-05-18" for i in range(int(10e6))]}, dtype=np.uint32)

In [31]: df.info()
<class 'pandas.core.frame.DataFrame'>
Int64Index: 10000000 entries, 0 to 9999999
Data columns (total 1 columns):
date_text    object
dtypes: object(1)
memory usage: 152.6+ MB

In [32]: pd.to_datetime(df['date_text'],infer_datetime_format=True, box=False).values.view('i8')/10**9
Out[32]: 
array([1431907200, 1431907200, 1431907200, ..., 1431907200, 1431907200,
       1431907200])

In [33]: %timeit pd.to_datetime(df['date_text'],infer_datetime_format=True, box=False).values.view('i8')/10**9
1 loops, best of 3: 1.96 s per loop