Comment appliquer une fonction à deux colonnes de trame de données Pandas

368

Supposons que j'aie un dfqui a des colonnes de 'ID', 'col_1', 'col_2'. Et je définis une fonction:

f = lambda x, y : my_function_expression.

Maintenant , je veux appliquer la fà dfdeux colonnes » 'col_1', 'col_2'pour calculer élément par élément une nouvelle colonne 'col_3', un peu comme:

df['col_3'] = df[['col_1','col_2']].apply(f)  
# Pandas gives : TypeError: ('<lambda>() takes exactly 2 arguments (1 given)'

Comment faire ?

** Ajouter un échantillon détaillé comme ci-dessous ***

import pandas as pd

df = pd.DataFrame({'ID':['1','2','3'], 'col_1': [0,2,3], 'col_2':[1,4,5]})
mylist = ['a','b','c','d','e','f']

def get_sublist(sta,end):
    return mylist[sta:end+1]

#df['col_3'] = df[['col_1','col_2']].apply(get_sublist,axis=1)
# expect above to output df as below 

  ID  col_1  col_2            col_3
0  1      0      1       ['a', 'b']
1  2      2      4  ['c', 'd', 'e']
2  3      3      5  ['d', 'e', 'f']
gros bug
la source
4
pouvez-vous appliquer f directement aux colonnes: df ['col_3'] = f (df ['col_1'], df ['col_2'])
btel
1
serait utile de savoir ce qui fse passe
tehmisvh
2
non, df ['col_3'] = f (df ['col_1'], df ['col_2']) ne fonctionne pas. Pour f n'accepte que les entrées scalaires, pas les entrées vectorielles. OK, vous pouvez supposer que f = lambda x, y: x + y. (bien sûr, mon vrai f n'est pas si simple, sinon je peux directement df ['col_3'] = df ['col_1'] + df ['col_2'])
bigbug
1
J'ai trouvé un Q & A associé à l'URL ci-dessous, mais mon problème est de calculer une nouvelle colonne par deux colonnes existantes, pas 2 de 1. stackoverflow.com/questions/12356501/…
bigbug
Je pense que ma réponse stackoverflow.com/a/52854800/5447172 répond à cela de la manière la plus pythonique / pandanique, sans contournement ni indexation numérique. Il produit exactement la sortie dont vous avez besoin dans votre exemple.
ajrwhite

Réponses:

291

Voici un exemple d'utilisation applysur le dataframe, avec lequel j'appelle axis = 1.

Notez que la différence est qu'au lieu d'essayer de passer deux valeurs à la fonction f, réécrivez la fonction pour accepter un objet Pandas Series, puis indexez la série pour obtenir les valeurs nécessaires.

In [49]: df
Out[49]: 
          0         1
0  1.000000  0.000000
1 -0.494375  0.570994
2  1.000000  0.000000
3  1.876360 -0.229738
4  1.000000  0.000000

In [50]: def f(x):    
   ....:  return x[0] + x[1]  
   ....:  

In [51]: df.apply(f, axis=1) #passes a Series object, row-wise
Out[51]: 
0    1.000000
1    0.076619
2    1.000000
3    1.646622
4    1.000000

Selon votre cas d'utilisation, il est parfois utile de créer un groupobjet pandas , puis de l'utiliser applysur le groupe.

Un homme
la source
Oui, j'ai essayé d'utiliser Apply, mais je ne trouve pas l'expression de syntaxe valide. Et si chaque ligne de df est unique, utilisez-vous toujours groupby?
bigbug
Ajout d'un exemple à ma réponse, j'espère que cela fait ce que vous cherchez. Sinon, veuillez fournir un exemple de fonction plus spécifique car il sumest résolu avec succès par l'une des méthodes suggérées jusqu'à présent.
Aman
1
Souhaitez-vous coller votre code? Je réécris la fonction: def get_sublist (x): return mylist [x [1]: x [2] + 1] and df ['col_3'] = df.apply (get_sublist, axis = 1) donne 'ValueError: les opérandes pourraient ne pas être diffusé avec des formes (2) (3) '
bigbug
3
@Aman: avec Pandas version 0.14.1 (et peut-être plus tôt), use peut également utiliser une expression lambda. Donnez l' dfobjet que vous avez défini, une autre approche (avec des résultats équivalents) est df.apply(lambda x: x[0] + x[1], axis = 1).
Jubbles
2
@CanCeylan, vous pouvez simplement utiliser les noms de colonne dans la fonction au lieu d'index, vous n'avez donc pas à vous soucier du changement d'ordre ou à obtenir l'index par nom, par exemple, voir stackoverflow.com/questions/13021654/…
Davos
167

Il existe un moyen propre et unique de le faire dans Pandas:

df['col_3'] = df.apply(lambda x: f(x.col_1, x.col_2), axis=1)

Cela permet fd'être une fonction définie par l'utilisateur avec plusieurs valeurs d'entrée, et utilise des noms de colonne (sûrs) plutôt que des indices numériques (dangereux) pour accéder aux colonnes.

Exemple avec des données (basé sur la question d'origine):

import pandas as pd

df = pd.DataFrame({'ID':['1', '2', '3'], 'col_1': [0, 2, 3], 'col_2':[1, 4, 5]})
mylist = ['a', 'b', 'c', 'd', 'e', 'f']

def get_sublist(sta,end):
    return mylist[sta:end+1]

df['col_3'] = df.apply(lambda x: get_sublist(x.col_1, x.col_2), axis=1)

Sortie de print(df):

  ID  col_1  col_2      col_3
0  1      0      1     [a, b]
1  2      2      4  [c, d, e]
2  3      3      5  [d, e, f]

Si les noms de vos colonnes contiennent des espaces ou partagent un nom avec un attribut de trame de données existant, vous pouvez indexer entre crochets:

df['col_3'] = df.apply(lambda x: f(x['col 1'], x['col 2']), axis=1)
ajrwhite
la source
2
Remarque: si vous utilisez axis=1et que votre colonne est appelée, nameelle ne renverra pas réellement vos données de colonne mais le index. Similaire à la mise nameen a groupby(). J'ai résolu cela en renommant ma colonne.
Tom Hemmes
2
ÇA Y EST! Je ne savais tout simplement pas que vous pouviez insérer des fonctions définies par l'utilisateur avec plusieurs paramètres d'entrée dans lambdas. Il est important de noter (je pense) que vous utilisez DF.apply () plutôt que Series.apply (). Cela vous permet d'indexer le df en utilisant les deux colonnes souhaitées et de passer la colonne entière dans la fonction, mais comme vous utilisez apply (), il applique la fonction de manière élémentaire sur toute la colonne. Brillant! Merci d'avoir posté!
Data-phile
1
ENFIN! Tu m'as sauvé la journée!
Mysterio
Je crois que la façon suggérée de le faire est df.loc [:, 'new col'] = df.apply .....
valearner
@valearner Je ne pense pas qu'il y ait de raison de préférer .locdans l'exemple. Il peut être nécessaire de l'adapter à un autre paramètre de problème (par exemple, travailler avec des tranches).
ajrwhite
86

Une solution simple est:

df['col_3'] = df[['col_1','col_2']].apply(lambda x: f(*x), axis=1)
sjm
la source
1
En quoi cette réponse est-elle différente de l'approche de la question: df ['col_3'] = df [['col_1', 'col_2']]. Appliquer (f) juste pour confirmer, l'approche de la question n'a pas fonctionné car le l'affiche n'a pas spécifié cet axe = 1, la valeur par défaut est axis = 0?
Lost1
1
Cette réponse est comparable à la réponse de @ Anman mais un peu plus lisse. Il construit une fonction anonyme qui prend un itérable, et le décompresse avant de le passer à la fonction f.
tiao
39

Une question intéressante! ma réponse comme ci-dessous:

import pandas as pd

def sublst(row):
    return lst[row['J1']:row['J2']]

df = pd.DataFrame({'ID':['1','2','3'], 'J1': [0,2,3], 'J2':[1,4,5]})
print df
lst = ['a','b','c','d','e','f']

df['J3'] = df.apply(sublst,axis=1)
print df

Production:

  ID  J1  J2
0  1   0   1
1  2   2   4
2  3   3   5
  ID  J1  J2      J3
0  1   0   1     [a]
1  2   2   4  [c, d]
2  3   3   5  [d, e]

J'ai changé le nom de la colonne en ID, J1, J2, J3 pour garantir l'ID <J1 <J2 <J3, afin que la colonne s'affiche dans le bon ordre.

Une autre version brève:

import pandas as pd

df = pd.DataFrame({'ID':['1','2','3'], 'J1': [0,2,3], 'J2':[1,4,5]})
print df
lst = ['a','b','c','d','e','f']

df['J3'] = df.apply(lambda row:lst[row['J1']:row['J2']],axis=1)
print df

la source
23

La méthode que vous recherchez est Series.combine. Cependant, il semble que certains types de données doivent être pris en compte. Dans votre exemple, vous appelleriez (comme je l'ai fait lors du test de la réponse) naïvement

df['col_3'] = df.col_1.combine(df.col_2, func=get_sublist)

Cependant, cela jette l'erreur:

ValueError: setting an array element with a sequence.

Ma meilleure supposition est qu'il semble s'attendre à ce que le résultat soit du même type que la série appelant la méthode (df.col_1 ici). Cependant, les travaux suivants:

df['col_3'] = df.col_1.astype(object).combine(df.col_2, func=get_sublist)

df

   ID   col_1   col_2   col_3
0   1   0   1   [a, b]
1   2   2   4   [c, d, e]
2   3   3   5   [d, e, f]
JoeCondron
la source
12

La façon dont vous avez écrit f nécessite deux entrées. Si vous regardez le message d'erreur, il indique que vous ne fournissez pas deux entrées à f, une seule. Le message d'erreur est correct.
La non-concordance est due au fait que df [['col1', 'col2']] renvoie une seule trame de données avec deux colonnes, pas deux colonnes distinctes.

Vous devez modifier votre f pour qu'il prenne une seule entrée, conserver le bloc de données ci-dessus en entrée, puis le décomposer en x, y à l' intérieur du corps de la fonction. Faites ensuite tout ce dont vous avez besoin et renvoyez une seule valeur.

Vous avez besoin de cette signature de fonction car la syntaxe est .apply (f) Donc, f doit prendre la seule chose = dataframe et non deux choses, ce qui est ce que votre f actuel attend.

Puisque vous n'avez pas fourni le corps de f, je ne peux pas vous aider plus en détail - mais cela devrait fournir la sortie sans changer fondamentalement votre code ou utiliser d'autres méthodes plutôt que d'appliquer

Nitine
la source
12

Je vais voter pour np.vectorize. Il vous permet de simplement tirer sur x nombre de colonnes et de ne pas traiter le cadre de données dans la fonction, il est donc idéal pour les fonctions que vous ne contrôlez pas ou faire quelque chose comme l'envoi de 2 colonnes et une constante dans une fonction (par exemple col_1, col_2, 'foo').

import numpy as np
import pandas as pd

df = pd.DataFrame({'ID':['1','2','3'], 'col_1': [0,2,3], 'col_2':[1,4,5]})
mylist = ['a','b','c','d','e','f']

def get_sublist(sta,end):
    return mylist[sta:end+1]

#df['col_3'] = df[['col_1','col_2']].apply(get_sublist,axis=1)
# expect above to output df as below 

df.loc[:,'col_3'] = np.vectorize(get_sublist, otypes=["O"]) (df['col_1'], df['col_2'])


df

ID  col_1   col_2   col_3
0   1   0   1   [a, b]
1   2   2   4   [c, d, e]
2   3   3   5   [d, e, f]
Trae Wallace
la source
1
Cela ne répond pas vraiment à la question des pandas.
mnky9800n
18
La question est "Comment appliquer une fonction à deux colonnes de trame de données Pandas" et non "Comment appliquer une fonction à deux colonnes de trame de données Pandas en utilisant uniquement des méthodes Pandas" et numpy est une dépendance de Pandas, vous devez donc l'installer de toute façon, donc cela semble être une étrange objection.
Trae Wallace
12

Renvoyer une liste applyest une opération dangereuse car l'objet résultant n'est pas garanti d'être une série ou un DataFrame. Et des exceptions pourraient être soulevées dans certains cas. Passons en revue un exemple simple:

df = pd.DataFrame(data=np.random.randint(0, 5, (5,3)),
                  columns=['a', 'b', 'c'])
df
   a  b  c
0  4  0  0
1  2  0  1
2  2  2  2
3  1  2  2
4  3  0  0

Il y a trois résultats possibles en renvoyant une liste de apply

1) Si la longueur de la liste renvoyée n'est pas égale au nombre de colonnes, une série de listes est renvoyée.

df.apply(lambda x: list(range(2)), axis=1)  # returns a Series
0    [0, 1]
1    [0, 1]
2    [0, 1]
3    [0, 1]
4    [0, 1]
dtype: object

2) Lorsque la longueur de la liste retournée est égale au nombre de colonnes, un DataFrame est renvoyé et chaque colonne obtient la valeur correspondante dans la liste.

df.apply(lambda x: list(range(3)), axis=1) # returns a DataFrame
   a  b  c
0  0  1  2
1  0  1  2
2  0  1  2
3  0  1  2
4  0  1  2

3) Si la longueur de la liste retournée est égale au nombre de colonnes pour la première ligne mais a au moins une ligne où la liste a un nombre d'éléments différent du nombre de colonnes, une ValueError est levée.

i = 0
def f(x):
    global i
    if i == 0:
        i += 1
        return list(range(3))
    return list(range(4))

df.apply(f, axis=1) 
ValueError: Shape of passed values is (5, 4), indices imply (5, 3)

Répondre au problème sans appliquer

L'utilisation applyavec axe = 1 est très lente. Il est possible d'obtenir de bien meilleures performances (en particulier sur des ensembles de données plus importants) avec des méthodes itératives de base.

Créer une trame de données plus grande

df1 = df.sample(100000, replace=True).reset_index(drop=True)

Timings

# apply is slow with axis=1
%timeit df1.apply(lambda x: mylist[x['col_1']: x['col_2']+1], axis=1)
2.59 s ± 76.8 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)

# zip - similar to @Thomas
%timeit [mylist[v1:v2+1] for v1, v2 in zip(df1.col_1, df1.col_2)]  
29.5 ms ± 534 µs per loop (mean ± std. dev. of 7 runs, 10 loops each)

@Thomas répond

%timeit list(map(get_sublist, df1['col_1'],df1['col_2']))
34 ms ± 459 µs per loop (mean ± std. dev. of 7 runs, 10 loops each)
Ted Petrou
la source
1
C'est agréable de voir des réponses si détaillées d'où il est possible d'apprendre.
Andrea Moro
7

Je suis sûr que ce n'est pas aussi rapide que les solutions utilisant des opérations Pandas ou Numpy, mais si vous ne voulez pas réécrire votre fonction, vous pouvez utiliser la carte. Utilisation des données d'exemple d'origine -

import pandas as pd

df = pd.DataFrame({'ID':['1','2','3'], 'col_1': [0,2,3], 'col_2':[1,4,5]})
mylist = ['a','b','c','d','e','f']

def get_sublist(sta,end):
    return mylist[sta:end+1]

df['col_3'] = list(map(get_sublist,df['col_1'],df['col_2']))
#In Python 2 don't convert above to list

Nous pourrions passer autant d'arguments que nous le voulions dans la fonction de cette façon. La sortie est ce que nous voulions

ID  col_1  col_2      col_3
0  1      0      1     [a, b]
1  2      2      4  [c, d, e]
2  3      3      5  [d, e, f]
Thomas
la source
1
C'est en fait beaucoup plus rapide ces réponses qui s'utilisent applyavecaxis=1
Ted Petrou
2

Mon exemple à vos questions:

def get_sublist(row, col1, col2):
    return mylist[row[col1]:row[col2]+1]
df.apply(get_sublist, axis=1, col1='col_1', col2='col_2')
Qing Liu
la source
2

Si vous avez un énorme ensemble de données, vous pouvez utiliser une méthode simple mais plus rapide (temps d'exécution) à l'aide de swifter:

import pandas as pd
import swifter

def fnc(m,x,c):
    return m*x+c

df = pd.DataFrame({"m": [1,2,3,4,5,6], "c": [1,1,1,1,1,1], "x":[5,3,6,2,6,1]})
df["y"] = df.swifter.apply(lambda x: fnc(x.m, x.x, x.c), axis=1)
durjoy
la source
1

Je suppose que vous ne voulez pas changer de get_sublistfonction et que vous voulez simplement utiliser la applyméthode DataFrame pour faire le travail. Pour obtenir le résultat souhaité, j'ai écrit deux fonctions d'aide: get_sublist_listet unlist. Comme le nom de la fonction le suggère, obtenez d'abord la liste des sous-listes, puis extrayez la deuxième sous-liste de cette liste. Enfin, nous devons appeler la applyfonction pour appliquer ces deux fonctions au df[['col_1','col_2']]DataFrame par la suite.

import pandas as pd

df = pd.DataFrame({'ID':['1','2','3'], 'col_1': [0,2,3], 'col_2':[1,4,5]})
mylist = ['a','b','c','d','e','f']

def get_sublist(sta,end):
    return mylist[sta:end+1]

def get_sublist_list(cols):
    return [get_sublist(cols[0],cols[1])]

def unlist(list_of_lists):
    return list_of_lists[0]

df['col_3'] = df[['col_1','col_2']].apply(get_sublist_list,axis=1).apply(unlist)

df

Si vous n'utilisez pas []pour entourer la get_sublistfonction, la get_sublist_listfonction renverra une liste simple, elle augmentera ValueError: could not broadcast input array from shape (3) into shape (2), comme l'a mentionné @Ted Petrou.

allenyllee
la source