【问题标题】:Pandas - Improve Performance of apply methodPandas - 提高应用方法的性能
【发布时间】:2020-05-27 18:15:58
【问题描述】:

我有一个场景,我需要根据同一行中另一列中存在的值以及另一个数据框中的值来转换特定列的值。

例子-

print(parent_df)
       school         location      modifed_date
0      school_1       New Delhi     2020-04-06
1      school_2       Kolkata       2020-04-06
2      school_3       Bengaluru     2020-04-06
3      school_4       Mumbai        2020-04-06
4      school_5       Chennai       2020-04-06

print(location_df)
       school          location     
0      school_10       New Delhi
1      school_20       Kolkata     
2      school_30       Bengaluru
3      school_40       Mumbai       
4      school_50       Chennai

根据此用例,我需要根据同一 df 中的 location 列和 location_df 中的 location 属性转换 parent_df 中的学校名称

为了实现这个转换,我写了下面的方法。

def transform_school_name(row, location_df):
    name_alias = location_df[location_df['location'] == row['location']]
    if len(name_alias) > 0:
        return location_df.school.iloc[0]
    else:
        return row['school']

这就是我调用这个方法的方式

parent_df['school'] = parent_df.apply(UtilityMethods.transform_school_name, args=(self.location_df,), axis=1)

问题在于,对于仅 46K 的记录,我看到整个转换发生在大约 2 分钟内,这太慢了。如何提高此解决方案的性能?

已编辑

以下是我正在处理的实际场景,其中需要完成一个小的转换,然后才能替换原始列中的值。我不确定这是否可以在以下答案之一中提到的replace() 方法中完成。

print(parent_df)
       school         location                  modifed_date    type
0      school_1       _pre_New Delhi_post       2020-04-06      Govt
1      school_2       _pre_Kolkata_post         2020-04-06      Private
2      school_3       _pre_Bengaluru_post       2020-04-06      Private
3      school_4       _pre_Mumbai_post          2020-04-06      Govt
4      school_5       _pre_Chennai_post         2020-04-06      Private

print(location_df)
           school          location     type
    0      school_10       New Delhi    Govt
    1      school_20       Kolkata      Private
    2      school_30       Bengaluru    Private

自定义方法代码

def transform_school_name(row, location_df):
location_values = row['location'].split('_')
name_alias = location_df[location_df['location'] == location_values[1]]
name_alias = name_alias[name_alias['type'] == location_df['type']]
if len(name_alias) > 0:
    return location_df.school.iloc[0]
else:
    return row['school']


def transform_school_name(row, location_df):
    name_alias = location_df[location_df['location'] == row['location']]
    if len(name_alias) > 0:
        return location_df.school.iloc[0]
    else:
        return row['school']

这是我需要处理的实际情况,因此使用replace() 方法将无济于事。

【问题讨论】:

    标签: python python-3.x pandas


    【解决方案1】:

    你可以使用map/replace:

    parent_df['school'] = parent_df.location.replace(location_df.set_index('location')['school'])
    

    输出:

          school   location modifed_date
    0  school_10  New Delhi   2020-04-06
    1  school_20    Kolkata   2020-04-06
    2  school_30  Bengaluru   2020-04-06
    3  school_40     Mumbai   2020-04-06
    4  school_50    Chennai   2020-04-06
    

    【讨论】:

    • 值不存在的情况怎么办?在这种情况下,我需要保持原始值不变。
    • @jezrael 只有当位置存在于location_df 时才会更新,因此replace 而不是map
    • 我觉得有重复,好像只有一所学校
    • @MitakshGupta 这就是replace 所做的。只更新现有的值,不存在的保持不变。 map 需要一个额外的 fillna 跟随。
    • 重新打开,不匹配。
    【解决方案2】:

    IIUC,这更像是一个正则表达式问题,因为模式不完全匹配。首先提取所需的模式,在 parent_df 中创建 location 到 location_df 的映射,映射值。

    pat =  '.*?' + '(' + '|'.join(location_df['location']) + ')' + '.*?' 
    
    mapping = parent_df['location'].str.extract(pat)[0].map(location_df.set_index('location')['school'])
    
    parent_df['school'] = mapping.combine_first(parent_df['school'])
    parent_df
    
    
        school      location            modifed_date    type
    0   school_10   _pre_New Delhi_post 2020-04-06      Govt
    1   school_20   _pre_Kolkata_post   2020-04-06      Private
    2   school_30   _pre_Bengaluru_post 2020-04-06      Private
    3   school_4    _pre_Mumbai_post    2020-04-06      Govt
    4   school_5    _pre_Chennai_post   2020-04-06      Private
    

    【讨论】:

      【解决方案3】:

      据我了解 edited 任务,将执行以下更新:

      • 对于parent_df中的每一行,
      • location_df 中找到具有匹配位置的行( 位置列和类型),
      • 如果找到,用 school 覆盖 parent_df 中的 school 列 从刚刚找到的行中。

      要做到这一点,请执行以下操作:

      第 1 步:生成 MultiIndex 以按城市和 学校类型:

      ind = pd.MultiIndex.from_arrays([parent_df.location.str
          .split('_', expand=True)[2], parent_df.type])
      

      对于您的示例数据,结果是:

      MultiIndex([('New Delhi',    'Govt'),
                  (  'Kolkata', 'Private'),
                  ('Bengaluru', 'Private'),
                  (   'Mumbai',    'Govt'),
                  (  'Chennai', 'Private')],
                 names=[2, 'type'])
      

      别担心奇怪的一级列名(2),它很快就会消失。

      第 2 步:生成“新”位置列表:

      locList = location_df.set_index(['location', 'type']).school[ind].tolist()
      

      结果是:

      ['school_10', 'school_20', 'school_30', nan, nan]
      

      前 3 所学校找到了一些东西,后 2 所 - 什么也没有。

      第 3 步:使用上述列表执行实际更新,通过“非空” 面具:

      parent_df.school = parent_df.school.mask(pd.notnull(locList), locList)
      

      执行速度

      由于使用了矢量化操作和按索引查找,我的代码 应用到每一行的运行速度明显更快。

      示例:我复制了您的 parent_df 10,000 次并检查了 %timeit 你的代码的执行时间(其实有点变化 版本,如下所述)和我的。

      为了允许重复执行,我更改了两个版本,以便它们设置 school_2 列,school 保持不变。

      您的代码运行 34.9 秒,而我的代码 - 只有 161 毫秒 - 261 快几倍。

      更快的版本

      如果 parent_dfdefault 索引(从 0 开始的连续数字), 那么整个操作可以用一个single指令来执行:

      parent_df.school = location_df.set_index(['location', 'type']).school[
          pd.MultiIndex.from_arrays(
              [parent_df.location.str.split('_', expand=True)[2],
               parent_df.type])
          ]\
          .reset_index(drop=True)\
          .combine_first(parent_df.school)
      

      步骤:

      • location_df.set_index(...) - 将索引设置为 2 个“标准”列。
      • .school - 仅保留 school 列(具有上述索引)。
      • [...] - 从中​​检索 MultiIndex 指示的元素 在里面定义。
      • pd.MultiIndex.from_arrays( - 创建多索引。
      • parent_df.location.str.split('_', expand=True)[2] - 第一级 MultiIndex 的 - location 中的“城市”部分。
      • parent_df.type - MultiIndex 的第二级 - type
      • reset_index(...) - 将 MultiIndex 更改为默认索引 (现在索引与 parent_df 中的索引相同。
      • combine_first(...) - 覆盖生成结果中的 NaN 值 到目前为止,来自学校的原始值。
      • parent_df.school = - 将结果保存回 school 列。 出于测试目的,为了检查执行速度,您可以更改它 与 parent_df['school_2']

      根据我的评估,执行时间比 9 % 对于我原来的解决方案。

      更正您的代码

      1. 看看location_values[1]]。它检索 pre 段,而 实际上应该检索下一个段(城市名称)。

      2. 不需要创建临时列表,基于第一个条件 然后缩小范围,用第二个条件过滤。 您的条件(locationtype 相等)都可以 在一条指令中执行,因此执行时间有点 更短。

      3. “肯定”情况下返回的值应该来自name_alias, 不是 location_df

      因此,如果出于某种原因您想保留代码,请更改 相应的片段:

      name_alias = location_df[location_df['location'].eq(location_values[2]) &
          location_df['type'].eq(row.type)]
      if len(name_alias) > 0:
          return name_alias.school.iloc[0]
      else:
          return row['school']
      

      【讨论】:

        【解决方案4】:

        如果我没有正确阅读问题,那么您使用 apply 方法实现的是一种连接操作。 Pandas 擅长矢量化操作,加上其基于 c 的 join ('merge') 实现几乎可以肯定比基于 python / apply 的实现更快。因此,我会尝试使用以下解决方案:

        parent_df["location_short"] = parent_df.location.str.split("_", expand=True)[2]
        parent_df = pd.merge(parent_df, location_df, how = "left", left_on=["location_short", "type"], 
                             right_on=["location", "type"], suffixes = ["", "_by_location"])
        
        parent_df.loc[parent_df.school_by_location.notna(), "school"] = \
              parent_df.loc[parent_df.school_by_location.notna(), "school_by_location"]
        

        据我所知,它会产生您正在寻找的东西:

        【讨论】:

          猜你喜欢
          • 2017-08-24
          • 2019-03-20
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 2021-08-12
          • 2017-04-13
          • 2011-12-19
          • 2019-06-14
          相关资源
          最近更新 更多