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

Comment faire cela dans les pandas:

j'ai une fonction extract_text_features sur une seule colonne de texte, retournant plusieurs colonnes de sortie. Plus précisément, la fonction renvoie 6 valeurs.

la fonction fonctionne, cependant il ne semble pas y avoir de type de retour approprié (pandas DataFrame/ numpy array / Python list) tel que la sortie puisse être correctement assignée df.ix[: ,10:16] = df.textcol.map(extract_text_features)

donc je pense que je dois revenir à iterating avec df.iterrows() , comme dans ce ?

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

125
demandé sur Community 2013-04-26 16:38:33

10 réponses

construire à partir de la réponse de l'utilisateur 1827356 ,vous pouvez faire la tâche en un seul passage 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
69
répondu Zelazny7 2013-04-26 20:57:06

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
109
répondu ostrokach 2017-07-26 14:28:24

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

édition complète

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
62
répondu user1827356 2015-01-22 17:53:40

C'est la façon correcte et la plus facile d'accomplir ceci 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
29
répondu Michael David Watson 2017-11-27 23:59:17

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

Pour cette solution, le nombre de colonnes que vous créez doit être égal au nombre de colonnes à utiliser comme entrée pour l' .appliquer() fonction. Si vous voulez faire autre chose, regardez les autres réponses.

détails Disons que vous avez une base de données à deux colonnes. La première colonne est la taille d'une personne quand ils sont 10; le second est dit de la personne de la hauteur quand ils sont 20.

supposons que vous devez calculer à la fois la moyenne des hauteurs de chaque personne et la somme des hauteurs de chaque personne. C'est deux valeurs pour chaque 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 suit:

 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 dans le incorporée dans un jeu partiel dataframe et renvoie une liste.)

cependant, si vous faites ceci:

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

vous allez créer une nouvelle colonne qui contient les listes [mean,sum], que vous voudriez probablement éviter, parce que cela nécessiterait un autre Lambda/Apply.

à la place, vous voulez 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)
12
répondu Evan W. 2018-03-08 22:34:57

j'ai regardé plusieurs façons de faire ceci et la méthode montrée ici (retourner une série de pandas) ne semble pas être la plus efficace.

si nous commençons par une grande base 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 ci - dessous:

# 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, le meilleur de 3: 2.77 s par boucle

une autre méthode:

# 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, le meilleur de 3: 8,85 ms par boucle

à mon avis, il est beaucoup plus efficace de prendre une série de tuples et de la convertir en une base de données. Je serais intéressé d'entendre les gens penser si il ya une erreur dans mon travail.

7
répondu RFox 2017-02-06 16:44:40

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

exemple avec de fausses données de caractère

créer 100 000 chaînes dans une 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

disons que nous voulions extraire quelques traits de texte comme fait dans la question originale. Par exemple, extrayons le premier caractère, comptons l'occurrence de la lettre 'e' et capitalisons 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 boucle à travers 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éer 1 million de nombres aléatoires et tester la fonction powers 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)

attribuer chaque colonne est 25x 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 apply n'est généralement pas la voie à suivre.

6
répondu Ted Petrou 2017-11-03 19:49:30

En 2018, j'utilise apply() avec l'argument result_type='expand'

>>> appiled_df = df.apply(lambda row: fn(row.text), axis='columns', result_type='expand')
>>> df = pd.concat([df, appiled_df], axis='columns')
2
répondu ณัฐชนน นินยวี 2018-09-17 08:45:29

vous pouvez retourner la ligne entière au lieu des 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
1
répondu Saket Bajaj 2018-06-24 19:06:57

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

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

et utiliser ensuite appliquer comme suit pour créer des colonnes séparées:

df[['x**2','x**3']] = df.apply(lambda row: f(row['x']), axis=1)
0
répondu Dmytro Bugayev 2018-09-29 03:17:24