¿Cómo dividir la columna de tuplas en el marco de datos de pandas?

88

Tengo un marco de datos de pandas (esto es solo una pequeña parte)

>>> d1
   y norm test  y norm train  len(y_train)  len(y_test)  \
0    64.904368    116.151232          1645          549   
1    70.852681    112.639876          1645          549   

                                    SVR RBF  \
0   (35.652207342877873, 22.95533537448393)   
1  (39.563683797747622, 27.382483096332511)   

                                        LCV  \
0  (19.365430594452338, 13.880062435173587)   
1  (19.099614489458364, 14.018867136617146)   

                                   RIDGE CV  \
0  (4.2907610988480362, 12.416745648065584)   
1    (4.18864306788194, 12.980833914392477)   

                                         RF  \
0   (9.9484841581029428, 16.46902345373697)   
1  (10.139848213735391, 16.282141345406522)   

                                           GB  \
0  (0.012816232716538605, 15.950164822266007)   
1  (0.012814519804493328, 15.305745202851712)   

                                             ET DATA  
0  (0.00034337162272515505, 16.284800366214057)  j2m  
1  (0.00024811554516431878, 15.556506191784194)  j2m  
>>> 

Quiero dividir todas las columnas que contienen tuplas. Por ejemplo, quiero reemplazar la columna LCVcon las columnas LCV-ay LCV-b.

¿Cómo puedo hacer eso?

Donbeo
fuente

Respuestas:

159

Puede hacer esto haciendo pd.DataFrame(col.tolist())en esa columna:

In [2]: df = pd.DataFrame({'a':[1,2], 'b':[(1,2), (3,4)]})                                                                                                                      

In [3]: df                                                                                                                                                                      
Out[3]: 
   a       b
0  1  (1, 2)
1  2  (3, 4)

In [4]: df['b'].tolist()                                                                                                                                                        
Out[4]: [(1, 2), (3, 4)]

In [5]: pd.DataFrame(df['b'].tolist(), index=df.index)                                                                                                                                          
Out[5]: 
   0  1
0  1  2
1  3  4

In [6]: df[['b1', 'b2']] = pd.DataFrame(df['b'].tolist(), index=df.index)                                                                                                                       

In [7]: df                                                                                                                                                                      
Out[7]: 
   a       b  b1  b2
0  1  (1, 2)   1   2
1  2  (3, 4)   3   4

Nota: en una versión anterior, se recomienda usar esta respuesta en df['b'].apply(pd.Series)lugar de pd.DataFrame(df['b'].tolist(), index=df.index). Eso también funciona (porque hace de cada tupla una Serie, que luego se ve como una fila de un marco de datos), pero es más lento / usa más memoria que la tolistversión, como se indica en las otras respuestas aquí (gracias a @denfromufa) .
Actualicé esta respuesta para asegurarme de que la respuesta más visible tenga la mejor solución.

joris
fuente
2
¿Hay alguna forma de automatizarlo debido a la gran cantidad de columnas?
Donbeo
No directamente, creo. Pero puede escribir fácilmente una función para él usando el código anterior (+ eliminando el original)
joris
Si tiene una gran cantidad de columnas, es posible que desee considerar "ordenar" sus datos: vita.had.co.nz/papers/tidy-data.html Puede hacerlo usando la función de fusión.
Axel
.apply (pd.Series) funciona bien, pero para grandes conjuntos de datos consume mucha memoria y puede causar un error de memoria
Yury Wallet
26

En conjuntos de datos mucho más grandes, encontré que .apply() son pocos pedidos más lentos quepd.DataFrame(df['b'].values.tolist(), index=df.index)

Este problema de rendimiento se cerró en GitHub, aunque no estoy de acuerdo con esta decisión:

https://github.com/pandas-dev/pandas/issues/11615

EDITAR: basado en esta respuesta: https://stackoverflow.com/a/44196843/2230844

denfromufa
fuente
5
pd.DataFrame(df['b'].tolist())sin el .valuesparece funcionar bien también. (Y gracias, su solución es mucho más rápida que .apply())
Swier
Me preocupaba capturar el índice, por lo tanto, el uso explícito de .values.
denfromufa
1
La solución de @denfromufa funciona súper rápido df [['b1', 'b2']] = pd.DataFrame (df ['b']. values.tolist (), index = df.index) y no causa ningún error de memoria (como en comparación con .apply (pd.Series))
Yury Wallet
17

El strdescriptor de acceso que está disponible para los pandas.Seriesobjetos dedtype == object es en realidad un iterable.

Suponga un pandas.DataFrame df:

df = pd.DataFrame(dict(col=[*zip('abcdefghij', range(10, 101, 10))]))

df

        col
0   (a, 10)
1   (b, 20)
2   (c, 30)
3   (d, 40)
4   (e, 50)
5   (f, 60)
6   (g, 70)
7   (h, 80)
8   (i, 90)
9  (j, 100)

Podemos probar si es iterable

from collections import Iterable

isinstance(df.col.str, Iterable)

True

Luego podemos asignar a partir de él como hacemos con otros iterables:

var0, var1 = 'xy'
print(var0, var1)

x y

La solución más sencilla

Entonces en una línea podemos asignar ambas columnas

df['a'], df['b'] = df.col.str

df

        col  a    b
0   (a, 10)  a   10
1   (b, 20)  b   20
2   (c, 30)  c   30
3   (d, 40)  d   40
4   (e, 50)  e   50
5   (f, 60)  f   60
6   (g, 70)  g   70
7   (h, 80)  h   80
8   (i, 90)  i   90
9  (j, 100)  j  100

Solución más rápida

Solo un poco más complicado, podemos usar zippara crear un iterable similar

df['c'], df['d'] = zip(*df.col)

df

        col  a    b  c    d
0   (a, 10)  a   10  a   10
1   (b, 20)  b   20  b   20
2   (c, 30)  c   30  c   30
3   (d, 40)  d   40  d   40
4   (e, 50)  e   50  e   50
5   (f, 60)  f   60  f   60
6   (g, 70)  g   70  g   70
7   (h, 80)  h   80  h   80
8   (i, 90)  i   90  i   90
9  (j, 100)  j  100  j  100

En línea

Es decir, no mute los existentes. df
Esto funciona porque assigntoma argumentos de palabras clave donde las palabras clave son los nombres de columna nuevos (o existentes) y los valores serán los valores de la nueva columna. Puede usar un diccionario y descomprimirlo **y hacer que actúe como argumentos de palabras clave. Así que esta es una forma inteligente de asignar una nueva columna llamada 'g'que es el primer elemento del df.col.striterable y 'h'que es el segundo elemento del df.col.striterable.

df.assign(**dict(zip('gh', df.col.str)))

        col  g    h
0   (a, 10)  a   10
1   (b, 20)  b   20
2   (c, 30)  c   30
3   (d, 40)  d   40
4   (e, 50)  e   50
5   (f, 60)  f   60
6   (g, 70)  g   70
7   (h, 80)  h   80
8   (i, 90)  i   90
9  (j, 100)  j  100

Mi versión del list enfoque

Con comprensión de listas moderna y desempaquetado de variables.
Nota: también en línea usandojoin

df.join(pd.DataFrame([*df.col], df.index, [*'ef']))

        col  g    h
0   (a, 10)  a   10
1   (b, 20)  b   20
2   (c, 30)  c   30
3   (d, 40)  d   40
4   (e, 50)  e   50
5   (f, 60)  f   60
6   (g, 70)  g   70
7   (h, 80)  h   80
8   (i, 90)  i   90
9  (j, 100)  j  100

La versión mutante sería

df[['e', 'f']] = pd.DataFrame([*df.col], df.index)

Prueba de ingenuo

DataFrame corto

Utilice uno definido anteriormente

%timeit df.assign(**dict(zip('gh', df.col.str)))
%timeit df.assign(**dict(zip('gh', zip(*df.col))))
%timeit df.join(pd.DataFrame([*df.col], df.index, [*'gh']))

1.16 ms ± 21.5 µs per loop (mean ± std. dev. of 7 runs, 1000 loops each)
635 µs ± 18.7 µs per loop (mean ± std. dev. of 7 runs, 1000 loops each)
795 µs ± 42.5 µs per loop (mean ± std. dev. of 7 runs, 1000 loops each)
DataFrame largo

10 ^ 3 veces más grande

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

%timeit df.assign(**dict(zip('gh', df.col.str)))
%timeit df.assign(**dict(zip('gh', zip(*df.col))))
%timeit df.join(pd.DataFrame([*df.col], df.index, [*'gh']))

11.4 ms ± 1.53 ms per loop (mean ± std. dev. of 7 runs, 100 loops each)
2.1 ms ± 41.4 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)
2.33 ms ± 35.1 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)
piRSquared
fuente
2
Considere agregar TL; DR: df['a'], df['b'] = df.col.str:)
mirekphd
11

Creo que una forma más sencilla es:

>>> import pandas as pd
>>> df = pd.DataFrame({'a':[1,2], 'b':[(1,2), (3,4)]}) 
>>> df
   a       b
0  1  (1, 2)
1  2  (3, 4)
>>> df['b_a']=df['b'].str[0]
>>> df['b_b']=df['b'].str[1]
>>> df
   a       b  b_a  b_b
0  1  (1, 2)    1    2
1  2  (3, 4)    3    4
Jinhua Wang
fuente
1
Esta solución es de hecho mucho más simple
ApplePie
@jinhuawang parece que esto es un truco sobre la strrepresentación de un pd.Seriesobjeto. ¿Puedes explicar cómo funciona esto?
denfromufa
Creo que es así como funciona el objeto str. puede acceder al objeto de matriz con str
Jinhua Wang
¿Qué pasa si algunas de las filas tienen tuplas con un número diferente de valores?
mammykins
Creo que este debería ser el aceptado. Es más 'pandas-onic' ... si eso es algo.
Natacha
8

Sé que esto es de hace un tiempo, pero una advertencia de la segunda solución:

pd.DataFrame(df['b'].values.tolist())

es que descartará explícitamente el índice y agregará un índice secuencial predeterminado, mientras que la respuesta aceptada

apply(pd.Series)

no lo hará, ya que el resultado de aplicar conservará el índice de fila. Si bien el orden se retiene inicialmente de la matriz original, los pandas intentarán hacer coincidir los índices de los dos marcos de datos.

Esto puede ser muy importante si está intentando establecer las filas en una matriz indexada numéricamente, y los pandas intentarán hacer coincidir automáticamente el índice de la nueva matriz con el anterior, y causarán cierta distorsión en el orden.

Una mejor solución híbrida sería establecer el índice del marco de datos original en el nuevo, es decir

pd.DataFrame(df['b'].values.tolist(), index=df.index)

Lo que mantendrá la velocidad de usar el segundo método mientras asegura que el orden y la indexación se conservan en el resultado.

Miguel
fuente
Edité mi respuesta según su observación de indexación, ¡gracias!
denfromufa