Question Flux de travail «grandes données» utilisant des pandas


J'ai essayé de trouver une réponse à cette question pendant plusieurs mois tout en apprenant des pandas. J'utilise SAS pour mon travail de tous les jours et c'est formidable pour son support hors-noyau. Cependant, SAS est horrible en tant que logiciel pour de nombreuses autres raisons.

Un jour, j'espère remplacer mon utilisation de SAS par python et pandas, mais je n'ai pas de workflow pour les grands ensembles de données. Je ne parle pas de «big data» qui nécessite un réseau distribué, mais plutôt des fichiers trop volumineux pour tenir dans la mémoire mais assez petits pour tenir sur un disque dur.

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

Quels sont les meilleurs workflows pour accomplir les tâches suivantes:

  1. Chargement de fichiers plats dans une structure de base de données permanente sur disque
  2. Interrogation de cette base de données pour extraire des données à alimenter dans une structure de données pandas
  3. Mise à jour de la base de données après manipulation de pièces sur des pandas

Des exemples du monde réel seraient très appréciés, surtout de la part de quiconque utilise des pandas sur des «données volumineuses».

Edit - un exemple de comment je voudrais que cela fonctionne:

  1. Importez de manière itérative un grand fichier plat et stockez-le dans une structure de base de données permanente sur disque. Ces fichiers sont généralement trop volumineux pour tenir dans la mémoire.
  2. Afin d'utiliser Pandas, je voudrais lire des sous-ensembles de ces données (généralement juste quelques colonnes à la fois) qui peuvent tenir dans la mémoire.
  3. Je créerais de nouvelles colonnes en effectuant diverses opérations sur les colonnes sélectionnées.
  4. Je devrais alors ajouter ces nouvelles colonnes dans la structure de la base de données.

J'essaie de trouver une façon pratique d'effectuer ces étapes. En lisant des liens sur les pandas et les pytables, il semble que l'ajout d'une nouvelle colonne pourrait poser problème.

Éditer - 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 comprennent les caractéristiques de téléphone, de SSN et d'adresse; valeurs de propriété; des données désobligeantes comme des casiers judiciaires, des faillites, etc ... Les jeux de données que j'utilise chaque jour ont près de 1.000 à 2.000 champs en moyenne de types de données mixtes: variables continues, nominales et ordinales de données numériques et de caractères. J'ajoute rarement des lignes, mais j'effectue de nombreuses opérations qui créent de nouvelles colonnes.
  2. Les opérations typiques impliquent de combiner plusieurs colonnes en utilisant une 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 ensemble de données.
  3. Enfin, je voudrais ajouter ces nouvelles colonnes dans 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 au modèle.
  4. Un fichier de projet typique est généralement d'environ 1 Go. Les fichiers sont organisés de telle sorte qu'une rangée consiste en un enregistrement de données de consommation. 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-ensembles par des lignes lors de la création d'une nouvelle colonne. Cependant, il m'est assez courant de sous-créer des lignes lors de la création de rapports ou de la génération de statistiques descriptives. Par exemple, je pourrais vouloir créer une fréquence simple pour un secteur d'activité spécifique, disons les cartes de crédit au détail. Pour ce faire, je sélectionne uniquement les enregistrements dont le secteur d'activité = vente au détail, en plus des colonnes sur lesquelles je souhaite créer un rapport. Lors de la création de nouvelles colonnes, cependant, je tirerais toutes les lignes de données et seulement les colonnes dont j'ai besoin pour les opérations.
  6. Le processus de modélisation nécessite que j'analyse chaque colonne, que je recherche des relations intéressantes avec une variable de résultat et que je crée de nouvelles colonnes composées décrivant ces relations. Les colonnes que j'explore sont généralement faites en petits ensembles. Par exemple, je vais me concentrer sur un ensemble de 20 colonnes traitant simplement des valeurs de propriété et observer comment elles se rapportent à la défaillance sur un prêt. Une fois que ceux-ci sont explorés et de nouvelles colonnes sont créées, je passe ensuite à un autre groupe de colonnes, disons l'éducation collégiale, et répète le processus. Ce que je fais est de 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 des lignes à l'ensemble de données. Je vais presque toujours créer de nouvelles colonnes (variables ou caractéristiques dans le langage statistique / apprentissage automatique).


733
2018-01-10 16:20


origine


Réponses:


J'utilise couramment des dizaines de gigaoctets de données de cette façon par exemple. J'ai des tables sur disque que j'ai lues via des requêtes, créer des données et rajouter.

Ça vaut le coup de lire les docs et tard dans ce fil pour plusieurs suggestions sur la façon de stocker vos données.

Détails qui affecteront la façon dont vous stockez vos données, comme:
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; ajoutez-vous lignes, ou juste des colonnes?
  2. À quoi ressembleront les opérations typiques? Par exemple. faire une requête sur les colonnes pour sélectionner un groupe de lignes et de colonnes spécifiques, puis effectuer une opération (en mémoire), créer de nouvelles colonnes, les sauvegarder.
    (Donner un exemple de jouet pourrait nous permettre d'offrir des recommandations plus spécifiques.)
  3. Après ce traitement, alors que faites-vous? L'étape 2 est-elle ad hoc ou répétable?
  4. Entrer les fichiers plats: combien, taille totale approximative en Gb. Comment sont-ils organisés par exemple? par des enregistrements? Est-ce que chacun contient des champs différents, ou ont-ils 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)? puis faites quelque chose, ou sélectionnez-vous simplement les champs A, B, C avec tous les enregistrements (et ensuite faites quelque chose)?
  6. Travaillez-vous sur toutes vos colonnes (en groupes), ou y a-t-il une bonne proportion que vous ne pouvez utiliser que pour les rapports (par exemple, vous voulez conserver les données, mais vous n'avez pas besoin d'extraire temps de résultat final)?

Solution

Assurez-vous d'avoir pandas au moins 0.10.1 installée.

Lis itérer les fichiers par morceaux et plusieurs requêtes de table.

Puisque pytables est optimisé pour fonctionner en ligne (ce que vous recherchez), 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 (qui fonctionnera avec une grande table, mais il est plus efficace de le faire de cette façon ... Je pense que je pourrai peut-être corriger cette limite dans le futur ... c'est plus intuitif de toute façon):
(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'] ]))

Lire dans les fichiers et créer le stockage (essentiellement faire quoi append_to_multiple Est-ce que):

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 dans le 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 post_processing:

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

A propos de data_columns, vous n'avez pas besoin de définir TOUT data_columns; Ils vous permettent de sélectionner des lignes en fonction de la colonne. Par exemple. quelque chose comme:

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

Ils peuvent vous intéresser au stade de la génération du rapport final (essentiellement, une colonne de données est séparée des autres colonnes, ce qui peut avoir un impact sur l'efficacité si vous définissez beaucoup).

Vous pourriez aussi vouloir:

  • créez une fonction qui prend une liste de champs, recherche les groupes dans groups_map, puis les sélectionne et concatène les résultats pour obtenir l'image résultante (c'est essentiellement ce que fait select_as_multiple). De cette façon, la structure serait assez transparente pour vous.
  • indexes sur certaines colonnes de données (rend la segmentation des lignes beaucoup plus rapide).
  • activer la compression.

Faites-moi savoir quand vous avez des questions!


466
2018-01-10 22:57



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

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

Exemple: Dans le cas de 30 jours d'échange de données d'une taille de ~ 30 Go, je le casse dans un fichier par jour de ~ 1 Go de taille. Je traite ensuite chaque fichier séparément et regroupe les résultats à la fin

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

L'autre avantage est que la manipulation de fichiers (comme l'ajout / suppression de dates dans l'exemple) peut être accomplie par des commandes shell régulières, ce qui n'est pas possible dans des formats de fichiers plus avancés / compliqués

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


100
2017-12-19 19:46



Si vos jeux de données sont entre 1 et 20 Go, vous devriez obtenir un poste de travail avec 48 Go de RAM. Ensuite, les Pandas peuvent contenir tout le jeu de données en RAM. Je sais que ce n'est pas la réponse que vous cherchez ici, mais faire du calcul scientifique sur un ordinateur portable avec 4 Go de RAM n'est pas raisonnable.


53
2017-11-02 07:14



Il y a maintenant, deux ans après la question, un équivalent de pandas «out-of-core»: dask. C'est excellent! Bien qu'il ne supporte pas toutes les fonctionnalités de pandas, vous pouvez aller très loin avec lui.


48
2018-03-23 20:30



Je sais que c'est un vieux fil mais je pense que le Flamber bibliothèque vaut le détour. Il est construit pour ces types de situations.

De la docs:

Blaze étend l'utilisation de NumPy et Pandas à l'informatique distribuée et hors-noyau. Blaze fournit une interface similaire à celle de NumPy ND-Array ou Pandas DataFrame mais fait correspondre ces interfaces familières à une variété d'autres moteurs de calcul tels que Postgres ou Spark.

Modifier: En passant, il est soutenu par ContinuumIO et Travis Oliphant, auteur de NumPy.


47
2017-12-03 22:09



C'est le cas du pymongo. J'ai aussi prototypé en utilisant sql server, sqlite, HDF, ORM (SQLAlchemy) en python. Tout d'abord pymongo est un document basé sur DB, de sorte que chaque personne serait un document (dict des attributs). Beaucoup de gens forment une collection et vous pouvez avoir de nombreuses collections (personnes, bourse, revenu).

pd.dateframe -> pymongo Note: J'utilise le chunksize dans read_csv pour le garder à 5 à 10k enregistrements (pymongo laisse tomber la prise si elle est plus grande)

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

interroger: gt = plus grand que ...

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

.find() retourne un itérateur donc j'utilise couramment ichunked couper en itérateurs plus petits.

Que diriez-vous d'une jointure puisque je reçois normalement 10 sources de données à coller ensemble:

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

alors (dans mon cas, parfois je dois agg sur aJoinDF d'abord avant son "fusionnable".)

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

Et vous pouvez ensuite écrire les nouvelles informations à 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 recherches plus petites, 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 faire un dict recherche lorsque vous créez des documents.

Maintenant, vous avez un bon jeu de données basé sur une personne, vous pouvez libérer votre logique sur chaque cas et faire plus d'attributs. Enfin, vous pouvez lire dans les pandas vos 3 indicateurs de mémoire maximum et faire des pivots / agg / exploration de données. Cela fonctionne pour moi pour 3 millions d'enregistrements avec des nombres / gros texte / catégories / codes / flotteurs / ...

Vous pouvez également utiliser les deux méthodes intégrées à MongoDB (MapReduce et framework global). 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. Notez que je n'ai pas besoin de définir mes champs ou relations, et je peux ajouter des éléments à un document. À l'état actuel de l'ensemble des outils PNMP, pandas, python qui change rapidement, MongoDB m'aide à me mettre au travail :)


43
2018-01-11 22:11



Je l'ai repéré un peu en retard, mais je travaille avec un problème similaire (modèles de prépaiement hypothécaire). Ma solution a été d'ignorer la couche HDFStore de pandas et d'utiliser des pytables droits. 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 à partir de la base de données. Je le gomme, donc ce n'est pas aussi énorme. Ensuite, je convertis cela en un fichier HDF5 orienté ligne, en l'itérant sur 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 pas de mémoire, car il ne fonctionne que ligne par ligne. Ensuite, je "transpose" le fichier HDF5 orienté lignes dans un fichier HDF5 orienté colonnes.

La table transpose 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 ressemble alors à:

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)

Maintenant, je l'exécute généralement sur une machine avec une tonne de mémoire, donc 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'ensemble des données.

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

Edit: Le vrai avantage de cette approche, par rapport à la valeur par défaut de pytables du tableau d'enregistrements, est que je peux ensuite charger les données dans R en utilisant h5r, qui ne peut pas gérer les tables. Ou, au moins, j'ai été incapable de l'obtenir pour charger des tables hétérogènes.


36
2018-03-21 21:19



Une autre variation

De nombreuses opérations effectuées sur des pandas peuvent également être effectuées en tant que requête db (sql, mongo)

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

Plus tard, vous pouvez effectuer un post-traitement en utilisant des pandas.

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

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


12
2018-04-28 05:22



Une astuce que j'ai trouvée utile pour les cas d'utilisation de «grandes données» est de réduire le volume des données en réduisant la précision du flottant à 32 bits. Il n'est pas applicable dans tous les cas, mais dans de nombreuses applications, la précision 64 bits est excessive et les économies de mémoire 2x 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

11
2018-03-26 05:59



Considérer Ruffus si vous allez le chemin simple de créer un pipeline de données qui est divisé en plusieurs fichiers plus petits.


7
2017-10-09 19:07