使用多个键值计算DataFrame中的连续日期,避免使用Pandas循环

时间:2018-02-19 13:10:23

标签: python performance pandas

我一直在使用熊猫进行股票分析,我正在讨论一个非常棘手的概念,称为“实际覆盖”,这个概念在临时分析中才有意义,因为“实际覆盖”意味着措施(以天为单位)当前库存量将持续多少,假设从那一点开始不会有任何补货。

例如:

TIMESTAMP   MATERIAL_GOODS  STOCK_POS   SALES
2017-03-29  PRODUCT A       47          2
2017-03-30  PRODUCT A       43          4
2017-03-31  PRODUCT A       38          5
2017-04-01  PRODUCT A       49          11
2017-04-02  PRODUCT A       49          0
2017-04-03  PRODUCT A       45          4
2017-04-04  PRODUCT A       38          7
2017-04-05  PRODUCT A       30          8
2017-04-06  PRODUCT A       44          6
2017-04-07  PRODUCT A       36          8   
2017-04-08  PRODUCT A       47          10  
2017-04-09  PRODUCT A       46          1   
2017-04-11  PRODUCT A       31          8   
2017-04-10  PRODUCT A       39          7   

我想出了这个解决方案(正在运行......)

actual_cover = []

for i in DF.index:
    z = 1
    counter = 0
    rest = DF['STOCK_POS'].iloc[i]
    while (rest >= 0)&(i+z < DF.index.max()):
        rest -= DF['SALES'].iloc[i+z]
        counter += 1
        z += 1    

    actual_cover.append(counter)
    print('Progress: {}%'.format(round((i/len(DF.index))*100,2)), end="\r", flush=True)

这是示例的输出,实际上它应该是这样的:

TIMESTAMP   MATERIAL_GOODS  STOCK_POS   SALES   ACTUAL_COVER(days)
2017-03-29  PRODUCT A       47          2       9
2017-03-30  PRODUCT A       43          4       8 
2017-03-31  PRODUCT A       38          5       7
2017-04-01  PRODUCT A       49          11      9
2017-04-02  PRODUCT A       49          0       8
2017-04-03  PRODUCT A       45          4       7
2017-04-04  PRODUCT A       38          7       6
2017-04-05  PRODUCT A       30          8       5
2017-04-06  PRODUCT A       44          6       7
2017-04-07  PRODUCT A       36          8       6
2017-04-08  PRODUCT A       47          10      12
2017-04-09  PRODUCT A       46          1       11
2017-04-11  PRODUCT A       31          8       8
2017-04-10  PRODUCT A       39          7       10

但是使用此代码,计算一个商店中一个商品的实际封面大约需要1秒。由于我需要在2k商店进行大约40k的计算,这不是一个实际的解决方案。

我尝试使用滚动和其他pandas工具进行操作,但无法正确计算。

我的问题是:有更多“Pythonic”,快速,有效的方法进行相同的计算吗?

修改

所以.. @ Haleemur Ali实际上给出了一个很好的线索,因为:

def actual_cover(rownum, frame):
    mask = frame.SALES[rownum+1:].cumsum() > frame.STOCK_POS[rownum]
    not_covered = np.where(mask.values)[0]
    return np.nan if not_covered.size == 0 else not_covered[0]+1

如果DataFrame只有一个商品而且只有一个商店,则可以正常使用,但我原来的问题看起来更像是这样:

TIMESTAMP   ITEM        STORE   STOCK_POS       SALES   
2017-01-01  4251695     1216    0.0             0.0         
2017-01-01  4251695     1269    1.0             0.0         
2017-01-01  4264750     1269    0.0             0.0         
2017-01-01  4264750     L101    0.0             0.0         
2017-01-01  4252056     L836    308.0           0.0         
2017-01-01  4252056     L856    158.0           1.0         
2017-01-01  4255732     L101    360.0           0.0         
2017-01-01  4255732     L110    101.0           0.0         
2017-01-01  4262145     L715    8.0             0.0         
2017-01-01  4262145     L794    0.0             0.0         

当我将actual_cover函数应用于一个项目(4252056),一个商店(1001)时,过滤DataFrame,如下所示:

DF = DF[(DF['ITEM'] == 4252056)&(DF['STORE'] == '1001')]
DF.reset_index(drop=True, inplace=True)
DF['ACTUAL_COVER'] = DF.apply(lambda x: actual_cover(x.name, DF), axis=1)

我得到了那个结果:

TIMESTAMP   ITEM        STORE   STOCK_POS       SALES    ACTUAL_COVER
2017-01-01  4252056     1001    551             0        35.0
2017-01-02  4252056     1001    531             20       34.0
2017-01-03  4252056     1001    514             17       33.0
2017-01-04  4252056     1001    1146            28       64.0
2017-01-05  4252056     1001    1130            16       63.0
2017-01-06  4252056     1001    1865            15       76.0
2017-01-07  4252056     1001    1843            22       75.0
2017-01-08  4252056     1001    1833            10       74.0
2017-01-09  4252056     1001    1814            19       73.0
2017-01-10  4252056     1001    1808            6        72.0

哪个完美。但由于我有许多像键一样工作的商店(1300),我需要groupby种解决方案。

使用当前功能:

def actual_cover_grouped(grp):
    return grp.apply(lambda x: actual_cover(x.name, grp), axis=1)

像这样(处理时间约为50分钟):

group_item_store = DF.groupby(by=[DF['ITEM'], DF['STORE']])
DF['ACTUAL_COVER'] = group_item_store.apply(actual_cover_grouped
                                            ).values.flatten()

这是同一段(item-4252056 / store-1001)的结果:

TIMESTAMP   ITEM        STORE   STOCK_POS       SALES    ACTUAL_COVER
    2017-01-01  4252056     1001    551             0        NaN
    2017-01-02  4252056     1001    531             20       NaN
    2017-01-03  4252056     1001    514             17       NaN
    2017-01-04  4252056     1001    1146            28       NaN
    2017-01-05  4252056     1001    1130            16       NaN
    2017-01-06  4252056     1001    1865            15       NaN
    2017-01-07  4252056     1001    1843            22       NaN
    2017-01-08  4252056     1001    1833            10       NaN
    2017-01-09  4252056     1001    1814            19       NaN
    2017-01-10  4252056     1001    1808            6        NaN

为什么分组版本不起作用?

2 个答案:

答案 0 :(得分:0)

此类代码的第一个优化是使用本机numpy / pandas函数替换循环并使用pandas.DataFrame.apply

使用实际封面的定义

  

当前股票头寸将持续多少的度量(以天为单位)

可以等同地说,实际封面是

the first day such that the cumulative sum of sales for all following days exceeds 
the stock position on a given day

使用这个实际覆盖的定义,下面的函数返回给出行号的actual_cover

def actual_cover(rownum, frame):
    mask = frame.SALES[rownum+1:].cumsum() > frame.STOCK_POS[rownum]
    not_covered = np.where(mask.values)[0]
    return np.nan if not_covered.size == 0 else not_covered[0]+1

然后,您可以将其应用于数据框并将值分配给新列

df['ACTUAL_COVER(days)'] = df.apply(lambda x: actual_cover(x.name, df), axis=1)

注释:

我使用了名称df而不是DF,所以当您在数据集上尝试使用此代码时,必须更改该名称

该函数使用行索引值来确定天数。因此,为了使函数正常工作,每天必须有一行,即使当天没有销售,行也必须按时间戳排序

应用于上述数据框片段的函数将返回np.nan,表示累积总和永远不会超过库存位置的行,即输出以下内容:

df.apply(lambda x: actual_cover(x.name, df), axis=1)
# output
0     9.0
1     8.0
2     7.0
3     9.0
4     8.0
5     7.0
6     6.0
7     5.0
8     NaN
9     NaN
10    NaN
11    NaN
12    NaN
13    NaN

这与您提供的示例输出不同,因为您在示例中截断了整个数据集中的行

actual_cover功能可以应用于分组数据框,但需要进一步按摩

def actual_cover_grouped(grp):
    return grp.apply(lambda x: actual_cover(x.name, grp), axis=1)

grouped = df.groupby('MATERIAL_GOODS')

df['Actual Cover'] = grouped.apply(actual_cover_grouped).values.flatten()

答案 1 :(得分:0)

我并不完全满意,但我能够使用以下代码在一个中转换3个循环:

aux_dict = {}
counter = 0
begin = time.time()
for name, group in grouped_cob:
    AUX_DF = group.copy()
    AUX_DF.reset_index(drop=True, inplace=True)
    AUX_DF["ACTUAL_COVER"] = AUX_DF.apply(lambda x: actual_cover(x.name, AUX_DF), axis=1)
    aux_dict.update({name: AUX_DF})

    final = time.time()
    counter +=1
    print('Progress: {}%'.format(round((counter/len(grouped_cob))*100,2)) + 
          ' Parcial processing time: '+str(final-inicio), end="\r", flush=True)


TESTE = pd.concat(aux_dict)

并且计算正确。