Pandas:分配具有多个条件和日期阈值的列
Pandas: assigning columns with multiple conditions and date thresholds
已编辑:
我在 pandas 数据框 df 中有一个金融投资组合,其中索引是日期,每个日期我有多个金融股票。
例如数据框:
Date Stock Weight Percentile Final weight
1/1/2000 Apple 0.010 0.75 0.010
1/1/2000 IBM 0.011 0.4 0
1/1/2000 Google 0.012 0.45 0
1/1/2000 Nokia 0.022 0.81 0.022
2/1/2000 Apple 0.014 0.56 0
2/1/2000 Google 0.015 0.45 0
2/1/2000 Nokia 0.016 0.55 0
3/1/2000 Apple 0.020 0.52 0
3/1/2000 Google 0.030 0.51 0
3/1/2000 Nokia 0.040 0.47 0
我通过在 Percentile
大于 0.7
时分配 Weight
的值来创建 Final_weight
现在我希望它更复杂一些,我仍然希望在 Percentile is > 0.7
时将 Weight
分配给 Final_weight
,但是在此日期之后(在未来),而不是在股票 Percentile
不是 >0.7
时变为 0,只要股票 Percentile
高于 0.5
(即持有超过一天的职位)。
然后,如果股票跌破 0.5
(在不久的将来),那么 Final_weight would become 0
。
例如从上面修改的数据框:
Date Stock Weight Percentile Final weight
1/1/2000 Apple 0.010 0.75 0.010
1/1/2000 IBM 0.011 0.4 0
1/1/2000 Google 0.012 0.45 0
1/1/2000 Nokia 0.022 0.81 0.022
2/1/2000 Apple 0.014 0.56 0.014
2/1/2000 Google 0.015 0.45 0
2/1/2000 Nokia 0.016 0.55 0.016
3/1/2000 Apple 0.020 0.52 0.020
3/1/2000 Google 0.030 0.51 0
3/1/2000 Nokia 0.040 0.47 0
每天的投资组合都不一样,与前一天的股票并不总是相同的。
- 我会先将
'Stock'
放入索引
- 然后
unstack
将它们放入列
- 然后我将
w
拆分为权重,将 p
拆分为百分位数
- 然后用一系列
where
进行操作
d1 = df.set_index('Stock', append=True)
d2 = d1.unstack()
w, p = d2.Weight, d2.Percentile
d1.join(w.where(p > .7, w.where((p.shift() > .7) & (p > .5), 0)).stack().rename('Final Weight'))
Weight Percentile Final Weight
Date Stock
2000-01-01 Apple 0.010 0.75 0.010
IBM 0.011 0.40 0.000
Google 0.012 0.45 0.000
Nokia 0.022 0.81 0.022
2000-02-01 Apple 0.014 0.56 0.014
Google 0.015 0.45 0.000
Nokia 0.016 0.55 0.016
我想你可能想使用 pandas.Series rolling window 方法。
也许是这样的:
import pandas as pd
grouped = df.groupby('Stock')
df['MaxPercentileToDate'] = np.NaN
df.index = df['Date']
for name, group in grouped:
df.loc[df.Stock==name, 'MaxPercentileToDate'] = group['Percentile'].rolling(min_periods=0, window=4).max()
# Mask selects rows that have ever been greater than 0.75 (including current row in max)
# and are currently greater than 0.5
mask = ((df['MaxPercentileToDate'] > 0.75) & (df['Percentile'] > 0.5))
df.loc[mask, 'Finalweight'] = df.loc[mask, 'Weight']
我相信这假设值是按日期排序的(您的初始数据集似乎有),并且您还必须将 min_periods
参数调整为每只股票的最大条目数。
一种方法,避免循环和有限的回溯期。
使用你的例子:
import pandas as pd
import numpy as np
>>>df = pd.DataFrame([['1/1/2000', 'Apple', 0.010, 0.75],
['1/1/2000', 'IBM', 0.011, 0.4],
['1/1/2000', 'Google', 0.012, 0.45],
['1/1/2000', 'Nokia', 0.022, 0.81],
['2/1/2000', 'Apple', 0.014, 0.56],
['2/1/2000', 'Google', 0.015, 0.45],
['2/1/2000', 'Nokia', 0.016, 0.55],
['3/1/2000', 'Apple', 0.020, 0.52],
['3/1/2000', 'Google', 0.030, 0.51],
['3/1/2000', 'Nokia', 0.040, 0.47]],
columns=['Date', 'Stock', 'Weight', 'Percentile'])
首先,确定何时开始或停止追踪股票的最终权重:
>>>df['bought'] = np.where(df['Percentile'] >= 0.7, 1, np.nan)
>>>df['bought or sold'] = np.where(df['Percentile'] < 0.5, 0, df['bought'])
“1”表示要买入的股票,“0”表示要卖出的股票(如果持有)。
据此,您可以判断该股票是否被持有。请注意,这需要数据框已经按时间顺序排序,如果您在任何时候在没有日期索引的数据框上使用它:
>>>df['own'] = df.groupby('Stock')['bought or sold'].fillna(method='ffill').fillna(0)
'ffill'
是正向填充,从买卖日期向前传播所有权状态。 .fillna(0)
捕获整个数据帧中保持在 .5 和 .7 之间的所有股票。
然后,计算最终权重
>>>df['Final Weight'] = df['own']*df['Weight']
乘法,df['own']
是恒等式或零,比另一个 np.where 快一点,结果相同。
编辑:
由于速度是一个问题,按照@cronos 的建议,在一列中完成所有操作确实可以提高速度,在我的测试中,20 行时提高了大约 37%,或者 2,000,000 时提高了 18%。我可以想象,如果存储中间列要超过某种内存使用阈值,或者有其他涉及我没有经历过的系统细节的东西,我可以想象后者更大。
这看起来像:
>>>df['Final Weight'] = np.where(df['Percentile'] >= 0.7, 1, np.nan)
>>>df['Final Weight'] = np.where(df['Percentile'] < 0.5, 0, df['Final Weight'])
>>>df['Final Weight'] = df.groupby('Stock')['Final Weight'].fillna(method='ffill').fillna(0)
>>>df['Final Weight'] = df['Final Weight']*df['Weight']
使用此方法或删除中间字段都会得到结果:
>>>df
Date Stock Weight Percentile Final Weight
0 1/1/2000 Apple 0.010 0.75 0.010
1 1/1/2000 IBM 0.011 0.40 0.000
2 1/1/2000 Google 0.012 0.45 0.000
3 1/1/2000 Nokia 0.022 0.81 0.022
4 2/1/2000 Apple 0.014 0.56 0.014
5 2/1/2000 Google 0.015 0.45 0.000
6 2/1/2000 Nokia 0.016 0.55 0.016
7 3/1/2000 Apple 0.020 0.52 0.020
8 3/1/2000 Google 0.030 0.51 0.000
9 3/1/2000 Nokia 0.040 0.47 0.000
为了进一步改进,我会考虑添加一种方法来设置拥有股票的初始条件,然后分解数据帧以查看更小的时间范围。这可以通过为这些较小数据帧之一覆盖的时间段添加初始条件,然后更改
>>>df['Final Weight'] = np.where(df['Percentile'] >= 0.7, 1, np.nan)
类似于
>>>df['Final Weight'] = np.where((df['Percentile'] >= 0.7) | (df['Final Weight'] != 0), 1, np.nan)
允许它被识别和传播。
设置
Dataframe:
Stock Weight Percentile Finalweight
Date
2000-01-01 Apple 0.010 0.75 0
2000-01-01 IBM 0.011 0.40 0
2000-01-01 Google 0.012 0.45 0
2000-01-01 Nokia 0.022 0.81 0
2000-02-01 Apple 0.014 0.56 0
2000-02-01 Google 0.015 0.45 0
2000-02-01 Nokia 0.016 0.55 0
2000-03-01 Apple 0.020 0.52 0
2000-03-01 Google 0.030 0.51 0
2000-03-01 Nokia 0.040 0.57 0
解决方案
df = df.reset_index()
#find historical max percentile for a Stock
df['max_percentile'] = df.apply(lambda x: df[df.Stock==x.Stock].iloc[:x.name].Percentile.max() if x.name>0 else x.Percentile, axis=1)
#set weight according to max_percentile and the current percentile
df['Finalweight'] = df.apply(lambda x: x.Weight if (x.Percentile>0.7) or (x.Percentile>0.5 and x.max_percentile>0.7) else 0, axis=1)
Out[1041]:
Date Stock Weight Percentile Finalweight max_percentile
0 2000-01-01 Apple 0.010 0.75 0.010 0.75
1 2000-01-01 IBM 0.011 0.40 0.000 0.40
2 2000-01-01 Google 0.012 0.45 0.000 0.45
3 2000-01-01 Nokia 0.022 0.81 0.022 0.81
4 2000-02-01 Apple 0.014 0.56 0.014 0.75
5 2000-02-01 Google 0.015 0.45 0.000 0.51
6 2000-02-01 Nokia 0.016 0.55 0.016 0.81
7 2000-03-01 Apple 0.020 0.52 0.020 0.75
8 2000-03-01 Google 0.030 0.51 0.000 0.51
9 2000-03-01 Nokia 0.040 0.57 0.040 0.81
备注
在示例数据的最后一行中,诺基亚的百分位数为 0.57,而在您的结果中则变为 0.47。在此示例中,我使用了 0.57,因此最后一行的输出与您的略有不同。
这个解决方案更明确,更少 pandas-esque,但它只涉及一次遍历所有行而不创建大量临时列,因此可能更快。它需要一个额外的状态变量,我将其包装到一个闭包中,这样就不必创建 class。
def closure():
cur_weight = {}
def func(x):
if x["Percentile"] > 0.7:
next_weight = x["Weight"]
elif x["Percentile"] < 0.5 :
next_weight = 0
else:
next_weight = x["Weight"] if cur_weight.get(x["Stock"], 0) > 0 else 0
cur_weight[x["Stock"]] = next_weight
return next_weight
return func
df["FinalWeight"] = df.apply(closure(), axis=1)
已编辑:
我在 pandas 数据框 df 中有一个金融投资组合,其中索引是日期,每个日期我有多个金融股票。
例如数据框:
Date Stock Weight Percentile Final weight
1/1/2000 Apple 0.010 0.75 0.010
1/1/2000 IBM 0.011 0.4 0
1/1/2000 Google 0.012 0.45 0
1/1/2000 Nokia 0.022 0.81 0.022
2/1/2000 Apple 0.014 0.56 0
2/1/2000 Google 0.015 0.45 0
2/1/2000 Nokia 0.016 0.55 0
3/1/2000 Apple 0.020 0.52 0
3/1/2000 Google 0.030 0.51 0
3/1/2000 Nokia 0.040 0.47 0
我通过在 Percentile
大于 0.7
Weight
的值来创建 Final_weight
现在我希望它更复杂一些,我仍然希望在 Percentile is > 0.7
时将 Weight
分配给 Final_weight
,但是在此日期之后(在未来),而不是在股票 Percentile
不是 >0.7
时变为 0,只要股票 Percentile
高于 0.5
(即持有超过一天的职位)。
然后,如果股票跌破 0.5
(在不久的将来),那么 Final_weight would become 0
。
例如从上面修改的数据框:
Date Stock Weight Percentile Final weight
1/1/2000 Apple 0.010 0.75 0.010
1/1/2000 IBM 0.011 0.4 0
1/1/2000 Google 0.012 0.45 0
1/1/2000 Nokia 0.022 0.81 0.022
2/1/2000 Apple 0.014 0.56 0.014
2/1/2000 Google 0.015 0.45 0
2/1/2000 Nokia 0.016 0.55 0.016
3/1/2000 Apple 0.020 0.52 0.020
3/1/2000 Google 0.030 0.51 0
3/1/2000 Nokia 0.040 0.47 0
每天的投资组合都不一样,与前一天的股票并不总是相同的。
- 我会先将
'Stock'
放入索引 - 然后
unstack
将它们放入列 - 然后我将
w
拆分为权重,将p
拆分为百分位数 - 然后用一系列
where
进行操作
d1 = df.set_index('Stock', append=True)
d2 = d1.unstack()
w, p = d2.Weight, d2.Percentile
d1.join(w.where(p > .7, w.where((p.shift() > .7) & (p > .5), 0)).stack().rename('Final Weight'))
Weight Percentile Final Weight
Date Stock
2000-01-01 Apple 0.010 0.75 0.010
IBM 0.011 0.40 0.000
Google 0.012 0.45 0.000
Nokia 0.022 0.81 0.022
2000-02-01 Apple 0.014 0.56 0.014
Google 0.015 0.45 0.000
Nokia 0.016 0.55 0.016
我想你可能想使用 pandas.Series rolling window 方法。
也许是这样的:
import pandas as pd
grouped = df.groupby('Stock')
df['MaxPercentileToDate'] = np.NaN
df.index = df['Date']
for name, group in grouped:
df.loc[df.Stock==name, 'MaxPercentileToDate'] = group['Percentile'].rolling(min_periods=0, window=4).max()
# Mask selects rows that have ever been greater than 0.75 (including current row in max)
# and are currently greater than 0.5
mask = ((df['MaxPercentileToDate'] > 0.75) & (df['Percentile'] > 0.5))
df.loc[mask, 'Finalweight'] = df.loc[mask, 'Weight']
我相信这假设值是按日期排序的(您的初始数据集似乎有),并且您还必须将 min_periods
参数调整为每只股票的最大条目数。
一种方法,避免循环和有限的回溯期。
使用你的例子:
import pandas as pd
import numpy as np
>>>df = pd.DataFrame([['1/1/2000', 'Apple', 0.010, 0.75],
['1/1/2000', 'IBM', 0.011, 0.4],
['1/1/2000', 'Google', 0.012, 0.45],
['1/1/2000', 'Nokia', 0.022, 0.81],
['2/1/2000', 'Apple', 0.014, 0.56],
['2/1/2000', 'Google', 0.015, 0.45],
['2/1/2000', 'Nokia', 0.016, 0.55],
['3/1/2000', 'Apple', 0.020, 0.52],
['3/1/2000', 'Google', 0.030, 0.51],
['3/1/2000', 'Nokia', 0.040, 0.47]],
columns=['Date', 'Stock', 'Weight', 'Percentile'])
首先,确定何时开始或停止追踪股票的最终权重:
>>>df['bought'] = np.where(df['Percentile'] >= 0.7, 1, np.nan)
>>>df['bought or sold'] = np.where(df['Percentile'] < 0.5, 0, df['bought'])
“1”表示要买入的股票,“0”表示要卖出的股票(如果持有)。
据此,您可以判断该股票是否被持有。请注意,这需要数据框已经按时间顺序排序,如果您在任何时候在没有日期索引的数据框上使用它:
>>>df['own'] = df.groupby('Stock')['bought or sold'].fillna(method='ffill').fillna(0)
'ffill'
是正向填充,从买卖日期向前传播所有权状态。 .fillna(0)
捕获整个数据帧中保持在 .5 和 .7 之间的所有股票。
然后,计算最终权重
>>>df['Final Weight'] = df['own']*df['Weight']
乘法,df['own']
是恒等式或零,比另一个 np.where 快一点,结果相同。
编辑:
由于速度是一个问题,按照@cronos 的建议,在一列中完成所有操作确实可以提高速度,在我的测试中,20 行时提高了大约 37%,或者 2,000,000 时提高了 18%。我可以想象,如果存储中间列要超过某种内存使用阈值,或者有其他涉及我没有经历过的系统细节的东西,我可以想象后者更大。
这看起来像:
>>>df['Final Weight'] = np.where(df['Percentile'] >= 0.7, 1, np.nan)
>>>df['Final Weight'] = np.where(df['Percentile'] < 0.5, 0, df['Final Weight'])
>>>df['Final Weight'] = df.groupby('Stock')['Final Weight'].fillna(method='ffill').fillna(0)
>>>df['Final Weight'] = df['Final Weight']*df['Weight']
使用此方法或删除中间字段都会得到结果:
>>>df
Date Stock Weight Percentile Final Weight
0 1/1/2000 Apple 0.010 0.75 0.010
1 1/1/2000 IBM 0.011 0.40 0.000
2 1/1/2000 Google 0.012 0.45 0.000
3 1/1/2000 Nokia 0.022 0.81 0.022
4 2/1/2000 Apple 0.014 0.56 0.014
5 2/1/2000 Google 0.015 0.45 0.000
6 2/1/2000 Nokia 0.016 0.55 0.016
7 3/1/2000 Apple 0.020 0.52 0.020
8 3/1/2000 Google 0.030 0.51 0.000
9 3/1/2000 Nokia 0.040 0.47 0.000
为了进一步改进,我会考虑添加一种方法来设置拥有股票的初始条件,然后分解数据帧以查看更小的时间范围。这可以通过为这些较小数据帧之一覆盖的时间段添加初始条件,然后更改
>>>df['Final Weight'] = np.where(df['Percentile'] >= 0.7, 1, np.nan)
类似于
>>>df['Final Weight'] = np.where((df['Percentile'] >= 0.7) | (df['Final Weight'] != 0), 1, np.nan)
允许它被识别和传播。
设置
Dataframe:
Stock Weight Percentile Finalweight
Date
2000-01-01 Apple 0.010 0.75 0
2000-01-01 IBM 0.011 0.40 0
2000-01-01 Google 0.012 0.45 0
2000-01-01 Nokia 0.022 0.81 0
2000-02-01 Apple 0.014 0.56 0
2000-02-01 Google 0.015 0.45 0
2000-02-01 Nokia 0.016 0.55 0
2000-03-01 Apple 0.020 0.52 0
2000-03-01 Google 0.030 0.51 0
2000-03-01 Nokia 0.040 0.57 0
解决方案
df = df.reset_index()
#find historical max percentile for a Stock
df['max_percentile'] = df.apply(lambda x: df[df.Stock==x.Stock].iloc[:x.name].Percentile.max() if x.name>0 else x.Percentile, axis=1)
#set weight according to max_percentile and the current percentile
df['Finalweight'] = df.apply(lambda x: x.Weight if (x.Percentile>0.7) or (x.Percentile>0.5 and x.max_percentile>0.7) else 0, axis=1)
Out[1041]:
Date Stock Weight Percentile Finalweight max_percentile
0 2000-01-01 Apple 0.010 0.75 0.010 0.75
1 2000-01-01 IBM 0.011 0.40 0.000 0.40
2 2000-01-01 Google 0.012 0.45 0.000 0.45
3 2000-01-01 Nokia 0.022 0.81 0.022 0.81
4 2000-02-01 Apple 0.014 0.56 0.014 0.75
5 2000-02-01 Google 0.015 0.45 0.000 0.51
6 2000-02-01 Nokia 0.016 0.55 0.016 0.81
7 2000-03-01 Apple 0.020 0.52 0.020 0.75
8 2000-03-01 Google 0.030 0.51 0.000 0.51
9 2000-03-01 Nokia 0.040 0.57 0.040 0.81
备注
在示例数据的最后一行中,诺基亚的百分位数为 0.57,而在您的结果中则变为 0.47。在此示例中,我使用了 0.57,因此最后一行的输出与您的略有不同。
这个解决方案更明确,更少 pandas-esque,但它只涉及一次遍历所有行而不创建大量临时列,因此可能更快。它需要一个额外的状态变量,我将其包装到一个闭包中,这样就不必创建 class。
def closure():
cur_weight = {}
def func(x):
if x["Percentile"] > 0.7:
next_weight = x["Weight"]
elif x["Percentile"] < 0.5 :
next_weight = 0
else:
next_weight = x["Weight"] if cur_weight.get(x["Stock"], 0) > 0 else 0
cur_weight[x["Stock"]] = next_weight
return next_weight
return func
df["FinalWeight"] = df.apply(closure(), axis=1)