【问题标题】:Pandas: custom WMAPE function aggregation function to multiple columns without for-loop?Pandas:自定义 WMAPE 函数聚合函数到多列而没有 for 循环?
【发布时间】:2019-07-16 19:10:16
【问题描述】:

目标: 使用自定义 WMAPE(加权平均绝对百分比误差)函数对多个预测列和一个实际数据列进行分组,无需 for 循环。我知道输出数据帧的for循环和合并可以解决问题。我想高效地做到这一点。

有: WMAPE 功能,在数据框的一个预测列上成功使用 WMAPE 功能。一列实际数据,可变数量的预测列。

输入数据: Pandas DataFrame 包含多个分类列(City、Person、DT、HOUR)、一个实际数据列(Actual)和四个预测列(Forecast_1 ... Forecast_4)。参见 csv 链接: https://www.dropbox.com/s/tidf9lj80a1dtd8/data_small_2.csv?dl=1

需要:在 groupby 期间对多列应用 WMAPE 函数,并将预测列列表馈入 groupby 行。

所需输出:输出数据框,包含分类组列和 WMAPE 的所有列。标记是首选但不需要(下面的输出图像)。

到目前为止成功的代码: 两个 WMAPE 函数:一个接收两个系列并输出一个浮点值 (wmape),一个结构化用于 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')

输出看起来像(前两行):

所需的输出将一次性包含所有预测(Forecast_2 ... Forecast_4 的虚拟数据)。我可以已经使用 for 循环来做到这一点。我只想在 groupby 内做。我想调用一个 wmape 函数四次。如有任何帮助,我将不胜感激。

【问题讨论】:

    标签: python pandas pandas-groupby forecasting pandas-apply


    【解决方案1】:

    如果您修改wmape 以使用广播处理数组,那么您可以一次性完成:

    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)}
    

    然后在正确的列上使用apply

    # 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()
    

    这会产生一个包含单个字典列的数据框。

    单列可以转换为多列,格式正确:

    # 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)
    

    结果:

    【讨论】:

    • 嗨@willk,您提供的解决方案在 Spyder (Python 3.6) 中完美运行。但是,如果我尝试在 Jupyter 笔记本中运行它,我会在您重写的函数的返回行上收到一个非常奇怪的错误。你有想法吗? :文件“”,第 88 行返回 {f'Forecast_{i+1}_wmape': wmape for i, wmape in enumerate(ft_wmape_forecast)} ^ SyntaxError: invalid syntax
    • 您在笔记本中运行的 Python 版本是什么?听起来你笔记本中的 Python 可能没有 f-string formatting(从 Python 3.6 开始可用)
    • 如果您使用的是旧版本的 Python,则必须使用字符串格式,例如:return {'Forecast_%d_wmape:' % i: wmape for i, wmape in enumerate(ft_wmape_forecast)}
    • 正如您所建议的,这似乎是我的两个 python 环境之间的版本不匹配。很抱歉,谢谢您的回复。
    • 没问题,知道你使用的是哪个版本的 Python 和哪个版本的外部库是很好的。通常错误可以追溯到版本不匹配,我对此非常熟悉!
    【解决方案2】:

    这是一个展示如何在 pandas 中优化 groupby.apply 的非常好的问题。我使用两个原则来帮助解决这些问题。

    1. 任何独立于组的计算都不应在 groupby 内完成
    2. 如果有内置的groupby方法,使用前先使用 申请

    让我们逐行查看您的wmape_gr 函数。

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

    这条线完全独立于任何组。您应该在应用之外进行此计算。下面我对每个预测列执行此操作:

    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']
    

    让我们看看下一行:

    ft_actual_sum = df_in[st_actual].sum()
    

    这一行取决于组,因此我们必须在这里使用 groupby,但不必将其放在 apply 函数中。后面会计算。

    让我们移到下一行:

    se_actual_prod_mape = df_in[st_actual] * se_mape
    

    这又是独立于组的。让我们在整个 DataFrame 上进行计算。

    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']
    

    让我们继续看最后两行:

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

    这些行再次依赖于组,但我们仍然不需要使用 apply。现在,我们已经独立于组计算了 4 个“forecast_wampe”列中的每一个。我们只需要对每组中的每一个求和即可。 “实际”列也是如此。

    我们可以运行两个单独的 groupby 操作来对每一列求和,如下所示:

    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()
    

    我们得到以下 Series 和 DataFrame 返回

    然后我们只需要将DataFrame中的每一列除以Series即可。我们需要使用div 方法来改变分割的方向,以便索引对齐

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

    这会返回我们的答案:

    【讨论】:

      【解决方案3】:

      不改变功能

      申请四次

      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')
      

      把他们联合起来

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

      获取城市、人员和 DT 的列

      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))]
      

      重命名列并更改顺序

      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)
      

      【讨论】:

        猜你喜欢
        • 2019-06-08
        • 2019-11-05
        • 2017-08-04
        • 2015-07-04
        • 2014-09-19
        • 1970-01-01
        • 1970-01-01
        • 2020-10-05
        • 1970-01-01
        相关资源
        最近更新 更多