【问题标题】:Pandas: decompress date range to individual dates熊猫:将日期范围解压缩为单个日期
【发布时间】:2014-07-26 20:03:16
【问题描述】:

数据集:我有一个 1GB 的股票数据集,其中包含日期范围之间的值。日期范围没有重叠,数据集按 (ticker, start_date) 排序。

>>> df.head()
             start_date    end_date                   val    
ticker         
AAPL         2014-05-01  2014-05-01         10.0000000000
AAPL         2014-06-05  2014-06-10         20.0000000000
GOOG         2014-06-01  2014-06-15         50.0000000000
MSFT         2014-06-16  2014-06-16                  None
TWTR         2014-01-17  2014-05-17         10.0000000000

目标:我想解压缩数据框,以便我有单独的日期而不是日期范围。例如,AAPL 行将从只有 2 行变为 7 行:

>>> AAPL_decompressed.head()
                   val
date                       
2014-05-01         10.0000000000
2014-06-05         20.0000000000
2014-06-06         20.0000000000
2014-06-07         20.0000000000
2014-06-08         20.0000000000

我希望 pandas 有一个很好的优化方法,比如 resample 可以在几行内完成。

【问题讨论】:

    标签: python pandas time-series


    【解决方案1】:

    不止几行,但我认为它会导致您提出的问题:

    从您的数据框开始:

    In [70]: df
    Out[70]:
           start_date   end_date  val  row
    ticker
    AAPL   2014-05-01 2014-05-01   10    0
    AAPL   2014-06-05 2014-06-10   20    1
    GOOG   2014-06-01 2014-06-15   50    2
    MSFT   2014-06-16 2014-06-16  NaN    3
    TWTR   2014-01-17 2014-05-17   10    4
    

    首先,我将此数据框重塑为具有一个 date 列的数据框(因此对于 start_dateend_date 的每个日期,每行重复两次(并且我添加了一个名为 row 的计数器列):

    In [60]: df['row'] = range(len(df))
    In [61]: starts = df[['start_date', 'val', 'row']].rename(columns={'start_date': 'date'})
    In [62]: ends = df[['end_date', 'val', 'row']].rename(columns={'end_date':'date'})
    In [63]: df_decomp = pd.concat([starts, ends])
    In [64]: df_decomp = df_decomp.set_index('row', append=True)
    In [65]: df_decomp.sort_index()
    Out[65]:
                     date  val
    ticker row
    AAPL   0   2014-05-01   10
           0   2014-05-01   10
           1   2014-06-05   20
           1   2014-06-10   20
    GOOG   2   2014-06-01   50
           2   2014-06-15   50
    MSFT   3   2014-06-16  NaN
           3   2014-06-16  NaN
    TWTR   4   2014-01-17   10
           4   2014-05-17   10
    

    基于这个新的数据框,我可以按tickerrow 对其进行分组,并在每个组和fillna 上应用每日resample(使用“pad”方法进行前向填充)

    In [66]: df_decomp = df_decomp.groupby(level=[0,1]).apply(lambda x: x.set_index('date').resample('D').fillna(method='pad'))
    
    In [67]: df_decomp = df_decomp.reset_index(level=1, drop=True)
    

    最后一个命令是删除现在多余的row 索引级别。
    当我们访问 AAPL 行时,它会提供您想要的输出:

    In [69]: df_decomp.loc['AAPL']
    Out[69]:
                val
    date
    2014-05-01   10
    2014-06-05   20
    2014-06-06   20
    2014-06-07   20
    2014-06-08   20
    2014-06-09   20
    2014-06-10   20
    

    【讨论】:

    • 非常好的答案。请忽略我之前的评论,我错了。
    • @joris 如果 val 是混合类型,有什么方法可以重新采样?我的 val 列也可以包含日期和字符串。
    • 问题是如果 val 不是数字的,它会被丢弃。
    • 是的,一个简单的解决方案是在resample 调用中添加how='first'(默认为mean,它不适用于非数字值)。在您的情况下,结果将是相同的。但除此之外,真的不建议使用这种混合 dtype(您可能想做的其他操作也会有这个问题)
    【解决方案2】:

    我认为您可以分五步完成:

    1) 过滤股票行情以找到您想要的股票

    2) 使用pandas.bdate_range 构建startend 之间的日期范围列表

    3) 使用 reduce 展平此列表

    4) 重新索引您的新过滤数据帧

    5) 使用pad方法填充nans

    代码如下:

    >>> import pandas as pd
    >>> import datetime
    
    >>> data = [('AAPL', datetime.date(2014, 4, 28), datetime.date(2014, 5, 2), 90),
                ('AAPL', datetime.date(2014, 5, 5), datetime.date(2014, 5, 9), 80),
                ('MSFT', datetime.date(2014, 5, 5), datetime.date(2014, 5, 9), 150),
                ('AAPL', datetime.date(2014, 5, 12), datetime.date(2014, 5, 16), 85)]
    >>> df = pd.DataFrame(data=data, columns=['ticker', 'start', 'end', 'val'])
    
    >>> df_new = df[df['ticker'] == 'AAPL']
    >>> df_new.name = 'AAPL'
    >>> df_new.index = df_new['start']
    >>> df_new.index.name = 'date'
    >>> df_new.index = df_new.index.to_datetime()
    
    >>> from functools import reduce #for py3k only
    >>> new_index = [pd.bdate_range(**d) for d in df_new[['start','end']].to_dict('record')]
    >>> new_index_flat = reduce(pd.tseries.index.DatetimeIndex.append, new_index)
    
    >>> df_new = df_new.reindex(new_index_flat)
    >>> df_new = df_new.fillna(method='pad')
    >>> df_new
                   ticker       start         end  val
        2014-04-28   AAPL  2014-04-28  2014-05-02   90
        2014-04-29   AAPL  2014-04-28  2014-05-02   90
        2014-04-30   AAPL  2014-04-28  2014-05-02   90
        2014-05-01   AAPL  2014-04-28  2014-05-02   90
        2014-05-02   AAPL  2014-04-28  2014-05-02   90
        2014-05-05   AAPL  2014-05-05  2014-05-09   80
        2014-05-06   AAPL  2014-05-05  2014-05-09   80
        2014-05-07   AAPL  2014-05-05  2014-05-09   80
        2014-05-08   AAPL  2014-05-05  2014-05-09   80
        2014-05-09   AAPL  2014-05-05  2014-05-09   80
        2014-05-12   AAPL  2014-05-12  2014-05-16   85
        2014-05-13   AAPL  2014-05-12  2014-05-16   85
        2014-05-14   AAPL  2014-05-12  2014-05-16   85
        2014-05-15   AAPL  2014-05-12  2014-05-16   85
        2014-05-16   AAPL  2014-05-12  2014-05-16   85
    
        [15 rows x 4 columns]
    

    希望对你有帮助!

    【讨论】:

    • 感谢您的回复。不幸的是,您上面的示例并没有解决问题,因为例如,我想要日期为 2014-05-05 到 2014-05-11 的行。另请注意,您使用的数据集存在错误,即第 3 行中的 AAPL 日期范围与第 1 行中的日期范围重叠。
    • 明白。从这里开始,我们需要做的就是对序列进行排序、重新采样和填充。让我换个答案。
    • 编辑:@tuva,我已经编辑了答案以反映您的评论。我现在使用不重叠的日期,跨越 3 周,但间隔并不需要固定;但真正重要的是开始日期必须是营业日期,否则重新索引会导致该间隔丢失。
    【解决方案3】:

    这是一种很老套的方法——我发布了这个糟糕的答案(记住——我不会编码 :-))因为我是 pandas 的新手,不介意有人改进它。

    这会读取包含最初发布数据的文件 - 然后创建一个基于 stock_id 和 end_date 的多索引。下面的 get_val 函数采用整个帧,例如一个股票代码。 'AAPL' 和一个日期并使用 index.searchsorted,它的行为类似于 C++ 中的 map::upper_bound - 即找到要插入日期的索引 - 即找到最接近但在日期之后的结束日期问题 - 这将具有我们想要的值,我们用 get_val 返回它。

    然后,我根据“AAPL”的 stock_id 从具有此 Multiindex 的系列中获取横截面。然后我们形成一个空列表,该列表将用于从具有“AAPL”键的多索引中展平日期元组列表。这些日期成为系列的索引和值。然后我将此系列映射到 get_val 以获取所需的股票价格。

    我知道这可能是错误的......但是......很高兴学习。

    我不会惊讶地发现有一种简单的方法可以使用一些前向填充插值方法来膨胀这样的数据帧...

    stocks=pd.read_csv('stocks2.csv', parse_dates=['start_date', 'end_date'], index_col='ticker')
    mi=zip(stocks.index, pd.Series(zip(stocks['start_date'],stocks['end_date'].values)).map(lambda z: tuple(pd.date_range(start=z[0], end=z[1]))).values)
    mi=pd.MultiIndex.from_tuples(mi)
    ticker='AAPL'
    s=pd.Series(index=mi,data=0)
    s=list(s.xs(key=ticker).index)
    l=[]
    map(lambda x: l.extend(x), s)
    s=pd.Series(index=l,data=l)
    stocks_byticker=stocks[stocks.index==ticker].set_index('end_date')
    print(s.map(lambda x: stocks_byticker.ix[stocks_byticker.index.searchsorted(x), 'val']))
    
    2014-05-01    10
    2014-06-05    20
    2014-06-06    20
    2014-06-07    20
    2014-06-08    20
    2014-06-09    20
    2014-06-10    20
    

    【讨论】:

    • 感谢您的提交。仅供参考,我将数据集更改为 tickers 而不是 stock_ids 以使问题更易于阅读。
    • 感谢@tuva 提出的问题——joris 给出的 resample.fillna 成语帮助我完成了今天正在做的一些事情。我为采样方法使用了自定义工作日,这是一个很好的奖励功能 - .resample(CDay(1, holiday=holiday_array)).etc 它似乎仍然应该有 Pandas 可以提供的更简单的解决方案。可能值得建议 Pandas 人员创建一个 DataFrame.inflate(start_col,end_col,resample_freq, interp_method) 函数 - 如果 interp_method 可以通过使用前一个值来计算下一个值来递增地工作,那就太酷了。
    • 感谢@FinanceGuyThatCantCode。很高兴我们都能从中受益。也许我们可以做一个 Pull Request 来添加这种功能。
    【解决方案4】:

    这是一种更通用的方法,它扩展了 joris 的好答案,但允许它与任意数量的附加列一起使用:

    import pandas as pd 
    
    df['join_id'] = range(len(df)) 
    starts = df[['start_date', 'join_id']].rename(columns={'start_date': 'date'})
    ends = df[['end_date', 'join_id']].rename(columns={'end_date': 'date'})
    start_end = pd.concat([starts, ends]).set_index('date')
    
    fact_table = start_end.groupby("task_id").apply(lambda x: x.resample('D').fillna(method='pad'))
    del fact_table["join_id"]
    fact_table = fact_table.reset_index()
    final = fact_table.merge(df, right_on='join', left_on='join', how='left')
    

    【讨论】:

      猜你喜欢
      • 2018-07-13
      • 2018-11-24
      • 1970-01-01
      • 2019-09-04
      • 1970-01-01
      • 2019-07-07
      • 1970-01-01
      • 2019-04-17
      • 1970-01-01
      相关资源
      最近更新 更多