将Pandas列中的列表拆分为多个列

371

我有一个只有一列数据的Pandas DataFrame:

import pandas as pd

df = pd.DataFrame({"teams": [["SF", "NYG"] for _ in range(7)]})

       teams
0  [SF, NYG]
1  [SF, NYG]
2  [SF, NYG]
3  [SF, NYG]
4  [SF, NYG]
5  [SF, NYG]
6  [SF, NYG]

如何将此列列表拆分为两列?

期望结果:

  team1 team2
0    SF   NYG
1    SF   NYG
2    SF   NYG
3    SF   NYG
4    SF   NYG
5    SF   NYG
6    SF   NYG
13个回答

519

你可以使用DataFrame构造函数以由to_list创建的lists

import pandas as pd

d1 = {'teams': [['SF', 'NYG'],['SF', 'NYG'],['SF', 'NYG'],
                ['SF', 'NYG'],['SF', 'NYG'],['SF', 'NYG'],['SF', 'NYG']]}
df2 = pd.DataFrame(d1)
print (df2)
       teams
0  [SF, NYG]
1  [SF, NYG]
2  [SF, NYG]
3  [SF, NYG]
4  [SF, NYG]
5  [SF, NYG]
6  [SF, NYG]

df2[['team1','team2']] = pd.DataFrame(df2.teams.tolist(), index= df2.index)
print (df2)
       teams team1 team2
0  [SF, NYG]    SF   NYG
1  [SF, NYG]    SF   NYG
2  [SF, NYG]    SF   NYG
3  [SF, NYG]    SF   NYG
4  [SF, NYG]    SF   NYG
5  [SF, NYG]    SF   NYG
6  [SF, NYG]    SF   NYG

对于一个新的DataFrame

df3 = pd.DataFrame(df2['teams'].to_list(), columns=['team1','team2'])
print (df3)
  team1 team2
0    SF   NYG
1    SF   NYG
2    SF   NYG
3    SF   NYG
4    SF   NYG
5    SF   NYG
6    SF   NYG

apply(pd.Series)的解决方案非常慢:

#7k rows
df2 = pd.concat([df2]*1000).reset_index(drop=True)

In [121]: %timeit df2['teams'].apply(pd.Series)
1.79 s ± 52.5 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)

In [122]: %timeit pd.DataFrame(df2['teams'].to_list(), columns=['team1','team2'])
1.63 ms ± 54.3 µs per loop (mean ± std. dev. of 7 runs, 1000 loops each)

让我们在聊天中继续这个讨论 - AMC
列表方法将重置索引。如果您想保留索引,请在使用 df1.index = d2.index 构建 df2 后复制索引。 - AMA
1
df1.apply(lambda x: x["teams"], result_type="expand",axis=1) 这个怎么样? - Roy Assis
@RoyAssis - 你能测试一下性能吗?我认为它很慢。 - jezrael
1
似乎“tolist”方法只在所有列表长度相同时才有效。 - Paul Coccoli
显示剩余2条评论

117

更简单的解决方案:

pd.DataFrame(df2["teams"].to_list(), columns=['team1', 'team2'])

产量,

  team1 team2
-------------
0    SF   NYG
1    SF   NYG
2    SF   NYG
3    SF   NYG
4    SF   NYG
5    SF   NYG
6    SF   NYG
7    SF   NYG

如果您想要拆分一个包含分隔符字符串的列而不是列表,您可以类似地执行:

pd.DataFrame(df["teams"].str.split('<delim>', expand=True).values,
             columns=['team1', 'team2'])

38
如果每个列表的元素数量不均,会怎样? - ikel
7
如果您想分割一个包含分隔符字符串的列而不是列表,您可以采用类似的方法:df["teams"].str.split('<delim>', expand=True)。这已经返回了一个 DataFrame,所以只需简单地重命名列可能会更简单。 - AMC
谢谢@AMC,这对我很有帮助,但是我的UUID“列表”实际上是一个假装成列表的“str”,所以我首先需要应用lambda函数来去除方括号。 - yeliabsalohcin

75

此解决方案保留了df2 DataFrame 的索引,而不像使用tolist()的任何解决方案:

df3 = df2.teams.apply(pd.Series)
df3.columns = ['team1', 'team2']

这是结果:

  team1 team2
0    SF   NYG
1    SF   NYG
2    SF   NYG
3    SF   NYG
4    SF   NYG
5    SF   NYG
6    SF   NYG

2
.apply(pd.Series) 很容易记忆和输入。不幸的是,正如其他答案所述,对于大量观测数据来说速度非常慢。如果要保留的索引很容易访问,则可以使用 DataFrame 构造函数方法进行保留,只需将 index 参数传递给构造函数即可,就像其他答案中所示。在方法链的中间,一个解决方法是使用赋值表达式(Python 3.8+)存储一个中间 Series 或 DataFrame,然后从那里访问索引。 - Attila the Fun

23

似乎有一种语法更简单、易于记忆的方式,与提议的解决方案相比更容易。我假设在数据框 df 中,该列被称为“meta”:

df2 = pd.DataFrame(df['meta'].str.split().values.tolist())

这个答案很有帮助,可以配合这个技术的视觉演示一起使用:https://www.youtube.com/watch?v=vPKwm1XZjp8 - yeliabsalohcin

16

我希望推荐一种更高效和符合Python风格的方法。

首先将DataFrame定义为原始帖子:

df = pd.DataFrame({"teams": [["SF", "NYG"] for _ in range(7)]})

我的解决方案:

%%timeit
df['team1'], df['team2'] = zip(*list(df['teams'].values))
>> 761 µs ± 8.35 µs per loop

相比之下,最受赞同的解决方案:

%%timeit
df[['team1','team2']] = pd.DataFrame(df.teams.tolist(), index=df.index)
df = pd.DataFrame(df['teams'].to_list(), columns=['team1','team2'])
>> 1.31 ms ± 11.2 µs per loop

我的解决方案节省了40%的时间并且更短。唯一需要记住的是如何使用zip(*list)来展开和重塑一个二维列表。


12

列表推导式

使用列表推导式进行简单实现(我的最爱)

df = pd.DataFrame([pd.Series(x) for x in df.teams])
df.columns = ['team_{}'.format(x+1) for x in df.columns]
输出时机:
CPU times: user 0 ns, sys: 0 ns, total: 0 ns
Wall time: 2.71 ms

输出:

team_1    team_2
0    SF    NYG
1    SF    NYG
2    SF    NYG
3    SF    NYG
4    SF    NYG
5    SF    NYG
6    SF    NYG

11

之前的解决方案对我没有用,因为我的dataframe中有nan观测值。在我的情况下,df2[['team1','team2']] = pd.DataFrame(df2.teams.values.tolist(), index= df2.index)产生以下结果:

object of type 'float' has no len()

我使用列表推导来解决这个问题。以下是可复制的示例:

import pandas as pd
import numpy as np
d1 = {'teams': [['SF', 'NYG'],['SF', 'NYG'],['SF', 'NYG'],
            ['SF', 'NYG'],['SF', 'NYG'],['SF', 'NYG'],['SF', 'NYG']]}
df2 = pd.DataFrame(d1)
df2.loc[2,'teams'] = np.nan
df2.loc[4,'teams'] = np.nan
df2

输出:

        teams
0   [SF, NYG]
1   [SF, NYG]
2   NaN
3   [SF, NYG]
4   NaN
5   [SF, NYG]
6   [SF, NYG]

df2['team1']=np.nan
df2['team2']=np.nan

使用列表推导式解决问题,

for i in [0,1]:
    df2['team{}'.format(str(i+1))]=[k[i] if isinstance(k,list) else k for k in df2['teams']]

df2

产生:
    teams   team1   team2
0   [SF, NYG]   SF  NYG
1   [SF, NYG]   SF  NYG
2   NaN        NaN  NaN
3   [SF, NYG]   SF  NYG
4   NaN        NaN  NaN
5   [SF, NYG]   SF  NYG
6   [SF, NYG]   SF  NYG

7
这里是另一个解决方案,使用 df.transformdf.set_index:
>>> from operator import itemgetter
>>> df['teams'].transform({'item1': itemgetter(0), 'item2': itemgetter(1)})

  team1 team2
0    SF   NYG
1    SF   NYG
2    SF   NYG
3    SF   NYG
4    SF   NYG
5    SF   NYG
6    SF   NYG

当然,这可以概括为:
>>> indices = range(len(df['teams'][0]))

>>> df['teams'].transform({f'team{i+1}': itemgetter(i) for i in indices})

  team1 team2
0    SF   NYG
1    SF   NYG
2    SF   NYG
3    SF   NYG
4    SF   NYG
5    SF   NYG
6    SF   NYG

这种方法的附加好处是可以提取所需的索引:

>>> df
                 teams
0  [SF, NYG, XYZ, ABC]
1  [SF, NYG, XYZ, ABC]
2  [SF, NYG, XYZ, ABC]
3  [SF, NYG, XYZ, ABC]
4  [SF, NYG, XYZ, ABC]
5  [SF, NYG, XYZ, ABC]
6  [SF, NYG, XYZ, ABC]

>>> indices = [0, 2]
>>> df['teams'].transform({f'team{i+1}': itemgetter(i) for i in indices})

  team1 team3
0    SF   XYZ
1    SF   XYZ
2    SF   XYZ
3    SF   XYZ
4    SF   XYZ
5    SF   XYZ
6    SF   XYZ

5

基于之前的回答,这里提供另一种解决方案,可以在更快的运行时间内返回与 df2.teams.apply(pd.Series) 相同的结果:

pd.DataFrame([{x: y for x, y in enumerate(item)} for item in df2['teams'].values.tolist()], index=df2.index)

时间:

In [1]:
import pandas as pd
d1 = {'teams': [['SF', 'NYG'],['SF', 'NYG'],['SF', 'NYG'],
                ['SF', 'NYG'],['SF', 'NYG'],['SF', 'NYG'],['SF', 'NYG']]}
df2 = pd.DataFrame(d1)
df2 = pd.concat([df2]*1000).reset_index(drop=True)

In [2]: %timeit df2['teams'].apply(pd.Series)

8.27 s ± 2.73 s per loop (mean ± std. dev. of 7 runs, 1 loop each)

In [3]: %timeit pd.DataFrame([{x: y for x, y in enumerate(item)} for item in df2['teams'].values.tolist()], index=df2.index)

35.4 ms ± 5.22 ms per loop (mean ± std. dev. of 7 runs, 10 loops each)

1
如果有人来这里寻找现成的函数,我已经写了一个。
  • 它会查找所有包含列表的列并展开它们(如果没有指定columns);
  • 添加的列名为column_name_0column_name_1等;
  • 在最终数据框中保留列的顺序;
  • 如果strict=True,则检查给定列中的列表是否具有相同的大小。
欢迎提出改进和评论。
def unfold_columns(df, columns=[], strict=False):
    assert isinstance(columns, list), "Columns should be a list of column names"
    if len(columns) == 0:
        columns = [
            column for column in df.columns 
            if df.applymap(lambda x: isinstance(x, list)).all()[column]
        ]
    else:
        assert(all([(column in df.columns) for column in columns])), \
            "Not all given columns are found in df"
    columns_order = df.columns
    for column_name in columns:
        if df[column_name].apply(lambda x: isinstance(x, list)).all():
            if strict:
                assert len(set(df[column_name].apply(lambda x: len(x)))) == 1, \
                    f"Lists in df['{column_name}'] are not of equal length"
            unfolded = pd.DataFrame(df[column_name].tolist())
            unfolded.columns = [f'{column_name}_{x}' for x in unfolded.columns]
            columns_order = [
                *columns_order[:list(columns_order).index(column_name)], 
                *unfolded.columns, 
                *columns_order[list(columns_order).index(column_name)+1:]
            ]
            df = df.join(unfolded).drop([column_name], axis=1)
    return df[columns_order]

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