web-dev-qa-db-fra.com

Indicateur de progression pendant pandas opérations

J'effectue régulièrement des opérations pandas) sur des trames de données de plus de 15 millions de lignes et j'aimerais beaucoup avoir accès à un indicateur de progression pour des opérations particulières.

Existe-t-il un indicateur de progression textuel pour les opérations pandas split-apply-combine)?

Par exemple, dans quelque chose comme:

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

feature_rollup est une fonction assez complexe qui utilise plusieurs colonnes DF et crée de nouvelles colonnes utilisateur à l'aide de différentes méthodes. Ces opérations peuvent prendre un certain temps pour les grandes trames de données; j'aimerais donc savoir si c'est le cas. Il est possible d’obtenir une sortie texte dans un cahier iPython qui m’informe de l’avancement des travaux.

Jusqu'ici, j'ai essayé des indicateurs canoniques de progression de boucle pour Python mais ils n'interagissent pas avec pandas de manière significative).

J'espère que quelque chose que j'ai oublié dans la bibliothèque/documentation pandas) permet de connaître l'état d'avancement d'une combinaison split-apply-combine. Une simple implémentation consisterait peut-être en nombre total des sous-ensembles de trames de données sur lesquels la fonction apply fonctionne et indique la progression en tant que fraction terminée de ces sous-ensembles.

Est-ce que c'est peut-être quelque chose qui doit être ajouté à la bibliothèque?

97
cwharland

A la demande générale, tqdm a ajouté le support pour pandas. Contrairement aux autres réponses, ceci ne ralentira pas sensiblement pandas down - voici un exemple pour 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)

Si cela vous intéresse (et comment le modifier pour vos propres rappels), voyez le exemples sur github , le documentation complète sur pypi ou importez le module et exécutez help(tqdm).

[~ # ~] éditer [~ # ~]


Pour répondre directement à la question initiale, remplacez:

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

avec:

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

Remarque: tqdm <= v4.8 : pour les versions de tqdm inférieures à 4.8, au lieu de tqdm.pandas(), vous deviez procéder comme suit:

from tqdm import tqdm, tqdm_pandas
tqdm_pandas(tqdm())
172
casper.dcl

Pour Tweak réponse de Jeff (et ont cela comme une fonction réutilisable).

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

Remarque: le pourcentage de progression de l'application mises à jour en ligne . Si votre fonction stdouts, cela ne fonctionnera pas.

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

In [12]: f = feature_rollup

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

Comme d'habitude, vous pouvez ajouter ceci à vos objets groupby en tant que méthode:

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

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

Comme mentionné dans les commentaires, ce n'est pas une fonctionnalité essentielle pandas serait intéressé par la mise en œuvre. Mais python vous permet de les créer pour plusieurs pandas objets/méthodes (cela serait un peu du travail ... bien que vous puissiez être capable de généraliser cette approche)).

13
Andy Hayden

Au cas où vous auriez besoin d'aide pour utiliser ce manuel dans un cahier Jupyter/ipython, voici un guide utile et une source pour article pertinent :

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)

Notez le trait de soulignement dans la déclaration d'importation pour _tqdm_notebook. Comme mentionné dans l'article mentionné, le développement est en phase bêta tardive.

7
Victor Vulovic

Vous pouvez facilement le faire avec un décorateur

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)

puis utilisez simplement la modified_function (et changez quand vous voulez que ça soit imprimé)

4
Jeff

Pour tous ceux qui cherchent à appliquer tqdm sur leur code personnalisé, appliquez pandas.

(J'ai essayé certaines bibliothèques pour la parallélisation au fil des ans, mais je n'ai jamais trouvé de solution de parallélisation à 100%, principalement pour la fonction apply, et je devais toujours revenir pour mon code "manuel".)

df_multi_core - c'est celui que vous appelez. Il accepte:

  1. Votre objet df
  2. Le nom de la fonction que vous souhaitez appeler
  3. Le sous-ensemble de colonnes sur lequel la fonction peut être exécutée (aide à réduire le temps/la mémoire)
  4. Nombre de travaux à exécuter en parallèle (-1 ou omis pour tous les cœurs)
  5. Tout autre kwargs accepté par la fonction df (comme "axe")

_ df_split - il s'agit d'une fonction d'assistance interne qui doit être positionnée globalement sur le module en cours d'exécution (Pool.map est "dépendant de l'emplacement"), sinon I ' d le localiser en interne ..

voici le code de mon Gist (j'ajouterai d'autres pandas)):

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

Ci-dessous un code de test pour un parallélisé appliquer avec tqtm "progress_apply".

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

if __== '__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))

Dans la sortie, vous pouvez voir une barre de progression pour une exécution sans parallélisation et des barres de progression par cœur lors de l'exécution avec une parallélisation. Il y a un léger hickup et parfois le reste des cœurs apparaissent en même temps, mais même dans ce cas, je pense que c'est utile puisque vous obtenez les statistiques de progression par cœurs (nombre d'enregistrements par seconde et total, par exemple).

enter image description here

Merci @abcdaa pour cette superbe bibliothèque!

2
mork

J'ai changé réponse de Jeff , pour inclure un total, de sorte que vous puissiez suivre les progrès et une variable pour imprimer toutes les itérations X (cela améliore en fait la performance de beaucoup, si le "print_at" est raisonnablement élevé)

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 fonction clear_output () est de

from IPython.core.display import clear_output

sinon sur la réponse de IPython Andy Hayden le fait sans

0
Filipe Silva