【问题标题】:Efficient way to unnest (explode) multiple list columns in a pandas DataFrame在 pandas DataFrame 中取消嵌套(分解)多个列表列的有效方法
【发布时间】:2021-12-21 07:36:00
【问题描述】:

我正在将多个 JSON 对象读入一个 DataFrame。问题是某些列是列表。此外,数据非常大,因此我无法使用互联网上可用的解决方案。它们非常慢且内存效率低

我的数据如下所示:

df = pd.DataFrame({'A': ['x1','x2','x3', 'x4'], 'B':[['v1','v2'],['v3','v4'],['v5','v6'],['v7','v8']], 'C':[['c1','c2'],['c3','c4'],['c5','c6'],['c7','c8']],'D':[['d1','d2'],['d3','d4'],['d5','d6'],['d7','d8']], 'E':[['e1','e2'],['e3','e4'],['e5','e6'],['e7','e8']]})
    A       B          C           D           E
0   x1  [v1, v2]    [c1, c2]    [d1, d2]    [e1, e2]
1   x2  [v3, v4]    [c3, c4]    [d3, d4]    [e3, e4]
2   x3  [v5, v6]    [c5, c6]    [d5, d6]    [e5, e6]
3   x4  [v7, v8]    [c7, c8]    [d7, d8]    [e7, e8]

这是我的数据的形状:(441079, 12)

我想要的输出是:

    A       B          C           D           E
0   x1      v1         c1         d1          e1
0   x1      v2         c2         d2          e2
1   x2      v3         c3         d3          e3
1   x2      v4         c4         d4          e4
.....

编辑:在被标记为重复之后,我想强调一个事实,即在这个问题中,我正在寻找一种高效分解多列的方法。因此,批准的答案能够有效地在非常大的数据集上分解任意数量的列。另一个问题的答案未能做到的事情(这就是我在测试这些解决方案后提出这个问题的原因)。

【问题讨论】:

  • 是的,如果你有包含 Python lists 的 object dtype 列,那么一切都会很慢并且内存效率低下。从一开始就不要创建这样的数据框,这个问题可能会更好地解决。
  • @juanpa.arrivillaga 我能否以不同的方式读取 JSON 文件以免造成这种混乱? pd.read_csv 可以选择定义转换器,但我找不到任何类似的 pd.read_json
  • 您可能需要编写一些东西,将反序列化的 json 数据转换成更易于管理的东西。
  • @juanpa.arrivillaga 令我惊讶的是,答案非常高效!

标签: python json pandas dataframe


【解决方案1】:

熊猫 >= 0.25

假设所有列都有相同数量的列表,您可以在每一列上调用Series.explode

df.set_index(['A']).apply(pd.Series.explode).reset_index()

    A   B   C   D   E
0  x1  v1  c1  d1  e1
1  x1  v2  c2  d2  e2
2  x2  v3  c3  d3  e3
3  x2  v4  c4  d4  e4
4  x3  v5  c5  d5  e5
5  x3  v6  c6  d6  e6
6  x4  v7  c7  d7  e7
7  x4  v8  c8  d8  e8

想法是将所有必须首先展开的列设置为索引,然后再重置索引。


它也更快

%timeit df.set_index(['A']).apply(pd.Series.explode).reset_index()
%%timeit
(df.set_index('A')
   .apply(lambda x: x.apply(pd.Series).stack())
   .reset_index()
   .drop('level_1', 1))


2.22 ms ± 98.6 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)
9.14 ms ± 329 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)

【讨论】:

  • 在我的测试中,当不同列中的列表大小不同时,此解决方案不起作用。否则它就像一个魅力!
  • 确实如此,因此是第 1 句中的免责声明。
  • @cs95 如果你有多个非列表列..就像上面的例子一样,如果我再添加一个名为 F 的列并且它有 1,2,3,4 非列表类型跨度>
  • 这个答案很不错。适用于相同长度的列。如果长度不一样,那我觉得反正也不会有什么标准答案;取决于你如何处理它。谢谢@cs95
  • 这给了我ValueError: cannot handle a non-unique multi-index! 但是,@Zero 下面的回答就像一个魅力。
【解决方案2】:
def explode(df, lst_cols, fill_value=''):
    # make sure `lst_cols` is a list
    if lst_cols and not isinstance(lst_cols, list):
        lst_cols = [lst_cols]
    # all columns except `lst_cols`
    idx_cols = df.columns.difference(lst_cols)

    # calculate lengths of lists
    lens = df[lst_cols[0]].str.len()

    if (lens > 0).all():
        # ALL lists in cells aren't empty
        return pd.DataFrame({
            col:np.repeat(df[col].values, df[lst_cols[0]].str.len())
            for col in idx_cols
        }).assign(**{col:np.concatenate(df[col].values) for col in lst_cols}) \
          .loc[:, df.columns]
    else:
        # at least one list in cells is empty
        return pd.DataFrame({
            col:np.repeat(df[col].values, df[lst_cols[0]].str.len())
            for col in idx_cols
        }).assign(**{col:np.concatenate(df[col].values) for col in lst_cols}) \
          .append(df.loc[lens==0, idx_cols]).fillna(fill_value) \
          .loc[:, df.columns]

用法:

In [82]: explode(df, lst_cols=list('BCDE'))
Out[82]:
    A   B   C   D   E
0  x1  v1  c1  d1  e1
1  x1  v2  c2  d2  e2
2  x2  v3  c3  d3  e3
3  x2  v4  c4  d4  e4
4  x3  v5  c5  d5  e5
5  x3  v6  c6  d6  e6
6  x4  v7  c7  d7  e7
7  x4  v8  c8  d8  e8

【讨论】:

【解决方案3】:

A 和其余列applystack 上使用set_index 值。所有这些都浓缩成一个衬里。

In [1253]: (df.set_index('A')
              .apply(lambda x: x.apply(pd.Series).stack())
              .reset_index()
              .drop('level_1', 1))
Out[1253]:
    A   B   C   D   E
0  x1  v1  c1  d1  e1
1  x1  v2  c2  d2  e2
2  x2  v3  c3  d3  e3
3  x2  v4  c4  d4  e4
4  x3  v5  c5  d5  e5
5  x3  v6  c6  d6  e6
6  x4  v7  c7  d7  e7
7  x4  v8  c8  d8  e8

【讨论】:

  • 我真的很喜欢这个答案,因为它很简单。我正在尝试类似的方法,但无法使其工作
  • 这是一个很好的答案!一个小建议是将最后两个命令组合为:.reset_index(level=1, drop=True)
  • 这不起作用@bnaul,你需要一个额外的:.reset_index(level=1, drop=True).reset_index()
  • 这是@cs95 对相同或不同大小列表的回答的概括形式。
【解决方案4】:

基于@cs95 的回答,我们可以在lambda 函数中使用if 子句,而不是将所有其他列设置为index。这样做有以下优点:

  • 保留列顺序
  • 让您可以使用要修改的集合x.name in [...] 轻松指定列,或不修改x.name not in [...]
df.apply(lambda x: x.explode() if x.name in ['B', 'C', 'D', 'E'] else x)

     A   B   C   D   E
0   x1  v1  c1  d1  e1
0   x1  v2  c2  d2  e2
1   x2  v3  c3  d3  e3
1   x2  v4  c4  d4  e4
2   x3  v5  c5  d5  e5
2   x3  v6  c6  d6  e6
3   x4  v7  c7  d7  e7
3   x4  v8  c8  d8  e8

【讨论】:

    【解决方案5】:

    截至pandas 1.3.0

    • DataFrame.explode() 现在支持分解多列。它的 column 参数现在还接受一个 str 或 tuples 列表,以便同时在多个列上爆炸 (GH39240)

    What’s new in 1.3.0 (July 2, 2021)


    所以现在这个操作很简单:

    df.explode(['B', 'C', 'D', 'E'])
    
        A   B   C   D   E
    0  x1  v1  c1  d1  e1
    0  x1  v2  c2  d2  e2
    1  x2  v3  c3  d3  e3
    1  x2  v4  c4  d4  e4
    2  x3  v5  c5  d5  e5
    2  x3  v6  c6  d6  e6
    3  x4  v7  c7  d7  e7
    3  x4  v8  c8  d8  e8
    

    或者如果想要唯一索引:

    df.explode(['B', 'C', 'D', 'E'], ignore_index=True)
    
        A   B   C   D   E
    0  x1  v1  c1  d1  e1
    1  x1  v2  c2  d2  e2
    2  x2  v3  c3  d3  e3
    3  x2  v4  c4  d4  e4
    4  x3  v5  c5  d5  e5
    5  x3  v6  c6  d6  e6
    6  x4  v7  c7  d7  e7
    7  x4  v8  c8  d8  e8
    

    【讨论】:

    • 这很好,但它只在列具有匹配的元素计数时才有效。
    • 正如文档所说“所有指定的列,它们的列表式数据在同一行必须具有匹配的长度”[强调我的]。但是,accepted answerexplode functionapply explode also 要求同一行中的所有类似列表的数据长度相同,否则它们会引发一个或另一个错误。因此,这与此处显示的大多数其他方法一样有限。
    • 同意它具有相同的限制和更少的语法,这就是我喜欢它的原因。只是希望在同一行上使用不同长度的列表更容易。
    【解决方案6】:

    这是我使用“应用”功能的解决方案。主要特点/区别:

    1. 提供选项以指定选定的多列或所有列
    2. 提供选项来指定值以填充“缺失”位置(通过参数 fill_mode = 'external'; 'internal'; 或 'trim',解释会很长,请参阅下面的示例并尝试自己更改选项并检查结果)

    注意:选项“修剪”是根据我的需要开发的,超出了这个问题的范围

    def cell_size_equalize2(row, cols='', fill_mode='internal', fill_value=''):
        jcols = [j for j,v in enumerate(row.index) if v in cols]
        if len(jcols)<1:
            jcols = range(len(row.index))
        Ls = [lenx(x) for x in row.values]
        if not Ls[:-1]==Ls[1:]:
            vals = [v if isinstance(v,list) else [v] for v in row.values]
            if fill_mode=='external':
                vals = [[e] + [fill_value]*(max(Ls)-1) if (not j in jcols) and (isinstance(row.values[j],list))
                        else e + [fill_value]*(max(Ls)-lenx(e))
                        for j,e in enumerate(vals)]
            elif fill_mode == 'internal':
                vals = [[e]+[e]*(max(Ls)-1) if (not j in jcols) and (isinstance(row.values[j],list))
                        else e+[e[-1]]*(max(Ls)-lenx(e)) 
                        for j,e in enumerate(vals)]
            else:
                vals = [e[0:min(Ls)] for e in vals]
            row = pd.Series(vals,index=row.index.tolist())
        return row
    

    例子:

    df=pd.DataFrame({
        'a':[[1],2,3],
        'b':[[4,5,7],[5,4],4],
        'c':[[4,5],5,[6]]
    })
    print(df)
    df1 = df.apply(cell_size_equalize2, cols='', fill_mode='external', fill_value = "OK", axis=1).apply(pd.Series.explode)
    print('\nfill_mode=\'external\', all columns, fill_value = \'OK\'\n', df1)
    df2 = df.apply(cell_size_equalize2, cols=['a', 'b'], fill_mode='external', fill_value = "OK", axis=1).apply(pd.Series.explode)
    print('\nfill_mode=\'external\', cols = [\'a\', \'b\'], fill_value = \'OK\'\n', df2)
    df3 = df.apply(cell_size_equalize2, cols=['a', 'b'], fill_mode='internal', axis=1).apply(pd.Series.explode)
    print('\nfill_mode=\'internal\', cols = [\'a\', \'b\']\n', df3)
    df4 = df.apply(cell_size_equalize2, cols='', fill_mode='trim', axis=1).apply(pd.Series.explode)
    print('\nfill_mode=\'trim\', all columns\n', df4)
    

    输出:

         a          b       c
    0  [1]  [4, 5, 7]  [4, 5]
    1    2     [5, 4]       5
    2    3          4     [6]
    
    fill_mode='external', all columns, fill_value = 'OK'
         a  b   c
    0   1  4   4
    0  OK  5   5
    0  OK  7  OK
    1   2  5   5
    1  OK  4  OK
    2   3  4   6
    
    fill_mode='external', cols = ['a', 'b'], fill_value = 'OK'
         a  b       c
    0   1  4  [4, 5]
    0  OK  5      OK
    0  OK  7      OK
    1   2  5       5
    1  OK  4      OK
    2   3  4       6
    
    fill_mode='internal', cols = ['a', 'b']
        a  b       c
    0  1  4  [4, 5]
    0  1  5  [4, 5]
    0  1  7  [4, 5]
    1  2  5       5
    1  2  4       5
    2  3  4       6
    
    fill_mode='trim', all columns
        a  b  c
    0  1  4  4
    1  2  5  5
    2  3  4  6
    

    【讨论】:

      猜你喜欢
      • 2019-04-12
      相关资源
      最近更新 更多