web-dev-qa-db-fra.com

Flux de travail "données volumineuses" utilisant pandas

J'ai essayé de trouver une réponse à cette question pendant de nombreux mois tout en apprenant les pandas. J'utilise SAS pour mon travail quotidien et il est idéal pour une prise en charge insuffisante. Cependant, SAS est horrible en tant que logiciel pour de nombreuses autres raisons.

Un jour, j’espère pouvoir remplacer mon utilisation de SAS par python et de pandas, mais je n’ai actuellement pas de flux de travail out-core pour les grands ensembles de données. Je ne parle pas de "données volumineuses" nécessitant un réseau distribué, mais plutôt de fichiers trop volumineux pour contenir de la mémoire, mais suffisamment petits pour tenir sur un disque dur.

Ma première pensée a été d’utiliser HDFStore pour stocker de grands ensembles de données sur disque et extraire uniquement les éléments dont j’ai besoin dans des cadres de données pour analyse. D'autres ont mentionné MongoDB comme une alternative plus facile à utiliser. Ma question est la suivante:

Quels sont les meilleurs workflows pour atteindre les objectifs suivants:

  1. Chargement de fichiers à plat dans une structure de base de données permanente sur disque
  2. Interroger cette base de données pour récupérer des données à alimenter dans une structure de données pandas
  3. Mise à jour de la base de données après manipulation de morceaux dans des pandas

Des exemples concrets seraient très appréciés, en particulier de la part de ceux qui utilisent pandas sur des "données volumineuses".

Modifier - un exemple de la façon dont j'aimerais que cela fonctionne:

  1. Importez de manière itérative un fichier plat volumineux et stockez-le dans une structure de base de données permanente sur disque. Ces fichiers sont généralement trop volumineux pour tenir en mémoire.
  2. Pour pouvoir utiliser les pandas, j'aimerais lire des sous-ensembles de ces données (généralement quelques colonnes à la fois) pouvant tenir dans la mémoire.
  3. Je créerais de nouvelles colonnes en effectuant diverses opérations sur les colonnes sélectionnées.
  4. Il me faudrait ensuite ajouter ces nouvelles colonnes à la structure de la base de données.

J'essaie de trouver le meilleur moyen d'effectuer ces étapes. En lisant des liens sur pandas et sur pytables, il semble que l’ajout d’une nouvelle colonne puisse poser problème.

Edit - Répondre aux questions de Jeff spécifiquement:

  1. Je construis des modèles de risque de crédit à la consommation. Les types de données incluent les caractéristiques de téléphone, SSN et d’adresse; valeurs de propriété; informations désobligeantes comme les casiers judiciaires, les faillites, etc. Les jeux de données que j'utilise quotidiennement contiennent en moyenne 1 000 à 2 000 champs de types de données mixtes: variables continues, nominales et ordinales de données numériques et caractères. J'ajoute rarement des lignes, mais j'effectue beaucoup d'opérations qui créent de nouvelles colonnes.
  2. Les opérations typiques impliquent la combinaison de plusieurs colonnes utilisant la logique conditionnelle dans une nouvelle colonne composée. Par exemple, if var1 > 2 then newvar = 'A' Elif var2 = 4 then newvar = 'B'. Le résultat de ces opérations est une nouvelle colonne pour chaque enregistrement de mon jeu de données.
  3. Enfin, j'aimerais ajouter ces nouvelles colonnes à la structure de données sur disque. Je répète l'étape 2 en explorant les données avec des tableaux croisés et des statistiques descriptives en essayant de trouver des relations intéressantes et intuitives à modéliser.
  4. Un fichier de projet typique fait habituellement environ 1 Go. Les fichiers sont organisés de manière à ce qu'une rangée consiste en un enregistrement de données de consommateurs. Chaque ligne a le même nombre de colonnes pour chaque enregistrement. Ce sera toujours le cas.
  5. Il est assez rare que je sous-classe par rangées lors de la création d'une nouvelle colonne. Cependant, il est assez courant pour moi de créer un sous-ensemble de lignes lors de la création de rapports ou de la génération de statistiques descriptives. Par exemple, je souhaiterais peut-être créer une fréquence simple pour un secteur d'activité spécifique, par exemple les cartes de crédit pour particuliers. Pour ce faire, je ne sélectionnerais que les enregistrements dans lesquels le secteur d'activité = la vente au détail s'ajoutent aux colonnes sur lesquelles je souhaite faire rapport. Lors de la création de nouvelles colonnes, toutefois, je voudrais extraire toutes les lignes de données et uniquement les colonnes dont j'ai besoin pour les opérations.
  6. Le processus de modélisation nécessite d'analyser chaque colonne, de rechercher des relations intéressantes avec certaines variables de résultat et de créer de nouvelles colonnes composées décrivant ces relations. Les colonnes que j'explore se font généralement par petites séries. Par exemple, je vais me concentrer sur une vingtaine de colonnes traitant uniquement des valeurs de propriété et observer comment elles se rapportent à un défaut de paiement d'un prêt. Une fois que ceux-ci sont explorés et que de nouvelles colonnes sont créées, je passe ensuite à un autre groupe de colonnes, par exemple enseignement collégial, et répète le processus. Ce que je fais, c'est créer des variables candidates qui expliquent la relation entre mes données et certains résultats. À la toute fin de ce processus, j'applique des techniques d'apprentissage qui créent une équation à partir de ces colonnes composées.

Il est rare que j'ajoute jamais des lignes à l'ensemble de données. Je créerai presque toujours de nouvelles colonnes (variables ou caractéristiques dans le langage statistique/apprentissage automatique).

886
Zelazny7

J'utilise régulièrement des dizaines de giga-octets de données de cette manière, p. Ex. J'ai des tables sur le disque que je lis via des requêtes, crée des données et les rajoute.

Il vaut la peine de lire la documentation et à la fin de ce fil pour plusieurs suggestions sur la façon de stocker vos données.

Des détails qui affecteront la manière dont vous stockez vos données, tels que:
Donnez autant de détails que possible. et je peux vous aider à développer une structure.

  1. Taille des données, nombre de lignes, colonnes, types de colonnes; est-ce que vous ajoutez des lignes ou juste des colonnes?
  2. À quoi ressembleront les opérations typiques? Par exemple. Effectuez une requête sur les colonnes pour sélectionner un ensemble de lignes et des colonnes spécifiques, puis effectuez une opération (en mémoire), créez de nouvelles colonnes, enregistrez-les.
    (Donner un exemple de jouet pourrait nous permettre de formuler des recommandations plus spécifiques.)
  3. Après ce traitement, que faites-vous alors? L'étape 2 est-elle ad hoc ou reproductible?
  4. Fichiers plats en entrée: combien, taille totale approximative en Go. Comment sont-ils organisés, par exemple par des enregistrements? Chacun contient-il des champs différents ou at-il des enregistrements par fichier avec tous les champs de chaque fichier?
  5. Avez-vous déjà sélectionné des sous-ensembles de lignes (enregistrements) en fonction de critères (par exemple, sélectionnez les lignes avec le champ A> 5)? et ensuite, faites quelque chose ou sélectionnez-vous simplement les champs A, B, C avec tous les enregistrements (puis faites quelque chose)?
  6. Travaillez-vous sur toutes vos colonnes (en groupes), ou existe-t-il une bonne proportion que vous ne pouvez utiliser que pour les rapports (par exemple, vous voulez conserver les données, mais n'avez pas besoin d'extraire cette colonne explicitement jusqu'à ce que résultat final temps)?

Solution

Assurez-vous que vous avez pandas au moins 0.10.1 installé.

Lire itération de fichiers morceau par morcea et requêtes à plusieurs tables .

Puisque pytables est optimisé pour fonctionner en lignes (ce sur quoi vous interrogez), nous allons créer une table pour chaque groupe de champs. De cette façon, il est facile de sélectionner un petit groupe de champs (ce qui fonctionnera avec une grande table, mais il est plus efficace de le faire de cette façon ... Je pense que je serai peut-être capable de corriger cette limitation à l'avenir ... c'est plus intuitif quand même):
(Ce qui suit est un pseudocode.)

import numpy as np
import pandas as pd

# create a store
store = pd.HDFStore('mystore.h5')

# this is the key to your storage:
#    this maps your fields to a specific group, and defines 
#    what you want to have as data_columns.
#    you might want to create a Nice class wrapping this
#    (as you will want to have this map and its inversion)  
group_map = dict(
    A = dict(fields = ['field_1','field_2',.....], dc = ['field_1',....,'field_5']),
    B = dict(fields = ['field_10',......        ], dc = ['field_10']),
    .....
    REPORTING_ONLY = dict(fields = ['field_1000','field_1001',...], dc = []),

)

group_map_inverted = dict()
for g, v in group_map.items():
    group_map_inverted.update(dict([ (f,g) for f in v['fields'] ]))

Lecture dans les fichiers et création du stockage (essentiellement ce que append_to_multiple fait):

for f in files:
   # read in the file, additional options hmay be necessary here
   # the chunksize is not strictly necessary, you may be able to Slurp each 
   # file into memory in which case just eliminate this part of the loop 
   # (you can also change chunksize if necessary)
   for chunk in pd.read_table(f, chunksize=50000):
       # we are going to append to each table by group
       # we are not going to create indexes at this time
       # but we *ARE* going to create (some) data_columns

       # figure out the field groupings
       for g, v in group_map.items():
             # create the frame for this group
             frame = chunk.reindex(columns = v['fields'], copy = False)    

             # append it
             store.append(g, frame, index=False, data_columns = v['dc'])

Maintenant, vous avez toutes les tables dans le fichier (en fait, vous pouvez les stocker dans des fichiers séparés si vous le souhaitez, vous devrez probablement ajouter le nom de fichier à group_map, mais ce n'est probablement pas nécessaire).

Voici comment obtenir des colonnes et en créer de nouvelles:

frame = store.select(group_that_I_want)
# you can optionally specify:
# columns = a list of the columns IN THAT GROUP (if you wanted to
#     select only say 3 out of the 20 columns in this sub-table)
# and a where clause if you want a subset of the rows

# do calculations on this frame
new_frame = cool_function_on_frame(frame)

# to 'add columns', create a new group (you probably want to
# limit the columns in this new_group to be only NEW ones
# (e.g. so you don't overlap from the other tables)
# add this info to the group_map
store.append(new_group, new_frame.reindex(columns = new_columns_created, copy = False), data_columns = new_columns_created)

Lorsque vous êtes prêt pour le post-traitement:

# This may be a bit tricky; and depends what you are actually doing.
# I may need to modify this function to be a bit more general:
report_data = store.select_as_multiple([groups_1,groups_2,.....], where =['field_1>0', 'field_1000=foo'], selector = group_1)

En ce qui concerne data_columns, vous n'avez pas réellement besoin de définir ANY data_columns; ils vous permettent de sous-sélectionner des lignes en fonction de la colonne. Par exemple. quelque chose comme:

store.select(group, where = ['field_1000=foo', 'field_1001>0'])

Elles vous intéresseront peut-être davantage à l’étape finale de la génération du rapport (une colonne de données est essentiellement séparée des autres colonnes, ce qui peut avoir une incidence sur l’efficacité si vous définissez un lot).

Vous voudrez peut-être aussi:

  • créez une fonction qui prend une liste de champs, recherche les groupes dans la groupes_map, puis les sélectionne et concatène les résultats afin que vous obteniez le cadre résultant (c'est essentiellement ce que select_as_multiple fait). Ainsi, la structure serait assez transparente pour vous.
  • index sur certaines colonnes de données (rend la sous-définition de lignes beaucoup plus rapide).
  • activer la compression.

Faites-moi savoir quand vous avez des questions!

564
Jeff

Je pense que les réponses ci-dessus manquent d'une approche simple que j'ai trouvée très utile.

Lorsque j'ai un fichier trop volumineux pour être chargé en mémoire, je le divise en plusieurs fichiers plus petits (par rangée ou par colonne)

Exemple: dans le cas de données commerciales d’une taille d’environ 30 Go échangées pendant 30 jours, je les divise en un fichier par jour d’une taille d’environ 1 Go. Je traite ensuite chaque fichier séparément et agrège les résultats à la fin.

L'un des principaux avantages est qu'il permet le traitement en parallèle des fichiers (plusieurs threads ou processus).

L'autre avantage est que la manipulation de fichier (comme l'ajout/la suppression de dates dans l'exemple) peut être réalisée à l'aide de commandes Shell classiques, ce qui n'est pas possible avec des formats de fichier plus avancés/compliqués.

Cette approche ne couvre pas tous les scénarios, mais est très utile dans beaucoup d'entre eux.

121
user1827356

Il y a maintenant, deux ans après la question, un équivalent "hors-cœur" pandas: dask . C'est excellent! Bien que toutes les fonctionnalités de pandas ne soient pas supportées, vous pouvez aller très loin avec.

63
Private

Si vos jeux de données font entre 1 et 20 Go, vous devriez vous procurer un poste de travail doté de 48 Go de RAM. Ensuite, Pandas peut contenir l'intégralité du jeu de données dans la RAM. Je sais que ce n’est pas la réponse que vous cherchez ici, mais faire de l’informatique scientifique sur un ordinateur portable avec 4 Go de RAM n’est pas raisonnable.

60
rjurney

Je sais que c’est un vieux sujet mais je pense que la bibliothèque Blaze vaut la peine d’être vérifiée. Il est construit pour ces types de situations.

À partir de la documentation:

Blaze étend la convivialité de NumPy et Pandas à l'informatique distribuée et hors cœur. Blaze fournit une interface similaire à celle de NumPy ND-Array ou Pandas DataFrame, mais mappe ces interfaces familières sur une variété d'autres moteurs de calcul tels que Postgres ou Spark.

Edit: Au fait, il est soutenu par ContinuumIO et Travis Oliphant, auteur de NumPy.

51
chishaku

C'est le cas du pymongo. J'ai également prototypé en utilisant SQL Server, SQLite, HDF, ORM (SQLAlchemy) en python. Le pymongo est avant tout un DB à base de documents, ainsi chaque personne serait un document (dict d’attributs). Beaucoup de gens forment une collection et vous pouvez avoir plusieurs collections (personnes, marché boursier, revenus).

pd.dateframe -> pymongo Note: J'utilise le chunksize dans read_csv pour le conserver entre 5 et 10 000 enregistrements (pymongo supprime la prise si elle est plus grande)

aCollection.insert((a[1].to_dict() for a in df.iterrows()))

interrogation: gt = supérieur à ...

pd.DataFrame(list(mongoCollection.find({'anAttribute':{'$gt':2887000, '$lt':2889000}})))

.find() retourne un itérateur, donc j’utilise couramment ichunked pour découper des itérateurs plus petits.

Que diriez-vous d’une jointure puisque normalement, je dois coller 10 sources de données:

aJoinDF = pandas.DataFrame(list(mongoCollection.find({'anAttribute':{'$in':Att_Keys}})))

ensuite (dans mon cas, il faut parfois que j'agresse sur aJoinDF avant de le "fusionner".)

df = pandas.merge(df, aJoinDF, on=aKey, how='left')

Et vous pouvez ensuite écrire les nouvelles informations dans votre collection principale via la méthode de mise à jour ci-dessous. (collection logique vs sources de données physiques).

collection.update({primarykey:foo},{key:change})

Sur les petites recherches, il suffit de dénormaliser. Par exemple, vous avez du code dans le document et vous ajoutez simplement le texte du code de champ et effectuez une recherche dict lorsque vous créez des documents.

Maintenant que vous avez un ensemble de données Nice basé sur une personne, vous pouvez libérer votre logique pour chaque cas et créer plus d'attributs. Enfin, vous pouvez lire dans pandas vos 3 indicateurs clés de mémoire maximum et effectuer une exploration des pivots/agg/données. Cela fonctionne pour moi pour 3 millions d'enregistrements avec des nombres/grand texte/catégories/codes/flotteurs/...

Vous pouvez également utiliser les deux méthodes intégrées à MongoDB (MapReduce et Aggregate Framework). Voir ici pour plus d'informations sur le cadre global , car il semble être plus facile que MapReduce et semble pratique pour un travail rapide global. Remarquez que je n'ai pas besoin de définir mes champs ou relations et que je peux ajouter des éléments à un document. Dans l'état actuel des numpy, des pandas, python et de leurs mutations, MongoDB m'aide à me mettre au travail :)

46
brian_the_bungler

J'ai repéré cela un peu tard, mais je travaille avec un problème similaire (modèles de remboursement anticipé d'hypothèque). Ma solution a consisté à ignorer la couche pandas HDFStore et à utiliser des tables droites. Je sauvegarde chaque colonne en tant que tableau HDF5 individuel dans mon fichier final.

Mon flux de travail de base consiste à obtenir d’abord un fichier CSV de la base de données. Je l'ai gzip, donc ce n'est pas aussi énorme. Ensuite, je convertis cela en un fichier HDF5 orienté ligne, en l'itérant en python, en convertissant chaque ligne en un type de données réel et en l'écrivant dans un fichier HDF5. Cela prend quelques dizaines de minutes, mais il n’utilise aucune mémoire, car il ne fonctionne que rangée par rangée. Ensuite, je "transpose" le fichier HDF5 orienté ligne dans un fichier HDF5 orienté colonne.

La table transposée ressemble à:

def transpose_table(h_in, table_path, h_out, group_name="data", group_path="/"):
    # Get a reference to the input data.
    tb = h_in.getNode(table_path)
    # Create the output group to hold the columns.
    grp = h_out.createGroup(group_path, group_name, filters=tables.Filters(complevel=1))
    for col_name in tb.colnames:
        logger.debug("Processing %s", col_name)
        # Get the data.
        col_data = tb.col(col_name)
        # Create the output array.
        arr = h_out.createCArray(grp,
                                 col_name,
                                 tables.Atom.from_dtype(col_data.dtype),
                                 col_data.shape)
        # Store the data.
        arr[:] = col_data
    h_out.flush()

La relire ensuite ressemble à ceci:

def read_hdf5(hdf5_path, group_path="/data", columns=None):
    """Read a transposed data set from a HDF5 file."""
    if isinstance(hdf5_path, tables.file.File):
        hf = hdf5_path
    else:
        hf = tables.openFile(hdf5_path)

    grp = hf.getNode(group_path)
    if columns is None:
        data = [(child.name, child[:]) for child in grp]
    else:
        data = [(child.name, child[:]) for child in grp if child.name in columns]

    # Convert any float32 columns to float64 for processing.
    for i in range(len(data)):
        name, vec = data[i]
        if vec.dtype == np.float32:
            data[i] = (name, vec.astype(np.float64))

    if not isinstance(hdf5_path, tables.file.File):
        hf.close()
    return pd.DataFrame.from_items(data)

À présent, je l’utilise généralement sur une machine disposant de beaucoup de mémoire. Par conséquent, je ne suis peut-être pas assez prudent avec mon utilisation de la mémoire. Par exemple, par défaut, l'opération de chargement lit l'intégralité du jeu de données.

Cela fonctionne généralement pour moi, mais c'est un peu maladroit et je ne peux pas utiliser la magie des pytables sophistiqués.

Edit: Le véritable avantage de cette approche, par rapport au tableau de paramètres par défaut de Pytables, est que je peux ensuite charger les données dans R à l’aide de h5r, qui ne peut pas gérer les tables. Ou du moins, je n'ai pas réussi à le faire charger des tables hétérogènes.

37
Johann Hibschman

Une astuce que j'ai trouvée utile pour données volumineuses consiste à réduire le volume des données en réduisant la précision de flottement à 32 bits. Cela ne s'applique pas dans tous les cas, mais dans de nombreuses applications, la précision 64 bits est excessive et les économies de mémoire 2x réalisées en valent la peine. Pour rendre un point évident encore plus évident:

>>> df = pd.DataFrame(np.random.randn(int(1e8), 5))
>>> df.info()
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 100000000 entries, 0 to 99999999
Data columns (total 5 columns):
...
dtypes: float64(5)
memory usage: 3.7 GB

>>> df.astype(np.float32).info()
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 100000000 entries, 0 to 99999999
Data columns (total 5 columns):
...
dtypes: float32(5)
memory usage: 1.9 GB
27
ytsaig

Une autre variation

De nombreuses opérations effectuées dans pandas peuvent également être effectuées sous la forme d'une requête de base de données (sql, mongo)

L'utilisation d'un SGBDR ou de mongodb vous permet d'effectuer certaines des agrégations de la requête DB (optimisées pour les données volumineuses et qui utilisent efficacement le cache et les index).

Plus tard, vous pourrez effectuer un post-traitement à l'aide de pandas.

L'avantage de cette méthode est que vous obtenez les optimisations de base de données pour travailler avec des données volumineuses, tout en définissant la logique dans une syntaxe déclarative de haut niveau - sans avoir à vous occuper des détails pour décider quoi faire en mémoire et quoi faire. du noyau.

Et bien que le langage de requête et lespandas soient différents, il n’est généralement pas compliqué de traduire une partie de la logique de l’un en l’autre.

12
Ophir Yoktan

Il convient de mentionner ici Ray aussi,
est un framework de calcul distribué, qui a sa propre implémentation pour pandas de manière distribuée.

Il suffit de remplacer l’importation pandas et le code devrait fonctionner tel quel:

# import pandas as pd
import ray.dataframe as pd

#use pd as usual

peut lire plus de détails ici:

https://rise.cs.berkeley.edu/blog/pandas-on-ray/

10
lev

Considérez Ruffus si vous choisissez le chemin simple pour créer un pipeline de données divisé en plusieurs fichiers plus petits.

8
Golf Monkey

Je suis récemment tombé sur un problème similaire. J'ai trouvé simplement que la lecture des données dans des morceaux et l'ajout comme je l'écris dans des morceaux au même fichier CSV fonctionne bien. Mon problème consistait à ajouter une colonne de date en fonction des informations d'une autre table, en utilisant la valeur de certaines colonnes comme suit. Cela peut aider ceux qui sont déconcertés par dask et hdf5 mais qui sont plus familiers avec pandas comme moi.

def addDateColumn():
"""Adds time to the daily rainfall data. Reads the csv as chunks of 100k 
   rows at a time and outputs them, appending as needed, to a single csv. 
   Uses the column of the raster names to get the date.
"""
    df = pd.read_csv(pathlist[1]+"CHIRPS_tanz.csv", iterator=True, 
                     chunksize=100000) #read csv file as 100k chunks

    '''Do some stuff'''

    count = 1 #for indexing item in time list 
    for chunk in df: #for each 100k rows
        newtime = [] #empty list to append repeating times for different rows
        toiterate = chunk[chunk.columns[2]] #ID of raster nums to base time
        while count <= toiterate.max():
            for i in toiterate: 
                if i ==count:
                    newtime.append(newyears[count])
            count+=1
        print "Finished", str(chunknum), "chunks"
        chunk["time"] = newtime #create new column in dataframe based on time
        outname = "CHIRPS_tanz_time2.csv"
        #append each output to same csv, using no header
        chunk.to_csv(pathlist[2]+outname, mode='a', header=None, index=None)
6
timpjohns

Je voudrais souligner le paquet Vaex.

Vaex est une bibliothèque python pour les DataFrames paresseux hors cœur (similaires aux Pandas), permettant de visualiser et d'explorer de grands ensembles de données tabulaires. Il peut calculer des statistiques telles que la moyenne, la somme, le nombre, l’écart type, etc., sur une grille à N dimensions jusqu’à un milliard (109) objets/lignes par seconde. La visualisation s'effectue à l'aide d'histogrammes, de graphiques de densité et de rendu de volume 3D, permettant une exploration interactive des mégadonnées. Vaex utilise un mappage de la mémoire, une politique de copie sans mémoire et des calculs différés pour des performances optimales (aucun gaspillage de mémoire).

Consultez la documentation: https://vaex.readthedocs.io/en/latest/ L'API est très proche de l'API des pandas.

2
Rob