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

我有数据保存在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?

244204 次浏览

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

试试这个:从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
df = pd.concat([df['a'], df.b.apply(pd.Series)], axis=1)

梅林的答案更好,也超级简单,但我们不需要lambda函数。对dictionary的求值可以通过以下两种方法中的任何一种安全地忽略,如下所示:

方法一:两步

# 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

你可以使用joinpop + tolist。性能与带有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

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

import pandas as pd


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

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

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

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

它比

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

当df的值很大时。

一种解决方案如下:

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

我已经在一个方法中连接了这些步骤,你只需要传递数据帧和包含字典的列来展开:

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

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

.. 将正确地解析字典(将每个字典键放入单独的df列,键值放入df行),因此字典将不会首先被压缩到单个列中。

    根据此回答Shijith执行的时间分析,标准化平坦的一层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列是否具有dictstr类型可能并不清楚。
    • 如果列中的字典是__ABC0类型,它们必须转换回__ABC1类型,使用ast.literal_eval,或json.loads(…)
  3. 使用pd.json_normalize转换dictskeys作为头文件,values作为行文件。
    • 还有一些附加参数(例如:record_path &meta)用于处理嵌套的dicts
  4. 使用pandas.DataFrame.join将原始数据帧df与使用pd.json_normalize创建的列结合起来
    注意,如果列有任何NaN,它们必须用空的dict填充
      <李> df.Pollutants = df.Pollutants.fillna({i: {} for i in df.index})
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)

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

如何用熊猫将一列字典拆分为单独的列?

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

这是用彩色图表证明的。

enter image description here

基准测试代码供参考。

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

此外,pop会破坏性地改变输入数据帧,使得它更难在基准测试代码中运行,因为基准测试代码假设输入在测试运行中没有改变。


对其他解决方案的批评

  • 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数组。