comment diviser la colonne de tuples dans pandas dataframe?

88

J'ai un dataframe pandas (ce n'est qu'un petit morceau)

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

Je veux diviser toutes les colonnes contenant des tuples. Par exemple, je souhaite remplacer la colonne LCVpar les colonnes LCV-aet LCV-b.

Comment puis je faire ça?

Donbeo
la source

Réponses:

159

Vous pouvez le faire en faisant pd.DataFrame(col.tolist())sur cette colonne:

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

Remarque: dans une version antérieure, cette réponse recommandait d'utiliser à la df['b'].apply(pd.Series)place de pd.DataFrame(df['b'].tolist(), index=df.index). Cela fonctionne aussi bien (car cela fait de chaque tuple une série, qui est alors vue comme une ligne d'une dataframe), mais est plus lent / utilise plus de mémoire que la tolistversion, comme indiqué par les autres réponses ici (grâce à @denfromufa) .
J'ai mis à jour cette réponse pour m'assurer que la réponse la plus visible a la meilleure solution.

joris
la source
2
existe-t-il un moyen de l'automatiser en raison du grand nombre de colonnes?
Donbeo
Pas directement je pense. Mais vous pouvez facilement écrire une fonction pour cela en utilisant le code ci-dessus (+ en supprimant l'original)
joris
Si vous avez un grand nombre de colonnes, vous pouvez envisager de «ranger» vos données: vita.had.co.nz/papers/tidy-data.html Vous pouvez le faire en utilisant la fonction melt.
Axel
.apply (pd.Series) fonctionne bien, mais pour les grands ensembles de données consomme beaucoup de mémoire et peut provoquer une erreur de mémoire
Yury Wallet
26

Sur des ensembles de données beaucoup plus volumineux, j'ai trouvé que .apply()quelques commandes étaient plus lentes quepd.DataFrame(df['b'].values.tolist(), index=df.index)

Ce problème de performances a été résolu dans GitHub, même si je ne suis pas d'accord avec cette décision:

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

EDIT: basé sur cette réponse: https://stackoverflow.com/a/44196843/2230844

denfromufa
la source
5
pd.DataFrame(df['b'].tolist())sans le .valuessemble fonctionner très bien aussi. (Et merci, votre solution est beaucoup plus rapide que .apply())
Swier
J'étais inquiet de la capture d'index, d'où l'utilisation explicite de .values.
denfromufa
1
solution par @denfromufa fonctionne très rapidement df [['b1', 'b2']] = pd.DataFrame (df ['b']. values.tolist (), index = df.index) et ne cause aucune erreur de mémoire (comme comparé à .apply (pd.Series))
Yury Wallet
17

L' straccesseur qui est disponible pour les pandas.Seriesobjets de dtype == objectest en fait un itérable.

Supposons que 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)

Nous pouvons tester si c'est un itérable

from collections import Iterable

isinstance(df.col.str, Iterable)

True

Nous pouvons ensuite l'assigner comme nous le faisons pour d'autres itérables:

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

x y

La solution la plus simple

Donc, en une seule ligne, nous pouvons affecter les deux colonnes

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

Solution plus rapide

Seulement un peu plus compliqué, nous pouvons utiliser zippour créer un itérable similaire

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 ligne

Signification, ne pas muter l'existant df
Cela fonctionne car assignprend des arguments de mot-clé où les mots-clés sont les noms de colonne nouveaux (ou existants) et les valeurs seront les valeurs de la nouvelle colonne. Vous pouvez utiliser un dictionnaire et le décompresser avec **et le faire agir comme arguments de mot-clé. C'est donc une façon intelligente d'attribuer une nouvelle colonne nommée 'g'qui est le premier élément de l' df.col.stritérable et 'h'qui est le deuxième élément de l' df.col.stritérable.

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

Ma version de l' listapproche

Avec une compréhension de liste moderne et un déballage variable.
Remarque: également en ligne en utilisantjoin

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 version mutante serait

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

Test de temps naïf

DataFrame court

Utilisez celui défini ci-dessus

%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)
Long DataFrame

10 ^ 3 fois plus grand

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
la source
2
Pensez à ajouter TL; DR: df['a'], df['b'] = df.col.str:)
mirekphd
11

Je pense qu'un moyen plus simple est:

>>> 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
la source
1
Cette solution est en effet beaucoup plus simple
ApplePie
@jinhuawang, il semble que ce soit un piratage au-dessus de la strreprésentation d'un pd.Seriesobjet. Pouvez-vous expliquer comment cela fonctionne?!
denfromufa
Je pense que c'est juste comment fonctionne l'objet str? vous pouvez accéder à l'objet tableau avec str
Jinhua Wang
Que faire si certaines des lignes ont des tuples avec un nombre de valeurs différent?
mammykins
Je pense que cela devrait être accepté. C'est plus «pandas-onic» ... si c'est une chose.
Natacha le
8

Je sais que cela date d'il y a quelque temps, mais une mise en garde concernant la deuxième solution:

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

est qu'il supprimera explicitement l'index et ajoutera un index séquentiel par défaut, alors que la réponse acceptée

apply(pd.Series)

ne le sera pas, car le résultat de apply conservera l'index de ligne. Alors que l'ordre est initialement conservé à partir du tableau d'origine, les pandas essaieront de faire correspondre les indices des deux dataframes.

Cela peut être très important si vous essayez de définir les lignes dans un tableau indexé numériquement, et les pandas essaieront automatiquement de faire correspondre l'index du nouveau tableau à l'ancien, et provoqueront une certaine distorsion dans l'ordre.

Une meilleure solution hybride consisterait à définir l'indice de la trame de données d'origine sur le nouveau, c'est-à-dire

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

Ce qui conservera la rapidité d'utilisation de la deuxième méthode tout en garantissant que l'ordre et l'indexation sont conservés sur le résultat.

Mike
la source
J'ai édité ma réponse en fonction de votre observation d'indexation, merci!
denfromufa