使用pandas将包含字典的列拆分/分解为单独的列

371

我有数据存储在一个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?

13个回答

345
为了将字符串转换为实际的字典,您可以执行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

4
如果它是一个字符串,你可以在将其转换为 DataFrame 之前使用 df[col].map(eval) 将其转换为实际的字典。 - joris
5
如果字符串可能来自未经过净化的用户输入,那么.map(eval)是否存在安全风险? - Robert Muil
1
是的,但您可以例如使用literal_eval(https://docs.python.org/3/library/ast.html#ast.literal_eval)来限制解析。 - joris
10
功能完美,但比Lech Birek 2019年贡献的新解决方案慢得多。链接:https://dev59.com/floT5IYBdhLWcg3wvBek#55355928 - drasc
2
使用 apply(pd.Series) 太慢了! - CutePoison
显示剩余4条评论

331

我知道这个问题很旧了,但我在寻找答案时来到了这里。现在有一种更好(更快)的方法,可以使用 json_normalize 来完成:


import pandas as pd

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

这样可以避免昂贵的应用函数...


10
我的测试表明,这确实比已被接受的答案中使用的.apply()方法要快得多。 - victorx
16
如果您有一列字典,pd.DataFrame(df['b'].tolist())normalize 更适用于扁平结构。normalize 在处理深度嵌套的字典时需要进行大量工作,因此速度较慢。如果您有一列字典,请考虑参考 Trenton McKinney 的回答 - cs95
听起来很有趣,但在我的情况下,当值不均匀且某些键可能比其他键少时,这种方法行不通。 - Reza Keshavarz
2
我有一个字典列表的字符串。有可能让它正常工作吗? - raaj
对于我的数据集,这种方法大约快了40倍(0.7秒 vs. 29.2秒)!是一个很好的选择! - Michael Dorner
显示剩余2条评论

98
  • 对于一级的平面dicts列,最快的标准化方法是根据Shijith在这个答案中进行的时间分析:
    • df.join(pd.DataFrame(df.pop('Pollutants').values.tolist()))
    • 它不会解决其他问题,例如包含NaN或嵌套dicts的列表或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_normalizedicts转换为具有keys作为标题和values作为行的列
    • 对于处理嵌套dicts,有其他参数(例如record_pathmeta)可供使用。
  4. 使用pandas.DataFrame.join将原始数据帧df与使用pd.json_normalize创建的列组合起来
  • 请注意,如果该列中有任何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

# check the type of the first value in Pollutants
>>> print(type(df.iloc[0, 1]))
<class 'str'>

# 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

# remove and normalize the column of dictionaries, and join the result to df
df = df.join(pd.json_normalize(df.pop('Pollutants')))

# 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]*20000).reset_index(drop=True)

%%timeit
dfb.join(pd.json_normalize(dfb.Pollutants))
[out]:
46.9 ms ± 201 µs per loop (mean ± std. dev. of 7 runs, 10 loops each)

%%timeit
pd.concat([dfb.drop(columns=['Pollutants']), dfb.Pollutants.apply(pd.Series)], axis=1)
[out]:
7.75 s ± 52.9 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)

27

尝试一下:从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

1
这将我的执行时间从40分钟缩短到了4.5分钟。谢谢! - Private

19

注意:对于深度为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()))是最快的。


17

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

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

当数据框的大小巨大时,这比使用以下方法更快:

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


如果您能解释一下这是如何运作的,以及为什么它更好,那就太好了!对我来说,它总是更快,一旦超过1000行,速度会快200倍左右。 - Sam Mason
2
@SamMason 当你使用 apply 时,整个数据框由 pandas 管理,但当涉及到 values 时,它只与 numpy ndarrays 打交道,这是因为它具有纯 c 实现,从而内在地更快。 - Sagar Kar
最好的方式,非常感谢。 - falselight

17

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

pd.DataFrame(df['val'].tolist())是将包含字典的列拆分为多个列的标准方法。

以下是使用彩色图表证明它的方法:

enter image description here

可供参考的基准测试代码

请注意,我只计时了拆分操作,因为这是回答此问题中最有趣的部分。结果构建的其他方面(例如是否使用popdrop)与讨论无关,可以忽略(但应该注意的是,使用pop避免了后续的drop调用,因此最终的解决方案更有效率,但不管怎样,我们仍然将列转换成列表并将其传递给pd.DataFrame)。

此外,pop会破坏性地改变输入的DataFrame,使其难以在基准测试代码中运行,因为该代码假定输入在测试运行期间不会改变。


对其他解决方案的评价

  • df['val'].apply(pd.Series) 对于大量数据的情况非常慢,因为 Pandas 会为每一行构建 Series 对象,然后再从这些对象中构建 DataFrame。对于更大的数据,性能可能下降到几分钟或几小时的级别。

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

  • 一些答案建议使用 df.pop('val').values.tolist()df.pop('val').to_numpy().tolist()。我认为将序列转成列表和将 Numpy 数组转成列表没有太大的区别。将序列直接转换成列表可以少一步操作,并且并不会更慢,所以我建议避免在中间步骤生成 Numpy 数组。


1
这种比较很有用,但我不确定它是否适用于形状不同的字典。如果所有字典都具有相同的键,则肯定可以正常工作。 - szeitlin

14

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

方法1:分两步

# 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

第二种方法:上述两个步骤可以合并为一步:

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

您可以使用pop+tolistjoin相结合。性能可与drop+tolistconcat相媲美,但有些人可能会觉得这种语法更清晰:

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

5
一行解决方案如下:
>>> 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

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接