【问题标题】:Rolling window percentile rank over a multi-index Pandas DataFrame多索引 Pandas DataFrame 上的滚动窗口百分位数排名
【发布时间】:2019-12-29 06:20:24
【问题描述】:

我正在创建一个滚动时间窗口的百分位排名,并希望帮助改进我的方法。

我的 DataFrame 有一个多索引,第一级设置为日期时间,第二级设置为标识符。最终,我希望滚动窗口能够评估包括当前周期在内的尾随 n 个周期,并生成相应的百分位排名。

我参考了下面显示的帖子,但发现他们处理数据的方式与我的意图有所不同。在这些帖子中,最终函数按标识符分组结果,然后按日期时间分组,而我希望在我的函数中使用滚动数据面板(日期和标识符)。

using rolling functions on multi-index dataframe in pandas

Panda rolling window percentile rank

这是我所追求的一个例子。

创建一个示例 DataFrame:

num_days = 5
np.random.seed(8675309)

stock_data = {
    "AAPL": np.random.randint(1, max_value, size=num_days),
    "MSFT": np.random.randint(1, max_value, size=num_days),
    "WMT": np.random.randint(1, max_value, size=num_days),
    "TSLA": np.random.randint(1, max_value, size=num_days)
}

dates = pd.date_range(
    start="2013-01-03", 
    periods=num_days, 
    freq=BDay()
)

sample_df = pd.DataFrame(stock_data, index=dates)
sample_df = sample_df.stack().to_frame(name='data')
sample_df.index.names = ['date', 'ticker']

哪些输出:

date       ticker      
2013-01-03 AAPL       2
           MSFT      93
           TSLA      39
           WMT       21
2013-01-04 AAPL     141
           MSFT      43
           TSLA     205
           WMT       20
2013-01-07 AAPL     256
           MSFT      93
           TSLA     103
           WMT       25
2013-01-08 AAPL     233
           MSFT      60
           TSLA      13
           WMT      104
2013-01-09 AAPL      19
           MSFT     120
           TSLA     282
           WMT      293

下面的代码将sample_df 分解为 2 天的增量,并在滚动的时间窗口内生成排名与排名。所以它很接近,但不是我想要的。

sample_df.reset_index(level=1, drop=True)[['data']] \
.apply(
    lambda x: x.groupby(pd.Grouper(level=0, freq='2d')).rank()
)

然后我尝试了下面显示的内容,但运气不佳。

from scipy.stats import rankdata

def rank(x):
    return rankdata(x, method='ordinal')[-1]

sample_df.reset_index(level=1, drop=True) \
.rolling(window="2d", min_periods=1) \
.apply(
    lambda x: rank(x)
)

我终于得到了我正在寻找的输出,但公式似乎有点做作,所以我希望找到一种更优雅的方法(如果存在的话)。

import numpy as np
import pandas as pd
from pandas.tseries.offsets import BDay

window_length = 1
target_column = "data"

def rank(df, target_column, ids, window_length):

    percentile_ranking = []
    list_of_ids = []

    date_index = df.index.get_level_values(0).unique()

    for date in date_index:
        rolling_start_date = date - BDay(window_length)
        first_date = date_index[0] + BDay(window_length)
        trailing_values = df.loc[rolling_start_date:date, target_column]

        # Only calc rolling percentile after the rolling window has lapsed
        if date < first_date:
            pass
        else:
            percentile_ranking.append(
                df.loc[date, target_column].apply(
                    lambda x: stats.percentileofscore(trailing_values, x, kind="rank")
                )
            )

            list_of_ids.append(df.loc[date, ids])

    ranks, output_ids = pd.concat(percentile_ranking), pd.concat(list_of_ids)

    df = pd.DataFrame(
        ranks.values, index=[ranks.index, output_ids], columns=["percentile_rank"]
         )

    return df

ranks = rank(
    sample_df.reset_index(level=1), 
    window_length=1, 
    ids='ticker', 
    target_column="data"
)

sample_df.join(ranks)

我觉得我的rank 功能比这里需要的要多。我感谢任何想法/反馈,以帮助简化此代码以达到下面的输出。谢谢!

                   data  percentile_rank
date       ticker                       
2013-01-03 AAPL       2              NaN
           MSFT      93              NaN
           TSLA      39              NaN
           WMT       21              NaN
2013-01-04 AAPL     141             87.5
           MSFT      43             62.5
           TSLA     205            100.0
           WMT       20             25.0
2013-01-07 AAPL     256            100.0
           MSFT      93             50.0
           TSLA     103             62.5
           WMT       25             25.0
2013-01-08 AAPL     233             87.5
           MSFT      60             37.5
           TSLA      13             12.5
           WMT      104             75.0
2013-01-09 AAPL      19             25.0
           MSFT     120             62.5
           TSLA     282             87.5
           WMT      293            100.0

【问题讨论】:

    标签: python-3.x pandas rolling-computation


    【解决方案1】:

    已编辑:最初的答案是采用没有滚动效果的 2d 组,只对出现的前两天进行分组。如果您想每 2 天滚动一次:

    1. 数据框旋转以将日期保留为索引,将股票代码保留为列
    pivoted = sample_df.reset_index().pivot('date','ticker','data')
    

    输出

    ticker      AAPL    MSFT    TSLA    WMT
    date                
    2013-01-03  2       93       39      21
    2013-01-04  141     43      205      20
    2013-01-07  256     93      103      25
    2013-01-08  233     60       13     104
    2013-01-09  19     120      282     293
    
    1. 现在我们可以应用 rolling 函数并考虑滚动内同一窗口中的所有股票
    from scipy.stats import rankdata
    
    def pctile(s):
        wdw = sample_df.loc[s.index,:].values.flatten() ##get all stock values in the period
        ranked = rankdata(wdw) / len(wdw)*100 ## their percentile
        return ranked[np.where(wdw == s[len(s)-1])][0] ## return this value's percentile
    
    pivoted_pctile = pivoted.rolling('2D').apply(pctile, raw=False)
    

    输出

    ticker      AAPL    MSFT    TSLA    WMT
    date                
    2013-01-03   25.0   100.0    75.0    50.0
    2013-01-04   87.5    62.5   100.0    25.0
    2013-01-07  100.0    50.0    75.0    25.0
    2013-01-08   87.5    37.5    12.5    75.0
    2013-01-09   25.0    62.5    87.5   100.0
    

    要恢复原始格式,我们只需将结果融合:

    pd.melt(pivoted_pctile.reset_index(),'date')\
        .sort_values(['date', 'ticker']).reset_index()
    

    输出

                        value
    date    ticker  
    2013-01-03  AAPL     25.0
                MSFT    100.0
                TSLA     75.0
                WMT      50.0
    2013-01-04  AAPL     87.5
                MSFT     62.5
                TSLA    100.0
                WMT      25.0
    2013-01-07  AAPL    100.0
                MSFT     50.0
                TSLA     75.0
                WMT      25.0
    2013-01-08  AAPL     87.5
                MSFT     37.5
                TSLA     12.5
                WMT      75.0
    2013-01-09  AAPL     25.0
                MSFT     62.5
                TSLA     87.5
                WMT     100.0
    

    如果您更喜欢一次性执行:

    pd.melt(
        sample_df\
        .reset_index()\
        .pivot('date','ticker','data')\
        .rolling('2D').apply(pctile, raw=False)\
        .reset_index(),'date')\
        .sort_values(['date', 'ticker']).set_index(['date','ticker'])
    

    注意,在第 7 天,这与您显示的不同。这实际上是滚动的,所以在第 7 天,因为没有第 6 天,所以仅对当天的值进行排名,因为数据窗口只有 4 个值,并且窗口不向前。这与您当天的结果不同。

    原创

    这是您可能正在寻找的东西吗?我将日期(2 天)上的 groupbytransform 结合起来,因此观察的数量与提供的系列相同。如您所见,我保留了对窗口组的第一次观察。

    df = sample_df.reset_index()
    
    df['percentile_rank'] = df.groupby([pd.Grouper(key='date',freq='2D')]['data']\
                               .transform(lambda x: x.rank(ascending=True)/len(x)*100)
    

    输出

    Out[19]: 
             date ticker  data  percentile_rank
    0  2013-01-03   AAPL     2             12.5
    1  2013-01-03   MSFT    93             75.0
    2  2013-01-03    WMT    39             50.0
    3  2013-01-03   TSLA    21             37.5
    4  2013-01-04   AAPL   141             87.5
    5  2013-01-04   MSFT    43             62.5
    6  2013-01-04    WMT   205            100.0
    7  2013-01-04   TSLA    20             25.0
    8  2013-01-07   AAPL   256            100.0
    9  2013-01-07   MSFT    93             50.0
    10 2013-01-07    WMT   103             62.5
    11 2013-01-07   TSLA    25             25.0
    12 2013-01-08   AAPL   233             87.5
    13 2013-01-08   MSFT    60             37.5
    14 2013-01-08    WMT    13             12.5
    15 2013-01-08   TSLA   104             75.0
    16 2013-01-09   AAPL    19             25.0
    17 2013-01-09   MSFT   120             50.0
    18 2013-01-09    WMT   282             75.0
    19 2013-01-09   TSLA   293            100.0
    

    【讨论】:

    • 感谢您的快速回复!它看起来接近我所追求的,除了第 9 位的值没有相对于第 8 位和第 9 位进行排名。结果,第 17 - 19 行最终与我最初发布的不同。我刚刚以粗体运行了代码块,看起来2013-01-09 被单独排名,而不是与2013-01-08 一起排名。 df.groupby([pd.Grouper(key='date',freq='2D')])['data']\ .transform(lambda x: x.rank()) 我跑了之前也使用 pd.Grouper() 进行了讨论。有什么想法吗?
    • 感谢@calestini 的更新,这是一个有趣的方法!我打算在其上使用的数据框(df)相对于样本来说相当大(3,700 个日期,每个日期在 0 到 1,000 个代码之间),因此 .pivot() 最终需要 35mb 的 df 超过 1GB,这使得对其进行操作困难(我实际上无法让它运行)。鉴于我的 df 的大小,你会说我最初发布的 for 循环是最好的方法吗?
    • 是的,它可能是@user11963761
    猜你喜欢
    • 1970-01-01
    • 2013-01-04
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2018-09-02
    • 2020-08-14
    • 2016-10-11
    • 2019-02-26
    相关资源
    最近更新 更多