web-dev-qa-db-fra.com

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 colonne de texte unique, 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/numpy array/Python list) de sorte que la sortie puisse être correctement affectée df.ix[: ,10:16] = df.textcol.map(extract_text_features)

Donc, je pense que je dois revenir à itérer avec df.iterrows(), selon this ?

UPDATE: Itérer avec df.iterrows() est au moins 20 fois plus lent. Je me suis donc rendu et ai divisé la fonction en six appels .map(lambda ...) distincts.

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

163
smci

En vous basant sur la réponse de user1827356, vous pouvez effectuer l’affectation 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

EDIT: S'il vous plaît être conscient de l'énorme consommation de mémoire et de faible vitesse: https://ys-l.github.io/posts/ 2015/08/28/comment-ne-pas-utiliser-pandas-apply / !

87
Zelazny7

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

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

Éditer pour être complet

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

C’est le moyen le plus approprié et le plus simple d’y parvenir dans 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
51

Summary: Si vous voulez seulement créer 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 nouvelles colonnes que vous créez doit être égal au nombre de colonnes que vous utilisez en entrée de la fonction .apply (). Si vous voulez faire autre chose, regardez les autres réponses.

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

Supposons que vous deviez calculer à la fois la moyenne des hauteurs de chaque personne et la somme de ses hauteurs. 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 apply prend en compte les valeurs de chaque ligne du cadre de données sous-défini 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 créerez une nouvelle colonne contenant les listes [mean, sum], que vous voudriez probablement éviter, car cela nécessiterait un autre Lambda/Apply.

Au lieu de cela, vous souhaitez répartir 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)
16
Evan W.

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')
12
Ben

Pour moi cela a fonctionné:

Entrée df

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

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

Sortie:

   col x  square x  cube x
0      1         1       1
1      2         4       8
2      3         9      27
10
Joe

J'ai examiné plusieurs façons de procéder et la méthode présentée ici (renvoyer une série de pandas) ne semble pas être 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, maximum de 3: 8,85 ms par boucle

À mon avis, il est bien plus efficace de prendre une série de n-uplets, puis de le convertir en un DataFrame. Je serais intéressé d'entendre les gens penser cependant s'il y a une erreur dans mon travail.

10
RFox

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 lente également avec les données numériques. Si chaque nouvelle colonne peut être calculée indépendamment des autres, j'attribuerais simplement chacune d'elles sans utiliser apply.

Exemple avec de fausses données de caractères

Créer 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 caractéristiques du texte comme dans la question initiale. 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 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 fonction powers ci-dessus.

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 pour expliquer pourquoi apply n'est généralement pas la solution.

8
Ted Petrou

Ont posté la même réponse dans deux autres questions similaires. Pour ce faire, je préfère résumer les valeurs de retour de la fonction dans une série:

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

Et utilisez ensuite apply comme suit pour créer des colonnes séparées:

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

Il suffit d'utiliser 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")
4
Abhishek

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

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

où la fonction retourne la ligne

def extract_text_features(row):
      row['new_col1'] = value1
      row['new_col2'] = value2
      return row
1
Saket Bajaj