Renvoie plusieurs colonnes de pandas apply ()

103

J'ai un dataframe pandas géants, df_test. Il contient une colonne «taille» qui représente la taille en octets. J'ai calculé Ko, Mo et Go à l'aide du code suivant:

df_test = pd.DataFrame([
    {'dir': '/Users/uname1', 'size': 994933},
    {'dir': '/Users/uname2', 'size': 109338711},
])

df_test['size_kb'] = df_test['size'].astype(int).apply(lambda x: locale.format("%.1f", x / 1024.0, grouping=True) + ' KB')
df_test['size_mb'] = df_test['size'].astype(int).apply(lambda x: locale.format("%.1f", x / 1024.0 ** 2, grouping=True) + ' MB')
df_test['size_gb'] = df_test['size'].astype(int).apply(lambda x: locale.format("%.1f", x / 1024.0 ** 3, grouping=True) + ' GB')

df_test


             dir       size       size_kb   size_mb size_gb
0  /Users/uname1     994933      971.6 KB    0.9 MB  0.0 GB
1  /Users/uname2  109338711  106,776.1 KB  104.3 MB  0.1 GB

[2 rows x 5 columns]

J'ai exécuté cela sur 120000 lignes et cela prend environ 2,97 secondes par colonne * 3 = ~ 9 secondes selon% timeit.

Est-ce que je peux faire cela plus rapidement? Par exemple, puis-je au lieu de renvoyer une colonne à la fois à partir de Apply et de l'exécuter 3 fois, puis-je retourner les trois colonnes en un seul passage pour les réinsérer dans le dataframe d'origine?

Les autres questions que j'ai trouvées veulent toutes prendre plusieurs valeurs et renvoyer une seule valeur . Je veux prendre une seule valeur et renvoyer plusieurs colonnes .

PaulMest
la source

Réponses:

115

C'est une vieille question, mais par souci d'exhaustivité, vous pouvez renvoyer une série à partir de la fonction appliquée qui contient les nouvelles données, évitant ainsi d'avoir à itérer trois fois. Le passage axis=1à la fonction apply applique la fonction sizesà chaque ligne du dataframe, renvoyant une série à ajouter à un nouveau dataframe. Cette série, s, contient les nouvelles valeurs, ainsi que les données d'origine.

def sizes(s):
    s['size_kb'] = locale.format("%.1f", s['size'] / 1024.0, grouping=True) + ' KB'
    s['size_mb'] = locale.format("%.1f", s['size'] / 1024.0 ** 2, grouping=True) + ' MB'
    s['size_gb'] = locale.format("%.1f", s['size'] / 1024.0 ** 3, grouping=True) + ' GB'
    return s

df_test = df_test.append(rows_list)
df_test = df_test.apply(sizes, axis=1)
Nelz11
la source
11
Je suis surpris qu'il ait passé près de 2 ans sans la bonne réponse. Je cherchais autre chose et je suis tombé dessus. J'espère qu'il n'est pas trop tard pour être utile!
Nelz11
10
Que rows_listcontient cette réponse?
David Stansby le
C'est juste une liste de séries pour construire le Dataframe.
Nelz11
1
Si pd.Series a besoin d'un index, vous devez le fournir avec pd.Series(data, index=...). Sinon, vous obtenez des erreurs cryptiques lorsque vous essayez de réattribuer le résultat dans la trame de données parent.
smci
96

Utilisez appliquer et zip 3 fois plus vite que la série.

def sizes(s):    
    return locale.format("%.1f", s / 1024.0, grouping=True) + ' KB', \
        locale.format("%.1f", s / 1024.0 ** 2, grouping=True) + ' MB', \
        locale.format("%.1f", s / 1024.0 ** 3, grouping=True) + ' GB'
df_test['size_kb'],  df_test['size_mb'], df_test['size_gb'] = zip(*df_test['size'].apply(sizes))

Les résultats du test sont:

Separate df.apply(): 

    100 loops, best of 3: 1.43 ms per loop

Return Series: 

    100 loops, best of 3: 2.61 ms per loop

Return tuple:

    1000 loops, best of 3: 819 µs per loop
Jesse
la source
Je suis surpris que cela n'ait pas reçu plus de votes positifs. Merci d'avoir partagé la variante supplémentaire et les données de synchronisation.
gumption
Pourriez-vous s'il vous plaît expliquer comment vous avez renvoyé tuple? Cela semble être l'option la plus rapide
Camilo
Veuillez vous référer à mon exemple de code, c'est la manière tuple.
Jesse
semble aussi le plus rapide et le plus simple. surpris de ne pas pouvoir le trouver moi-même.
Shahir Ansari
59

Certaines des réponses actuelles fonctionnent bien, mais je veux proposer une autre option, peut-être plus «pandifiée». Cela fonctionne pour moi avec les pandas actuels 0.23 ( je ne sais pas si cela fonctionnera dans les versions précédentes):

import pandas as pd

df_test = pd.DataFrame([
  {'dir': '/Users/uname1', 'size': 994933},
  {'dir': '/Users/uname2', 'size': 109338711},
])

def sizes(s):
  a = locale.format("%.1f", s['size'] / 1024.0, grouping=True) + ' KB'
  b = locale.format("%.1f", s['size'] / 1024.0 ** 2, grouping=True) + ' MB'
  c = locale.format("%.1f", s['size'] / 1024.0 ** 3, grouping=True) + ' GB'
  return a, b, c

df_test[['size_kb', 'size_mb', 'size_gb']] = df_test.apply(sizes, axis=1, result_type="expand")

Notez que l'astuce est sur le result_typeparamètre de apply, qui étendra son résultat en un DataFramequi peut être directement assigné aux nouvelles / anciennes colonnes.

Jaumebonet
la source
1
C'est vrai ... désolé ... après quelques vérifications, cela fonctionnera avec 0,22 dans certains cas, mais j'étais dans un environnement virtuel et exécutais en fait 0,23 quand j'ai essayé cela ...: /
jaumebonet
5
C'est la réponse la plus optimale. Merci
AdR
17

Juste une autre manière lisible. Ce code ajoutera trois nouvelles colonnes et ses valeurs, renvoyant des séries sans paramètres d'utilisation dans la fonction apply.

def sizes(s):

    val_kb = locale.format("%.1f", s['size'] / 1024.0, grouping=True) + ' KB'
    val_mb = locale.format("%.1f", s['size'] / 1024.0 ** 2, grouping=True) + ' MB'
    val_gb = locale.format("%.1f", s['size'] / 1024.0 ** 3, grouping=True) + ' GB'
    return pd.Series([val_kb,val_mb,val_gb],index=['size_kb','size_mb','size_gb'])

df[['size_kb','size_mb','size_gb']] = df.apply(lambda x: sizes(x) , axis=1)

Un exemple général tiré de: https://pandas.pydata.org/pandas-docs/stable/generated/pandas.DataFrame.apply.html

df.apply(lambda x: pd.Series([1, 2], index=['foo', 'bar']), axis=1)

#foo  bar
#0    1    2
#1    1    2
#2    1    2
alvaro nortes
la source
9

Des réponses vraiment cool! Merci Jesse et jaumebonet! Juste une observation en ce qui concerne:

  • zip(* ...
  • ... result_type="expand")

Bien que expand soit plus élégant ( pandifié ), zip est au moins ** 2x plus rapide . Sur cet exemple simple ci-dessous, j'ai été 4x plus rapide .

import pandas as pd

dat = [ [i, 10*i] for i in range(1000)]

df = pd.DataFrame(dat, columns = ["a","b"])

def add_and_sub(row):
    add = row["a"] + row["b"]
    sub = row["a"] - row["b"]
    return add, sub

df[["add", "sub"]] = df.apply(add_and_sub, axis=1, result_type="expand")
# versus
df["add"], df["sub"] = zip(*df.apply(add_and_sub, axis=1))
famaral42
la source
8

La performance entre les principales réponses est considérablement variée, et Jesse & famaral42 en ont déjà discuté, mais il vaut la peine de partager une comparaison équitable entre les principales réponses et de développer un détail subtil mais important de la réponse de Jesse: l'argument transmis au fonction, affecte également les performances .

(Python 3.7.4, Pandas 1.0.3)

import pandas as pd
import locale
import timeit


def create_new_df_test():
    df_test = pd.DataFrame([
      {'dir': '/Users/uname1', 'size': 994933},
      {'dir': '/Users/uname2', 'size': 109338711},
    ])
    return df_test


def sizes_pass_series_return_series(series):
    series['size_kb'] = locale.format_string("%.1f", series['size'] / 1024.0, grouping=True) + ' KB'
    series['size_mb'] = locale.format_string("%.1f", series['size'] / 1024.0 ** 2, grouping=True) + ' MB'
    series['size_gb'] = locale.format_string("%.1f", series['size'] / 1024.0 ** 3, grouping=True) + ' GB'
    return series


def sizes_pass_series_return_tuple(series):
    a = locale.format_string("%.1f", series['size'] / 1024.0, grouping=True) + ' KB'
    b = locale.format_string("%.1f", series['size'] / 1024.0 ** 2, grouping=True) + ' MB'
    c = locale.format_string("%.1f", series['size'] / 1024.0 ** 3, grouping=True) + ' GB'
    return a, b, c


def sizes_pass_value_return_tuple(value):
    a = locale.format_string("%.1f", value / 1024.0, grouping=True) + ' KB'
    b = locale.format_string("%.1f", value / 1024.0 ** 2, grouping=True) + ' MB'
    c = locale.format_string("%.1f", value / 1024.0 ** 3, grouping=True) + ' GB'
    return a, b, c

Voici les résultats:

# 1 - Accepted (Nels11 Answer) - (pass series, return series):
9.82 ms ± 377 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)

# 2 - Pandafied (jaumebonet Answer) - (pass series, return tuple):
2.34 ms ± 48.6 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)

# 3 - Tuples (pass series, return tuple then zip):
1.36 ms ± 62.8 µs per loop (mean ± std. dev. of 7 runs, 1000 loops each)

# 4 - Tuples (Jesse Answer) - (pass value, return tuple then zip):
752 µs ± 18.5 µs per loop (mean ± std. dev. of 7 runs, 1000 loops each)

Notez que le renvoi de tuples est la méthode la plus rapide, mais ce qui est passé en argument affecte également les performances. La différence dans le code est subtile mais l'amélioration des performances est significative.

Le test n ° 4 (passage d'une seule valeur) est deux fois plus rapide que le test n ° 3 (passage en série), même si l'opération effectuée est apparemment identique.

Mais il y a plus ...

# 1a - Accepted (Nels11 Answer) - (pass series, return series, new columns exist):
3.23 ms ± 141 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)

# 2a - Pandafied (jaumebonet Answer) - (pass series, return tuple, new columns exist):
2.31 ms ± 39.3 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)

# 3a - Tuples (pass series, return tuple then zip, new columns exist):
1.36 ms ± 58.4 µs per loop (mean ± std. dev. of 7 runs, 1000 loops each)

# 4a - Tuples (Jesse Answer) - (pass value, return tuple then zip, new columns exist):
694 µs ± 3.9 µs per loop (mean ± std. dev. of 7 runs, 1000 loops each)

Dans certains cas (# 1a et # 4a), appliquer la fonction à un DataFrame dans lequel les colonnes de sortie existent déjà est plus rapide que de les créer à partir de la fonction.

Voici le code pour exécuter les tests:

# Paste and run the following in ipython console. It will not work if you run it from a .py file.
print('\nAccepted Answer (pass series, return series, new columns dont exist):')
df_test = create_new_df_test()
%timeit result = df_test.apply(sizes_pass_series_return_series, axis=1)
print('Accepted Answer (pass series, return series, new columns exist):')
df_test = create_new_df_test()
df_test = pd.concat([df_test, pd.DataFrame(columns=['size_kb', 'size_mb', 'size_gb'])])
%timeit result = df_test.apply(sizes_pass_series_return_series, axis=1)

print('\nPandafied (pass series, return tuple, new columns dont exist):')
df_test = create_new_df_test()
%timeit df_test[['size_kb', 'size_mb', 'size_gb']] = df_test.apply(sizes_pass_series_return_tuple, axis=1, result_type="expand")
print('Pandafied (pass series, return tuple, new columns exist):')
df_test = create_new_df_test()
df_test = pd.concat([df_test, pd.DataFrame(columns=['size_kb', 'size_mb', 'size_gb'])])
%timeit df_test[['size_kb', 'size_mb', 'size_gb']] = df_test.apply(sizes_pass_series_return_tuple, axis=1, result_type="expand")

print('\nTuples (pass series, return tuple then zip, new columns dont exist):')
df_test = create_new_df_test()
%timeit df_test['size_kb'],  df_test['size_mb'], df_test['size_gb'] = zip(*df_test.apply(sizes_pass_series_return_tuple, axis=1))
print('Tuples (pass series, return tuple then zip, new columns exist):')
df_test = create_new_df_test()
df_test = pd.concat([df_test, pd.DataFrame(columns=['size_kb', 'size_mb', 'size_gb'])])
%timeit df_test['size_kb'],  df_test['size_mb'], df_test['size_gb'] = zip(*df_test.apply(sizes_pass_series_return_tuple, axis=1))

print('\nTuples (pass value, return tuple then zip, new columns dont exist):')
df_test = create_new_df_test()
%timeit df_test['size_kb'],  df_test['size_mb'], df_test['size_gb'] = zip(*df_test['size'].apply(sizes_pass_value_return_tuple))
print('Tuples (pass value, return tuple then zip, new columns exist):')
df_test = create_new_df_test()
df_test = pd.concat([df_test, pd.DataFrame(columns=['size_kb', 'size_mb', 'size_gb'])])
%timeit df_test['size_kb'],  df_test['size_mb'], df_test['size_gb'] = zip(*df_test['size'].apply(sizes_pass_value_return_tuple))
Rocky K
la source
Merci également d'avoir décomposé les caractéristiques de performance!
PaulMest
3

Je crois que la version 1.1 rompt le comportement suggéré dans la réponse supérieure ici.

import pandas as pd
def test_func(row):
    row['c'] = str(row['a']) + str(row['b'])
    row['d'] = row['a'] + 1
    return row

df = pd.DataFrame({'a': [1, 2, 3], 'b': ['i', 'j', 'k']})
df.apply(test_func, axis=1)

Le code ci-dessus exécuté sur pandas 1.1.0 renvoie:

   a  b   c  d
0  1  i  1i  2
1  1  i  1i  2
2  1  i  1i  2

Alors que dans pandas 1.0.5, il est retourné:

   a   b    c  d
0  1   i   1i  2
1  2   j   2j  3
2  3   k   3k  4

Ce à quoi je pense que vous vous attendez.

Je ne sais pas comment les notes de publication expliquent ce comportement, mais comme expliqué ici, éviter la mutation des lignes d'origine en les copiant ressuscite l'ancien comportement. c'est à dire:

def test_func(row):
    row = row.copy()   #  <---- Avoid mutating the original reference
    row['c'] = str(row['a']) + str(row['b'])
    row['d'] = row['a'] + 1
    return row
meuglement
la source
Je pense que votre exemple de code peut avoir eu une erreur de copier / coller. Pourriez-vous le vérifier et voir si c'est ce que vous aviez l'intention de soumettre?
PaulMest le
1
Merci @PaulMest vous aviez raison. J'ai corrigé les deux fautes de frappe et ajouté un nouveau lien / référence où la question est répondue.
moo
1
Bienvenue dans Stack Overflow! @moo
PaulMest
1

Généralement, pour renvoyer plusieurs valeurs, c'est ce que je fais

def gimmeMultiple(group):
    x1 = 1
    x2 = 2
    return array([[1, 2]])
def gimmeMultipleDf(group):
    x1 = 1
    x2 = 2
    return pd.DataFrame(array([[1,2]]), columns=['x1', 'x2'])
df['size'].astype(int).apply(gimmeMultiple)
df['size'].astype(int).apply(gimmeMultipleDf)

Le retour d'un dataframe a définitivement ses avantages, mais parfois pas obligatoire. Vous pouvez regarder ce que les apply()retours et jouer un peu avec les fonctions;)

FooBar
la source
Merci pour cet échantillon. Cependant, cela ne génère pas une seule trame de données pour tous les résultats. Quand j'essaye de l'ajouter de nouveau au dataframe original, j'obtiens "ValueError: array n'est pas diffusable à la forme correcte".
PaulMest
Pouvez-vous fournir du code pour produire un petit échantillon de données?
FooBar
Chose sûre. Je viens de mettre à jour le code dans mon article d'origine pour inclure des exemples de données et de sortie.
PaulMest
0

Il donne un nouveau dataframe avec deux colonnes de l'original.

import pandas as pd
df = ...
df_with_two_columns = df.apply(lambda row:pd.Series([row['column_1'], row['column_2']], index=['column_1', 'column_2']),axis = 1)
Waldeyr Mendes da Silva
la source