将pandas列中的类json字符串转换为DataFrame

3

我从一个 API 中获取到以下的 DataFrame:

df = pd.DataFrame({'keys': {0: "[{'contract': 'G'}, {'contract_type': 'C'}, {'strike': '560'}, {'strip': '10/1/2022'}]",
                            1: "[{'contract': 'G'}, {'contract_type': 'P'}, {'strike': '585'}, {'strip': '10/1/2022'}]",
                            2: "[{'contract': 'G'}, {'contract_type': 'C'}, {'strike': '580'}, {'strip': '10/1/2022'}]",
                            3: "[{'contract': 'G'}, {'contract_type': 'C'}, {'strike': '545'}, {'strip': '10/1/2022'}]",
                            4: "[{'contract': 'G'}, {'contract_type': 'P'}, {'strike': '555'}, {'strip': '10/1/2022'}]"},
                   'value': {0: 353.3, 1: 25.8, 2: 336.65, 3: 366.05, 4: 20.8}})

>>> df
                                                keys   value
0  [{'contract': 'G'}, {'contract_type': 'C'}, {'...  353.30
1  [{'contract': 'G'}, {'contract_type': 'P'}, {'...   25.80
2  [{'contract': 'G'}, {'contract_type': 'C'}, {'...  336.65
3  [{'contract': 'G'}, {'contract_type': 'C'}, {'...  366.05
4  [{'contract': 'G'}, {'contract_type': 'P'}, {'...   20.80

“keys”列的每一行都是一个字符串(不是JSON,因为值用单引号而不是双引号括起来)。例如:

>>> df.at[0, keys]
"[{'contract': 'G'}, {'contract_type': 'C'}, {'strike': '560'}, {'strip': '10/1/2022'}]"

我想将“keys”列转换为DataFrame,并将其附加到df作为新列。

我目前正在执行以下操作:

  1. 用双引号替换单引号并传递给json.loads,以读取具有以下结构的字典列表:
[{'contract': 'G'}, {'contract_type': 'C'}, {'strike': '560'}, {'strip': '10/1/2022'}]

将字典合并为一个字典,使用字典推导式:
{'contract': 'G', 'contract_type': 'C', 'strike': '560', 'strip': '10/1/2022'}
  1. 对每一行应用此操作,并在结果上调用pd.DataFrame构造函数。
  2. 与原始的df进行join操作。

我的代码可以写成一行:

>>> df.drop("keys", axis=1).join(pd.DataFrame(df["keys"].apply(lambda x: {k: v for d in json.loads(x.replace("'","\"")) for k, v in d.items()}).tolist()))

    value contract contract_type strike      strip
0  353.30        G             C    560  10/1/2022
1   25.80        G             P    585  10/1/2022
2  336.65        G             C    580  10/1/2022
3  366.05        G             C    545  10/1/2022
4   20.80        G             P    555  10/1/2022

我在想是否有更好的方法来做这件事。


1
我正在看这篇文章,想着“哇,这是一个写得很好的pandas问题。”然后我向下滚动了一下... ;) - user17242583
1
@richardec - 我很感激 :D - not_speshal
2个回答

2
您可以使用内置函数ast.literal_eval将字典字符串转换为实际的字典格式,然后再使用pd.json_normalize函数,并将参数record_path=[[]]传入,以将对象转换成表格格式:
import ast
new_df = pd.json_normalize(df['keys'].apply(ast.literal_eval), record_path=[[]]).apply(lambda col: col.dropna().tolist())

输出:

>>> new_df
  contract contract_type strike      strip
0        G             C    560  10/1/2022
1        G             P    585  10/1/2022
2        G             C    580  10/1/2022
3        G             C    545  10/1/2022
4        G             P    555  10/1/2022

另一种解决方案是使用字符串替换将分离的字典合并为一个:
import ast
new_df = pd.DataFrame(df['keys'].str.replace("'}, {'", "', '", regex=True).apply(ast.literal_eval).str[0].tolist())

输出:


另一种选择是使用内置的functools.reduce函数:

import ast
new_df = pd.DataFrame(df['keys'].apply(ast.literal_eval).apply(lambda row: functools.reduce(lambda x, y: x | y, row)).tolist())

1
这段代码对我的示例数据绝对有效,但是如果任何一行缺少一个键,则使用dropna可能会很危险。 - not_speshal
@not_speshal 现在检查答案。我添加了更多的解决方案;) - user17242583
1
我喜欢functools的解决方案。但是,这只是稍微快了一点,可能是因为有双重的apply。仍然给我一个赞。谢谢! :) - not_speshal
那么,@not_speshal,你是在寻找一个更快的解决方案还是一个更干净的解决方案?如果两者都是,你更看重哪一个? - user17242583
每天都要优先考虑性能!但是这篇文章中的解决方案已经足够好了。 - not_speshal

2
你可以使用 ast.literal_evalChainMap 集合 将字典列表合并为一个字典。
from collections import ChainMap

df['keys'] = df['keys'].apply(ast.literal_eval).apply(lambda x: dict(ChainMap(*x)))

print(df)
                                               keys   value
0  {'strip': '10/1/2022', 'strike': '560', 'contr...  353.30
1  {'strip': '10/1/2022', 'strike': '585', 'contr...   25.80
2  {'strip': '10/1/2022', 'strike': '580', 'contr...  336.65
3  {'strip': '10/1/2022', 'strike': '545', 'contr...  366.05
4  {'strip': '10/1/2022', 'strike': '555', 'contr...   20.80

然后使用.apply(pd.Series)将字典列拆分为单独的列,并使用concat将其与数据框的其余部分组合。
df_ = pd.concat([df['keys'].apply(pd.Series), df['value']], axis=1)

print(df_)
       strip strike contract_type contract   value
0  10/1/2022    560             C        G  353.30
1  10/1/2022    585             P        G   25.80
2  10/1/2022    580             C        G  336.65
3  10/1/2022    545             C        G  366.05
4  10/1/2022    555             P        G   20.80

谢谢!这已经比我在一个比例比示例数据框大得多的数据框上拥有的速度稍微快一些了。 - not_speshal
1
@not_speshal 这里的问题在于使用了3个 apply,将前两个合并为一个 apply,然后用 pd.DataFrame 替换最后一个会更快,例如 pd.DataFrame(df['keys'].apply(lambda x: dict(ChainMap(*eval(x)))).tolist())。我比这个答案的原始想法快了3倍。然后当然需要进行连接(concat)。 - Ben.T
2
请勿使用 eval。它非常不安全。请改用 ast.literal_eval - user17242583

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