如何高效地扩展/压缩pandas数据框

4
我有一个数据集,其中一个列的每个元素都是一个列表。我想将其展开,使得每个列表元素都有自己的一行。
我已经使用iterrows,dict和append解决了这个问题(见下文),但是在我的真实DF中速度太慢了。是否有一种方法可以加快速度?
如果有必要的话,我可以考虑用另一种格式(也许是分层的df?)替换每个元素的列表列。
编辑: 我有很多列,有些可能会在未来改变。我唯一确定的是我有fields列。这就是为什么我在我的解决方案中使用了dict。
为了玩耍而创建一个df的最小示例:
import StringIO
df = pd.read_csv(StringIO.StringIO("""
id|name|fields
1|abc|[qq,ww,rr]
2|efg|[zz,xx,rr]
"""), sep='|')
df.fields = df.fields.apply(lambda s: s[1:-1].split(','))
print df

生成的数据框:

   id name        fields
0   1  abc  [qq, ww, rr]
1   2  efg  [zz, xx, rr]

我的(较慢)解决方案:

new_df = pd.DataFrame(index=[], columns=df.columns)

for _, i in df.iterrows():
    flattened_d = [dict(i.to_dict(), fields=c) for c in i.fields]
    new_df = new_df.append(flattened_d )

结果为

    id name fields
0  1.0  abc     qq
1  1.0  abc     ww
2  1.0  abc     rr
0  2.0  efg     zz
1  2.0  efg     xx
2  2.0  efg     rr
3个回答

4

你可以使用numpy来提高性能:

这两种解决方案都主要使用了numpy.repeat

from  itertools import chain

vals = df.fields.str.len()
df1 = pd.DataFrame({
        "id": np.repeat(df.id.values,vals),
        "name": np.repeat(df.name.values, vals),
        "fields": list(chain.from_iterable(df.fields))})
df1 = df1.reindex_axis(df.columns, axis=1)
print (df1)
   id name fields
0   1  abc     qq
1   1  abc     ww
2   1  abc     rr
3   2  efg     zz
4   2  efg     xx
5   2  efg     rr

另一种解决方案: df[['id','name']].values 将列转换为 numpy array 并通过 numpy.repeat 复制它们,然后通过 numpy.hstack 将值堆叠在 lists 中,并通过 numpy.column_stack 添加它。请注意,保留了 HTML 标签。
df1 = pd.DataFrame(np.column_stack((df[['id','name']].values.
                   repeat(list(map(len,df.fields)),axis=0),np.hstack(df.fields))),
                   columns=df.columns)

print (df1)
  id name fields
0  1  abc     qq
1  1  abc     ww
2  1  abc     rr
3  2  efg     zz
4  2  efg     xx
5  2  efg     rr

更通用的解决方案是过滤掉fields列,然后将其添加到DataFrame构造器中,因为它始终是最后一列:
cols = df.columns[df.columns != 'fields'].tolist()
print (cols)
['id', 'name']

df1 = pd.DataFrame(np.column_stack((df[cols].values.
                   repeat(list(map(len,df.fields)),axis=0),np.hstack(df.fields))), 
                   columns=cols + ['fields'])

print (df1)
  id name fields
0  1  abc     qq
1  1  abc     ww
2  1  abc     rr
3  2  efg     zz
4  2  efg     xx
5  2  efg     rr

谢谢。我有很多列,其中一些可能会在未来更改。我唯一确定的是我有“fields”列。是否有一种重构您的解决方案的方法,以便我不必手动输入“id”、“name”?这就是为什么在我的解决方案中我使用了dict()的原因。 - Yuval Atzmon
是的,我认为第二种解决方案更好。给我一分钟。 - jezrael
它运行得很快。您能在正文中解释构造函数的输入吗? - Yuval Atzmon
请注意,这个列表是多余的。map(len, df.fields)已经返回了一个列表。 - Yuval Atzmon
1
抱歉,这是Python 3所必需的,在Python 2中可以省略它。 - jezrael

2
如果你的CSV文件有成千上万行,那么使用下面的using_string_methods可能比使用using_iterrowsusing_repeat更快:

使用

csv = 'id|name|fields'+("""
1|abc|[qq,ww,rr]
2|efg|[zz,xx,rr]"""*10000)

In [210]: %timeit using_string_methods(csv)
10 loops, best of 3: 100 ms per loop

In [211]: %timeit using_itertuples(csv)
10 loops, best of 3: 119 ms per loop

In [212]: %timeit using_repeat(csv)
10 loops, best of 3: 126 ms per loop

In [213]: %timeit using_iterrows(csv)
1 loop, best of 3: 1min 7s per loop

因此,对于一份有10000行的CSV文件,using_string_methodsusing_iterrows快600倍以上,并且比using_repeat稍微快一些。


import pandas as pd
try: from cStringIO import StringIO         # for Python2
except ImportError: from io import StringIO # for Python3

def using_string_methods(csv):
    df = pd.read_csv(StringIO(csv), sep='|', dtype=None)
    other_columns = df.columns.difference(['fields']).tolist()
    fields = (df['fields'].str.extract(r'\[(.*)\]', expand=False)
              .str.split(r',', expand=True))
    df = pd.concat([df.drop('fields', axis=1), fields], axis=1)
    result = (pd.melt(df, id_vars=other_columns, value_name='field')
              .drop('variable', axis=1))
    result = result.dropna(subset=['field'])
    return result


def using_iterrows(csv):
    df = pd.read_csv(StringIO(csv), sep='|')
    df.fields = df.fields.apply(lambda s: s[1:-1].split(','))
    new_df = pd.DataFrame(index=[], columns=df.columns)

    for _, i in df.iterrows():
        flattened_d = [dict(i.to_dict(), fields=c) for c in i.fields]
        new_df = new_df.append(flattened_d )
    return new_df

def using_repeat(csv):
    df = pd.read_csv(StringIO(csv), sep='|')
    df.fields = df.fields.apply(lambda s: s[1:-1].split(','))
    cols = df.columns[df.columns != 'fields'].tolist()
    df1 = pd.DataFrame(np.column_stack(
        (df[cols].values.repeat(list(map(len,df.fields)),axis=0),
         np.hstack(df.fields))), columns=cols + ['fields'])
    return df1

def using_itertuples(csv):
    df = pd.read_csv(StringIO(csv), sep='|')
    df.fields = df.fields.apply(lambda s: s[1:-1].split(','))
    other_columns = df.columns.difference(['fields']).tolist()
    data = []
    for tup in df.itertuples():
        data.extend([[getattr(tup, col) for col in other_columns]+[field] 
                     for field in tup.fields])
    return pd.DataFrame(data, columns=other_columns+['field'])

csv = 'id|name|fields'+("""
1|abc|[qq,ww,rr]
2|efg|[zz,xx,rr]"""*10000)

通常情况下,只有在数据采用本地的NumPy dtype格式(例如int64float64,或字符串)时,才能实现快速的NumPy/Pandas操作。一旦将列表(非本地NumPy dtype格式)放入DataFrame中,结果就不尽如人意 - 您将被迫使用Python速度很慢的循环来处理列表。

因此,为了提高性能,您需要避免将列表放入DataFrame。

using_string_methodsfields数据加载为字符串:

df = pd.read_csv(StringIO(csv), sep='|', dtype=None)

尽量避免使用apply方法(通常与普通的Python循环一样慢):

df.fields = df.fields.apply(lambda s: s[1:-1].split(','))

相反,它使用更快的向量化字符串方法将字符串分成单独的列:

fields = (df['fields'].str.extract(r'\[(.*)\]', expand=False)
          .str.split(r',', expand=True))

一旦您将字段分列,您可以使用pd.melt将DataFrame重塑为所需格式。

pd.melt(df, id_vars=['id', 'name'], value_name='field')

顺便说一下,你可能会想知道,通过轻微修改 using_iterrows 可以和 using_repeat 一样快。我在 using_itertuples 中展示了这些变化。 df.itertuples 往往比 df.iterrows 稍微快一点,但差别不大。绝大部分的速度提升是通过避免在 for 循环中调用 df.append,因为那会导致二次复制。


谢谢。我喜欢你的方法,但在我的情况下,原始数据并不真正来自CSV,所以这不是问题。 - Yuval Atzmon

1
您可以通过将 pandas.Series 应用于 fields,然后将其合并到 idname 中,以此将 fields 列表分成多列:
cols = df.columns[df.columns != 'fields'].tolist() # adapted from @jezrael 
df = df[cols].join(df.fields.apply(pandas.Series))

然后您可以使用set_indexstack来融合产生的新列,然后重置索引:
df = df.set_index(cols).stack().reset_index()

最后,删除reset_index生成的冗余列,并将生成的列重命名为“field”:
df = df.drop(df.columns[-2], axis=1).rename(columns={0: 'field'})

第一条命令失败了。错误是 MergeError: No common columns to perform merge on - Yuval Atzmon
抱歉,我本来想用join的,它是根据索引值工作的。我已经更正了我的答案。 - cmaher
仍然无法正常运行。以下是结果(展开为一行):id name level_2 0 0 1 abc fields [qq,ww,rr] 1 2 efg fields [zz,xx,rr] - Yuval Atzmon
但这并没有解决主要问题,即DF没有扩展。 - Yuval Atzmon
啊,我的答案取决于“fields”元素实际上是列表;当使用StringIO读取数据帧时,每个元素实际上是一个字符串,例如“'[qq,ww,rr]'”。这些“列表”元素是否总是存在需要解析的字符串形式? - cmaher
显示剩余2条评论

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