DataFrame 每 3 行取一次并向前填充

DataFrame take every 3rd row and forward fill

我有一个 DataFrame,索引中有 'Date''Id',列中有 'Portfolio'。价值是投资组合中证券的权重。在索引的日期级别内,我想每隔 3 个日期将安全权重向前填充到下一个 "every third" 日期之后的日期。

设置

这是一个通用的 DataFrame 生产者。最后分配 df

import pandas as pd
import numpy as np
from string import uppercase

def generic_portfolio_df(start, end, freq, num_port, num_sec, seed=314):
    np.random.seed(seed)
    portfolios = pd.Index(['Portfolio {}'.format(i) for i in uppercase[:num_port]],
                          name='Portfolio')
    securities = ['s{:02d}'.format(i) for i in range(num_sec)]
    dates = pd.date_range(start, end, freq=freq)
    return pd.DataFrame(np.random.rand(len(dates) * num_sec, num_port),
                        index=pd.MultiIndex.from_product([dates, securities],
                                                         names=['Date', 'Id']),
                        columns=portfolios
                       ).groupby(level=0).apply(lambda x: x / x.sum())    

df = generic_portfolio_df('2014-12-31', '2015-05-30', 'BM', 3, 5)

df 看起来像这样:

Portfolio       Portfolio A  Portfolio B  Portfolio C
Date       Id                                        
2014-12-31 s00     0.326164     0.201597     0.085340
           s01     0.278614     0.314448     0.266392
           s02     0.258958     0.089224     0.293570
           s03     0.092760     0.262511     0.084208
           s04     0.043503     0.132221     0.270490
2015-01-30 s00     0.094124     0.041722     0.248013
           s01     0.197860     0.346862     0.265287
           s02     0.232504     0.261939     0.125719
           s03     0.193050     0.286359     0.337316
           s04     0.282462     0.063118     0.023664
2015-02-27 s00     0.266900     0.484163     0.074970
           s01     0.239319     0.083138     0.123289
           s02     0.067958     0.262626     0.262548
           s03     0.181974     0.108668     0.301149
           s04     0.243849     0.061405     0.238044
2015-03-31 s00     0.321438     0.149010     0.125168
           s01     0.217779     0.067209     0.040285
           s02     0.173066     0.293539     0.417372
           s03     0.048929     0.415637     0.216490
           s04     0.238788     0.074605     0.200685
2015-04-30 s00     0.089122     0.135514     0.234565
           s01     0.048235     0.028141     0.327739
           s02     0.026016     0.039664     0.073588
           s03     0.413139     0.397875     0.323671
           s04     0.423487     0.398807     0.040437
2015-05-29 s00     0.135831     0.071604     0.235099
           s01     0.240086     0.242436     0.131698
           s02     0.304451     0.380368     0.101653
           s03     0.213468     0.035276     0.372894
           s04     0.106164     0.270317     0.158656

问题

Within the dates level of the index, I'd like to take every 3rd date and forward fill the security weight to the date subsequent to the next "every third" date.

我希望它看起来像:

Portfolio       Portfolio A  Portfolio B  Portfolio C
Date       Id                                        
2014-12-31 s00     0.326164     0.201597     0.085340
           s01     0.278614     0.314448     0.266392
           s02     0.258958     0.089224     0.293570
           s03     0.092760     0.262511     0.084208
           s04     0.043503     0.132221     0.270490
2015-01-30 s00     0.326164     0.201597     0.085340
           s01     0.278614     0.314448     0.266392
           s02     0.258958     0.089224     0.293570
           s03     0.092760     0.262511     0.084208
           s04     0.043503     0.132221     0.270490
2015-02-27 s00     0.326164     0.201597     0.085340
           s01     0.278614     0.314448     0.266392
           s02     0.258958     0.089224     0.293570
           s03     0.092760     0.262511     0.084208
           s04     0.043503     0.132221     0.270490
2015-03-31 s00     0.321438     0.149010     0.125168
           s01     0.217779     0.067209     0.040285
           s02     0.173066     0.293539     0.417372
           s03     0.048929     0.415637     0.216490
           s04     0.238788     0.074605     0.200685
2015-04-30 s00     0.321438     0.149010     0.125168
           s01     0.217779     0.067209     0.040285
           s02     0.173066     0.293539     0.417372
           s03     0.048929     0.415637     0.216490
           s04     0.238788     0.074605     0.200685
2015-05-29 s00     0.321438     0.149010     0.125168
           s01     0.217779     0.067209     0.040285
           s02     0.173066     0.293539     0.417372
           s03     0.048929     0.415637     0.216490
           s04     0.238788     0.074605     0.200685

结论

虽然我仍然对其他人的答案感兴趣。由于以下原因,我选择了亚历山大的回答而不是我自己的回答:

%%timeit
    df = generic_portfolio_df('2014-12-31', '2015-05-30', 'BM', 3, 5)
    df = df.unstack()
    df.iloc[3:] = np.nan
    df = df.ffill(limit=3).stack()

100 loops, best of 3: 11.6 ms per loop

%%timeit
    df = generic_portfolio_df('2014-12-31', '2015-05-30', 'BM', 3, 5)
    df0 = df.loc[pd.IndexSlice[::3, :], :]
    diff = df.index.difference(df0.index)
    df.ix[diff] = np.nan
    df.groupby(level=1).ffill(limit=3)

100 loops, best of 3: 21 ms per loop

显然,使用 stackunstack 效率更高。

# Create Boolean index of rows to delete (every third row is marked as False).
idx = len(df.unstack())
idx = [i % 3 > 0 for i in range(idx)]
>>> idx
[False, True, True, False, True, True]

# Unstack the dataframe so you just have a column of dates 
df = df.unstack()

# Delete those in the `idx` index.
df.loc[idx, :] = np.nan

# Forward fill the retained dates, and then restack your dataframe.
df = df.ffill(limit=3).stack()

>>> df.tail()
Portfolio       Portfolio A  Portfolio B  Portfolio C
Date       Id                                        
2015-05-29 s00     0.321438     0.149010     0.125168
           s01     0.217779     0.067209     0.040285
           s02     0.173066     0.293539     0.417372
           s03     0.048929     0.415637     0.216490
           s04     0.238788     0.074605     0.200685

我认为在这种情况下(使用 'BM' 作为频率)一行就可以了:

df2 = df.unstack().resample('3BM').first().resample('1BM').ffill(limit=3).stack()

当然,对于其他频率字符串freq,您可以分别使用'3'+freq'1'+freq

更新

我刚刚注意到上面的代码可能会在索引中增加一天(resample('3BM'),所以我们必须额外控制数据帧的长度。

一般情况下,还是一行搞定的。为了更具可读性,我将它分成两部分。首先,我在要保留的未堆叠数据框中创建行索引:

idx = np.arange(np.ceil(len(df.unstack())/3), dtype = int)*3
df2 = df.unstack().iloc[idx].loc[df_t.index].fillna(method = 'ffill').stack()

它没有添加不需要的行的问题,更不等同于亚历山大的回答。总之,我觉得Alexander的回答更清晰优雅。

解决方案

df0 = df.loc[pd.IndexSlice[::3, :], :]
diff = df.index.difference(df0.index)
df.ix[diff] = np.nan
df.groupby(level=1).ffill(limit=3)

这与亚历山大的回答几乎相同。这是我用来制作样品的。

亮点

  • pd.IndexSlice 我喜欢这个工具。前两行代码定义索引设置为 np.nan 而不需要 unstack()
  • 又是
  • groupby(level=1).ffill(limit=3),不用在unstacked()模式下操作
  • limit=3 是必需的,尽管我给出的例子并不明显。可能 'Id' 可能很早就存在并从投资组合中消失。如果发生这种情况,该列的其余部分将 'NaN' 并受 ffill 约束。 limit=3 防止这种情况。