【问题标题】:Pandas, efficiently converting from timespan dataframe to year-month enumerated dataframePandas,有效地将时间跨度数据帧转换为年月枚举数据帧
【发布时间】:2018-03-26 22:57:35
【问题描述】:

我目前有一个数据框,其中包含 ID、类别和由开始日期和结束日期给出的时间跨度。我想将此时间跨度数据框转换为一个,其中每一行对应于每个 ID 和类别的给定 YYYY MM。

下面的代码显示了一个开始 df 的示例,以及我通常会做什么来创建这个 YYYY MM 枚举数据框。日期有一些烦人的数学运算,以确保我捕获开始日期和结束日期之间的每个 YYYY MM 包括在内,但这对我的问题来说并不是非常重要。

我遇到的问题是,实际上,我需要在具有近 600 万个时间跨度条目的 df 上运行它。我想知道是否有更好的方法来利用熊猫而不是基本上用 for 循环来完成这个?这将运行,但最终需要几个小时才能爬过整个数据框。对我来说,我似乎不太明显可以使用循环以外的任何方法来完成此操作?

import pandas as pd
from dateutil.relativedelta import relativedelta
from datetime import timedelta

df = pd.DataFrame({'ID': [1,1,2], 'start': ['2001-01-01', '2002-01-02', '2001-05-07'],
             'end': ['2002-01-12', '2002-01-14', '2002-05-01'], 'category': ['A', 'B', 'A']})
df['start'] = pd.to_datetime(df['start'])
df['end'] = pd.to_datetime(df['end'])

df_list = []
for index,row in df.iterrows():
    start = row.start - timedelta(days=row.start.day-1)
    stop = row.end - timedelta(days=row.end.day-1) + relativedelta(months=1)
    tempdf = pd.DataFrame({'ID': row.ID, 'year':pd.date_range(start, stop, freq='1M').year,
                         'month': pd.date_range(start, stop, freq='1M').month, 
                         'category': row.category})
    df_list.append(tempdf)

newdf = pd.concat(df_list, ignore_index=True)

【问题讨论】:

  • 如果 3 行扩展到 27 行(增加 9 倍),那么平均 6M 将增加到 162M 行,我怀疑您是否可以将其全部放入内存。
  • 内存应该不是问题,因为我可以将大部分字段存储为小整数。由于我已经清理了 span df 以确保给定 ID-category 组合的时间跨度不会重叠,我总是可以只处理较小的文件块。但这对速度没有帮助......
  • 为了提供更多上下文,我有第三个数据框,ID YYYY 和 MM 作为列。我想向该数据框添加一个类别列。由于合并条件是针对一个 df 的 YYYY MM 在另一个时间跨度内,所以并不那么简单。编码的查找太慢了,所以我认为这个 YYYY MM 解析的数据帧应该允许我使用 DataFrame.merge(on=['ID', 'year', 'month']) 更快地完成这个任务
  • 最终它需要大约 40 GB 的内存才能完成,这并不可怕。通过将循环外的开始和停止计算为矢量化表达式,我能够大大加快它的速度,但它仍然需要大约 3-4 小时才能运行。我想这是不可避免的,因为我基本上是在创建一个全新的数据框:(

标签: python pandas


【解决方案1】:

这很有效,主要基于找到的解决方案 herehere

这是问题的设置。我进一步包含了一个价值不高的时间跨度。

import pandas as pd
import numpy as np

df = pd.DataFrame({'ID': [1,1,2,2], 'start': ['2001-01-01', '2002-01-02', '2001-05-07', '7'],
             'end': ['2002-01-12', '2002-01-17', '2002-05-01', '2002-05-01'], 'category': ['A', 'B', 'A', 'C']})
df['start'] = pd.to_datetime(df['start'], errors='coerce')
df['end'] = pd.to_datetime(df['end'], errors='coerce')

然后是解决方案。使用这种方法,实际上不需要对日期进行凌乱的代数。重采样获取 YYYY MM 枚举,包括开始日期和结束日期。

df['temp_id'] = range(len(df))
real_span = np.logical_and(df.start.notnull(), df.end.notnull())

df_mm = (df.loc[real_span, ['temp_id', 'start', 'end']].set_index('temp_id').stack()
         .reset_index(level=-1, drop=True).rename('period').to_frame())
df_mm = df_mm.groupby('temp_id').apply(lambda x: x.set_index('period').resample('M').asfreq()).reset_index()

df_mm['month'] = df_mm['period'].dt.month
df_mm['year'] = df_mm['period'].dt.year
df_mm.merge(df, on=['temp_id']).drop(columns=['temp_id', 'period', 'end', 'start'])

唯一的问题是,如果某件事在同一日期开始和结束。在这种情况下,set_index 将抛出ValueError: cannot reindex from a duplicate axis。将df.start != df.end 的条件添加到real_span 掩码以过滤掉它们并不难。或者,如果应该包含它们,那么这将捕获它们而不会引发错误。它基本上只是将日期更改为月份的第一天和最后一天,这不会影响对 YYYY MM 枚举 DataFrame 的重新采样,但确保开始日期永远不会等于结束日期。

from datetime import timedelta
from pandas.tseries.offsets import MonthEnd

df['start'] = (df.start[df.start.notnull()] 
    - df.start[df.start.notnull()].dt.day.apply(timedelta) 
    + timedelta(days=1))
df['end'] = (df.end[df.end.notnull()] 
    - df.end[df.end.notnull()].dt.day.apply(timedelta) 
    + timedelta(days=1)) + MonthEnd(1)

【讨论】:

    猜你喜欢
    • 2018-04-08
    • 2016-11-16
    • 2017-02-04
    • 1970-01-01
    • 2021-01-24
    • 2018-05-24
    • 2019-11-27
    • 2021-11-16
    • 2016-09-27
    相关资源
    最近更新 更多