Appliquer la fonction pandas à la colonne pour créer plusieurs nouvelles colonnes?

216

Comment faire cela chez les pandas:

J'ai une fonction extract_text_featuressur une seule colonne de texte, renvoyant plusieurs colonnes de sortie. Plus précisément, la fonction renvoie 6 valeurs.

La fonction fonctionne, mais il ne semble pas y avoir de type de retour approprié (pandas DataFrame / tableau numpy / liste Python) de sorte que la sortie puisse être correctement affectée df.ix[: ,10:16] = df.textcol.map(extract_text_features)

Je pense donc que je dois revenir à l'itération avec df.iterrows(), selon cela ?

MISE À JOUR: Itérer avec df.iterrows()est au moins 20 fois plus lent, j'ai donc abandonné et divisé la fonction en six .map(lambda ...)appels distincts .

MISE À JOUR 2: cette question a été posée autour de la v0.11.0 . Par conséquent, une grande partie des questions et réponses ne sont pas trop pertinentes.

smci
la source
1
Je ne pense pas que vous pouvez faire plusieurs missions comme vous l' avez écrit: df.ix[: ,10:16]. Je pense que vous devrez intégrer mergevos fonctionnalités dans l'ensemble de données.
Zelazny7
1
Pour ceux qui veulent une solution beaucoup plus performante, vérifiez celle-ci ci-dessous qui n'utilise pasapply
Ted Petrou
La plupart des opérations numériques avec des pandas peuvent être vectorisées - cela signifie qu'elles sont beaucoup plus rapides que l'itération conventionnelle. OTOH, certaines opérations (telles que la chaîne et l'expression régulière) sont intrinsèquement difficiles à vectoriser. Dans ce cas, il est important de comprendre comment boucler sur vos données. Pour plus d'informations sur le moment et la manière de boucler vos données, veuillez lire Pour les boucles avec Pandas - Quand dois-je m'en soucier? .
cs95
@coldspeed: le problème principal n'était pas de choisir quelle était la plus performante parmi plusieurs options, il luttait contre la syntaxe des pandas pour que cela fonctionne du tout, autour de la v0.11.0 .
smci
En effet, le commentaire est destiné aux futurs lecteurs qui recherchent des solutions itératives, qui ne connaissent pas mieux ou qui savent ce qu'ils font.
cs95

Réponses:

109

À partir de la réponse de user1827356, vous pouvez effectuer l'affectation en une seule passe en utilisant df.merge:

df.merge(df.textcol.apply(lambda s: pd.Series({'feature1':s+1, 'feature2':s-1})), 
    left_index=True, right_index=True)

    textcol  feature1  feature2
0  0.772692  1.772692 -0.227308
1  0.857210  1.857210 -0.142790
2  0.065639  1.065639 -0.934361
3  0.819160  1.819160 -0.180840
4  0.088212  1.088212 -0.911788

EDIT: Veuillez être conscient de l'énorme consommation de mémoire et de la faible vitesse: https://ys-l.github.io/posts/2015/08/28/how-not-to-use-pandas-apply/ !

Zelazny7
la source
2
juste par curiosité, est-il censé utiliser beaucoup de mémoire en faisant cela? Je fais cela sur une trame de données qui contient des lignes de 2,5 mil, et j'ai failli rencontrer des problèmes de mémoire (c'est aussi beaucoup plus lent que de retourner seulement 1 colonne).
Jeffrey04
2
'df.join (df.textcol.apply (lambda s: pd.Series ({' feature1 ': s + 1,' feature2 ': s-1})))' serait une meilleure option je pense.
Shivam K. Thakkar
@ShivamKThakkar pourquoi pensez-vous que votre suggestion serait une meilleure option? Serait-il plus efficace selon vous ou aurait-il moins de mémoire?
tsando
1
Veuillez tenir compte de la vitesse et de la mémoire requises: ys-l.github.io/posts/2015/08/28/how-not-to-use-pandas-apply
Make42
190

Je le fais habituellement en utilisant zip:

>>> df = pd.DataFrame([[i] for i in range(10)], columns=['num'])
>>> df
    num
0    0
1    1
2    2
3    3
4    4
5    5
6    6
7    7
8    8
9    9

>>> def powers(x):
>>>     return x, x**2, x**3, x**4, x**5, x**6

>>> df['p1'], df['p2'], df['p3'], df['p4'], df['p5'], df['p6'] = \
>>>     zip(*df['num'].map(powers))

>>> df
        num     p1      p2      p3      p4      p5      p6
0       0       0       0       0       0       0       0
1       1       1       1       1       1       1       1
2       2       2       4       8       16      32      64
3       3       3       9       27      81      243     729
4       4       4       16      64      256     1024    4096
5       5       5       25      125     625     3125    15625
6       6       6       36      216     1296    7776    46656
7       7       7       49      343     2401    16807   117649
8       8       8       64      512     4096    32768   262144
9       9       9       81      729     6561    59049   531441
ostrokach
la source
8
Mais que faites-vous si vous avez ajouté 50 colonnes comme celle-ci plutôt que 6?
max
14
@maxtemp = list(zip(*df['num'].map(powers))); for i, c in enumerate(columns): df[c] = temp[c]
ostrokach
8
@ostrokach Je pense que vous vouliez dire for i, c in enumerate(columns): df[c] = temp[i]. Grâce à cela, j'ai vraiment eu le but de enumerate: D
rocarvaj
4
C'est de loin la solution la plus élégante et la plus lisible que j'ai rencontrée pour cela. Sauf si vous rencontrez des problèmes de performances, l'idiome zip(*df['col'].map(function))est probablement la voie à suivre.
François Leblanc
84

C'est ce que j'ai fait dans le passé

df = pd.DataFrame({'textcol' : np.random.rand(5)})

df
    textcol
0  0.626524
1  0.119967
2  0.803650
3  0.100880
4  0.017859

df.textcol.apply(lambda s: pd.Series({'feature1':s+1, 'feature2':s-1}))
   feature1  feature2
0  1.626524 -0.373476
1  1.119967 -0.880033
2  1.803650 -0.196350
3  1.100880 -0.899120
4  1.017859 -0.982141

Modification pour l'exhaustivité

pd.concat([df, df.textcol.apply(lambda s: pd.Series({'feature1':s+1, 'feature2':s-1}))], axis=1)
    textcol feature1  feature2
0  0.626524 1.626524 -0.373476
1  0.119967 1.119967 -0.880033
2  0.803650 1.803650 -0.196350
3  0.100880 1.100880 -0.899120
4  0.017859 1.017859 -0.982141
user1827356
la source
concat () semble plus simple que merge () pour connecter les nouveaux cols à la trame de données d'origine.
cumin
2
bonne réponse, vous n'avez pas besoin d'utiliser un dict ou une fusion si vous spécifiez les colonnes en dehors de l'applicationdf[['col1', 'col2']] = df['col3'].apply(lambda x: pd.Series('val1', 'val2'))
Matt
66

C'est la manière correcte et la plus simple d'accomplir cela pour 95% des cas d'utilisation:

>>> df = pd.DataFrame(zip(*[range(10)]), columns=['num'])
>>> df
    num
0    0
1    1
2    2
3    3
4    4
5    5

>>> def example(x):
...     x['p1'] = x['num']**2
...     x['p2'] = x['num']**3
...     x['p3'] = x['num']**4
...     return x

>>> df = df.apply(example, axis=1)
>>> df
    num  p1  p2  p3
0    0   0   0    0
1    1   1   1    1
2    2   4   8   16
3    3   9  27   81
4    4  16  64  256
Michael David Watson
la source
ne devriez-vous pas écrire: df = df.apply (exemple (df), axis = 1) corrigez-moi si je me trompe, je suis juste un débutant
user299791
1
@ user299791, Non dans ce cas, vous traitez l'exemple comme un objet de première classe, vous passez donc la fonction elle-même. Cette fonction sera appliquée à chaque ligne.
Michael David Watson
salut Michael, ta réponse m'a aidé dans mon problème. Certainement, votre solution est meilleure que la méthode df.assign () des pandas d'origine, car c'est une fois par colonne. En utilisant assign (), si vous voulez créer 2 nouvelles colonnes, vous devez utiliser df1 pour travailler sur df pour obtenir une nouvelle colonne1, puis utiliser df2 pour travailler sur df1 pour créer la deuxième nouvelle colonne ... c'est assez monotone. Mais ta méthode m'a sauvé la vie !!! Merci!!!
commentallez-vous
1
Cela n'exécutera-t-il pas le code d'affectation de colonne une fois par ligne? Ne serait-il pas préférable de renvoyer un pd.Series({k:v})et de sérialiser l'affectation de colonne comme dans la réponse d'Ewan?
Denis de Bernardy
Si cela aide quelqu'un, bien que cette approche soit correcte et aussi la plus simple de toutes les solutions présentées, la mise à jour directe de la ligne comme celle-ci a été étonnamment lente - un ordre de grandeur plus lent que l'application avec les solutions 'expand' + pd.concat
Dmytro Bugayev
31

En 2018, j'utilise apply()avec argumentresult_type='expand'

>>> appiled_df = df.apply(lambda row: fn(row.text), axis='columns', result_type='expand')
>>> df = pd.concat([df, appiled_df], axis='columns')
Ben
la source
6
C'est comme ça que vous le faites, de nos jours!
Make42
1
Cela a fonctionné hors de la boîte en 2020 alors que de nombreuses autres questions n'ont pas fonctionné. De plus, il n'utilise pas pd.Series ce qui est toujours agréable en ce qui concerne les problèmes de performances
Théo Rubenach
1
C'est une bonne solution. Le seul problème est que vous ne pouvez pas choisir le nom des 2 colonnes nouvellement ajoutées. Vous devez plus tard faire df.rename (colonnes = {0: 'col1', 1: 'col2'})
pedram bashiri
2
@pedrambashiri Si la fonction à laquelle vous passez df.applyrenvoie a dict, les colonnes sortiront nommées en fonction des clés.
Seb
25

Utilisez simplement result_type="expand"

df = pd.DataFrame(np.random.randint(0,10,(10,2)), columns=["random", "a"])
df[["sq_a","cube_a"]] = df.apply(lambda x: [x.a**2, x.a**3], axis=1, result_type="expand")
Abhishek
la source
4
Il est utile de souligner que cette option est nouvelle dans la version 0.23 . La question a été posée le 0.11
smci
Bien, c'est simple et ça marche toujours bien. C'est celui que je cherchais. Merci
Isaac Sim
Duplique une réponse antérieure: stackoverflow.com/a/52363890/823470
tar
22

Résumé: si vous ne souhaitez créer que quelques colonnes, utilisezdf[['new_col1','new_col2']] = df[['data1','data2']].apply( function_of_your_choosing(x), axis=1)

Pour cette solution, le nombre de nouvelles colonnes que vous créez doit être égal au nombre de colonnes que vous utilisez comme entrée pour la fonction .apply (). Si vous voulez faire autre chose, jetez un œil aux autres réponses.

Détails Supposons que vous ayez une trame de données à deux colonnes. La première colonne est la taille d'une personne lorsqu'elle a 10 ans; la seconde est la taille de ladite personne à 20 ans.

Supposons que vous deviez calculer à la fois la moyenne des hauteurs de chaque personne et la somme des hauteurs de chaque personne. Cela représente deux valeurs par ligne.

Vous pouvez le faire via la fonction suivante, qui sera bientôt appliquée:

def mean_and_sum(x):
    """
    Calculates the mean and sum of two heights.
    Parameters:
    :x -- the values in the row this function is applied to. Could also work on a list or a tuple.
    """

    sum=x[0]+x[1]
    mean=sum/2
    return [mean,sum]

Vous pouvez utiliser cette fonction comme ceci:

 df[['height_at_age_10','height_at_age_20']].apply(mean_and_sum(x),axis=1)

(Pour être clair: cette fonction d'application prend les valeurs de chaque ligne de la trame de données sous-ensemble et renvoie une liste.)

Cependant, si vous procédez ainsi:

df['Mean_&_Sum'] = df[['height_at_age_10','height_at_age_20']].apply(mean_and_sum(x),axis=1)

vous allez créer 1 nouvelle colonne qui contient les listes [moyenne, somme], que vous voudriez probablement éviter, car cela nécessiterait une autre Lambda / Apply.

Au lieu de cela, vous souhaitez décomposer chaque valeur dans sa propre colonne. Pour ce faire, vous pouvez créer deux colonnes à la fois:

df[['Mean','Sum']] = df[['height_at_age_10','height_at_age_20']]
.apply(mean_and_sum(x),axis=1)
Evan W.
la source
4
Pour les pandas 0.23, vous devrez utiliser la syntaxe:df["mean"], df["sum"] = df[['height_at_age_10','height_at_age_20']] .apply(mean_and_sum(x),axis=1)
SummerEla
Cette fonction peut générer une erreur. La fonction de retour doit être return pd.Series([mean,sum])
Kanishk Mair
22

Pour moi, cela a fonctionné:

Entrée df

df = pd.DataFrame({'col x': [1,2,3]})
   col x
0      1
1      2
2      3

Fonction

def f(x):
    return pd.Series([x*x, x*x*x])

Créez 2 nouvelles colonnes:

df[['square x', 'cube x']] = df['col x'].apply(f)

Production:

   col x  square x  cube x
0      1         1       1
1      2         4       8
2      3         9      27
Joe
la source
13

J'ai cherché plusieurs façons de le faire et la méthode présentée ici (renvoyer une série de pandas) ne semble pas être la plus efficace.

Si nous commençons avec une grande trame de données de données aléatoires:

# Setup a dataframe of random numbers and create a 
df = pd.DataFrame(np.random.randn(10000,3),columns=list('ABC'))
df['D'] = df.apply(lambda r: ':'.join(map(str, (r.A, r.B, r.C))), axis=1)
columns = 'new_a', 'new_b', 'new_c'

L'exemple montré ici:

# Create the dataframe by returning a series
def method_b(v):
    return pd.Series({k: v for k, v in zip(columns, v.split(':'))})
%timeit -n10 -r3 df.D.apply(method_b)

10 boucles, meilleur de 3: 2,77 s par boucle

Une méthode alternative:

# Create a dataframe from a series of tuples
def method_a(v):
    return v.split(':')
%timeit -n10 -r3 pd.DataFrame(df.D.apply(method_a).tolist(), columns=columns)

10 boucles, meilleur de 3: 8,85 ms par boucle

À mon avis, il est beaucoup plus efficace de prendre une série de tuples, puis de la convertir en DataFrame. Je serais intéressé d'entendre les gens penser s'il y a une erreur dans mon travail.

RFox
la source
C'est vraiment utile! J'ai obtenu une accélération de 30x par rapport aux méthodes de retour de fonction.
Pushkar Nimkar
9

La solution acceptée va être extrêmement lente pour beaucoup de données. La solution avec le plus grand nombre de votes positifs est un peu difficile à lire et également lente avec les données numériques. Si chaque nouvelle colonne peut être calculée indépendamment des autres, je voudrais simplement attribuer chacune d'elles directement sans utiliser apply.

Exemple avec de fausses données de caractère

Créez 100 000 chaînes dans un DataFrame

df = pd.DataFrame(np.random.choice(['he jumped', 'she ran', 'they hiked'],
                                   size=100000, replace=True),
                  columns=['words'])
df.head()
        words
0     she ran
1     she ran
2  they hiked
3  they hiked
4  they hiked

Supposons que nous voulions extraire certaines fonctionnalités de texte comme cela a été fait dans la question d'origine. Par exemple, extrayons le premier caractère, comptons l'occurrence de la lettre «e» et mettons en majuscule la phrase.

df['first'] = df['words'].str[0]
df['count_e'] = df['words'].str.count('e')
df['cap'] = df['words'].str.capitalize()
df.head()
        words first  count_e         cap
0     she ran     s        1     She ran
1     she ran     s        1     She ran
2  they hiked     t        2  They hiked
3  they hiked     t        2  They hiked
4  they hiked     t        2  They hiked

Timings

%%timeit
df['first'] = df['words'].str[0]
df['count_e'] = df['words'].str.count('e')
df['cap'] = df['words'].str.capitalize()
127 ms ± 585 µs per loop (mean ± std. dev. of 7 runs, 10 loops each)

def extract_text_features(x):
    return x[0], x.count('e'), x.capitalize()

%timeit df['first'], df['count_e'], df['cap'] = zip(*df['words'].apply(extract_text_features))
101 ms ± 2.96 ms per loop (mean ± std. dev. of 7 runs, 10 loops each)

Étonnamment, vous pouvez obtenir de meilleures performances en parcourant chaque valeur

%%timeit
a,b,c = [], [], []
for s in df['words']:
    a.append(s[0]), b.append(s.count('e')), c.append(s.capitalize())

df['first'] = a
df['count_e'] = b
df['cap'] = c
79.1 ms ± 294 µs per loop (mean ± std. dev. of 7 runs, 10 loops each)

Un autre exemple avec de fausses données numériques

Créez 1 million de nombres aléatoires et testez la powersfonction d'en haut.

df = pd.DataFrame(np.random.rand(1000000), columns=['num'])


def powers(x):
    return x, x**2, x**3, x**4, x**5, x**6

%%timeit
df['p1'], df['p2'], df['p3'], df['p4'], df['p5'], df['p6'] = \
       zip(*df['num'].map(powers))
1.35 s ± 83.6 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)

L'attribution de chaque colonne est 25 fois plus rapide et très lisible:

%%timeit 
df['p1'] = df['num'] ** 1
df['p2'] = df['num'] ** 2
df['p3'] = df['num'] ** 3
df['p4'] = df['num'] ** 4
df['p5'] = df['num'] ** 5
df['p6'] = df['num'] ** 6
51.6 ms ± 1.9 ms per loop (mean ± std. dev. of 7 runs, 10 loops each)

J'ai fait une réponse similaire avec plus de détails ici sur pourquoi ce applyn'est généralement pas la voie à suivre.

Ted Petrou
la source
8

Ont posté la même réponse dans deux autres questions similaires. La façon dont je préfère le faire est de récapituler les valeurs de retour de la fonction dans une série:

def f(x):
    return pd.Series([x**2, x**3])

Et puis utilisez Appliquer comme suit pour créer des colonnes distinctes:

df[['x**2','x**3']] = df.apply(lambda row: f(row['x']), axis=1)
Dmytro Bugayev
la source
1

vous pouvez renvoyer la ligne entière au lieu de valeurs:

df = df.apply(extract_text_features,axis = 1)

où la fonction renvoie la ligne

def extract_text_features(row):
      row['new_col1'] = value1
      row['new_col2'] = value2
      return row
Saket Bajaj
la source
Non, je ne veux pas appliquer extract_text_featuresà chaque colonne du df, seulement à la colonne de textedf.textcol
smci
-2
def myfunc(a):
    return a * a

df['New Column'] = df['oldcolumn'].map(myfunc))

Cela a fonctionné pour moi. Une nouvelle colonne sera créée avec les anciennes données de colonne traitées.

user2902302
la source
2
Cela ne renvoie pas «plusieurs nouvelles colonnes»
pedram bashiri
Cela ne renvoie pas «plusieurs nouvelles colonnes», donc cela ne répond pas à la question. Pourriez-vous s'il vous plaît le supprimer?
smci