3
votes

Somme glissante sur une colonne avec pondération par autre colonne et position relative

J'ai une table comme celle-ci:

        col_to_roll  max_duration  rolled_col
    0             0             0         NaN
    1             0             0         NaN
    2             0             0         NaN
    3          2000            12         NaN
    4             0             0         NaN
    5             0             0         NaN
    6           700             8         NaN
    7             0             0      2000.0
    8             0             0      4000.0
    9           530             2      6000.0
    10         1000             5      8700.0
    11          820            15      1400.0
    12            0             0      2100.0
    13            0             0      3330.0
    14          200             3      2060.0

Pour chaque position de ligne i , je veux faire une somme glissante de col_to_roll entre les index i-7 et i-4 (tous deux inclus). La mise en garde est que je veux que les valeurs "plus loin dans le passé" soient comptées davantage, en fonction de la colonne max_duration (qui indique pour combien de pas temporels dans le futur cette valeur peut encore avoir un effet).
Il y a une limite supérieure qui correspond aux pas de temps restants à compter (min 1, max 4). Donc, si je suis sur la ligne numéro 7 en train de faire la somme min(max_duration[1],4) : la valeur de la ligne numéro 1 sera comptée min(max_duration[1],4) , la valeur de la ligne numéro 2 sera comptée min(max_duration[2],3) etc.

Je pourrais le faire à la manière de la force brute:

new_col = []
for i in range(7,len(ex)) : 
    rolled_val = sum([ex.iloc[j].col_to_roll*min(ex.iloc[j].max_duration , i-j+1-4) \
                     for j in range(i-7,i-3)])
    new_col.append(rolled_val)
ex['rolled_col'] = [np.nan]*7+new_col

Ce qui aboutit aux résultats suivants de l'exemple ci-dessus:

import pandas as pd
values = [0,0,0,2000,0,0,700,0,0,530,1000,820,0,0,200]
durations = [0,0,0,12,0,0,8,0,0,2,5,15,0,0,3]

ex = pd.DataFrame({'col_to_roll' : values, 'max_duration': durations})

    col_to_roll  max_duration
0             0             0
1             0             0
2             0             0
3          2000            12
4             0             0
5             0             0
6           700             8
7             0             0
8             0             0
9           530             2
10         1000             5
11          820            15
12            0             0
13            0             0
14          200             3

Cela étant dit, j'apprécierais un moyen plus élégant (et plus important encore, plus efficace) d'obtenir ce résultat avec des pandas magiques.


0 commentaires

3 Réponses :


0
votes

Juste pour partager mes idées, cela peut être résolu en utilisant numpy sans boucle for

import numpy as np

ex_len = ex.shape[0]
inds = np.vstack([range(i-7,i-3) for i in range(7,ex_len)])
# part one 
col_to_roll = np.take(ex.col_to_roll.values,inds)
# part two
max_duration = np.take(ex.max_duration.values,inds)
duration_to_compare = np.array([[i-j+1-4 for j in range(i-7,i-3)]for i in range(7,ex_len)])
min_mask = max_duration > duration_to_compare
max_duration[min_mask] = duration_to_compare[min_mask]

new_col = np.sum(col_to_roll*max_duration,axis=1)
ex['rolled_col'] = np.concatenate(([np.nan]*7,new_col))


0 commentaires

0
votes

Voici mon humble idée d'une méthode élégante et efficace pour cette tâche. Pour ne pas réinventer la roue, installons pandarallel en invoquant pip install pandarallel . Je suis un fan du multitraitement, et cela devrait aider avec des données plus volumineuses.

INFO: Pandarallel will run on 8 workers.
INFO: Pandarallel will use Memory file system to transfer data between the main process and workers.
    col_to_roll  max_duration  rolled_col
0             0             0         NaN
1             0             0         NaN
2             0             0         NaN
3          2000            12         NaN
4             0             0         NaN
5             0             0         NaN
6           700             8         NaN
7             0             0      2000.0
8             0             0      4000.0
9           530             2      6000.0
10         1000             5      8700.0
11          820            15      1400.0
12            0             0      2100.0
13            0             0      3330.0
14          200             3      2060.0

Production:

import pandas as pd
import numpy as np
from pandarallel import pandarallel

def rocknroll(index):
    if index>=7:
        a = ex['col_to_roll'].iloc[index-7:index-3]
        b = map(min, ex['max_duration'].iloc[index-7:index-3], [4,3,2,1])
        return sum(map(mul, a, b))
    else:
        return np.nan
    
pandarallel.initialize()
    
values = [0,0,0,2000,0,0,700,0,0,530,1000,820,0,0,200]
durations = [0,0,0,12,0,0,8,0,0,2,5,15,0,0,3]

ex = pd.DataFrame({'col_to_roll' : values, 'max_duration': durations})
ex['index_copy'] = list(range(0, len(ex)))
ex['rolled_col'] = ex['index_copy'].apply(rocknroll)
ex.drop(columns={'index_copy'}, inplace=True)
print(ex)

Vous trouverez ici de plus amples informations sur le bon fonctionnement élément par élément. Ajout élément par élément de 2 listes?


0 commentaires

0
votes

Vous pouvez utiliser pd.rolling() pour créer des fenêtres pd.rolling() en combinaison avec apply pour calculer la somme rolled_coll pour les fenêtres rolled_coll spécifiées

Calculez d' abord la taille de la fenêtre en utilisant la limite inférieure et supérieure (et ajoutez 1 pour inclure les deux indices). Cela vous permet de jouer avec différents intervalles de temps.

    col_to_roll     max_duration    rolled_col
0   0               0               NaN
1   0               0               NaN
2   0               0               NaN
3   2000            12              NaN
4   0               0               NaN
5   0               0               NaN
6   700             8               NaN
7   0               0               2000.0
8   0               0               4000.0
9   530             2               6000.0
10  1000            5               8700.0
11  820             15              1400.0
12  0               0               2100.0
13  0               0               3330.0
14  200             3               2060.0

Deuxièmement définir la fonction à apply sur chaque fenêtre glissante. Dans votre cas, prenez le product de col_to_roll et la valeur minimale de max_duration et une liste de plage de 4 à 0 et additionnez toutes les valeurs dans la fenêtre glissante.

ex.assign(rolled_col=lambda x: x.rolling(window_size)
                                .apply(lambda x: calculate_rolled_count(x, ex))
                                .shift(-upper_bound)['max_duration'])

Enfin, attribuez une nouvelle colonne rolled_coll à votre dataframe d'origine et appliquez la fonction définie sur toutes les fenêtres déroulantes. Nous devons décaler les colonnes pour que la valeur corresponde à la ligne souhaitée (car la fenêtre déroulante définit par défaut les valeurs dans la limite droite de la fenêtre)

def calculate_rolled_count(series, ex):
    index = series.index
    min_values = np.minimum(ex.loc[index, 'max_duration'].values, list(range(4, 0, -1)))
    return np.sum(ex.loc[index, 'col_to_roll'] * min_values)

Résultat

lower_bound = -7
upper_bound = -4
window_size = upper_bound - lower_bound + 1


1 commentaires

Maintenant, je sais que pd.rolling () existe. Merci!