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):
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.
3 Réponses :
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.
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>
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 "
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!
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.
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é
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:
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)
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))]
all1= pd.concat([df_gr1, df_gr2,df_gr3,df_gr4],axis=1, sort=False)
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')