Pandas: назначение столбцов с несколькими условиями и пороговыми значениями даты

Отредактировано:

У меня есть финансовый портфель в 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 как максимальное количество записей на акцию.