Отредактировано:
У меня есть финансовый портфель в pandas dataframe df, где индекс - это дата, и у меня есть несколько финансовых запасов за дату.
Например, dataframe:
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
Я создал Final_weight
, назначая значения Weight
, когда Percentile
больше, чем 0.7
Теперь я хочу, чтобы это было немного сложнее, я все еще хочу, чтобы Weight
был назначен Final_weight
при Percentile is > 0.7
, однако после этой даты (в любой момент в будущем) вместо того, чтобы стать 0, когда запасы Percentile
не >0.7
, мы все равно получаем вес до тех пор, пока запасы 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
Каждый день разные портфели не всегда имеют один и тот же запас с предыдущего дня.
Ответ 1
Это решение более явное и менее pandas -esque, но оно включает только один проход через все строки без создания тонны временных столбцов и, следовательно, возможно, быстрее. Ему нужна дополнительная переменная состояния, которую я завернул в закрытие, чтобы не создавать класс.
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)
Ответ 2
- Сначала я помещал
'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
Ответ 3
Один метод, избегая циклов и ограниченных периодов рекурсирования.
Используя ваш пример:
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)
ловит любые запасы, которые остались между 0,5 и 0,7 для всего кадра данных.
Затем вычислите конечный вес
>>>df['Final Weight'] = df['own']*df['Weight']
Умножение с df['own']
, являющимся тождеством или нулем, немного быстрее, чем другое np.where и дает тот же результат.
Edit:
Поскольку скорость вызывает беспокойство, все, что предлагается в одном столбце, как предлагает @cronos, обеспечивает ускорение скорости, приближаясь к 37% -му улучшению в 20 строках в моих тестах, или 18% при 2 000 000. Я мог бы представить, что последнее больше, если хранить промежуточные столбцы, чтобы пересечь какой-то порог использования памяти или было что-то еще, связанное с особенностями системы, которых я не испытывал.
Это будет выглядеть так:
>>>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)
чтобы это можно было распознавать и распространять.
Ответ 4
Настройка
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
Примечание
В последней строке вашего примера данные Nokia Percentile составляют 0,57, а в результатах - 0,47. В этом примере я использовал 0.57, поэтому вывод немного отличается от вашего для последней строки.
Ответ 5
Я думаю, вы можете использовать метод окна pandas.Series rolling.
Возможно, что-то вроде этого:
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
как максимальное количество записей на акцию.