将Pandas列中的字典/列表拆分为单独的列

时间:2016-07-06 18:47:56

标签: python pandas dictionary dataframe

我将数据保存在postgreSQL数据库中。我使用Python2.7查询这些数据并将其转换为Pandas DataFrame。但是,此数据框的最后一列中包含值的字典(或列表?)。 DataFrame看起来像这样:

[1] 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如下所示:

[2] 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。它们总是以相同的顺序出现(第一个,第二个,第三个)。

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

[3] df 
[4] objs = [df, pandas.DataFrame(df['Pollutant Levels'].tolist()).iloc[:, :3]]
[5] df2 = pandas.concat(objs, axis=1).drop('Pollutant Levels', axis=1)
[6] 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?

13 个答案:

答案 0 :(得分:87)

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

使用小例子,您可以使用.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

答案 1 :(得分:14)

试试这个:从SQL返回的数据必须转换为Dict。 或者"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

答案 2 :(得分:9)

  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_eval 将它们转换回dict类型。
  3. 使用pd.json_normalize来转换dicts,其中keys作为标题,values作为行。
    • 具有用于处理嵌套record_path的其他参数(例如metadicts)。
  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)

答案 3 :(得分:8)

Merlin的答案更好,更轻松,但我们不需要lambda功能。可以通过以下两种方式之一安全地忽略对字典的评估:

方式1:两步

hyperledger/fabric-ca:latest

方式2:以上两个步骤可以一次合并:

# 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

答案 4 :(得分:7)

您可以将joinpop + tolist一起使用。其性能与concat + drop的{​​{1}}相当,但有些人可能会发现此语法更简洁:

tolist

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

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

答案 5 :(得分:5)

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

import pandas as pd
from pandas.io.json import json_normalize

df2 = json_normalize(df['Pollutant Levels'])

这避免了昂贵的应用功能...

答案 6 :(得分:5)

<块引用>

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

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

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

enter image description here

Benchmarking code 供参考。

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

此外,pd.DataFrame 会破坏性地改变输入 DataFrame,使得在假设输入不会在测试运行中发生变化的基准代码中运行变得更加困难。


对其他解决方案的批评

  • pop 对于大 N 来说非常慢,因为 Pandas 为每一行构造 Series 对象,然后继续从它们构造一个 DataFrame。对于较大的 N,性能下降到几分钟或几小时的数量级。

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

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

答案 7 :(得分:2)

我强烈建议该方法提取“污染物”列:

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

快得多

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

当df很大时。

答案 8 :(得分:1)

以下是一种解决方案:

>> 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 :(得分:1)

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

..本来可以正确地解析字典(将每个字典键放入单独的df列中,并将键值放入df行中),所以首先不会将字典压入单个列中。

答案 10 :(得分:0)

我已将方法中的这些步骤串联起来,您只需要传递数据框和包含扩展字典的列即可:

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

答案 11 :(得分:0)

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

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

>>> df = pd.concat([df]*100000).reset_index(drop=True)
>>> df = pd.concat([df]*20).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())) 是最快的

答案 12 :(得分:-1)

在一行中:

df = pd.concat([df['a'], df.b.apply(pd.Series)], axis=1)`