Indicatore di avanzamento durante le operazioni dei panda


159

Eseguo regolarmente operazioni di panda su frame di dati superiori a circa 15 milioni di righe e mi piacerebbe avere accesso a un indicatore di avanzamento per determinate operazioni.

Esiste un indicatore di avanzamento testuale per le operazioni Panda-Split-Applica-Combina?

Ad esempio, in qualcosa del tipo:

df_users.groupby(['userID', 'requestDate']).apply(feature_rollup)

dove feature_rollup è una funzione in qualche modo coinvolta che accetta molte colonne DF e crea nuove colonne utente attraverso vari metodi. Queste operazioni possono richiedere del tempo per frame di dati di grandi dimensioni, quindi vorrei sapere se è possibile avere un output basato su testo in un notebook iPython che mi aggiorna sullo stato di avanzamento.

Finora, ho provato gli indicatori di progresso del ciclo canonico per Python ma non interagiscono con i panda in alcun modo significativo.

Spero che ci sia qualcosa che ho trascurato nella libreria / documentazione di Panda che consente di conoscere l'avanzamento di una divisione-applica-combina. Una semplice implementazione potrebbe forse esaminare il numero totale di sottoinsiemi di frame di dati su cui il fileapply funzione sta lavorando e riportare i progressi come la frazione completa di tali sottoinsiemi.

È forse qualcosa che deve essere aggiunto alla biblioteca?


hai fatto un% prun (profilo) sul codice? a volte è possibile eseguire operazioni sull'intero fotogramma prima di applicare per eliminare i colli di bottiglia
Jeff

@Jeff: puoi scommetterci, l'ho fatto prima per spremere fino all'ultimo bit di performance. Il problema si riduce davvero al limite di pseudo-riduzione della mappa a cui sto lavorando poiché le righe sono in decine di milioni, quindi non mi aspetto che gli aumenti della super velocità vogliano solo un feedback sui progressi.
Cwharland,

Prendi in considerazione il cythonising: pandas.pydata.org/pandas-docs/dev/…
Andy Hayden

@AndyHayden - Come ho commentato la tua risposta, la tua implementazione è abbastanza buona e aggiunge un po 'di tempo al lavoro complessivo. Ho anche citato tre operazioni all'interno del rollup delle funzionalità che ha riguadagnato tutto il tempo che ora è dedicato al progresso dei rapporti. Quindi alla fine scommetto che avrò barre di avanzamento con una riduzione del tempo di elaborazione totale se seguo il cython sull'intera funzione.
cwharland,

Risposte:


279

A grande richiesta, tqdmha aggiunto il supporto per pandas. A differenza delle altre risposte, questo non rallenterà sensibilmente i panda - ecco un esempio per DataFrameGroupBy.progress_apply:

import pandas as pd
import numpy as np
from tqdm import tqdm
# from tqdm.auto import tqdm  # for notebooks

df = pd.DataFrame(np.random.randint(0, int(1e8), (10000, 1000)))

# Create and register a new `tqdm` instance with `pandas`
# (can use tqdm_gui, optional kwargs, etc.)
tqdm.pandas()

# Now you can use `progress_apply` instead of `apply`
df.groupby(0).progress_apply(lambda x: x**2)

Se sei interessato a come funziona (e come modificarlo per i tuoi callback), vedi gli esempi su github , la documentazione completa su pypi o importa il modulo ed esegui help(tqdm).

MODIFICARE


Per rispondere direttamente alla domanda originale, sostituire:

df_users.groupby(['userID', 'requestDate']).apply(feature_rollup)

con:

from tqdm import tqdm
tqdm.pandas()
df_users.groupby(['userID', 'requestDate']).progress_apply(feature_rollup)

Nota: tqdm <= v4.8 : per le versioni di tqdm inferiori a 4.8, invece di tqdm.pandas()dover fare:

from tqdm import tqdm, tqdm_pandas
tqdm_pandas(tqdm())

5
tqdmè stato effettivamente creato per iterables solo pianura, paese d'origine: from tqdm import tqdm; for i in tqdm( range(int(1e8)) ): passIl supporto panda era un hack recente ho fatto :)
casper.dcl

6
A proposito, se usi i notebook Jupyter, puoi anche usare tqdm_notebooks per ottenere una barra più carina. Insieme ai panda avresti bisogno di istanziarlo come from tqdm import tqdm_notebook; tqdm_notebook().pandas(*args, **kwargs) vedi qui
grinsbaeckchen

2
A partire dalla versione 4.8.1 - utilizzare invece tqdm.pandas (). github.com/tqdm/tqdm/commit/…
mork il

1
Grazie, @mork è corretto. Stiamo lavorando (lentamente) verso la tqdmv5 che rende le cose più modularizzate.
casper.dcl

1
Per una recente raccomandazione sulla sintassi, consultare la documentazione di tqdm Pandas qui: pypi.python.org/pypi/tqdm#pandas-integration
Manu CJ

18

Per modificare la risposta di Jeff (e avere questa come funzione riutilizzabile).

def logged_apply(g, func, *args, **kwargs):
    step_percentage = 100. / len(g)
    import sys
    sys.stdout.write('apply progress:   0%')
    sys.stdout.flush()

    def logging_decorator(func):
        def wrapper(*args, **kwargs):
            progress = wrapper.count * step_percentage
            sys.stdout.write('\033[D \033[D' * 4 + format(progress, '3.0f') + '%')
            sys.stdout.flush()
            wrapper.count += 1
            return func(*args, **kwargs)
        wrapper.count = 0
        return wrapper

    logged_func = logging_decorator(func)
    res = g.apply(logged_func, *args, **kwargs)
    sys.stdout.write('\033[D \033[D' * 4 + format(100., '3.0f') + '%' + '\n')
    sys.stdout.flush()
    return res

Nota: applicare la percentuale di avanzamento aggiornamenti in linea . Se la tua funzione stdouts allora questo non funzionerà.

In [11]: g = df_users.groupby(['userID', 'requestDate'])

In [12]: f = feature_rollup

In [13]: logged_apply(g, f)
apply progress: 100%
Out[13]: 
...

Come al solito puoi aggiungerlo agli oggetti groupby come metodo:

from pandas.core.groupby import DataFrameGroupBy
DataFrameGroupBy.logged_apply = logged_apply

In [21]: g.logged_apply(f)
apply progress: 100%
Out[21]: 
...

Come accennato nei commenti, questa non è una caratteristica che i panda core sarebbero interessati a implementare. Ma python ti permette di crearli per molti oggetti / metodi panda (farlo sarebbe un bel po 'di lavoro ... anche se dovresti essere in grado di generalizzare questo approccio).


Dico "un bel po 'di lavoro", ma probabilmente potresti riscrivere l'intera funzione come decoratore (più generale).
Andy Hayden,

Grazie per l'espansione sul post di Jeff. Ho implementato entrambi e il rallentamento per ciascuno è piuttosto minimo (aggiunto un totale di 1,1 minuti a un'operazione che ha richiesto 27 minuti per essere completato). In questo modo posso vedere i progressi e, data la natura ad hoc di queste operazioni, penso che questo sia un rallentamento accettabile.
Cwharland,

Eccellente, felice che abbia aiutato. In realtà sono stato sorpreso dal rallentamento (quando ho provato un esempio), mi aspettavo che fosse molto peggio.
Andy Hayden,

1
Per aumentare ulteriormente l'efficienza dei metodi pubblicati, ero pigro sull'importazione dei dati (Panda è troppo bravo a gestire il csv disordinato !!) e alcune delle mie voci (~ 1%) avevano completamente cancellato gli inserimenti (penso intero record inseriti in singoli campi). L'eliminazione di questi fattori comporta una notevole accelerazione nel rollup delle funzionalità poiché non vi era alcuna ambiguità su cosa fare durante le operazioni di divisione-applicazione-combinazione.
cwharland,

1
Sono giù a 8 minuti ... ma ho aggiunto qualcosa al rollup delle funzionalità (più funzionalità -> migliore AUC!). Questo 8 minuti è per pezzo (due pezzi in totale in questo momento) con ogni pezzo nel quartiere di 12 milioni di file. Quindi sì ... 16 minuti per eseguire operazioni pesanti su 24 milioni di righe usando HDFStore (e c'è roba nltk nel rollup delle funzionalità). Abbastanza buono. Speriamo che Internet non mi giudichi in base all'ignoranza iniziale o all'ambivalenza nei confronti degli inserimenti incasinati =)
cwharland

11

Nel caso in cui abbiate bisogno di supporto su come utilizzarlo in un notebook Jupyter / ipython, come ho fatto io, ecco una guida utile e una fonte per l' articolo pertinente :

from tqdm._tqdm_notebook import tqdm_notebook
import pandas as pd
tqdm_notebook.pandas()
df = pd.DataFrame(np.random.randint(0, int(1e8), (10000, 1000)))
df.groupby(0).progress_apply(lambda x: x**2)

Nota il trattino basso nell'istruzione import per _tqdm_notebook. Come menzionato nell'articolo di riferimento, lo sviluppo è in fase beta avanzata.


8

Per chiunque stia cercando di applicare tqdm sul proprio codice di panda parallelo personalizzato.

(Ho provato alcune delle librerie per la parallelizzazione nel corso degli anni, ma non ho mai trovato una soluzione di parallelizzazione al 100%, principalmente per la funzione di applicazione, e ho sempre dovuto tornare per il mio codice "manuale".)

df_multi_core - questo è quello che chiami. Accetta:

  1. Il tuo oggetto df
  2. Il nome della funzione che desideri chiamare
  3. Il sottoinsieme di colonne su cui può essere eseguita la funzione (aiuta a ridurre il tempo / la memoria)
  4. Il numero di lavori da eseguire in parallelo (-1 o ometti per tutti i core)
  5. Qualsiasi altro kwarg accettato dalla funzione del df (come "axis")

_df_split - questa è una funzione di supporto interna che deve essere posizionata globalmente sul modulo in esecuzione (Pool.map è "dipendente dal posizionamento"), altrimenti lo localizzerei internamente.

ecco il codice dal mio succo (aggiungerò altri test di funzione panda lì):

import pandas as pd
import numpy as np
import multiprocessing
from functools import partial

def _df_split(tup_arg, **kwargs):
    split_ind, df_split, df_f_name = tup_arg
    return (split_ind, getattr(df_split, df_f_name)(**kwargs))

def df_multi_core(df, df_f_name, subset=None, njobs=-1, **kwargs):
    if njobs == -1:
        njobs = multiprocessing.cpu_count()
    pool = multiprocessing.Pool(processes=njobs)

    try:
        splits = np.array_split(df[subset], njobs)
    except ValueError:
        splits = np.array_split(df, njobs)

    pool_data = [(split_ind, df_split, df_f_name) for split_ind, df_split in enumerate(splits)]
    results = pool.map(partial(_df_split, **kwargs), pool_data)
    pool.close()
    pool.join()
    results = sorted(results, key=lambda x:x[0])
    results = pd.concat([split[1] for split in results])
    return results

Bellow è un codice di prova per un parallelized applicare con TQdM "progress_apply".

from time import time
from tqdm import tqdm
tqdm.pandas()

if __name__ == '__main__': 
    sep = '-' * 50

    # tqdm progress_apply test      
    def apply_f(row):
        return row['c1'] + 0.1
    N = 1000000
    np.random.seed(0)
    df = pd.DataFrame({'c1': np.arange(N), 'c2': np.arange(N)})

    print('testing pandas apply on {}\n{}'.format(df.shape, sep))
    t1 = time()
    res = df.progress_apply(apply_f, axis=1)
    t2 = time()
    print('result random sample\n{}'.format(res.sample(n=3, random_state=0)))
    print('time for native implementation {}\n{}'.format(round(t2 - t1, 2), sep))

    t3 = time()
    # res = df_multi_core(df=df, df_f_name='apply', subset=['c1'], njobs=-1, func=apply_f, axis=1)
    res = df_multi_core(df=df, df_f_name='progress_apply', subset=['c1'], njobs=-1, func=apply_f, axis=1)
    t4 = time()
    print('result random sample\n{}'.format(res.sample(n=3, random_state=0)))
    print('time for multi core implementation {}\n{}'.format(round(t4 - t3, 2), sep))

Nell'output è possibile visualizzare 1 barra di avanzamento per l'esecuzione senza parallelizzazione e barre di avanzamento per core durante l'esecuzione con parallelizzazione. C'è un leggero rallentamento e a volte il resto dei core appare contemporaneamente, ma anche allora penso che sia utile dal momento che ottieni le statistiche sui progressi per core (it / sec e record totali, per es.)

inserisci qui la descrizione dell'immagine

Grazie @abcdaa per questa fantastica biblioteca!


1
Grazie @mork - sentiti libero di aggiungere a github.com/tqdm/tqdm/wiki/How-to-make-a-great-Progress-Bar o di creare una nuova pagina su github.com/tqdm/tqdm/wiki
casper. dcl

Grazie, ma try: splits = np.array_split(df[subset], njobs) except ValueError: splits = np.array_split(df, njobs)ho dovuto cambiare queste parti: a causa dell'eccezione KeyError invece di ValueError, passare a Exception per gestire tutti i casi.
Marius,

Grazie @mork - questa risposta dovrebbe essere più alta.
Andy,

5

Puoi farlo facilmente con un decoratore

from functools import wraps 

def logging_decorator(func):

    @wraps
    def wrapper(*args, **kwargs):
        wrapper.count += 1
        print "The function I modify has been called {0} times(s).".format(
              wrapper.count)
        func(*args, **kwargs)
    wrapper.count = 0
    return wrapper

modified_function = logging_decorator(feature_rollup)

quindi basta usare la funzione modificata (e cambiare quando vuoi che sia stampata)


1
L'evidente avvertimento di questo rallenterà la tua funzione! Potresti anche averlo aggiornato con i progressi stackoverflow.com/questions/5426546/… ad esempio count / len in percentuale.
Andy Hayden,

sì - avrai un ordine (numero di gruppi), quindi a seconda di quale sia il tuo collo di bottiglia questo potrebbe fare la differenza
Jeff

forse la cosa intuitiva da fare è racchiuderla in una logged_apply(g, func)funzione, in cui si avrebbe accesso all'ordine e si potrebbe accedere dall'inizio.
Andy Hayden,

Nella mia risposta ho fatto quanto sopra, anche aggiornamento sfacciato delle percentuali. A dire il vero non sono riuscito a far funzionare il tuo ... Penso con il bit avvolgente. Se lo stai usando per applicare non è comunque così importante.
Andy Hayden,

1

Ho modificato la risposta di Jeff , per includere un totale, in modo che sia possibile tenere traccia dei progressi e di una variabile per stampare solo ogni X iterazioni (questo migliora effettivamente le prestazioni di molto, se "print_at" è ragionevolmente alto)

def count_wrapper(func,total, print_at):

    def wrapper(*args):
        wrapper.count += 1
        if wrapper.count % wrapper.print_at == 0:
            clear_output()
            sys.stdout.write( "%d / %d"%(calc_time.count,calc_time.total) )
            sys.stdout.flush()
        return func(*args)
    wrapper.count = 0
    wrapper.total = total
    wrapper.print_at = print_at

    return wrapper

la funzione clear_output () è di

from IPython.core.display import clear_output

se non su IPython, la risposta di Andy Hayden lo fa senza di essa

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.