Applicare la funzione Panda sulla colonna per creare più nuove colonne?


216

Come fare questo in Panda:

Ho una funzione extract_text_featuressu una singola colonna di testo, restituendo più colonne di output. In particolare, la funzione restituisce 6 valori.

La funzione funziona, tuttavia non sembra esserci alcun tipo di ritorno corretto (panda DataFrame / numpy array / Python list) in modo tale che l'output possa essere assegnato correttamente df.ix[: ,10:16] = df.textcol.map(extract_text_features)

Quindi penso di dover tornare a iterare con df.iterrows(), secondo questo ?

AGGIORNAMENTO: Iterare con df.iterrows()è almeno 20 volte più lento, quindi mi sono arreso e ho diviso la funzione in sei .map(lambda ...)chiamate distinte .

AGGIORNAMENTO 2: questa domanda è stata posta intorno alla v0.11.0 . Quindi gran parte della domanda e delle risposte non sono troppo rilevanti.


1
Non credo che si può fare assegnamento multiplo il modo in cui lo avete scritto: df.ix[: ,10:16]. Penso che dovrai inserire le mergetue funzionalità nel set di dati.
Zelazny7,

1
Per coloro che desiderano una soluzione molto più performante controlla questa qui sotto che non utilizzaapply
Ted Petrou,

La maggior parte delle operazioni numeriche con i panda può essere vettorializzata - ciò significa che sono molto più veloci dell'iterazione convenzionale. OTOH, alcune operazioni (come string e regex) sono intrinsecamente difficili da vettorializzare. In questo caso, è importante capire come eseguire il loop dei dati. Maggiori informazioni su quando e come eseguire il looping dei dati, leggi Per i loop con Panda - Quando dovrei preoccuparmi? .
cs95,

@coldspeed: il problema principale non era scegliere quale fosse il rendimento più elevato tra le varie opzioni, stava combattendo la sintassi dei panda per far funzionare tutto questo, intorno alla v0.11.0 .
smci,

In effetti, il commento è destinato ai futuri lettori che sono alla ricerca di soluzioni iterative, che non conoscono meglio o che sanno cosa stanno facendo.
cs95,

Risposte:


109

Partendo dalla risposta dell'utente1827356, puoi eseguire l'assegnazione in un solo passaggio usando 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: si prega di essere consapevoli dell'enorme consumo di memoria e della bassa velocità: https://ys-l.github.io/posts/2015/08/28/how-not-to-use-pandas-apply/ !


2
solo per curiosità, ci si aspetta che esaurisca molta memoria facendo questo? Lo sto facendo su un frame di dati che contiene 2,5 milioni di righe e mi sono quasi imbattuto in problemi di memoria (inoltre è molto più lento di restituire solo 1 colonna).
Jeffrey04

2
'df.join (df.textcol.apply (lambda s: pd.Series ({' feature1 ': s + 1,' feature2 ': s-1})))' sarebbe un'opzione migliore, credo.
Shivam K. Thakkar

@ShivamKThakkar perché pensi che il tuo suggerimento sia un'opzione migliore? Sarebbe più efficiente pensi o avresti meno costi di memoria?
Tsando,

1
Si prega di considerare la velocità e la memoria richiesta: ys-l.github.io/posts/2015/08/28/how-not-to-use-pandas-apply
Make42

190

Di solito lo faccio usando 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

8
Ma cosa fai se hai aggiunto 50 colonne come questa anziché 6?
max

14
@maxtemp = list(zip(*df['num'].map(powers))); for i, c in enumerate(columns): df[c] = temp[c]
ostrokach il

8
@ostrokach Penso che volevi dire for i, c in enumerate(columns): df[c] = temp[i]. Grazie a questo, ho davvero avuto lo scopo di enumerate: D
rocarvaj,

4
Questa è di gran lunga la soluzione più elegante e leggibile che ho trovato per questo. A meno che non si verifichino problemi di prestazioni, il linguaggio zip(*df['col'].map(function))è probabilmente la strada da percorrere.
François Leblanc,


84

Questo è quello che ho fatto in passato

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

Editing per completezza

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

concat () sembra più semplice di merge () per connettere i nuovi col al frame di dati originale.
cumino

2
bella risposta, non è necessario utilizzare un dict o un'unione se si specificano le colonne al di fuori dell'applicazionedf[['col1', 'col2']] = df['col3'].apply(lambda x: pd.Series('val1', 'val2'))
Matt

66

Questo è il modo più semplice e corretto per ottenere ciò nel 95% dei casi d'uso:

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

non dovresti scrivere: df = df.apply (esempio (df), axis = 1) correggimi se sbaglio, sono solo un novizio
user299791

1
@ user299791, No in questo caso stai trattando l'esempio come un oggetto di prima classe, quindi stai passando la funzione stessa. Questa funzione verrà applicata a ciascuna riga.
Michael David Watson,

ciao Michael, la tua risposta mi ha aiutato nel mio problema. Sicuramente la tua soluzione è migliore del metodo df.assign () originale di Panda, perché questa è una volta per colonna. Usando assegnare (), se vuoi creare 2 nuove colonne, devi usare df1 per lavorare su df per ottenere una nuova colonna1, quindi usare df2 per lavorare su df1 per creare la seconda nuova colonna ... questo è abbastanza monotono. Ma il tuo metodo mi ha salvato la vita !!! Grazie!!!
commentallez-vous

1
Non verrà eseguito il codice di assegnazione delle colonne una volta per riga? Non sarebbe meglio restituire a pd.Series({k:v})e serializzare l'assegnazione della colonna come nella risposta di Ewan?
Denis de Bernardy,

Se aiuta qualcuno, sebbene questo approccio sia corretto e anche la più semplice di tutte le soluzioni presentate, aggiornare la riga direttamente in questo modo ha finito per essere sorprendentemente lento - un ordine di grandezza più lento rispetto all'applicazione con soluzioni 'espandi' + pd.concat
Dmytro Bugayev

31

Nel 2018, uso apply()con argomentoresult_type='expand'

>>> appiled_df = df.apply(lambda row: fn(row.text), axis='columns', result_type='expand')
>>> df = pd.concat([df, appiled_df], axis='columns')

6
Oggi è così che fai!
Make42

1
Questo ha funzionato fuori dagli schemi nel 2020, mentre molte altre domande no. Inoltre non usa il pd.Series che è sempre bello per quanto riguarda i problemi di prestazioni
Théo Rubenach il

1
Questa è una buona soluzione L'unico problema è che non puoi scegliere il nome per le 2 colonne appena aggiunte. In seguito devi eseguire df.rename (colonne = {0: 'col1', 1: 'col2'})
pedram bashiri

2
@pedrambashiri Se la funzione che passi a df.applyrestituisce a dict, le colonne usciranno con il nome in base ai tasti.
Seb

25

Basta usare 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
Aiuta a sottolineare che l'opzione è nuova in 0.23 . La domanda è stata posta il 0.11
smci

Bello, questo è semplice e funziona ancora in modo ordinato. Questo è quello che stavo cercando. Grazie
Isaac Sim

Duplica una risposta precedente: stackoverflow.com/a/52363890/823470
tar

22

Riepilogo: se si desidera creare solo alcune colonne, utilizzaredf[['new_col1','new_col2']] = df[['data1','data2']].apply( function_of_your_choosing(x), axis=1)

Per questa soluzione, il numero di nuove colonne che stai creando deve essere uguale al numero di colonne che usi come input per la funzione .apply (). Se vuoi fare qualcos'altro, dai un'occhiata alle altre risposte.

Dettagli Supponiamo che tu abbia un frame di dati a due colonne. La prima colonna è l'altezza di una persona quando ha 10 anni; il secondo è l'altezza di detta persona quando ha 20 anni.

Supponiamo di dover calcolare sia la media delle altezze di ciascuna persona sia la somma delle altezze di ciascuna persona. Sono due valori per ogni riga.

È possibile farlo tramite la seguente funzione che verrà presto applicata:

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]

È possibile utilizzare questa funzione in questo modo:

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

(Per essere chiari: questa funzione di applicazione accetta i valori di ciascuna riga nel frame di dati sotto settato e restituisce un elenco.)

Tuttavia, se lo fai:

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

creerai 1 nuova colonna che contiene gli elenchi [media, somma], che presumibilmente vorresti evitare, perché ciò richiederebbe un altro Lambda / Apply.

Invece, si desidera suddividere ciascun valore nella propria colonna. Per fare ciò, puoi creare due colonne contemporaneamente:

df[['Mean','Sum']] = df[['height_at_age_10','height_at_age_20']]
.apply(mean_and_sum(x),axis=1)

4
Per i panda 0.23, dovrai usare la sintassi:df["mean"], df["sum"] = df[['height_at_age_10','height_at_age_20']] .apply(mean_and_sum(x),axis=1)
SummerEla,

Questa funzione potrebbe generare errori. La funzione di ritorno deve essere return pd.Series([mean,sum])
Kanishk Mair l'

22

Per me questo ha funzionato:

Ingresso df

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

Funzione

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

Crea 2 nuove colonne:

df[['square x', 'cube x']] = df['col x'].apply(f)

Produzione:

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

13

Ho cercato diversi modi per farlo e il metodo mostrato qui (restituendo una serie di panda) non sembra essere più efficiente.

Se iniziamo con un frame di dati di grandi dimensioni di dati casuali:

# 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'esempio mostrato qui:

# 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 loop, meglio di 3: 2,77 s per loop

Un metodo alternativo:

# 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 loop, meglio di 3: 8,85 ms per loop

Secondo me è molto più efficiente prendere una serie di tuple e poi convertirla in un DataFrame. Sarei interessato a sentire il pensiero delle persone se ci fosse un errore nel mio lavoro.


Questo è davvero utile! Ho ottenuto una velocità di 30 volte rispetto ai metodi di serie con ritorno di funzione.
Pushkar Nimkar,

9

La soluzione accettata sarà estremamente lenta per molti dati. La soluzione con il maggior numero di voti è un po 'difficile da leggere e anche lenta con i dati numerici. Se ogni nuova colonna può essere calcolata indipendentemente dalle altre, le assegnerei direttamente senza utilizzarle apply.

Esempio con dati di personaggi falsi

Crea 100.000 stringhe in 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

Diciamo che volevamo estrarre alcune funzionalità di testo come fatto nella domanda originale. Ad esempio, estraiamo il primo carattere, contiamo la ricorrenza della lettera 'e' e capitalizziamo la frase.

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

Tempi

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

Sorprendentemente, puoi ottenere prestazioni migliori eseguendo il ciclo attraverso ogni valore

%%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 altro esempio con dati numerici falsi

Crea 1 milione di numeri casuali e testa la powersfunzione dall'alto.

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'assegnazione di ogni colonna è 25 volte più veloce e molto leggibile:

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

Ho fatto una risposta simile con maggiori dettagli qui sul perché in applygenere non è la strada da percorrere.


8

Ho pubblicato la stessa risposta in altre due domande simili. Il modo in cui preferisco farlo è racchiudere i valori di ritorno della funzione in una serie:

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

Quindi utilizzare applica come segue per creare colonne separate:

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

1

puoi restituire l'intera riga anziché i valori:

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

dove la funzione restituisce la riga

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

No, non voglio applicare extract_text_featuresad ogni colonna del df, solo alla colonna di testodf.textcol
smci,

-2
def myfunc(a):
    return a * a

df['New Column'] = df['oldcolumn'].map(myfunc))

Questo ha funzionato per me. La nuova colonna verrà creata con i dati della vecchia colonna elaborati.


2
Questo non restituisce "più nuove colonne"
pedram bashiri

Questo non restituisce "più nuove colonne", quindi non risponde alla domanda. Potresti per favore cancellarlo?
smci,
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.