web-dev-qa-db-fra.com

Comment appliquer une fonction à deux colonnes d'un cadre de données Pandas

Supposons que j’ai une df qui 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 les f aux df colonnes 'col_1', 'col_2' pour calculer 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 exemple de détail 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']
225
bigbug

Voici un exemple utilisant apply sur la structure de données, que j'appelle avec axis = 1

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

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

En fonction de votre cas d'utilisation, il est parfois utile de créer un objet pandas group, puis d'utiliser apply sur le groupe. 

222
Aman

Une solution simple est:

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

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

Sortie:

  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 que 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
36
user4284784

Il existe un moyen simple et unique de procéder de la sorte dans les pandas:

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

Cela permet à f d'être une fonction définie par l'utilisateur avec plusieurs valeurs d'entrée et utilise des noms de colonne (sécurisés) plutôt que des index numériques (non sécurisés) pour accéder aux colonnes.

Exemple avec des données (basées sur la question initiale):

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]
18
ajrwhite

La méthode que vous recherchez est Series.combine. Cependant, il semble qu’il faille faire attention aux types de données. Dans votre exemple, vous appelez naïvement (comme je l’ai fait lors du test de la réponse) 

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 hypothèse 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 here). 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]
15
JoeCondron

La façon dont vous avez écrit a besoin de deux entrées. Si vous regardez le message d'erreur, il est indiqué que vous ne fournissez pas deux entrées à f, une seule. Le message d'erreur est correct.
La disparité est due au fait que df [['col1', 'col2']] renvoie une seule trame de données avec deux colonnes et non deux colonnes séparées.

Vous devez modifier votre f pour qu'il ne prenne qu'une seule entrée, conservez le bloc de données ci-dessus comme entrée, puis divisez-le en x, y inside le corps de la fonction. Faites ensuite ce dont vous avez besoin et renvoyez une valeur unique.

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 à quoi votre f actuel s'attend. 

Comme vous n'avez pas fourni le corps de f, je ne peux pas vous aider avec plus de détails - mais cela devrait vous permettre de sortir sans changer fondamentalement votre code ni utiliser d'autres méthodes que d'appliquer

11
Nitin

Je vais voter pour np.vectorize. Cela vous permet de créer des x 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 qui consistent à envoyer 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]
9
Trae Wallace

Renvoyer une liste de apply est une opération dangereuse, car l'objet résultant n'est pas nécessairement une série ni un DataFrame. Et des exceptions peuvent être soulevées dans certains cas. Passons en 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 avec le renvoi d'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 alors 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 renvoyée est égale au nombre de colonnes, un DataFrame est renvoyé et chaque colonne reçoit le 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 renvoyée est égale au nombre de colonnes de la première ligne mais comporte au moins une ligne où la liste comporte un nombre d'éléments différent du nombre de colonnes, une valeur ValueError est généré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

Utiliser apply avec axe = 1 est très lent. Il est possible d'obtenir de bien meilleures performances (en particulier sur des jeux de données plus volumineux) avec des méthodes itératives de base.

Créer une base de données plus grande  

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

Les horaires

# 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)
5
Ted Petrou

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

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 ainsi transmettre autant d’arguments que nous le souhaitions à la fonction. 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]
4
Thomas

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')
1
Qing Liu

Je suppose que vous ne voulez pas changer la fonction get_sublist et que vous voulez simplement utiliser la méthode apply de DataFrame pour faire le travail. Pour obtenir le résultat souhaité, j'ai écrit deux fonctions d'aide: get_sublist_list et unlist. Comme le suggère le nom de la fonction, commencez par obtenir la liste des sous-listes, puis extrayez-les de la liste. Enfin, nous devons appeler la fonction apply pour appliquer ces deux fonctions au df[['col_1','col_2']] DataFrame ultérieurement.

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 inclure la fonction get_sublist, la fonction get_sublist_list renverra une liste en clair, elle soulèvera ValueError: could not broadcast input array from shape (3) into shape (2), comme l'a mentionné @Ted Petrou.

0
allenyllee

Si vous avez un énorme ensemble de données, vous pouvez utiliser un moyen simple mais plus rapide (temps d'exécution) d'utiliser 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)
0
durjoy