我一直在使用熊猫进行股票分析,我正在讨论一个非常棘手的概念,称为“实际覆盖”,这个概念在临时分析中才有意义,因为“实际覆盖”意味着措施(以天为单位)当前库存量将持续多少,假设从那一点开始不会有任何补货。
例如:
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
为什么分组版本不起作用?
答案 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)
并且计算正确。