大型pandas read_csv在datetime索引上的速度改进

8

我有一些非常大的文件,看起来像这样:

05/31/2012,15:30:00.029,1306.25,1,E,0,,1306.25

05/31/2012,15:30:00.029,1306.25,8,E,0,,1306.25

我可以使用以下方法轻松读取它们:

  pd.read_csv(gzip.open("myfile.gz"), header=None,names=
  ["date","time","price","size","type","zero","empty","last"], parse_dates=[[0,1]])

有没有一种有效的方法将这样的日期解析成pandas时间戳?如果没有,是否有编写cython函数的指南,可以将其传递给date_parser=?

我尝试编写自己的解析器函数,但仍然需要太长时间才能完成我正在处理的项目。


那么read_csv函数满足了您的解析需求,但速度太慢了? - BKay
是的,基本上是这样。如果没有简单的解决方案,我想看看是否有人能够给出在Cython中处理此问题的指导方针。 - Michael WS
让我困惑的是pd.Timestamp无法正常工作(但单独使用例如pd.Timestamp('05/31/2012,15:30:00.029')可以正常工作)。它不能正常工作的原因很可能是一个错误。 - Andy Hayden
发布在 Github 上的问题:https://github.com/pydata/pandas/issues/2932。 - Andy Hayden
修复了 Git 主分支中的错误。 - Wes McKinney
3个回答

7
我使用以下的Cython代码有一个不可思议的加速效果(50倍):
从Python中调用: timestamps = convert_date_cython(df["date"].values, df["time"].values)
cimport numpy as np
import pandas as pd
import datetime
import numpy as np
def convert_date_cython(np.ndarray date_vec, np.ndarray time_vec):
    cdef int i
    cdef int N = len(date_vec)
    cdef out_ar = np.empty(N, dtype=np.object)
    date = None
    for i in range(N):
        if date is None or date_vec[i] != date_vec[i - 1]:
            dt_ar = map(int, date_vec[i].split("/"))
            date = datetime.date(dt_ar[2], dt_ar[0], dt_ar[1])
        time_ar = map(int, time_vec[i].split(".")[0].split(":"))
        time = datetime.time(time_ar[0], time_ar[1], time_ar[2])
        out_ar[i] = pd.Timestamp(datetime.datetime.combine(date, time))
    return out_ar

7

Michael WS的解决方案进行了改进:

  • 将转换为pandas.Timestamp最好在Cython代码外执行。
  • atoi和处理本地C字符串比Python函数稍快。
  • datetime-lib调用的数量从2个减少到1个(+1个偶尔的日期)。
  • 微秒也被处理。

NB!此代码中的日期顺序为日/月/年。

总的来说,该代码似乎比原始的convert_date_cython快大约10倍。然而,如果在read_csv之后调用此代码,则在SSD硬盘上由于读取开销,总时间的差异仅为几个百分点。我猜在普通HDD上,差异会更小。

cimport numpy as np
import datetime
import numpy as np
import pandas as pd
from libc.stdlib cimport atoi, malloc, free 
from libc.string cimport strcpy

### Modified code from Michael WS:
### https://dev59.com/Xm3Xa4cB1Zd3GeqPfHyL#15812787

def convert_date_fast(np.ndarray date_vec, np.ndarray time_vec):
    cdef int i, d_year, d_month, d_day, t_hour, t_min, t_sec, t_ms
    cdef int N = len(date_vec)
    cdef np.ndarray out_ar = np.empty(N, dtype=np.object)  
    cdef bytes prev_date = <bytes> 'xx/xx/xxxx'
    cdef char *date_str = <char *> malloc(20)
    cdef char *time_str = <char *> malloc(20)

    for i in range(N):
        if date_vec[i] != prev_date:
            prev_date = date_vec[i] 
            strcpy(date_str, prev_date) ### xx/xx/xxxx
            date_str[2] = 0 
            date_str[5] = 0 
            d_year = atoi(date_str+6)
            d_month = atoi(date_str+3)
            d_day = atoi(date_str)

        strcpy(time_str, time_vec[i])   ### xx:xx:xx:xxxxxx
        time_str[2] = 0
        time_str[5] = 0
        time_str[8] = 0
        t_hour = atoi(time_str)
        t_min = atoi(time_str+3)
        t_sec = atoi(time_str+6)
        t_ms = atoi(time_str+9)

        out_ar[i] = datetime.datetime(d_year, d_month, d_day, t_hour, t_min, t_sec, t_ms)
    free(date_str)
    free(time_str)
    return pd.to_datetime(out_ar)

2
日期时间字符串的基数并不是很大。例如,格式为%H-%M-%S的时间字符串的数量为24*60*60=86400。如果您的数据集行数比这个数量大得多或者您的数据包含大量重复的时间戳,那么在解析过程中添加缓存可以显著提高速度。

对于没有Cython的人来说,以下是纯Python的替代方案:

import numpy as np
import pandas as pd
from datetime import datetime


def parse_datetime(dt_array, cache=None):
    if cache is None:
        cache = {}
    date_time = np.empty(dt_array.shape[0], dtype=object)
    for i, (d_str, t_str) in enumerate(dt_array):
        try:
            year, month, day = cache[d_str]
        except KeyError:
            year, month, day = [int(item) for item in d_str[:10].split('-')]
            cache[d_str] = year, month, day
        try:
            hour, minute, sec = cache[t_str]
        except KeyError:
            hour, minute, sec = [int(item) for item in t_str.split(':')]
            cache[t_str] = hour, minute, sec
        date_time[i] = datetime(year, month, day, hour, minute, sec)
    return pd.to_datetime(date_time)


def read_csv(filename, cache=None):
    df = pd.read_csv(filename)
    df['date_time'] = parse_datetime(df.loc[:, ['date', 'time']].values, cache=cache)
    return df.set_index('date_time')

根据特定数据集,加速比为150x+:

$ ls -lh test.csv
-rw-r--r--  1 blurrcat  blurrcat   1.2M Apr  8 12:06 test.csv
$ head -n 4 data/test.csv
user_id,provider,date,time,steps
5480312b6684e015fc2b12bc,fitbit,2014-11-02 00:00:00,17:47:00,25
5480312b6684e015fc2b12bc,fitbit,2014-11-02 00:00:00,17:09:00,4
5480312b6684e015fc2b12bc,fitbit,2014-11-02 00:00:00,19:10:00,67

在IPython中:
In [1]: %timeit pd.read_csv('test.csv', parse_dates=[['date', 'time']])
1 loops, best of 3: 10.3 s per loop
In [2]: %timeit read_csv('test.csv', cache={})
1 loops, best of 3: 62.6 ms per loop

为了限制内存使用,可以将字典缓存替换为类似于 LRU 的东西。


在我的例子中,它是以毫秒为单位的时间戳。这个数值非常大:15:30:00.029。 - Michael WS
@MichaelWS 这样你就可以在缓存中使用额外的1k项,你懂的。 - blurrcat

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接