【问题标题】:Split / Explode a column of dictionaries into separate columns with pandas使用 pandas 将一列字典拆分/分解为单独的列
【发布时间】:2016-11-08 22:52:02
【问题描述】:

我将数据保存在postgreSQL 数据库中。我正在使用 Python2.7 查询这些数据并将其转换为 Pandas DataFrame。但是,这个数据框的最后一列里面有一个值字典。 DataFrame df 如下所示:

Station ID     Pollutants
8809           {"a": "46", "b": "3", "c": "12"}
8810           {"a": "36", "b": "5", "c": "8"}
8811           {"b": "2", "c": "7"}
8812           {"c": "11"}
8813           {"a": "82", "c": "15"}

我需要将此列拆分为单独的列,以便 DataFrame `df2 看起来像这样:

Station ID     a      b       c
8809           46     3       12
8810           36     5       8
8811           NaN    2       7
8812           NaN    NaN     11
8813           82     NaN     15

我遇到的主要问题是列表的长度不同。但所有列表最多只包含相同的 3 个值:“a”、“b”和“c”。而且它们总是以相同的顺序出现('a' 首先,'b' 第二,'c' 第三)。

以下代码用于工作并准确返回我想要的 (df2)。

objs = [df, pandas.DataFrame(df['Pollutant Levels'].tolist()).iloc[:, :3]]
df2 = pandas.concat(objs, axis=1).drop('Pollutant Levels', axis=1)
print(df2)

我上周才运行这段代码,它运行良好。但是现在我的代码被破坏了,我从第 [4] 行收到了这个错误:

IndexError: out-of-bounds on slice (end) 

我没有对代码进行任何更改,但现在出现错误。我觉得这是因为我的方法不够稳健或不恰当。

任何关于如何将此列列表拆分为单独列的建议或指导都将非常感激!

编辑:我认为 .tolist() 和 .apply 方法不适用于我的代码,因为它是一个 Unicode 字符串,即:

#My data format 
u{'a': '1', 'b': '2', 'c': '3'}

#and not
{u'a': '1', u'b': '2', u'c': '3'}

数据以这种格式从postgreSQL 数据库中导入。关于这个问题的任何帮助或想法?有没有办法转换 Unicode?​​p>

【问题讨论】:

    标签: python json pandas dictionary json-normalize


    【解决方案1】:
    • 根据Shijith 在此answer 中执行的时序分析,对一列平坦的单级dicts 进行标准化的最快方法:
      • df.join(pd.DataFrame(df.pop('Pollutants').values.tolist()))
      • 它不会解决下面提到的listdicts 列的其他问题,例如带有NaN 的行或嵌套的dicts
    1. pd.json_normalize(df.Pollutants) 明显快于 df.Pollutants.apply(pd.Series)
      • 请参阅下面的%%timeit。对于 1M 行,.json_normalize.apply 快 47 倍。
    2. 无论是从文件中读取数据,还是从数据库或 API 返回的对象中读取数据,可能不清楚dict 列是dict 还是str 类型。
      • 如果列中的字典是str 类型,则必须使用ast.literal_evaljson.loads(…) 将它们转换回dict 类型。
    3. 使用pd.json_normalize 转换dictskeys 作为标题,values 作为行。
      • 还有额外的参数(例如record_path & meta)用于处理嵌套的dicts
    4. 使用pandas.DataFrame.join 将原始DataFrame df 与使用pd.json_normalize 创建的列组合起来
      • 如果索引不是整数(如示例中所示),请先使用df.reset_index() 获取整数索引,然后再进行规范化和连接。
    5. 最后,使用pandas.DataFrame.drop,去掉dicts不需要的列
    • 注意,如果该列有任何NaN,则必须用空的dict 填充
    import pandas as pd
    from ast import literal_eval
    import numpy as np
    
    data = {'Station ID': [8809, 8810, 8811, 8812, 8813, 8814],
            'Pollutants': ['{"a": "46", "b": "3", "c": "12"}', '{"a": "36", "b": "5", "c": "8"}', '{"b": "2", "c": "7"}', '{"c": "11"}', '{"a": "82", "c": "15"}', np.nan]}
    
    df = pd.DataFrame(data)
    
    # display(df)
       Station ID                        Pollutants
    0        8809  {"a": "46", "b": "3", "c": "12"}
    1        8810   {"a": "36", "b": "5", "c": "8"}
    2        8811              {"b": "2", "c": "7"}
    3        8812                       {"c": "11"}
    4        8813            {"a": "82", "c": "15"}
    5        8814                               NaN
    
    # replace NaN with '{}' if the column is strings, otherwise replace with {}
    # df.Pollutants = df.Pollutants.fillna('{}')  # if the NaN is in a column of strings
    df.Pollutants = df.Pollutants.fillna({i: {} for i in df.index})  # if the column is not strings
    
    # Convert the column of stringified dicts to dicts
    # skip this line, if the column contains dicts
    df.Pollutants = df.Pollutants.apply(literal_eval)
    
    # reset the index if the index is not unique integers from 0 to n-1
    # df.reset_index(inplace=True)  # uncomment if needed
    
    # normalize the column of dictionaries and join it to df
    df = df.join(pd.json_normalize(df.Pollutants))
    
    # drop Pollutants
    df.drop(columns=['Pollutants'], inplace=True)
    
    # display(df)
       Station ID    a    b    c
    0        8809   46    3   12
    1        8810   36    5    8
    2        8811  NaN    2    7
    3        8812  NaN  NaN   11
    4        8813   82  NaN   15
    5        8814  NaN  NaN  NaN
    

    %%timeit

    # dataframe with 1M rows
    dfb = pd.concat([df]*200000).reset_index(drop=True)
    
    %%timeit
    dfb.join(pd.json_normalize(dfb.Pollutants))
    [out]:
    5.44 s ± 32.3 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)
    
    %%timeit
    pd.concat([dfb.drop(columns=['Pollutants']), dfb.Pollutants.apply(pd.Series)], axis=1)
    [out]:
    4min 17s ± 2.44 s per loop (mean ± std. dev. of 7 runs, 1 loop each)
    

    【讨论】:

      【解决方案2】:

      注意:对于深度=1(一级)的字典

      >>> df
      
         Station ID                        Pollutants
      0        8809  {"a": "46", "b": "3", "c": "12"}
      1        8810   {"a": "36", "b": "5", "c": "8"}
      2        8811              {"b": "2", "c": "7"}
      3        8812                       {"c": "11"}
      4        8813            {"a": "82", "c": "15"}
      

      1000 万行大型数据集的速度比较

      >>> df = pd.concat([df]*2000000).reset_index(drop=True)
      >>> print(df.shape)
      (10000000, 2)
      
      def apply_drop(df):
          return df.join(df['Pollutants'].apply(pd.Series)).drop('Pollutants', axis=1)  
      
      def json_normalise_drop(df):
          return df.join(pd.json_normalize(df.Pollutants)).drop('Pollutants', axis=1)  
      
      def tolist_drop(df):
          return df.join(pd.DataFrame(df['Pollutants'].tolist())).drop('Pollutants', axis=1)  
      
      def vlues_tolist_drop(df):
          return df.join(pd.DataFrame(df['Pollutants'].values.tolist())).drop('Pollutants', axis=1)  
      
      def pop_tolist(df):
          return df.join(pd.DataFrame(df.pop('Pollutants').tolist()))  
      
      def pop_values_tolist(df):
          return df.join(pd.DataFrame(df.pop('Pollutants').values.tolist()))
      
      
      >>> %timeit apply_drop(df.copy())
      1 loop, best of 3: 53min 20s per loop
      >>> %timeit json_normalise_drop(df.copy())
      1 loop, best of 3: 54.9 s per loop
      >>> %timeit tolist_drop(df.copy())
      1 loop, best of 3: 6.62 s per loop
      >>> %timeit vlues_tolist_drop(df.copy())
      1 loop, best of 3: 6.63 s per loop
      >>> %timeit pop_tolist(df.copy())
      1 loop, best of 3: 5.99 s per loop
      >>> %timeit pop_values_tolist(df.copy())
      1 loop, best of 3: 5.94 s per loop
      
      +---------------------+-----------+
      | apply_drop          | 53min 20s |
      | json_normalise_drop |    54.9 s |
      | tolist_drop         |    6.62 s |
      | vlues_tolist_drop   |    6.63 s |
      | pop_tolist          |    5.99 s |
      | pop_values_tolist   |    5.94 s |
      +---------------------+-----------+
      

      df.join(pd.DataFrame(df.pop('Pollutants').values.tolist()))最快

      【讨论】:

        【解决方案3】:

        如何使用 pandas 将一列字典拆分为单独的列?

        pd.DataFrame(df['val'].tolist()) 是爆炸一列字典的规范方法

        这是您使用彩色图表的证明。

        Benchmarking code供参考。

        请注意,我只是对爆炸进行计时,因为这是回答这个问题最有趣的部分 - 结果构造的其他方面(例如是否使用 popdrop)与讨论无关,可以忽略(但应该注意的是,使用pop 避免了后续的drop 调用,因此最终解决方案的性能更高一些,但我们仍在列出列并将其传递给pd.DataFrame

        此外,pop 破坏性地改变了输入 DataFrame,使得在假设输入在测试运行中没有改变的基准测试代码中运行变得更加困难。


        对其他解决方案的批评

        • df['val'].apply(pd.Series) 对于大 N 非常慢,因为 pandas 为每一行构造 Series 对象,然后继续从它们构造 DataFrame。对于较大的 N,性能下降到几分钟或几小时的数量级。

        • pd.json_normalize(df['val'])) 速度较慢仅仅是因为json_normalize 旨在处理更复杂的输入数据——尤其是具有多个记录路径和元数据的深度嵌套的 JSON。我们有一个简单的平面字典,pd.DataFrame 就足够了,所以如果你的字典是平面的,请使用它。

        • 一些答案​​建议df.pop('val').values.tolist()df.pop('val').to_numpy().tolist()。我认为无论您列出系列还是 numpy 数组都没有太大区别。直接列出系列少了一个操作,而且真的不慢,所以我建议避免在中间步骤中生成 numpy 数组。

        【讨论】:

        • 这种比较很有用,但我不清楚它是否适用于形状不同的字典。如果所有字典都有相同的键,它当然可以正常工作。
        【解决方案4】:

        我知道这个问题已经很老了,但我来这里是为了寻找答案。现在实际上有一种更好(更快)的方法可以使用json_normalize

        import pandas as pd
        
        df2 = pd.json_normalize(df['Pollutant Levels'])
        

        这避免了昂贵的应用函数...

        【讨论】:

        • 我的测试表明这确实比接受答案中的 .apply() 方法快得多
        • pd.DataFrame(df['b'].tolist()) 对于扁平结构来说比这更好,normalize 在深层嵌套的 dicts 的引擎盖下做了很多事情,并且会更慢。如果您有一列 dicts,请向此问题推荐 Trenton McKinney's answer
        • 听起来很有趣,但在我的情况下,当值不统一并且某些键可能比其他键少时,这不起作用。
        • 我有一串字典列表。我有什么机会让它发挥作用?
        • 对于我的数据集,这种方法大约快 40 倍(0.7 对 29.2 秒)!一个不错的选择!
        【解决方案5】:

        my_df = pd.DataFrame.from_dict(my_dict, orient='index', columns=['my_col'])

        .. 会正确解析 dict(将每个 dict 键放入单独的 df 列,并将键值放入 df 行),因此 dict 首先不会被压缩成单个列。

        【讨论】:

          【解决方案6】:
          df = pd.concat([df['a'], df.b.apply(pd.Series)], axis=1)
          

          【讨论】:

            【解决方案7】:

            我已经在一个方法中连接了这些步骤,您只需传递数据框和包含要扩展的 dict 的列:

            def expand_dataframe(dw: pd.DataFrame, column_to_expand: str) -> pd.DataFrame:
                """
                dw: DataFrame with some column which contain a dict to expand
                    in columns
                column_to_expand: String with column name of dw
                """
                import pandas as pd
            
                def convert_to_dict(sequence: str) -> Dict:
                    import json
                    s = sequence
                    json_acceptable_string = s.replace("'", "\"")
                    d = json.loads(json_acceptable_string)
                    return d    
            
                expanded_dataframe = pd.concat([dw.drop([column_to_expand], axis=1),
                                                dw[column_to_expand]
                                                .apply(convert_to_dict)
                                                .apply(pd.Series)],
                                                axis=1)
                return expanded_dataframe
            

            【讨论】:

              【解决方案8】:

              一行解决方案如下:

              >>> df = pd.concat([df['Station ID'], df['Pollutants'].apply(pd.Series)], axis=1)
              >>> print(df)
                 Station ID    a    b   c
              0        8809   46    3  12
              1        8810   36    5   8
              2        8811  NaN    2   7
              3        8812  NaN  NaN  11
              4        8813   82  NaN  15
              

              【讨论】:

                【解决方案9】:

                我强烈推荐提取“污染物”列的方法:

                df_pollutants = pd.DataFrame(df['Pollutants'].values.tolist(), index=df.index)

                快很多

                df_pollutants = df['Pollutants'].apply(pd.Series)

                当df的大小很大时。

                【讨论】:

                • 如果你能解释一下它是如何/为什么工作的并且更好!对我来说,它总是更快,一旦你获得超过 ~1000 行,它会快 ~200 倍
                • @SamMason 当您执行 apply 时,整个数据框由 pandas 管理,但是当涉及到 values 时,它只与 numpy ndarrays 一起播放,因为它本质上更快,因为它有纯粹的c 实现。
                【解决方案10】:

                Merlin 的答案更好而且超级简单,但我们不需要 lambda 函数。可以通过以下两种方式之一安全地忽略字典的评估,如下所示:

                方式一:两步

                # step 1: convert the `Pollutants` column to Pandas dataframe series
                df_pol_ps = data_df['Pollutants'].apply(pd.Series)
                
                df_pol_ps:
                    a   b   c
                0   46  3   12
                1   36  5   8
                2   NaN 2   7
                3   NaN NaN 11
                4   82  NaN 15
                
                # step 2: concat columns `a, b, c` and drop/remove the `Pollutants` 
                df_final = pd.concat([df, df_pol_ps], axis = 1).drop('Pollutants', axis = 1)
                
                df_final:
                    StationID   a   b   c
                0   8809    46  3   12
                1   8810    36  5   8
                2   8811    NaN 2   7
                3   8812    NaN NaN 11
                4   8813    82  NaN 15
                

                方式2:以上两步可以一次性组合起来:

                df_final = pd.concat([df, df['Pollutants'].apply(pd.Series)], axis = 1).drop('Pollutants', axis = 1)
                
                df_final:
                    StationID   a   b   c
                0   8809    46  3   12
                1   8810    36  5   8
                2   8811    NaN 2   7
                3   8812    NaN NaN 11
                4   8813    82  NaN 15
                

                【讨论】:

                  【解决方案11】:

                  您可以将joinpop + tolist 一起使用。性能与concatdrop + tolist 相当,但有些人可能会发现这种语法更简洁:

                  res = df.join(pd.DataFrame(df.pop('b').tolist()))
                  

                  用其他方法进行基准测试:

                  df = pd.DataFrame({'a':[1,2,3], 'b':[{'c':1}, {'d':3}, {'c':5, 'd':6}]})
                  
                  def joris1(df):
                      return pd.concat([df.drop('b', axis=1), df['b'].apply(pd.Series)], axis=1)
                  
                  def joris2(df):
                      return pd.concat([df.drop('b', axis=1), pd.DataFrame(df['b'].tolist())], axis=1)
                  
                  def jpp(df):
                      return df.join(pd.DataFrame(df.pop('b').tolist()))
                  
                  df = pd.concat([df]*1000, ignore_index=True)
                  
                  %timeit joris1(df.copy())  # 1.33 s per loop
                  %timeit joris2(df.copy())  # 7.42 ms per loop
                  %timeit jpp(df.copy())     # 7.68 ms per loop
                  

                  【讨论】:

                    【解决方案12】:

                    要将字符串转换为实际的字典,您可以使用df['Pollutant Levels'].map(eval)。之后,可以使用下面的解决方案将 dict 转换为不同的列。


                    使用一个小例子,可以使用.apply(pd.Series)

                    In [2]: df = pd.DataFrame({'a':[1,2,3], 'b':[{'c':1}, {'d':3}, {'c':5, 'd':6}]})
                    
                    In [3]: df
                    Out[3]:
                       a                   b
                    0  1           {u'c': 1}
                    1  2           {u'd': 3}
                    2  3  {u'c': 5, u'd': 6}
                    
                    In [4]: df['b'].apply(pd.Series)
                    Out[4]:
                         c    d
                    0  1.0  NaN
                    1  NaN  3.0
                    2  5.0  6.0
                    

                    要将其与数据框的其余部分结合起来,您可以concat 其他列与上述结果:

                    In [7]: pd.concat([df.drop(['b'], axis=1), df['b'].apply(pd.Series)], axis=1)
                    Out[7]:
                       a    c    d
                    0  1  1.0  NaN
                    1  2  NaN  3.0
                    2  3  5.0  6.0
                    

                    使用您的代码,如果我省略 iloc 部分,这也可以:

                    In [15]: pd.concat([df.drop('b', axis=1), pd.DataFrame(df['b'].tolist())], axis=1)
                    Out[15]:
                       a    c    d
                    0  1  1.0  NaN
                    1  2  NaN  3.0
                    2  3  5.0  6.0
                    

                    【讨论】:

                    • @llaffin 如果它是一个字符串,您可以在将其转换为 DataFrame 之前使用df[col].map(eval) 将其转换为实际的字典
                    • 是的,它似乎更快,但我认为 apply(pd.Series) 的意图更清晰,因为 apply 很灵活。
                    • 如果字符串可能在某个时候来自未经处理的用户输入,.map(eval) 是否存在安全风险?
                    • 工作完美,但比 Lech Birek stackoverflow.com/a/55355928/2721710 提供的新解决方案 (2019) 慢(很多)
                    • 使用apply(pd.Series) 非常慢!我
                    【解决方案13】:

                    试试这个:从 SQL 返回的数据必须转换成字典。 或者是"Pollutant Levels" 现在是Pollutants'

                       StationID                   Pollutants
                    0       8809  {"a":"46","b":"3","c":"12"}
                    1       8810   {"a":"36","b":"5","c":"8"}
                    2       8811            {"b":"2","c":"7"}
                    3       8812                   {"c":"11"}
                    4       8813          {"a":"82","c":"15"}
                    
                    
                    df2["Pollutants"] = df2["Pollutants"].apply(lambda x : dict(eval(x)) )
                    df3 = df2["Pollutants"].apply(pd.Series )
                    
                        a    b   c
                    0   46    3  12
                    1   36    5   8
                    2  NaN    2   7
                    3  NaN  NaN  11
                    4   82  NaN  15
                    
                    
                    result = pd.concat([df, df3], axis=1).drop('Pollutants', axis=1)
                    result
                    
                       StationID    a    b   c
                    0       8809   46    3  12
                    1       8810   36    5   8
                    2       8811  NaN    2   7
                    3       8812  NaN  NaN  11
                    4       8813   82  NaN  15
                    

                    【讨论】:

                      猜你喜欢
                      • 1970-01-01
                      • 1970-01-01
                      • 2017-11-02
                      • 2022-08-19
                      • 2020-02-08
                      • 2018-11-04
                      • 1970-01-01
                      相关资源
                      最近更新 更多