Comment importer des données de mongodb vers des pandas?


99

J'ai une grande quantité de données dans une collection dans mongodb que je dois analyser. Comment importer ces données dans les pandas?

Je suis nouveau aux pandas et aux numpy.

EDIT: La collection mongodb contient des valeurs de capteur étiquetées avec la date et l'heure. Les valeurs des capteurs sont de type flottant.

Exemple de données:

{
"_cls" : "SensorReport",
"_id" : ObjectId("515a963b78f6a035d9fa531b"),
"_types" : [
    "SensorReport"
],
"Readings" : [
    {
        "a" : 0.958069536790466,
        "_types" : [
            "Reading"
        ],
        "ReadingUpdatedDate" : ISODate("2013-04-02T08:26:35.297Z"),
        "b" : 6.296118156595,
        "_cls" : "Reading"
    },
    {
        "a" : 0.95574014778624,
        "_types" : [
            "Reading"
        ],
        "ReadingUpdatedDate" : ISODate("2013-04-02T08:27:09.963Z"),
        "b" : 6.29651468650064,
        "_cls" : "Reading"
    },
    {
        "a" : 0.953648289182713,
        "_types" : [
            "Reading"
        ],
        "ReadingUpdatedDate" : ISODate("2013-04-02T08:27:37.545Z"),
        "b" : 7.29679823731148,
        "_cls" : "Reading"
    },
    {
        "a" : 0.955931884300997,
        "_types" : [
            "Reading"
        ],
        "ReadingUpdatedDate" : ISODate("2013-04-02T08:28:21.369Z"),
        "b" : 6.29642922525632,
        "_cls" : "Reading"
    },
    {
        "a" : 0.95821381,
        "_types" : [
            "Reading"
        ],
        "ReadingUpdatedDate" : ISODate("2013-04-02T08:41:20.801Z"),
        "b" : 7.28956613,
        "_cls" : "Reading"
    },
    {
        "a" : 4.95821335,
        "_types" : [
            "Reading"
        ],
        "ReadingUpdatedDate" : ISODate("2013-04-02T08:41:36.931Z"),
        "b" : 6.28956574,
        "_cls" : "Reading"
    },
    {
        "a" : 9.95821341,
        "_types" : [
            "Reading"
        ],
        "ReadingUpdatedDate" : ISODate("2013-04-02T08:42:09.971Z"),
        "b" : 0.28956488,
        "_cls" : "Reading"
    },
    {
        "a" : 1.95667927,
        "_types" : [
            "Reading"
        ],
        "ReadingUpdatedDate" : ISODate("2013-04-02T08:43:55.463Z"),
        "b" : 0.29115237,
        "_cls" : "Reading"
    }
],
"latestReportTime" : ISODate("2013-04-02T08:43:55.463Z"),
"sensorName" : "56847890-0",
"reportCount" : 8
}

L'utilisation d' un type de champ personnalisé avec MongoEngine peut rendre le stockage et la récupération de Pandas DataFrames aussi simples quemongo_doc.data_frame = my_pandas_df
Jthorpe

Réponses:


133

pymongo pourrait vous donner un coup de main, voici quelques codes que j'utilise:

import pandas as pd
from pymongo import MongoClient


def _connect_mongo(host, port, username, password, db):
    """ A util for making a connection to mongo """

    if username and password:
        mongo_uri = 'mongodb://%s:%s@%s:%s/%s' % (username, password, host, port, db)
        conn = MongoClient(mongo_uri)
    else:
        conn = MongoClient(host, port)


    return conn[db]


def read_mongo(db, collection, query={}, host='localhost', port=27017, username=None, password=None, no_id=True):
    """ Read from Mongo and Store into DataFrame """

    # Connect to MongoDB
    db = _connect_mongo(host=host, port=port, username=username, password=password, db=db)

    # Make a query to the specific DB and Collection
    cursor = db[collection].find(query)

    # Expand the cursor and construct the DataFrame
    df =  pd.DataFrame(list(cursor))

    # Delete the _id
    if no_id:
        del df['_id']

    return df

Merci, c'est la méthode que j'ai fini par utiliser. J'avais également un tableau de documents intégrés dans chaque ligne. J'ai donc dû répéter cela également dans chaque ligne. Y a-t-il une meilleure manière de faire cela??
Nithin

Est-il possible de fournir des échantillons de la structure de votre mongodb?
waitkuo

3
Notez que l' list()intérieur df = pd.DataFrame(list(cursor))s'évalue comme une liste ou un générateur, pour garder le CPU au frais. Si vous avez un million d'items de données et que les quelques lignes suivantes les auraient raisonnablement divisées, au niveau de détail et coupées, le shmegegge entier est toujours en sécurité. Nice.
Phlip du

2
C'est très lent @ df = pd.DataFrame(list(cursor)). La requête de base de données pure est beaucoup plus rapide. Pouvons-nous changer le listcasting pour autre chose?
Peter.k

1
@Peter cette ligne a également attiré mon attention. Le cast d'un curseur de base de données, qui est conçu pour être itérable et encapsule potentiellement de grandes quantités de données, dans une liste en mémoire ne me semble pas intelligent.
Rafa

41

Vous pouvez charger vos données mongodb dans pandas DataFrame en utilisant ce code. Ça marche pour moi. J'espère que vous aussi.

import pymongo
import pandas as pd
from pymongo import MongoClient
client = MongoClient()
db = client.database_name
collection = db.collection_name
data = pd.DataFrame(list(collection.find()))

24

Monaryfait exactement cela, et c'est super rapide . ( un autre lien )

Voir ce post sympa qui comprend un tutoriel rapide et quelques horaires.


Monary prend-il en charge le type de données chaîne?
Snehal Parmar

J'ai essayé Monary, mais cela prend beaucoup de temps. Est-ce que je manque une optimisation? Essayé client = Monary(host, 27017, database="db_tmp") columns = ["col1", "col2"] data_type = ["int64", "int64"] arrays = client.query("db_tmp", "coll", {}, columns, data_type)pour les 50000enregistrements prend autour 200s.
nishant

Cela semble extrêmement lent ... Franchement, je ne sais pas quel est le statut de ce projet, maintenant, 4 ans plus tard ...
shx2

16

Selon PEP, simple vaut mieux que compliqué:

import pandas as pd
df = pd.DataFrame.from_records(db.<database_name>.<collection_name>.find())

Vous pouvez inclure des conditions comme vous le feriez avec la base de données mongoDB ordinaire ou même utiliser find_one () pour obtenir un seul élément de la base de données, etc.

et voila!


pd.DataFrame.from_records semble être aussi lent que DataFrame (list ()), mais les résultats sont très incohérents. %% time a montré quelque chose de 800 ms à 1,9 s
AFD

1
Ce n'est pas bon pour les enregistrements volumineux car cela ne montre pas d'erreur de mémoire, instread bloque le système pour des données trop volumineuses. tandis que pd.DataFrame (liste (curseur)) montre une erreur de mémoire.
Amulya Acharya

13
import pandas as pd
from odo import odo

data = odo('mongodb://localhost/db::collection', pd.DataFrame)

9

Pour traiter efficacement les données hors cœur (ne s'intégrant pas dans la RAM) (c'est-à-dire avec une exécution parallèle), vous pouvez essayer l' écosystème Python Blaze : Blaze / Dask / Odo.

Blaze (et Odo ) a des fonctions prêtes à l'emploi pour gérer MongoDB.

Quelques articles utiles pour commencer:

Et un article qui montre ce que des choses incroyables sont possibles avec la pile Blaze: Analyse de 1,7 milliard de commentaires Reddit avec Blaze et Impala (essentiellement, interroger 975 Go de commentaires Reddit en quelques secondes).

PS Je ne suis affilié à aucune de ces technologies.


1
J'ai également écrit un article à l' aide de Jupyter Notebook avec un exemple de la façon dont Dask aide à accélérer l'exécution même sur une mise en mémoire des données en utilisant plusieurs cœurs sur une seule machine.
Dennis Golomazov le

8

Une autre option que j'ai trouvée très utile est:

from pandas.io.json import json_normalize

cursor = my_collection.find()
df = json_normalize(cursor)

de cette façon, vous obtenez le dépliage des documents imbriqués mongodb gratuitement.


2
J'ai eu une erreur avec cette méthodeTypeError: data argument can't be an iterator
Gabriel Fair

2
Étrange, cela fonctionne sur mon python en 3.6.7utilisant des pandas 0.24.2. Peut-être pouvez-vous essayer à la df = json_normalize(list(cursor))place?
Ikar Pohorský

Pour +1. docs, l'argument max_level définit le niveau maximum de profondeur de dict. Je viens de faire un test et ce n'est pas vrai, donc certaines colonnes devraient être divisées avec des accessoires .str. Encore une fonctionnalité très intéressante pour travailler avec mongodb.
Mauricio Maroto le

5

En utilisant

pandas.DataFrame(list(...))

consommera beaucoup de mémoire si le résultat de l'itérateur / générateur est volumineux

mieux vaut générer de petits morceaux et concatter à la fin

def iterator2dataframes(iterator, chunk_size: int):
  """Turn an iterator into multiple small pandas.DataFrame

  This is a balance between memory and efficiency
  """
  records = []
  frames = []
  for i, record in enumerate(iterator):
    records.append(record)
    if i % chunk_size == chunk_size - 1:
      frames.append(pd.DataFrame(records))
      records = []
  if records:
    frames.append(pd.DataFrame(records))
  return pd.concat(frames)


1

Suite à cette excellente réponse en attendantkuo, je voudrais ajouter la possibilité de le faire en utilisant chunksize en ligne avec .read_sql () et .read_csv () . J'élargis la réponse de Deu Leung en évitant d'aller un à un chaque 'enregistrement' de l'itérateur / du 'curseur'. J'emprunterai la fonction read_mongo précédente .

def read_mongo(db, 
           collection, query={}, 
           host='localhost', port=27017, 
           username=None, password=None,
           chunksize = 100, no_id=True):
""" Read from Mongo and Store into DataFrame """


# Connect to MongoDB
#db = _connect_mongo(host=host, port=port, username=username, password=password, db=db)
client = MongoClient(host=host, port=port)
# Make a query to the specific DB and Collection
db_aux = client[db]


# Some variables to create the chunks
skips_variable = range(0, db_aux[collection].find(query).count(), int(chunksize))
if len(skips_variable)<=1:
    skips_variable = [0,len(skips_variable)]

# Iteration to create the dataframe in chunks.
for i in range(1,len(skips_variable)):

    # Expand the cursor and construct the DataFrame
    #df_aux =pd.DataFrame(list(cursor_aux[skips_variable[i-1]:skips_variable[i]]))
    df_aux =pd.DataFrame(list(db_aux[collection].find(query)[skips_variable[i-1]:skips_variable[i]]))

    if no_id:
        del df_aux['_id']

    # Concatenate the chunks into a unique df
    if 'df' not in locals():
        df =  df_aux
    else:
        df = pd.concat([df, df_aux], ignore_index=True)

return df

1

Une approche similaire comme Rafael Valero, waitkuo et Deu Leung utilisant la pagination :

def read_mongo(
       # db, 
       collection, query=None, 
       # host='localhost', port=27017, username=None, password=None,
       chunksize = 100, page_num=1, no_id=True):

    # Connect to MongoDB
    db = _connect_mongo(host=host, port=port, username=username, password=password, db=db)

    # Calculate number of documents to skip
    skips = chunksize * (page_num - 1)

    # Sorry, this is in spanish
    # https://www.toptal.com/python/c%C3%B3digo-buggy-python-los-10-errores-m%C3%A1s-comunes-que-cometen-los-desarrolladores-python/es
    if not query:
        query = {}

    # Make a query to the specific DB and Collection
    cursor = db[collection].find(query).skip(skips).limit(chunksize)

    # Expand the cursor and construct the DataFrame
    df =  pd.DataFrame(list(cursor))

    # Delete the _id
    if no_id:
        del df['_id']

    return df

0

Vous pouvez réaliser ce que vous voulez avec pdmongo en trois lignes:

import pdmongo as pdm
import pandas as pd
df = pdm.read_mongo("MyCollection", [], "mongodb://localhost:27017/mydb")

Si vos données sont très volumineuses, vous pouvez d'abord effectuer une requête agrégée en filtrant les données que vous ne souhaitez pas, puis les mapper aux colonnes souhaitées.

Voici un exemple de mappage Readings.asur colonne aet de filtrage par reportCountcolonne:

import pdmongo as pdm
import pandas as pd
df = pdm.read_mongo("MyCollection", [{'$match': {'reportCount': {'$gt': 6}}}, {'$unwind': '$Readings'}, {'$project': {'a': '$Readings.a'}}], "mongodb://localhost:27017/mydb")

read_mongoaccepte les mêmes arguments que l' agrégat pymongo

En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.