#!/usr/bin/env python
# -*- coding: utf-8 -*-
# # 2A.i - Données non structurées, programmation fonctionnelle
#
# Une table dans une base de données est déjà le résultat d'une réflexion sur la façon de les représenter.
# In[1]:
from jyquickhelper import add_notebook_menu
add_notebook_menu()
# ## Avant-propos : programmation fonctionnelle ou numpy ?
#
# * [toolz](https://github.com/pytoolz/toolz/)/[cytoolz](https://github.com/pytoolz/cytoolz) : [programmation fonctionnelle](https://fr.wikipedia.org/wiki/Programmation_fonctionnelle)
# * [numpy](http://www.numpy.org/) : [calcul matriciel](https://fr.wikipedia.org/wiki/Matrice_(math%C3%A9matiques))
#
# Données : [twitter_for_network_100000.db.zip](https://drive.google.com/open?id=0B6jkqYitZ0uTQ3k1NDZmLUJBZVk) or [twitter_for_network_100000.db.zip](http://www.xavierdupre.fr/enseignement/complements/twitter_for_network_100000.db.zip) (xavierdupre.fr).
# In[2]:
import pyensae.datasource
pyensae.datasource.download_data("twitter_for_network_100000.db.zip")
# In[3]:
import numpy as np
def my_sum(l):
res = 0
for it in l:
res += it
return res
l = list(range(100000))
a = np.arange(100000)
print("User defined method or cross-method")
get_ipython().run_line_magic('timeit', 'my_sum(a) # user defined with numpy array')
get_ipython().run_line_magic('timeit', 'sum(a) # built-in with numpy array')
get_ipython().run_line_magic('timeit', 'np.sum(l) # numpy function with list')
get_ipython().run_line_magic('timeit', 'my_sum(l) # user definedwith list')
print("Builtin function")
get_ipython().run_line_magic('timeit', 'sum(l) # built-in with list')
print("Numpy function")
get_ipython().run_line_magic('timeit', 'np.sum(a) # numpy function')
get_ipython().run_line_magic('timeit', 'a.sum() # numpy method')
# Il y a un rapport de 10 dans le temps d'exécution entre la méthode **"user defined"** et la méthode **"builtin"**.
# On retrouve ce même rapport entre la méthode "builtin" et les méthodes numpy.
# On peut noter que mélanger les objets numpy et non-numpy donne de très mauvais résultats.
#
# **Dans le cas de la programmation fonctionnelle, nous nous situerons plutôt dans le cas "builtin"** :
#
Project | Computation | Data Structures |
---|
Code de l'utilisateur | Python | Python |
CyToolz | C | Python |
Pandas/NumPy | C | C |
# Dans le cas de manipulation de donnée structurée de taille "raisonnable", [pandas](http://pandas.pydata.org/) et [numpy](http://www.numpy.org/) restent plus performants. Ils peuvent toutefois être limités par plusieurs points :
#
# - manipulation de données plus complexes avec des sous-listes, des champs manquants
# - ils sont construits sur le principe de chargement en mémoire des données
# In[4]:
import os, psutil, gc, sys
if not sys.platform.startswith("win"):
import resource
def memory_usage_psutil():
gc.collect()
process = psutil.Process(os.getpid())
mem = process.memory_info()[0] / float(2 ** 20)
print( "Memory used : %i MB" % mem )
if not sys.platform.startswith("win"):
print( "Max memory usage : %i MB" % (resource.getrusage(resource.RUSAGE_SELF).ru_maxrss//1024) )
# In[5]:
memory_usage_psutil()
# In[6]:
import cytoolz as ct # import groupby, valmap, compose
import cytoolz.curried as ctc ## pipe, map, filter, get
import sqlite3
import pprint
try:
import ujson as json
except:
print("ujson not available")
import json
conn_sqlite = sqlite3.connect("twitter_for_network_100000.db")
cursor_sqlite = conn_sqlite.cursor()
# Le code suivant va lire plusieurs gigaoctets de données, et la consommation maximale de mémoire du process ne va augmenter que de quelques Mo. De plus ce code manipule des dictionnaires qu'il serait compliqué de faire rentrer dans un [DataFrame pandas](http://pandas.pydata.org/pandas-docs/stable/generated/pandas.DataFrame.html).
# In[7]:
cursor_sqlite.execute('SELECT content FROM tw_users' )
object_to_sum = ctc.pluck( "followers_count", ctc.map( json.loads, ctc.pluck( 0, cursor_sqlite ) ) )
print(sum(object_to_sum))
# In[8]:
memory_usage_psutil()
# Dans le cadre du TP d'aujourd'hui, les données que nous allons utiliser peuvent largement tenir en mémoire, et de façon générale, lorsqu'on développe des codes pour gérer des gros volumes de données, on les teste sur des volumes de données qui tiennent en mémoire.
#
# Dans le cadre de la gestion de volume important de données, on ne pourra donc pas stocker des résultats intermédiaire, on va donc composer des fonctions pour qu'elles produisent directement le résultat final.
#
# Cas classique :
#
# ```
# resultat_intermediaire_1 = f( donnees )
# resultat_intermediaire_2 = g( resultat_intermediaire_1 )
# resultat_final = h( resultat_intermediaire_2 )
# ```
#
# Programmation fonctionnelle :
#
# ```
# resultat_final = h( g( f( donnees ) ) )
# ```
# # Données structurées SQL et NOSQL
# [SQL](https://fr.wikipedia.org/wiki/Structured_Query_Language) signifie *Structured Query Language*, les tables ont un nombre de colonnes fixes, et chaque colonne possède un type particulier.
# Comment gère-t-on un nombre d'objets variables ? La plupart du temps avec une table secondaire qui contiendra une ligne par élément de la liste.
#
# Par exemple, si l'on veut stocker la liste des films possédés par une personne.
# Table person | | Table related_items | Id | Person_id | Value |
---|
1 | 1 | Star wars | 2 | 1 | Cyrano | 3 | 1 | Lord of the rings | 4 | 2 | Mad max | 5 | 2 | Dr Horrible |
|
# Ce système est très "structuré" (comme son nom l'indique) et peut s'avérer assez lourd si l'on a affaire à des données moins bien structurées, avec beaucoup de listes, des données présentes ou non.
#
# On voit donc se développer de plus en plus des systèmes alternatifs, dit [NoSQL](https://fr.wikipedia.org/wiki/NoSQL) (pour Not Only Sql).
#
# Nous allons en voir trois :
#
# - [Sqlite3](https://docs.python.org/3/library/sqlite3.html) support for [Json](https://fr.wikipedia.org/wiki/JavaScript_Object_Notation)
# - [mongodb](https://www.mongodb.org/)
# - [PostGreSql](http://www.postgresql.org/)
#
# Ils sont analogues sur la nature des données stockées, elles le sont au format [Json](https://fr.wikipedia.org/wiki/JavaScript_Object_Notation).
# ## Json ? Qu'est ce que c'est ?
# Il s'agit du format majoritaire pour les API informatiques internet.
#
# Par exemple les données renvoyées par twitter (sur lesquelles nous travaillerons aujourd'hui) sont sous ce format.
#
# Il signifie : [JavaScript Object Notation](https://fr.wikipedia.org/wiki/JavaScript_Object_Notation).
# Il se base essentiellement sur des dictionnaires (association clé/valeur) et des listes.
# Il est très proche des objets python (modulo les false/False)
# In[9]:
import pprint
cursor_sqlite.execute('SELECT content FROM tw_users LIMIT 1')
user = cursor_sqlite.fetchone()[0]
print("#"*15 + " user raw json " + "#"*15)
print( user )
print("#"*15 + " user as python dict " + "#"*15)
pprint.pprint( json.loads( user ) )
cursor_sqlite.execute('SELECT content FROM tw_status LIMIT 1')
print("#"*15 + " status as python dict " + "#"*15)
pprint.pprint( json.loads( cursor_sqlite.fetchone()[0] ) )
# Le NoSql / json permet donc une alternative au schéma classique suivant :
# Table person | | Table related_items | Id | Person_id | Value |
---|
1 | 1 | Star wars | 2 | 1 | Cyrano | 3 | 1 | Lord of the rings | 4 | 2 | Mad max | 5 | 2 | Dr Horrible |
|
# Qui serait :
# Table person_with_items | Id | Name | Item_list |
---|
1 | Jean | ['Star wars', 'Cyrano', 'Lord of the rings'] | 2 | Paul | ['Mad max', 'Dr Horrible'] | 3 | Jacques | |
|
# Cette dernière structure serait vraiment un exemple de nosql dans son sens de Not Only Sql, il y a un mixe de données structurées et non structurées.
# Il y a également certaines base de données où il n'y a plus du tout de structure, comme mongodb, qui est qualifiée de document-oriented.
# Table person_with_items | {'Id': 1, 'Name': 'Jean', 'Item_list' : ['Star wars', 'Cyrano', 'Lord of the rings']} | {'Id': 2, 'Name': 'Paul', 'Item_list' : ['Mad max', 'Dr Horrible']} | {'Id': 3, 'Name': 'Jacques', 'Item_list' : []} |
|
# Il faut toutefois remarquer que cette dernière structure au moins deux inconvénients par rapport à une structure Sql avec une sous-table :
#
# - vous ne pouvez pas accéder directement aux objets de 'Item_list' sans passer par la table person
# - les informations de Item_list ne peuvent être partagées entre plusieurs objets, on peut donc avoir à restocker les informations
#
# Voir module [psycopg](https://pypi.python.org/pypi/psycopg2).
# In[10]:
try:
import psycopg2
from psycopg2.extras import Json
postgre_ok = True
except ImportError:
postgre_ok = False
if postgre_ok:
db_name = 'cours_ensae'
conn_string = "host='localhost' dbname='{0}' user='python' password='kyojin'".format( db_name )
try:
conn_psql = psycopg2.connect(conn_string)
cursor_psql = conn_psql.cursor()
postgre_ok = True
except psycopg2.OperationalError:
postgre_ok = False
# In[11]:
if postgre_ok:
conn_psql.server_version
# In[12]:
if postgre_ok:
conn_psql.rollback()
# In[13]:
if postgre_ok:
def get_data_sql(doc_id):
cursor_psql.execute("SELECT id, company FROM document WHERE id = %s", (doc_id,))
res_1 = cursor_psql.fetchone()
cursor_psql.execute("SELECT id FROM ticket WHERE document_id = %s ORDER BY id", (doc_id,))
res_2 = cursor_psql.fetchall()
tickets_id = [it[0] for it in res_2 ]
cursor_psql.execute("SELECT id FROM coupon WHERE ticket_id = ANY( %s ) ORDER BY id", (tickets_id,))
res_3 = cursor_psql.fetchall()
return res_1 + (res_2,) + (res_3,)
get_ipython().run_line_magic('timeit', 'get_data_sql(10000)')
get_data_sql(10000)
# In[14]:
if postgre_ok:
def get_data_sql_join(doc_id):
cursor_psql.execute("SELECT d.id, d.company, t.id, c.id FROM document as d \
JOIN ticket as t on d.id = t.document_id \
JOIN coupon as c on t.id = c.ticket_id \
WHERE d.id = %s", (doc_id,))
return cursor_psql.fetchall()
get_ipython().run_line_magic('timeit', 'get_data_sql_join(10000)')
get_data_sql_join(10000)
# In[15]:
if postgre_ok:
def get_data_nosql(doc_id):
cursor_psql.execute("SELECT id, company, content FROM document_nosql WHERE id = %s", (doc_id,))
return cursor_psql.fetchone()
get_ipython().run_line_magic('timeit', 'get_data_nosql(10000)')
get_data_nosql(10000)
# [mongodb](https://www.mongodb.org/) ([pymongo](https://api.mongodb.org/python/current/)) lui ,ne connait pas de colonnes, que des documents, dont le format est analogue à un objet json.
# Cela se traduit par une très grande simplicité, pas besoin de déclarer les tables, ni les bases de données ...
# In[16]:
mongo = False
if mongo:
import pymongo
mongo_client = pymongo.MongoClient( 'localhost', 27017 )
mongo_db = mongo_client.ensae_db
mongo_db.table_for_ensae.delete_many( {} )
mongo_db.table_for_ensae.insert_one( {'nom' : 'Martin', 'prenom' : 'Nicolas', 'grades': [20,18,7,12]} )
mongo_db.table_for_ensae.insert_one( {'nom' : 'Dupont', 'prenom' : 'Jean', 'grades': [11,5,7,12]} )
mongo_db.table_for_ensae.insert_one( {'nom' : 'Martin', 'prenom' : 'Gilles', 'grades': [10,10,10,10]} )
user = mongo_db.table_for_ensae.find_one( {'nom' : 'Dupont'} )
user_list = mongo_db.table_for_ensae.find( {} )
_ = list(map( pprint.pprint, user_list ))
# Par contre certaines syntaxes usuelles en sql, ici le groupby, ont une écriture nettement plus complexes en mongodb.
# In[17]:
if mongo:
result = mongo_db.table_for_ensae.group(['nom'],
None,
{'list': []}, # initial
'function(obj, prev) {prev.list.push(obj)}')
pprint.pprint( result )
# Mon retour :
#
# - [mongodb](https://www.mongodb.org/) malgré sa simplicité d'utilisation peut être très gourmand en ressources (consommation d'espace disque et/ou mémoire 15 fois supérieure à [PostGreSql](http://www.postgresql.org/) pour les mêmes données). Je la déconseille pour une application personnelle.
# - [Sqlite3](https://docs.python.org/3/library/sqlite3.html) est plus archaïque, mais le fait d'avoir une base de donnée contenue dans un fichier est très pratique pour certains usages (déploiement chez un client ou pour des élèves)
# - [PostGreSql](http://www.postgresql.org/) me semble le plus robuste pour un usage en serveur personnel.
# ## Et les fichiers plats ?
# Vous pouvez tout à fait utiliser des fichiers plats.
# Ils offrent beaucoup de simplicités d'utilisation.
# Ils auront des performances potentiellement très proches pour une lecture complète.
# In[18]:
cursor_sqlite.execute("SELECT content FROM tw_users LIMIT 10000" )
with open("tw_users.json", 'w') as f:
for it_user in cursor_sqlite:
f.write(it_user[0])
f.write("\n")
with open("tw_users.json", 'r') as f:
nb_total_followers = 0
for it_user in f:
nb_total_followers += json.loads( it_user )["followers_count"]
print( nb_total_followers )
# ## Et pandas ?
#
# Pandas attend lui des données plus ou moins structurées. Vous pouvez charger une base sous pandans avec la syntaxe suivante :
# In[19]:
import pandas as pd
df = pd.read_sql( "SELECT id, screen_name from tw_users", conn_sqlite )
print( df.head() )
print( df.shape )
# In[20]: