3
votes

Pandas: fonction d'agrégation de fonction WMAPE personnalisée sur plusieurs colonnes sans boucle for?

Objectif: regrouper le dataframe pandas à l'aide d'une fonction WMAPE (Weighted Mean Absolute Percent Error) personnalisée sur plusieurs colonnes de prévision et une colonne de données réelle, sans boucle for. Je sais qu'une boucle for et des fusions de dataframes de sortie feront l'affaire. Je veux le faire efficacement.

Avoir: la fonction WMAPE, une utilisation réussie de la fonction WMAPE sur une colonne de prévision du dataframe. Une colonne de données réelles, nombre variable de colonnes de prévision.

Données d'entrée: Pandas DataFrame avec plusieurs colonnes catégorielles (Ville, Personne, DT, HOUR), une colonne de données réelles ( Réel) et quatre colonnes de prévision (Forecast_1 ... Forecast_4). Voir le lien pour csv: https://www.dropbox.com/s/tidf9lj80a1dtd8/data_small_2. csv? dl = 1

Besoin: Fonction WMAPE appliquée pendant groupby sur plusieurs colonnes avec une liste de colonnes de prévision alimentée en ligne groupby.

Sortie souhaitée: Une trame de données de sortie avec des colonnes de groupes catégoriels et toutes les colonnes de WMAPE. L'étiquetage est préférable mais pas nécessaire (image de sortie ci-dessous).

Code réussi jusqu'à présent: Deux fonctions WMAPE: une pour prendre deux séries et afficher une seule valeur flottante (wmape), et une structurée pour une utilisation dans un groupby (wmape_gr):

def wmape(actual, forecast):
    # we take two series and calculate an output a wmape from it

    # make a series called mape
    se_mape = abs(actual-forecast)/actual

    # get a float of the sum of the actual
    ft_actual_sum = actual.sum()

    # get a series of the multiple of the actual & the mape
    se_actual_prod_mape = actual * se_mape

    # summate the prod of the actual and the mape
    ft_actual_prod_mape_sum = se_actual_prod_mape.sum()

    # float: wmape of forecast
    ft_wmape_forecast = ft_actual_prod_mape_sum / ft_actual_sum

    # return a float
    return ft_wmape_forecast

def wmape_gr(df_in, st_actual, st_forecast):
    # we take two series and calculate an output a wmape from it

    # make a series called mape
    se_mape = abs(df_in[st_actual] - df_in[st_forecast]) / df_in[st_actual]

    # get a float of the sum of the actual
    ft_actual_sum = df_in[st_actual].sum()

    # get a series of the multiple of the actual & the mape
    se_actual_prod_mape = df_in[st_actual] * se_mape

    # summate the prod of the actual and the mape
    ft_actual_prod_mape_sum = se_actual_prod_mape.sum()

    # float: wmape of forecast
    ft_wmape_forecast = ft_actual_prod_mape_sum / ft_actual_sum

    # return a float
    return ft_wmape_forecast

# read in data directly from Dropbox
df = pd.read_csv('https://www.dropbox.com/s/tidf9lj80a1dtd8/data_small_2.csv?dl=1',sep=",",header=0)

# grouping with 3 columns. wmape_gr uses the Actual column, and Forecast_1 as inputs
df_gr = df.groupby(['City','Person','DT']).apply(wmape_gr,'Actual','Forecast_1')

La sortie ressemble à (deux premières lignes):

entrez la description de l'image ici

La sortie souhaitée aurait toutes les prévisions en un seul coup (factice données pour Forecast_2 ... Forecast_4). Je peux déjà faire cela avec une boucle for. Je veux juste le faire au sein du groupby. Je veux appeler une fonction wmape quatre fois. J'apprécierais toute aide.


0 commentaires

3 Réponses :


2
votes

Si vous modifiez wmape pour travailler avec des tableaux utilisant la diffusion, vous pouvez le faire en une seule fois:

# Convert the dictionary in a single column into 4 columns with proper names
# and concantenate column-wise
df_grp = pd.concat([new_df.drop(columns=[0]), 
                    pd.DataFrame(list(new_df[0].values))], axis=1)

Ensuite, utilisez apply code > sur les colonnes appropriées:

# Group the dataframe and apply the function to appropriate columns
new_df = df.groupby(['City', 'Person', 'DT']).apply(lambda x: wmape(x['Actual'], 
                                        x[[c for c in x if 'Forecast' in c]])).\
            to_frame().reset_index()

Il en résulte un dataframe avec une seule colonne de dictionnaire. Résultats intermédiaires

La colonne unique peut être convertie en plusieurs colonnes pour le format correct:

def wmape(actual, forecast):
    # Take a series (actual) and a dataframe (forecast) and calculate wmape
    # for each forecast. Output shape is (1, num_forecasts)

    # Convert to numpy arrays for broadasting
    forecast = np.array(forecast.values)
    actual=np.array(actual.values).reshape((-1, 1))

    # Make an array of mape (same shape as forecast)
    se_mape = abs(actual-forecast)/actual

    # Calculate sum of actual values
    ft_actual_sum = actual.sum(axis=0)

    # Multiply the actual values by the mape
    se_actual_prod_mape = actual * se_mape

    # Take the sum of the product of actual values and mape
    # Make sure to sum down the rows (1 for each column)
    ft_actual_prod_mape_sum = se_actual_prod_mape.sum(axis=0)

    # Calculate the wmape for each forecast and return as a dictionary
    ft_wmape_forecast = ft_actual_prod_mape_sum / ft_actual_sum
    return {f'Forecast_{i+1}_wmape': wmape for i, wmape in enumerate(ft_wmape_forecast)}

Résultat: p>

 Résultat des opérations


5 commentaires

Salut @willk, la solution que vous avez fournie fonctionne parfaitement dans Spyder (Python 3.6). Cependant, si j'essaie de l'exécuter dans un notebook Jupyter, j'obtiens une erreur très étrange sur la ligne de retour de la fonction que vous avez réécrite. Avez-vous des pensées? : Fichier "", ligne 88 return {f'Forecast_ {i + 1} _wmape ': wmape pour i, wmape dans enumerate (ft_wmape_forecast)} ^ SyntaxError: syntaxe invalide


Quelle version de Python utilisez-vous dans votre notebook? Il semble que votre Python dans le notebook ne dispose peut-être pas de formatage f-string (disponible à partir du Python 3.6)


Si vous utilisez une ancienne version de Python, vous devrez utiliser un formatage de chaîne comme: return {'Forecast_% d_wmape:'% i: wmape for i, wmape in enumerate (ft_wmape_forecast)}


Cela semble être une incompatibilité de version entre mes deux environnements python, comme vous l'avez suggéré. Désolé, merci pour la réponse.


Pas de problème, il est bon de savoir quelle version de Python et quelle version de bibliothèques externes vous utilisez. Souvent, les erreurs peuvent être attribuées à une incompatibilité de version, ce que je ne connais que trop bien!



6
votes

C'est un très bon problème pour montrer comment optimiser une application groupby.apply dans les pandas. J'utilise deux principes pour résoudre ces problèmes.

  1. Tout calcul indépendant du groupe ne doit pas être effectué au sein d'un groupby
  2. S'il existe une méthode groupby intégrée, utilisez-la d'abord avant d'utiliser appliquer

Passons ligne par ligne à travers votre fonction wmape_gr .

forecast1_wampe_sum.div(actual_sum, axis='index')

Cette ligne est complètement indépendante de tout groupe. Vous devez faire ce calcul en dehors de l'application. Ci-dessous, je fais ceci pour chacune des colonnes de prévision:

g = df.groupby(['City', 'Person', 'DT'])
actual_sum = g['Actual'].sum()
forecast_wampe_cols = ['forecast1_wampe', 'forecast2_wampe', 'forecast3_wampe', 'forecast4_wampe']
forecast1_wampe_sum = g[forecast_wampe_cols].sum()

Jetons un coup d'œil à la ligne suivante:

ft_actual_prod_mape_sum = se_actual_prod_mape.sum()
ft_wmape_forecast = ft_actual_prod_mape_sum / ft_actual_sum

Cette ligne dépend du groupe, nous devons donc utiliser un groupby ici, mais il n'est pas nécessaire de le placer dans la fonction apply. Il sera calculé plus tard ci-dessous.

Passons à la ligne suivante:

df['forecast1_wampe'] = df['actual_forecast_diff_1'] *  df['Actual']
df['forecast2_wampe'] = df['actual_forecast_diff_2'] *  df['Actual']
df['forecast3_wampe'] = df['actual_forecast_diff_3'] *  df['Actual']
df['forecast4_wampe'] = df['actual_forecast_diff_4'] *  df['Actual']

Ceci est encore une fois indépendant du groupe. Calculons-le sur le DataFrame dans son ensemble.

se_actual_prod_mape = df_in[st_actual] * se_mape

Passons aux deux dernières lignes:

ft_actual_sum = df_in[st_actual].sum()

Ces les lignes dépendent à nouveau du groupe, mais nous n'avons toujours pas besoin d'utiliser apply. Nous avons maintenant chacune des 4 colonnes 'prévisions_wampe' calculée indépendamment du groupe. Nous devons simplement faire la somme de chacun par groupe. Il en va de même pour la colonne "Réel".

Nous pouvons exécuter deux opérations groupby distinctes pour additionner chacune de ces colonnes comme ceci:

df['actual_forecast_diff_1'] = (df['Actual'] - df['Forecast_1']).abs() / df['Actual']
df['actual_forecast_diff_2'] = (df['Actual'] - df['Forecast_2']).abs() / df['Actual']
df['actual_forecast_diff_3'] = (df['Actual'] - df['Forecast_3']).abs() / df['Actual']
df['actual_forecast_diff_4'] = (df['Actual'] - df['Forecast_4']).abs() / df['Actual']

Nous obtenons la série et le DataFrame suivants ont renvoyé

 entrez la description de l'image ici

 entrez la description de l'image ici

Ensuite il suffit de diviser chacune des colonnes du DataFrame par la série. Nous devrons utiliser la méthode div pour changer l'orientation de la division afin que les index s'alignent

se_mape = abs(df_in[st_actual] - df_in[st_forecast]) / df_in[st_actual]

Et cela renvoie notre réponse:

 entrez la description de l'image ici


0 commentaires

1
votes

sans changer les fonctions

appliquant quatre fois

df = all1.rename(columns={0:'Forecast_1_wmape', 1:'Forecast_2_wmape',2:'Forecast_3_wmape',3:'Forecast_4_wmape'})

df = df[['city','Person','DT','Forecast_1_wmape','Forecast_2_wmape','Forecast_3_wmape','Forecast_4_wmape']]

df=df.reset_index(drop=True)

associez-les

all1['city']= [all1.index[i][0]  for i in range(len(df_gr1))]
all1['Person']= [all1.index[i][1]  for i in range(len(df_gr1))]
all1['DT']= [all1.index[i][2]  for i in range(len(df_gr1))]

récupérez les colonnes pour ville, personne et DT

all1= pd.concat([df_gr1, df_gr2,df_gr3,df_gr4],axis=1, sort=False)

renommer les colonnes et modifier l'ordre

df_gr1 = df.groupby(['City','Person','DT']).apply(wmape_gr,'Actual','Forecast_1')
df_gr2 = df.groupby(['City','Person','DT']).apply(wmape_gr,'Actual','Forecast_2')
df_gr3 = df.groupby(['City','Person','DT']).apply(wmape_gr,'Actual','Forecast_3')
df_gr4 = df.groupby(['City','Person','DT']).apply(wmape_gr,'Actual','Forecast_4')

0 commentaires