在多索引Pandas DataFrame上滚动窗口百分位排名

时间:2019-08-24 01:41:26

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

我正在滚动的时间内创建一个百分等级,希望对我的方法有所帮助。

我的DataFrame具有多索引,其中第一级设置为日期时间,第二级设置为标识符。最终,我希望通过滚动窗口来评估后n个周期(包括当前周期),并产生相应的百分位等级。

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

using rolling functions on multi-index dataframe in pandas

Panda rolling window percentile rank

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

创建一个示例数据框:

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

1 个答案:

答案 0 :(得分:0)

已编辑:最初的答案是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