Pandas:解压日期范围为单个日期
数据集:我有一个1GB的股票数据集,这些数据包含了在特定日期范围内的股票值。日期范围之间没有重叠,并且数据集是按照(股票代码,开始日期)排序的。
>>> 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,能用几行代码就完成这个操作。
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')
这里有一个比较简单的方法 - 我发这个不太好的答案(记住 - 我不会编程 :-)),因为我刚接触pandas,希望有人能帮我改进一下。
这个方法读取了最初发布数据的文件,然后根据股票ID和结束日期创建了一个多重索引。下面的get_val函数接收整个数据框、一个股票代码,比如'AAPL',还有一个日期。它使用index.searchsorted,这个函数的作用类似于C++中的map::upper_bound - 也就是说,它可以找到如果你想插入这个日期,应该插入到哪个位置 - 也就是找到最接近但在这个日期之后的结束日期 - 这个日期的值就是我们想要的,get_val会返回这个值。
接着,我从这个多重索引中提取出与'AAPL'相关的部分。然后我们创建一个空列表,用来整理从多重索引中提取出的日期元组,这些元组的关键字是'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
我觉得你可以按照以下五个步骤来做:
1) 先筛选出你想要的股票,找到对应的股票代码
2) 使用 pandas.bdate_range
来生成一个从 start
到 end
的日期范围列表
3) 用 reduce
把这个列表压平
4) 重新给你筛选后的数据框(dataframe)编排索引
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]
希望这对你有帮助!
这段代码比几行多一点,但我觉得它能实现你想要的效果:
首先,从你的数据框开始:
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_date
和 end_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
基于这个新的数据框,我可以按 ticker
和 row
来分组,然后对每个组进行每日的 resample
操作,并用 fillna
(用 '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