python - seleccionar - ¿Cómo deshacer(explotar) una columna en un marco de datos de pandas?
seleccionar columnas pandas python (11)
Tengo el siguiente DataFrame donde una de las columnas es un objeto (celda de tipo de lista):
df=pd.DataFrame({''A'':[1,2],''B'':[[1,2],[1,2]]})
df
Out[458]:
A B
0 1 [1, 2]
1 2 [1, 2]
Mi salida esperada es:
A B
0 1 1
1 1 2
3 2 1
4 2 2
¿Qué debo hacer para lograr esto?
Pregunta relacionada
pandas: cuando el contenido de las celdas son listas, cree una fila para cada elemento de la lista
Buena pregunta y respuesta, pero solo maneja una columna con una lista (en mi respuesta, la función de autodefensa funcionará para varias columnas, también la respuesta aceptada es el uso que más tiempo consume, lo que no se recomienda, verifique más información ¿ Cuándo debería hacerlo? ¿Quieres usar pandas apply () en mi código? )
Algo bastante no recomendado (al menos funciona en este caso):
df=pd.concat([df]*2).sort_index()
it=iter(df[''B''].tolist()[0]+df[''B''].tolist()[0])
df[''B'']=df[''B''].apply(lambda x:next(it))
concat
+
sort_index
+
iter
+
apply
+
next
.
Ahora:
print(df)
Es:
A B
0 1 1
0 1 2
1 2 1
1 2 2
Si te importa el índice:
df=df.reset_index(drop=True)
Ahora:
print(df)
Es:
A B
0 1 1
1 1 2
2 2 1
3 2 2
Como usuario tanto de
R
como de
python
, he visto este tipo de pregunta un par de veces.
En R, tienen la función incorporada del paquete
tidyr
llamada
unnest
.
Pero en
Python
(
pandas
) no hay una función integrada para este tipo de pregunta.
Sé
object
type
columnas de
object
siempre hace que los datos sean difíciles de convertir con una función de
pandas
.
Cuando recibí los datos de esta manera, lo primero que me vino a la mente fue "aplanar" o desensamblar las columnas.
Estoy usando las funciones de
pandas
y
python
para este tipo de pregunta.
Si le preocupa la velocidad de las soluciones anteriores, verifique la respuesta del usuario 3483203, ya que está usando el
numpy
y la mayoría de las veces el
numpy
es más rápido.
Recomiendo
Cpython
y
numba
si la velocidad es importante en tu caso.
Método 0 [pandas <= 0.25]
A partir de
pandas 0.25
, si solo necesitas explotar
una
columna, puedes usar la función de
explode
:
df.explode(''B'')
A B
0 1 1
1 1 2
0 2 1
1 2 2
Método 1
apply + pd.Series
(fácil de entender pero en términos de rendimiento no recomendado).
df.set_index(''A'').B.apply(pd.Series).stack().reset_index(level=0).rename(columns={0:''B''})
Out[463]:
A B
0 1 1
1 1 2
0 2 1
1 2 2
Método 2
Usando la
repeat
con el constructor
DataFrame
, vuelva a crear su marco de datos (bueno en el rendimiento, no bueno en varias columnas)
df=pd.DataFrame({''A'':df.A.repeat(df.B.str.len()),''B'':np.concatenate(df.B.values)})
df
Out[465]:
A B
0 1 1
0 1 2
1 2 1
1 2 2
Método 2.1
por ejemplo, además de A, tenemos A.1 ..... An. Si aún usamos el método (
Método 2
) anterior, es difícil para nosotros recrear las columnas una por una.
Solución:
join
o
merge
con el
index
después de ''desobedecer'' las columnas individuales
s=pd.DataFrame({''B'':np.concatenate(df.B.values)},index=df.index.repeat(df.B.str.len()))
s.join(df.drop(''B'',1),how=''left'')
Out[477]:
B A
0 1 1
0 2 1
1 1 2
1 2 2
Si necesita el orden de las columnas exactamente igual que antes, agregue
reindex
al final.
s.join(df.drop(''B'',1),how=''left'').reindex(columns=df.columns)
Método 3
recrear la
list
pd.DataFrame([[x] + [z] for x, y in df.values for z in y],columns=df.columns)
Out[488]:
A B
0 1 1
1 1 2
2 2 1
3 2 2
Si hay más de dos columnas, use
s=pd.DataFrame([[x] + [z] for x, y in zip(df.index,df.B) for z in y])
s.merge(df,left_on=0,right_index=True)
Out[491]:
0 1 A B
0 0 1 1 [1, 2]
1 0 2 1 [1, 2]
2 1 1 2 [1, 2]
3 1 2 2 [1, 2]
Método 4
usando
reindex
o
loc
df.reindex(df.index.repeat(df.B.str.len())).assign(B=np.concatenate(df.B.values))
Out[554]:
A B
0 1 1
0 1 2
1 2 1
1 2 2
#df.loc[df.index.repeat(df.B.str.len())].assign(B=np.concatenate(df.B.values))
Método 5
cuando la lista solo contiene valores únicos:
df=pd.DataFrame({''A'':[1,2],''B'':[[1,2],[3,4]]})
from collections import ChainMap
d = dict(ChainMap(*map(dict.fromkeys, df[''B''], df[''A''])))
pd.DataFrame(list(d.items()),columns=df.columns[::-1])
Out[574]:
B A
0 1 1
1 2 1
2 3 2
3 4 2
Método 6
utilizando
numpy
para alto rendimiento:
newvalues=np.dstack((np.repeat(df.A.values,list(map(len,df.B.values))),np.concatenate(df.B.values)))
pd.DataFrame(data=newvalues[0],columns=df.columns)
A B
0 1 1
1 1 2
2 2 1
3 2 2
Método 7
usando la función de base
itertools
cycle
y
chain
: solución de python puro solo por diversión
from itertools import cycle,chain
l=df.values.tolist()
l1=[list(zip([x[0]], cycle(x[1])) if len([x[0]]) > len(x[1]) else list(zip(cycle([x[0]]), x[1]))) for x in l]
pd.DataFrame(list(chain.from_iterable(l1)),columns=df.columns)
A B
0 1 1
1 1 2
2 2 1
3 2 2
Generalizando a columnas múltiples
df=pd.DataFrame({''A'':[1,2],''B'':[[1,2],[3,4]],''C'':[[1,2],[3,4]]})
df
Out[592]:
A B C
0 1 [1, 2] [1, 2]
1 2 [3, 4] [3, 4]
Función de autodefensa:
def unnesting(df, explode):
idx = df.index.repeat(df[explode[0]].str.len())
df1 = pd.concat([
pd.DataFrame({x: np.concatenate(df[x].values)}) for x in explode], axis=1)
df1.index = idx
return df1.join(df.drop(explode, 1), how=''left'')
unnesting(df,[''B'',''C''])
Out[609]:
B C A
0 1 1 1
0 2 2 1
1 3 3 2
1 4 4 2
Columna sabio Unnesting
Todo el método anterior se refiere a la descomposición
vertical
y la explosión. Si necesita gastar la lista
horizontalmente
,
pd.DataFrame
con el constructor
pd.DataFrame
df.join(pd.DataFrame(df.B.tolist(),index=df.index).add_prefix(''B_''))
Out[33]:
A B C B_0 B_1
0 1 [1, 2] [1, 2] 1 2
1 2 [3, 4] [3, 4] 3 4
Función actualizada
def unnesting(df, explode, axis):
if axis==1:
idx = df.index.repeat(df[explode[0]].str.len())
df1 = pd.concat([
pd.DataFrame({x: np.concatenate(df[x].values)}) for x in explode], axis=1)
df1.index = idx
return df1.join(df.drop(explode, 1), how=''left'')
else :
df1 = pd.concat([
pd.DataFrame(df[x].tolist(), index=df.index).add_prefix(x) for x in explode], axis=1)
return df1.join(df.drop(explode, 1), how=''left'')
Salida de prueba
unnesting(df, [''B'',''C''], axis=0)
Out[36]:
B0 B1 C0 C1 A
0 1 2 1 2 1
1 3 4 3 4 2
Debido a que normalmente la longitud de la lista secundaria es diferente y la combinación / combinación es mucho más costosa computacionalmente. Volví a probar el método para diferentes sublistas de longitud y columnas más normales.
MultiIndex también debería ser una forma más fácil de escribir y tiene casi las mismas interpretaciones que las de numpy.
Sorprendentemente, en mi implementación, la forma de comprensión tiene el mejor rendimiento.
def stack(df):
return df.set_index([''A'', ''C'']).B.apply(pd.Series).stack()
def comprehension(df):
return pd.DataFrame([x + [z] for x, y in zip(df[[''A'', ''C'']].values.tolist(), df.B) for z in y])
def multiindex(df):
return pd.DataFrame(np.concatenate(df.B.values), index=df.set_index([''A'', ''C'']).index.repeat(df.B.str.len()))
def array(df):
return pd.DataFrame(
np.column_stack((
np.repeat(df[[''A'', ''C'']].values, df.B.str.len(), axis=0),
np.concatenate(df.B.values)
))
)
import pandas as pd
import matplotlib.pyplot as plt
import numpy as np
from timeit import timeit
res = pd.DataFrame(
index=[
''stack'',
''comprehension'',
''multiindex'',
''array'',
],
columns=[1000, 2000, 5000, 10000, 20000, 50000],
dtype=float
)
for f in res.index:
for c in res.columns:
df = pd.DataFrame({''A'': list(''abc''), ''C'': list(''def''), ''B'': [[''g'', ''h'', ''i''], [''j'', ''k''], [''l'']]})
df = pd.concat([df] * c)
stmt = ''{}(df)''.format(f)
setp = ''from __main__ import df, {}''.format(f)
res.at[f, c] = timeit(stmt, setp, number=20)
ax = res.div(res.min()).T.plot(loglog=True)
ax.set_xlabel("N")
ax.set_ylabel("time (relative)")
Actuación
Generalicé el problema un poco para ser aplicable a más columnas.
Resumen de lo que hace mi solución:
df = pd.DataFrame({''A'': [1, 2], ''B'': [[1, 2], [1, 2]]})
df.explode(''B'')
Ejemplo completo:
La explosión real se realiza en 3 líneas. El resto son cosméticos (explosión en varias columnas, manejo de cuerdas en lugar de listas en la columna de explosión, ...).
A B
0 1 1
0 1 2
1 2 1
1 2 2
Créditos a la respuesta de WeNYoBen.
La explosión de una columna similar a una lista se ha
simplificado significativamente en los pandas 0.25
con la adición del método
explode()
:
df=pd.DataFrame({''A'':[1,2],''B'':[[1,2],[1,2]]})
out = pd.concat([df.loc[:,''A''],(df.B.apply(pd.Series))], axis=1, sort=False)
out = out.set_index(''A'').stack().droplevel(level=1).reset_index().rename(columns={0:"B"})
A B
0 1 1
1 1 2
2 2 1
3 2 2
Afuera:
# Here''s the answer to the related question in:
# https://.com/q/56708671/11426125
# initial dataframe
df12=pd.DataFrame({''Date'':[''2007-12-03'',''2008-09-07''],''names'':
[[''Peter'',''Alex''],[''Donald'',''Stan'']]})
# convert dataframe to array for indexing list values (names)
a = np.array(df12.values)
# create a new, dataframe with dimensions for unnested
b = np.ndarray(shape = (4,2))
df2 = pd.DataFrame(b, columns = ["Date", "names"], dtype = str)
# implement loops to assign date/name values as required
i = range(len(a[0]))
j = range(len(a[0]))
for x in i:
for y in j:
df2.iat[2*x+y, 0] = a[x][0]
df2.iat[2*x+y, 1] = a[x][1][y]
# set Date column as Index
df2.Date=pd.to_datetime(df2.Date)
df2.index=df2.Date
df2.drop(''Date'',axis=1,inplace =True)
Mis 5 centavos:
df[[''B'', ''B2'']] = pd.DataFrame(df[''B''].values.tolist())
df[[''A'', ''B'']].append(df[[''A'', ''B2'']].rename(columns={''B2'': ''B''}),
ignore_index=True)
y otro 5
df[[''B1'', ''B2'']] = pd.DataFrame([*df[''B'']]) # if values.tolist() is too boring
(pd.wide_to_long(df.drop(''B'', 1), ''B'', ''A'', '''')
.reset_index(level=1, drop=True)
.reset_index())
ambos resultando en el mismo
A B
0 1 1
1 2 1
2 1 2
3 2 2
Una alternativa es aplicar la receta de malla de malla sobre las filas de las columnas para anular:
import numpy as np
import pandas as pd
def unnest(frame, explode):
def mesh(values):
return np.array(np.meshgrid(*values)).T.reshape(-1, len(values))
data = np.vstack(mesh(row) for row in frame[explode].values)
return pd.DataFrame(data=data, columns=explode)
df = pd.DataFrame({''A'': [1, 2], ''B'': [[1, 2], [1, 2]]})
print(unnest(df, [''A'', ''B''])) # base
print()
df = pd.DataFrame({''A'': [1, 2], ''B'': [[1, 2], [3, 4]], ''C'': [[1, 2], [3, 4]]})
print(unnest(df, [''A'', ''B'', ''C''])) # multiple columns
print()
df = pd.DataFrame({''A'': [1, 2, 3], ''B'': [[1, 2], [1, 2, 3], [1]],
''C'': [[1, 2, 3], [1, 2], [1, 2]], ''D'': [''A'', ''B'', ''C'']})
print(unnest(df, [''A'', ''B''])) # uneven length lists
print()
print(unnest(df, [''D'', ''B''])) # different types
print()
Salida
A B
0 1 1
1 1 2
2 2 1
3 2 2
A B C
0 1 1 1
1 1 2 1
2 1 1 2
3 1 2 2
4 2 3 3
5 2 4 3
6 2 3 4
7 2 4 4
A B
0 1 1
1 1 2
2 2 1
3 2 2
4 2 3
5 3 1
D B
0 A 1
1 A 2
2 B 1
3 B 2
4 B 3
5 C 1
Opción 1
Si todas las
numpy
en la otra columna tienen la misma longitud, el valor
numpy
puede ser una opción eficiente aquí:
vals = np.array(df.B.values.tolist())
a = np.repeat(df.A, vals.shape[1])
pd.DataFrame(np.column_stack((a, vals.ravel())), columns=df.columns)
A B
0 1 1
1 1 2
2 2 1
3 2 2
opcion 2
Si las listas secundarias tienen una longitud diferente, necesita un paso adicional:
vals = df.B.values.tolist()
rs = [len(r) for r in vals]
a = np.repeat(df.A, rs)
pd.DataFrame(np.column_stack((a, np.concatenate(vals))), columns=df.columns)
A B
0 1 1
1 1 2
2 2 1
3 2 2
Opcion 3
Intenté generalizar esto para aplanar las columnas
N
y las columnas
M
azulejos. Más adelante trabajaré para hacerlo más eficiente:
df = pd.DataFrame({''A'': [1,2,3], ''B'': [[1,2], [1,2,3], [1]],
''C'': [[1,2,3], [1,2], [1,2]], ''D'': [''A'', ''B'', ''C'']})
A B C D
0 1 [1, 2] [1, 2, 3] A
1 2 [1, 2, 3] [1, 2] B
2 3 [1] [1, 2] C
def unnest(df, tile, explode):
vals = df[explode].sum(1)
rs = [len(r) for r in vals]
a = np.repeat(df[tile].values, rs, axis=0)
b = np.concatenate(vals.values)
d = np.column_stack((a, b))
return pd.DataFrame(d, columns = tile + [''_''.join(explode)])
unnest(df, [''A'', ''D''], [''B'', ''C''])
A D B_C
0 1 A 1
1 1 A 2
2 1 A 1
3 1 A 2
4 1 A 3
5 2 B 1
6 2 B 2
7 2 B 3
8 2 B 1
9 2 B 2
10 3 C 1
11 3 C 1
12 3 C 2
Funciones
def wen1(df):
return df.set_index(''A'').B.apply(pd.Series).stack().reset_index(level=0).rename(columns={0: ''B''})
def wen2(df):
return pd.DataFrame({''A'':df.A.repeat(df.B.str.len()),''B'':np.concatenate(df.B.values)})
def wen3(df):
s = pd.DataFrame({''B'': np.concatenate(df.B.values)}, index=df.index.repeat(df.B.str.len()))
return s.join(df.drop(''B'', 1), how=''left'')
def wen4(df):
return pd.DataFrame([[x] + [z] for x, y in df.values for z in y],columns=df.columns)
def chris1(df):
vals = np.array(df.B.values.tolist())
a = np.repeat(df.A, vals.shape[1])
return pd.DataFrame(np.column_stack((a, vals.ravel())), columns=df.columns)
def chris2(df):
vals = df.B.values.tolist()
rs = [len(r) for r in vals]
a = np.repeat(df.A.values, rs)
return pd.DataFrame(np.column_stack((a, np.concatenate(vals))), columns=df.columns)
Tiempos
import pandas as pd
import matplotlib.pyplot as plt
import numpy as np
from timeit import timeit
res = pd.DataFrame(
index=[''wen1'', ''wen2'', ''wen3'', ''wen4'', ''chris1'', ''chris2''],
columns=[10, 50, 100, 500, 1000, 5000, 10000],
dtype=float
)
for f in res.index:
for c in res.columns:
df = pd.DataFrame({''A'': [1, 2], ''B'': [[1, 2], [1, 2]]})
df = pd.concat([df]*c)
stmt = ''{}(df)''.format(f)
setp = ''from __main__ import df, {}''.format(f)
res.at[f, c] = timeit(stmt, setp, number=50)
ax = res.div(res.min()).T.plot(loglog=True)
ax.set_xlabel("N")
ax.set_ylabel("time (relative)")
Actuación
import pandas as pd
import numpy as np
df=pd.DataFrame( {''A'': [''A1'',''A2'',''A3''],
''B'': [''B1'',''B2'',''B3''],
''C'': [ [''C1.1'',''C1.2''],[''C2.1'',''C2.2''],''C3''],
''columnD'': [ ''D1'',[''D2.1'',''D2.2'', ''D2.3''],[''D3.1'',''D3.2'']],
})
print(''df'',df, sep=''/n'')
def dfListExplode(df, explodeKeys):
if not isinstance(explodeKeys, list):
explodeKeys=[explodeKeys]
# recursive handling of explodeKeys
if len(explodeKeys)==0:
return df
elif len(explodeKeys)==1:
explodeKey=explodeKeys[0]
else:
return dfListExplode( dfListExplode(df, explodeKeys[:1]), explodeKeys[1:])
# perform explosion/unnesting for key: explodeKey
dfPrep=df[explodeKey].apply(lambda x: x if isinstance(x,list) else [x]) #casts all elements to a list
dfIndExpl=pd.DataFrame([[x] + [z] for x, y in zip(dfPrep.index,dfPrep.values) for z in y ], columns=[''explodedIndex'',explodeKey])
dfMerged=dfIndExpl.merge(df.drop(explodeKey, axis=1), left_on=''explodedIndex'', right_index=True)
dfReind=dfMerged.reindex(columns=list(df))
return dfReind
dfExpl=dfListExplode(df,[''C'',''columnD''])
print(''dfExpl'',dfExpl, sep=''/n'')
df=pd.DataFrame({''A'':[1,2],''B'':[[1,2],[1,2]]})
pd.concat([df[''A''], pd.DataFrame(df[''B''].values.tolist())], axis = 1)/
.melt(id_vars = ''A'', value_name = ''B'')/
.dropna()/
.drop(''variable'', axis = 1)
A B
0 1 1
1 2 1
2 1 2
3 2 2
¿Alguna opinión sobre este método que pensé? ¿O es tanto "concat" como "melt" considerado demasiado "caro"?
In[74]: df
Out[74]:
A B C columnD
0 A1 B1 [C1.1, C1.2] D1
1 A2 B2 [C2.1, C2.2] [D2.1, D2.2, D2.3]
2 A3 B3 C3 [D3.1, D3.2]
In[75]: dfListExplode(df,[''C'',''columnD''])
Out[75]:
A B C columnD
0 A1 B1 C1.1 D1
1 A1 B1 C1.2 D1
2 A2 B2 C2.1 D2.1
3 A2 B2 C2.1 D2.2
4 A2 B2 C2.1 D2.3
5 A2 B2 C2.2 D2.1
6 A2 B2 C2.2 D2.2
7 A2 B2 C2.2 D2.3
8 A3 B3 C3 D3.1
9 A3 B3 C3 D3.2
- Puede implementar esto como un forro, si no desea crear un objeto intermedio.