Manera eficiente de desanidar (explotar) múltiples columnas de lista en un DataFrame de pandas

Resuelto Moh asked hace 7 años • 7 respuestas

Estoy leyendo varios objetos JSON en un DataFrame. El problema es que algunas de las columnas son listas. Además, los datos son muy grandes y por eso no puedo utilizar las soluciones disponibles en Internet. Son muy lentos y consumen poca memoria.

Así es como se ven mis datos:

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]

Y esta es la forma de mis datos: (441079, 12)

Mi resultado deseado es:

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

EDITAR: Después de marcarlo como duplicado, me gustaría enfatizar el hecho de que en esta pregunta estaba buscando un método eficiente para explotar varias columnas. Por lo tanto, la respuesta aprobada puede explotar de manera eficiente un número arbitrario de columnas en conjuntos de datos muy grandes. Algo que las respuestas a la otra pregunta no lograron (y esa fue la razón por la que hice esta pregunta después de probar esas soluciones).

Moh avatar Aug 24 '17 01:08 Moh
Aceptado

pandas >= 1.3

En versiones más recientes, pandas le permite explotar varias columnas a la vez usando DataFrame.explode, siempre que todos los valores tengan listas del mismo tamaño. Por lo tanto, puedes utilizar esto:

df.explode(['B', 'C', 'D', 'E']).reset_index(drop=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

pandas >= 0,25

Para versiones ligeramente anteriores, puede aplicar Series.explodeen cada columna.

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

La idea es establecer como índice todas las columnas que NO deben explotarse primero y luego restablecer el índice.

df.explodeCuriosamente, según mis pruebas, esto es más rápido que llamar . YMMV.


explodeLos métodos son bastante eficaces en general:

df2 = pd.concat([df] * 100, ignore_index=True)

%timeit df2.explode(['B', 'C', 'D', 'E']).reset_index(drop=True)
%timeit df2.set_index(['A']).apply(pd.Series.explode).reset_index() # fastest
%%timeit
(df2.set_index('A')
    .apply(lambda x: x.apply(pd.Series).stack())
    .reset_index()
    .drop('level_1', axis=1))


2.59 ms ± 112 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)
1.27 ms ± 239 µs per loop (mean ± std. dev. of 7 runs, 1000 loops each)
120 ms ± 9.48 ms per loop (mean ± std. dev. of 7 runs, 10 loops each)
cs95 avatar Dec 13 '2019 21:12 cs95

Utilice set_indexuna Ay otra vez las columnas restantes applyy stacklos valores. Todo ello condensado en un solo cartel.

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
Zero avatar Aug 23 '2017 18:08 Zero
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]

Uso:

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
MaxU - stand with Ukraine avatar Aug 23 '2017 18:08 MaxU - stand with Ukraine