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.
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 / !
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
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
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
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)
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')
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
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.
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
.
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)
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.
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)
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")
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