Restituisce più colonne da panda apply ()


100

Ho un panda dataframe, df_test. Contiene una colonna "size" che rappresenta la dimensione in byte. Ho calcolato KB, MB e GB utilizzando il seguente codice:

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]

L'ho eseguito su 120.000 righe e il tempo impiega circa 2,97 secondi per colonna * 3 = ~ 9 secondi secondo% timeit.

C'è comunque che posso renderlo più veloce? Ad esempio, posso invece di restituire una colonna alla volta da applicare ed eseguirla 3 volte, posso restituire tutte e tre le colonne in un unico passaggio per reinserirle nel dataframe originale?

Le altre domande che ho trovato vogliono tutte prendere più valori e restituire un singolo valore . Voglio prendere un singolo valore e restituire più colonne .

Risposte:


114

Questa è una vecchia domanda, ma per completezza, puoi restituire una serie dalla funzione applicata che contiene i nuovi dati, evitando di dover ripetere tre volte. Il passaggio axis=1alla funzione apply applica la funzione sizesa ciascuna riga del dataframe, restituendo una serie da aggiungere a un nuovo dataframe. Questa serie, s, contiene i nuovi valori, nonché i dati originali.

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)

11
Sono sorpreso che siano passati quasi 2 anni senza la risposta giusta. Stavo cercando qualcos'altro e mi sono imbattuto in questo. Spero non sia troppo tardi per essere utile!
Nelz11

10
Cosa c'è rows_listin questa risposta?
David Stansby

È solo un elenco di serie per costruire il Dataframe.
Nelz11

1
Se pd.Series necessita di un indice, è necessario fornirlo con pd.Series(data, index=...). Altrimenti si ottengono errori criptici quando si tenta di riassegnare il risultato al dataframe padre.
smci

94

Usa applica e zip sarà 3 volte più veloce del modo Series.

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

I risultati del test sono:

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

Sono sorpreso che questo non abbia ricevuto più voti positivi. Grazie per aver condiviso la variante aggiuntiva e i dati sui tempi.
gumption

Potresti spiegare come hai restituito la tupla? Sembra essere l'opzione più veloce
Camilo

Fare riferimento al mio codice di esempio, questo è il modo tupla.
Jesse

sembra anche il più veloce e facile. sorpreso di non averlo trovato da solo.
Shahir Ansari

53

Alcune delle risposte attuali funzionano bene, ma desidero offrire un'altra opzione, forse più "ottimizzata". Questo funziona per me con gli attuali panda 0.23 (non sono sicuro se funzionerà nelle versioni precedenti):

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

Si noti che il trucco sta nel result_typeparametro di apply, che espanderà il suo risultato in un DataFrameche può essere assegnato direttamente a colonne nuove / vecchie.


1
Esatto ... scusa ... dopo un po 'di controllo, funziona con 0.22 in alcuni casi, ma ero in un ambiente virtuale e in realtà
eseguivo

4
Questa è la risposta più ottimale. Grazie
AdR

16

Solo un altro modo leggibile. Questo codice aggiungerà tre nuove colonne e i relativi valori, restituendo serie senza parametri di utilizzo nella funzione di applicazione.

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 esempio generale da: 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

9

Risposte davvero fantastiche! Grazie Jesse e jaumebonet! Solo qualche osservazione riguardo a:

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

Sebbene espandere sia un po 'più elegante ( pandify ), zip è almeno ** 2 volte più veloce . In questo semplice esempio qui sotto, sono stato 4 volte più veloce .

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

8

La performance tra le risposte migliori è notevolmente varia e Jesse e famaral42 ne hanno già discusso, ma vale la pena condividere un confronto equo tra le risposte migliori ed elaborare un dettaglio sottile ma importante della risposta di Jesse: l'argomento passato al funzione, influisce anche sulle prestazioni .

(Python 3.7.4, Panda 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

Ecco i risultati:

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

Notate come il ritorno tuple è il metodo più veloce, ma ciò che viene passato in come argomento, riguarda anche le prestazioni. La differenza nel codice è sottile ma il miglioramento delle prestazioni è significativo.

Il test n. 4 (passaggio di un singolo valore) è due volte più veloce del test n. 3 (passaggio in serie), anche se l'operazione eseguita è apparentemente identica.

Ma c'è di più ...

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

In alcuni casi (# 1a e # 4a), applicare la funzione a un DataFrame in cui esistono già le colonne di output è più veloce che crearle dalla funzione.

Ecco il codice per eseguire i test:

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

Grazie per aver analizzato anche le caratteristiche delle prestazioni!
PaulMest

2

Credo che la versione 1.1 rompa il comportamento suggerito nella risposta in alto qui.

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)

Il codice precedente è stato eseguito su panda 1.1.0 restituisce:

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

Mentre in Panda 1.0.5 è tornato:

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

Che penso sia quello che ti aspetteresti.

Non sono sicuro di come le note di rilascio spieghino questo comportamento, tuttavia, come spiegato qui, evitare la mutazione delle righe originali copiandole fa rivivere il vecchio comportamento. cioè:

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

Penso che il tuo esempio di codice possa aver avuto un errore di copia / incolla. Potresti controllarlo e vedere se è quello che intendevi inviare?
PaulMest

1
Grazie @PaulMest avevi ragione. Ho corretto i due errori di battitura e aggiunto un nuovo collegamento / riferimento in cui viene data risposta alla domanda.
moo

1
Benvenuto in Stack Overflow! @moo
PaulMest

1

In genere, per restituire più valori, questo è quello che faccio

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)

Restituire un dataframe in modo definitivo ha i suoi vantaggi, ma a volte non è necessario. Puoi guardare cosa apply()restituisce e giocare un po 'con le funzioni;)


Grazie per questo campione. Tuttavia, questo non restituisce un singolo dataframe per tutti i risultati. Quando provo ad aggiungerlo di nuovo al dataframe originale, ottengo "ValueError: array non è trasmettibile per correggere la forma".
PaulMest

Potete fornire il codice per produrre qualche piccolo campione di dati?
FooBar

Cosa certa. Ho appena aggiornato il codice nel mio post originale per includere dati e output di esempio.
PaulMest

0

Fornisce un nuovo dataframe con due colonne da quello originale.

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)
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.