在具有多个钥匙值的数据范围内连续计算,以避免使用熊猫循环



我一直在使用pandas进行库存分析,我对一个非常棘手的概念称为"实际封面",这是一个仅在临时分析中才有意义的概念由于"实际封面"是指(几天(的措施,即当前库存位置将持续多少,假设从那时起就不会进行任何补货。

ex:

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 Itens进行此计算,因此并不是一个实用的解决方案。

我尝试使用滚动和其他熊猫工具来处理一些东西,但无法正确计算。

我的问题是:有更多的" Pythonic",快速,有效地进行相同的计算?

编辑

so ..@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         

当我使用一个项目(4252056(,一个商店(1001(应用actual_cover函数时,请这样过滤数据框架:

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

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

这种类型的代码的第一个优化是用本机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

使用此实际封面的定义以下函数返回real_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

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

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()

我没有完全满足,但是我能够将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)

计算正确。

最新更新