sql – Sam & Max http://sametmax.com Du code, du cul Wed, 30 Oct 2019 15:34:04 +0000 en-US hourly 1 https://wordpress.org/?v=4.9.7 32490438 Les critiques des ORM sont à côté de la plaque http://sametmax.com/les-critiques-des-orm-sont-a-cote-de-la-plaque/ http://sametmax.com/les-critiques-des-orm-sont-a-cote-de-la-plaque/#comments Fri, 29 Dec 2017 10:31:05 +0000 http://sametmax.com/?p=23967 En ce moment, y a deux modes. Dire que les cryptomonnaies c’est génial, et dire que les ORM c’est de la merde.

Durant les derniers articles, je pense qu’on vous a assez parlé de crypto, donc on va parler des ORM.

Ou ORMs. Je sais jamais si les acronymes s’accordent.

Rappel: qu’est-ce qu’un ORM ?

Si vous avez lu le titre de l’article et que votre sang n’a fait qu’un tour, vous pouvez passer tout de suite à la partie suivante, puisqu’on va commencer par les révisions.

Un ORM, pour Object-relational Mapping, est une bibliothèque qui permet de décrire son modèle de données, et d’utiliser cette description pour faire le lien entre votre application et une base de données relationnelle. Dans sa forme la plus courante, l’ORM fournit les outils pour générer des requêtes SQL – sans écrire du SQL – et les exécuter, mais présente également les résultats sous forme d’objets du langage dans lequel il est écrit.

Par exemple, en Python, je peux faire de la base de données à la mano (sans ORM):

import sqlite3

# Création de la base
with sqlite3.connect('db.sqlite') as conn:

    # On se positionne sur la base
    c = conn.cursor()

    # Créer la table
    c.execute('''
        CREATE TABLE product (
            name text,
            price real
        )
    ''')

    # Insertion de données en base
    c.execute("INSERT INTO product VALUES ('Pizza', 5000)")
    c.execute("INSERT INTO product VALUES ('Love', 150)")
    c.execute("INSERT INTO product VALUES ('Nessie', 3.5)")

    # Sauvegarde des changements
    conn.commit()

    # Lecture de toute la base:
    for row in c.execute('SELECT * FROM product'):
        print(type(row), ':', *row)

Ce qui va me sortir:

python3 db.py
< class 'tuple' > : Pizza 5000.0
< class 'tuple' > : Love 150.0
< class 'tuple' > : Nessie 3.5

Et voici la même chose avec l’ORM peewee (apres pip install peewee :)):

import peewee as pw

# On créé la base
db = pw.SqliteDatabase('product2.db')

# Description de la table.
class Product(pw.Model):

    name = pw.CharField()
    price = pw.FloatField()

    class Meta:
        database = db

# Connection et création de la table
db.connect()
db.create_tables([Product])

# Insertion de données en base
Product.create(name='Pizza', price=5000)
Product.create(name='Love', price=150)
Product.create(name='Nessie', price=3.5)

for p in Product.select():
    print(type(p), ':', p.name, p.price)

db.close()

Ce qui sort:

python3 db.py
< class '__main__.Product' > : Pizza 5000.0
< class '__main__.Product' > : Love 150.0
< class '__main__.Product' > : Nessie 3.5

A priori, c’est la même chose, mais avec un style différent. Le premier exemple favorise l’écriture du SQL à la main, fait les requêtes explicitement et récupère les résultats sous forme de tuples. Le second a une surcouche, l’ORM, qui implique que tout est décrit en Python. Le SQL est généré sous le capot, et on récupère les résultats sous forme d’objets Product.

Il existe de nombreux ORM en Python, les plus populaires étant Peewee (pour les petits besoins), SQLAlchemy (pour les gros projets) et l’ORM de Django (ben, si vous utilisez Django ^^). Évidemment le concept des ORM n’est pas particulièrement lié à Python, et on en retrouve dans tous les langages, avec des variations de styles et de fonctionnalités.

Que reproche-t-on aux ORM ?

Avant de répondre aux détracteurs, listons d’abord leurs arguments.

  • C’est un niveau d’indirection de plus, avec de l’implicite et de la magie.
  • L’ORM va générer des requêtes non optimisées, voire lentes.
  • SQL est un excellent DSL spécialisé dans les requêtes. Au lieu d’apprendre une API de plus, autant aller à la source.
  • SQL marche pareil d’un client à l’autre, donc la connaissance est réutilisable, contrairement à celle de son ORM.
  • C’est une béquille pour ne pas apprendre le SQL, et par ailleurs va amener tot ou tard les ignorants à se tirer une balle dans le pied.
  • Les ORMs ne peuvent pas permettre l’usage de toutes les fonctionnalités de sa base de données, et donc limitent la puissance qu’on en tire.
  • Ça ne scale pas. Les gros sites, comme Instagram qui est écrit en Django, on tous finit par virer leurs ORM.

Et vous savez quoi ?

Ils ont raison.

Heu ?

Toutes ces critiques sont parfaitement justifiées.

Sauf qu’elles passent complètement à côté de la problématique.

Les ORM ne servent pas à éviter d’écrire du SQL.

Ça, c’est vaguement un effet de bord.

Ils servent à créer une API unifiée inspectable, une expérience homogène, un point d’entrée unique, un socle de référence explicite et central, pour le modèle de données.

Une fois qu’on a passé pas mal de temps à faire des projets, on note toujours la même chose: certaines parties du logiciel sont réécrites encore et encore. Si un projet commence à parler à une source de données à la main (API, base de données, crawling, fichiers, etc), tôt ou tard, quelqu’un dans l’équipe va commencer à écrire une abstraction. Celle-ci va grossir, et finir par implémenter au bout de nombreux mois, pour l’accès à la base de données, un semi ORM dégueulasse, mal testé / documenté.

Mais, si les requêtes SQL à la main c’est si bien, alors pourquoi ça arrive ?

Simplement parce que tout projet qui grandit a besoin d’une forme de gestion de la complexité. Ca passe par avoir un point, et un seul où sont décrites à quoi ressemblent les données, leurs accès, leurs garanties, leurs contraintes, et leurs validations. Ca passe aussi par permettre aux autres parties du projet d’utiliser automatiquement ces informations pour leur code métier, ainsi que la logique associée.

L’exemple de Django

L’ORM Django n’est pas vraiment le projet Python le plus propre. Il a plein de limitations et bizarreries, et son principal concurrent, SQLAlchemy est probablement une des meilleures libs au monde dans cette spécialité.

Mais !

Il est au coeur de ce qui a fait autant le succès colossal de Django.

Parce que Django est organisé autour de son ORM, il peut proposer:

  • Des validateurs générés automatiquement qui permettent de valider toute saisie de données, et sauvegarder les changements en base de données. Si besoin, il peuvent générer des formulaires HTML afin de proposer une saisie utilisateur automatiquement contrôlée et nettoyée, avec messages d’erreurs pre-traduits.
  • Des vues pour lire, lister, mettre à jour et supprimer les données, générées automatiquement. Y a plus qu’à mettre du HTML autour.
  • Une admin de base de données autogénérée. Un backend gratos et customisable pour votre projet.
  • Une pléthore d’outils pour gérer les données: signals, getters, auto-castage en objet natifs, validation avancées, etc.
  • Des points d’entrées pour étendre la manipulation de ces données de manière générique (fields, managers, etc).
  • De l’outillage pour les migrations.
  • Un worflow d’authentification, d’identification, de session, de cache et de permissions.
  • La normalisation automatique des valeurs: encoding, fuseaux horaires, devises, format de textes et nombres. Et les points d’entrées pour écrire les siens pour plugger ceux de quelqu’un d’autre.

A cela se rajoute le fait que tous les projets Django se ressemblent. Il est très facile de passer d’une équipe à une autre, d’un projet à une autre. La formalisation du schéma devient une documentation, et la seule source de la vérité à propos des données, et pas juste celle de la base. Et qui est commité dans Git. Pour savoir ce que fait un projet Django, il suffit de regarder urls.py, settings.py et models.py, et c’est parti.

Mais ce n’est pas tout. L’ORM ne fait pas que définir un point central inspectable et des outils réutilisables. Il donne aussi une base commune sur laquelle tout le monde peut compter.

Et pour cette raison, l’écosystème Django est très, très riche en modules tierces partis qui se pluggent en 3 coups de cuillère à pot:

La cerise sur le gâteau ? Parce que tout ça utilise l’ORM, tout ça est compatible ensemble. Votre authentification social auth va produire un objet user qui pourra se logger, consulter un dashboard qui contiendra le résultat d’un sondage sur un produit de la boutique.

Et ça marche avec MySQL, Oracle, SQLite ou PosgreSQL, comme l’intégralité du framework, gratos.

Ce n’est pas l’apanage de Django hein, RoR fait pareil.

Maintenant prenez un projet NodeJS. Pour le coup, pas parce que “JS ça pue” mais parce que la culture de l’ORM n’est pas très présente dans cette communauté. Non pas que ça n’existe pas, mais il n’y a pas de Django du monde du Javascript. Même les gros framework type Meteor n’ont rien d’aussi intégré.

Vous allez devoir réapprendre toute la mécanique de gestion de données si vous changez de projet, car ça sera fait différemment à chaque fois. Vous allez devoir former des gens.

Et surtout vous allez devoir réécrire tout ça.

Oh bien sûr, vous aurez une bibliothèque pour chaque chose, mais elle sera écrite différemment. Vous n’aurez pas d’objet User sur qui compter. Votre moyen de traduire le texte ? Pas le même. Vous utilisez Oracle mais l’auteur PostgreSQL ? Pas de bol. Vous voulez générer quelque chose à partir de votre modèle de données ? Ah, pourquoi vous croyez que facebook a créé GraphQL ! Une petite migration ? Vous avez le choix de l’embarras. Bon, maintenant vous allez gérer les dates, et 4 de vos bibliothèques les sérialisent différemment et une utilise l’heure locale au lieu d’UTC.

Évidemment, on sait que votre équipe ne testera pas tout ça, et ne documentera pas tout ça. La suivante non plus.

Donc non, l’ORM, ce n’est pas parce que “mais heu SQL c’est dur”.

C’est parce que ça permet de créer un monde autour.

Objection !

On a des abstractions qui ne sont pas des ORM…

L’important est d’avoir un modèle central, pas un ORM. Mais les ORM font ça particulièrement bien.

Il existe des abstractions (ex: LINQ) qui font un excellent travail pour masquer la source de données. Mais elles ne sont pas un remplacement pour un modèle introspectable central listant nature et contraintes.

Une bonne lib propose les deux.

Par exemple SQLALchemy propose un ORM, mais en vérité l’API de base est fonctionnelle et composable. Pour cette raison, on peut utiliser toutes fonctionnalités avancées de sa base de données avec SQLAlchemy car on a cette alternative à l’ORM à tout moment, et qui est compatible avec l’ORM.

Mais les perfs !

D’abord, on optimise pour les humains. En chemin, on optimise pour la machine. Quand ton ORM arrête de scaler, c’est un BON problème à avoir. Ca veut dire que le projet a atteint un certain succès, et qu’on peut investir dans la séparation avec l’ORM.

De plus, aucune technologie n’est faite pour être utilisée partout, tout le temps, pour tout le projet.

Google a connu ses débuts de succès en Python, et avec sa taille, a réécrit une partie en Java, C puis Go. C’est logique, on ne commence pas avec un langage bas niveau directement, c’est trop lent à écrire. Mais on ne garde pas un langage haut niveau pour tout quand ça monte dans les tours. Enfin les tours… Là on parle de centrifugeuse cosmique hein.

Car gardez en tête que vous n’êtes PAS Google. L’immense majorité des projets deviennent viables, rentables, puis pleins de succès, sans jamais atteindre l’échelle qui amène aux limites de l’ORM.

Quant à l’idée que votre stagiaire peut écrire une boucle avec une requête à chaque step… Il peut tout aussi bien écrire une requête SQL sans se protéger correctement l’injection de code. C’est con un stagiaire, faut le surveiller. C’est le principe, sinon il aurait un CDI.

Mais les fonctionnalités !

Les ORM bien faits n’empêchent pas d’utiliser toutes les fonctionnalités de son système. Et tous permettent d’écrire du SQL à la main si besoin. C’est comme les blocks unsafe de rust: on empêche pas, on isole.

L’idée c’est de garder ça pour le moment où on en a vraiment besoin. SQL est à la base de données ce que l’assembleur est à la machine. On n’écrit pas tout en assembleur de nos jours, ça n’est pas utile.

Root of all evil, tout ça.

Mais on ne change pas de base de données souvent !

L’argument “l’orm supporte plusieurs bases” n’est pas destiné à la migration de données d’un projet existant d’une base de données à une autre.

Pas. Du. Tout.

C’est pas que ça arrive jamais. Ça m’est déjà arrivé 2, 3 de fois.

Mais ce n’est pas le cas courant. Le cas courant c’est la réutilisation du code d’un projet précédent dans un nouveau projet utilisant une base de données différente. C’est la création d’un écosystème de modules qui ne sont pas dépendants de la base de données.

Si vous faites une “app” Django, vous pouvez la publier et elle sera utile pour toutes les bases de données supportées. Et c’est pour ça qu’il y autant d’outils.

Mais on pourrait avoir un modèle central sans ORM !

Oui, mais toutes les formes ne se valent pas.

Par exemple, Doctrine permet d’écrire son modèle dans un fichier YAML, et Hibernate dans un fichier XML.

Au final on écrit son modèle dans un langage moins complet, moins expressif, moins facile à débugger et avec moins de tooling. On perd encore plus en faisant ça qu’en passant de SQL à un ORM, sans autant de gains.

En prime, on peut vouloir de la logique de validation très complexe ou des choses à faire hors validation (signals, génération dynamique de modèle, etc), et là, pouet.

Une alternative, ça serait de se servir d’une lib de pur modèle (ex: l’excellent marshmallow) et de tout dériver de là. Une approche intéressante qui pourrait satisfaire tous les camps, mais que je n’ai jamais vu poussée jusqu’au bout dans aucun framework. Si vous cherchez un projet pour vos week-end :)

Lib VS framework

C’est un peu le vieux débat du découplage VS intégration qu’on retrouve dans la critique des ORM (ou dans vi VS vscode, POO vs fonctionnel, ta femme vs ta mère…).

Et comme d’habitude on retrouve toujours les meilleurs programmeurs du côté de ceux qui veulent le plus de liberté (vive SQL!) parce qu’ils ignorent complètement les problématiques qui vont plus loin que leur fichier de code. Faire fleurir un écosystème, gérer une communauté, favoriser la productivité, facilité l’intégration des membres de ses équipes… Tout ça sont des problématiques moins funs que de faire la requête parfaite avec le tout nouveau champ hyperloglog de PostGres.

Difficile de convaincre des gens qui sont non seulement excellents, mais qui sauront, seuls, être très productifs sans ORM. Surtout quand les gros projets qui atteignent des centaines de millions d’utilisateurs par jour finissent toujours par se séparer de leurs abstractions initiales.

Mais voilà, il ne faut pas perdre de vue que 2 projets sur 3 en informatique, échouent. Quasiment jamais pour des raisons techniques. Généralement la cause est humaine. Et l’ORM est là pour soutenir l’humain en créant un pivot sur lequel il peut compter, quitte à investir plus tard pour s’en passer.

C’est un excellent outil et une très belle réussite de l’informatique moderne. Si on sait l’aborder sans dogmatisme.

]]>
http://sametmax.com/les-critiques-des-orm-sont-a-cote-de-la-plaque/feed/ 41 23967
Automatiser un peu plus SQLAlchemy declarative http://sametmax.com/automatiser-un-peu-plus-sqlalchemy-declarative/ http://sametmax.com/automatiser-un-peu-plus-sqlalchemy-declarative/#comments Wed, 29 Oct 2014 12:34:00 +0000 http://sametmax.com/?p=12584 Avec l’intégration de l’interface déclarative d’SQLAlchemy, le projet Elexir est mort, et bien mort. Mais avec lui, la syntaxe la plus simple de déclaration pour cet ORM. En effet, par défaut, SQLA vous oblige à spécifier le nom de la table et l’attribut ID pour chaque classe :

from sqlalchemy import Column, Integer, String
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy import create_engine
 
Base = declarative_base()
 
class Person(Base):
    # explicit is better than... fuck it !
    __tablename__ = 'person' 
    id = Column(Integer, primary_key=True)
    name = Column(String(250), nullable=False)

class Animal(Base):
    __tablename__ = 'animal'
    id = Column(Integer, primary_key=True) # Paie ton DRY
    name = Column(String(250), nullable=False)

engine = create_engine('sqlite:///db.db.db...db')
Base.metadata.create_all(engine)

SQLA est très flexible, et il existe des tas de raisons pour vouloir un PK ou un nom de table custo. Mais dans beaucoup de projets, le nom de la table peut être le nom de la classe, et la PK peut être un ID auto incrémenté.

Heureusement, la lib vous permet de customiser absolument tout, même la manière dont on doit la customiser. Si c’est pas méta, tout ça…

Bref, on peut créer sa propre base déclarative qui va faire tout ça pour nous :

from sqlalchemy.ext.declarative import as_declarative, declared_attr

# Ceci sera le parent de tout nos objets
@as_declarative()
class Base(object):
 
    # Vu qu'on a pas vraiment envie de se réécrire la
    # métaclasse, SQLA nous file ce gentil décorateur 
    # pour déclarer des attributs qui ne sont pas des 
    # champs
    @declared_attr
    def __tablename__(cls):
        # le nom de la table est le nom de la classe
        return cls.__name__.lower()
    # L'id c'est la vie
    id = Column(Integer, primary_key=True)
 
class Person(Base):
    name = Column(String(250), nullable=False)

class Animal(Base):
    name = Column(String(250), nullable=False)

Evidemment il faut que vous soyez certains d’éviter les conflits liés à cette décision, mais c’est quand même vachement pratique.

]]>
http://sametmax.com/automatiser-un-peu-plus-sqlalchemy-declarative/feed/ 3 12584
Le pandas c’est bon, mangez en http://sametmax.com/le-pandas-cest-bon-mangez-en/ http://sametmax.com/le-pandas-cest-bon-mangez-en/#comments Sat, 10 May 2014 06:30:35 +0000 http://sametmax.com/?p=10113 Les bases de Numpy, je m'en vais vous présenter une lib qui roxx du poney dans le calcul numérique : Pandas.]]> Ceci est un post invité de joshuafr posté sous licence creative common 3.0 unported.

Bonjour à tous, jeunes tailleurs de bambou, suite à un article d’introduction à numpy par le grand maître Sam Les bases de Numpy, je m’en vais vous présenter une lib qui roxx du poney dans le calcul numérique : Pandas.

Pour faire simple, Pandas apporte à Python la possibilité de manipuler de grands volumes de données structurées de manière simple et intuitive, chose qui faisait défaut jusqu’ici. Il y a bien eu quelques tentatives comme larry, mais rien n’avait jamais pu égaler les fonctionnalités du langage R. Aujourd’hui Pandas y arrive en fournissant notamment le célèbre type dataframe de R, avec en prime tout un tas d’outils pour agréger, combiner, transformer des données, et tout ça sans se casser le cul. Que du bonheur!

Donc pour commencer, on installe le bousin par un simple : pip install pandas qui va si vous ne l’avez pas déjà fait, aussi télécharger/compiler/installer tout un tas de librairies dont numpy. Je vous conseille aussi d’utiliser ipython afin d’avoir une meilleure interaction avec les libs, notamment avec matplotlib en utilisant le switch ipython --pylab afin d’avoir directement les graphiques en mode interactif, ainsi que toute la bibliothèque numpy directement importée (en interne, ipython fera un import numpy as np).
On appelle la bête d’un simple:

In [1]: import pandas as pd

Oui je sais, la grande classe…

Tout est Series

Le type de base en Pandas est la Series. On peut le voir comme un tableau de données à une dimension:

In [2]: pd.Series(np.arange(1,5))
Out[2]: 
0    1
1    2
2    3
3    4
dtype: int64

La colonne de gauche représente l’index de la Series, normalement unique pour chaque entrée. La colonne de droite correspond à nos valeurs sur lesquelles nous voulons travailler.
L’index n’est pas forcément une suite d’entiers, et la Series peut être nommée:

In [3]: s=pd.Series([1,2,3.14,1e6], index=list('abcd'), name='ma_series')
In [4]: s
Out[4]: 
a          1.00
b          2.00
c          3.14
d    1000000.00
Name: ma_series, dtype: float64

A noter qu’un type-casting est systématiquement appliqué afin d’avoir un tableau de type uniforme (ici le data-type est du float64) qui peut être modifié (dans une certaine mesure) via Series.astype.

Le slicing c’est comme du fisting avec une bonne dose de vaseline, ça glisse tout seul:

In [5]: s['b':'d']
Out[5]: 
b          2.00
c          3.14
d    1000000.00
Name: ma_series, dtype: float64

Et oui, la sélection par indexation se fait sur… l’index de la Series. Ainsi s[‘a’] renverra la ligne dont l’index est ‘a’, mais Pandas est assez intelligent pour reconnaître si on lui demande de nous renvoyer des valeurs suivant l’ordonnancement du tableau de valeurs (comme numpy). Ainsi s[0] renverra la première valeur du tableau, qui ici est égale à s[‘a’].
Là où ça peut poser problème c’est quand notre index est une suite d’entiers, comme par exemple avec x=pd.Series(np.arange(1,5), index=np.arange(1,5)). Si vous demandez x[1], Pandas ne retrouve pas ses petits et vous retournera une zolie KeyError. Pour ces cas ambigus, il existe l’indexation stricte sur le nom de index de la Series via x.loc[nom_d'index], et l’indexation stricte sur le numéro d’ordre dans le tableau via x.iloc[numéro_d'ordre]. Essayez x.loc[0] et x.iloc[0] pour vous rendre compte de la différence.
Comme pour les préliminaires où il est bon de tâter un peu de tout avec de pénétrer dans le vif du sujet, laissons pour le moment l’indexation sur laquelle nous reviendrons plus tard, pour regarder d’un peu plus près comment faire joujou avec nos valeurs.

Un peu à la manière des arrays de numpy, on peut appliquer des fonctions mathématiques directement sur la Serie, ou passer par des fonctions raccourcis:

In [6]: s.sum()
Out[6]: 1000006.14

Ce qui revient au même que de faire np.sum(s) (rappelez vous, ipython avec –pylab a importé numpy dans la variable np).

La fonction describe est bien utile pour avoir un aperçu rapide de ce à quoi on a affaire:

In [7]: s.describe()
Out[7]: 
count          4.000000
mean      250001.535000
std       499998.976667
min            1.000000
25%            1.750000
50%            2.570000
75%       250002.355000
max      1000000.000000
Name: ma_series, dtype: float64

ce qui donne le nombre de données, la moyenne globale, la déviation standard, le minimum, les quartiles et le maximum de la Serie.

Le truc à retenir est que c’est l’index qui est primordial dans un grand nombre d’opérations. Ainsi si l’on veut additionner 2 Series ensemble, il faut que leurs index soient alignés :

In [8]: s2=pd.Series(np.random.rand(4), index=list('cdef'), name='autre_serie')

In [9]: s+s2
Out[9]: 
a               NaN
b               NaN
c          4.021591
d    1000000.401511
e               NaN
f               NaN
dtype: float64

Ici, seuls les index ‘c’ et ‘d’ étaient présents dans les 2 Series, Pandas effectuant avant l’opération d’addition une union basée sur l’index. Les autres entrées sont marquées en NaN, soit Not a Number. Une possibilité pour contrer ce phénomène et de dire à Pandas de remplacer les résultats manquants lors de l’union par 0:

In [10]: s.add(s2, fill_value=0)
Out[10]: 
a          1.000000
b          2.000000
c          4.021591
d    1000000.401511
e          0.563508
f          0.655915
Name: ma_series, dtype: float64

Mais si ce sont uniquement les valeurs qui nous intéressent, et non les indexations, il est possible de les supprimer:

In [11]: s.reset_index(drop=True)+s2.reset_index(drop=True)
Out[11]: 
0          1.881591
1          2.401511
2          3.703508
3    1000000.655915
dtype: float64

Oh joie, oh bonheur, je peux faire ce que je veux avec mes cheveux, enfin mes données…

Et PAN! dans ta frame

La DataFrame est l’extension en 2 dimensions des Series. Elle peut être vue comme un empilement de Series dont les index sont partagés (et donc intrinsèquement alignés), ou comme dans un tableur où les index sont les numéros de lignes et les noms des Series les noms des colonnes. Je ne vais pas décrire toutes les manières de créer une DataFrame, sachez juste qu’on peut les obtenir à partir de dictionnaires, de liste de liste ou de liste de Series, d’arrays ou de records numpy, de fichier excel ou csv et même depuis des bases de données, de fichier JSON ou HTML, et depuis le presse-papiers.

In [14]: genre=[['femme','homme'][x] for x in np.random.random_integers(0,1,100)]

In [15]: lateral=[['droite','gauche'][x] for x in np.random.random_integers(0,1,100)]

In [16]: age=np.random.random_integers(1,100,100)

In [17]: df=pd.DataFrame({'Genre':genre, 'Lateral':lateral, 'Age':age})

In [18]: df
Out[18]: 
    Age  Genre Lateral
0    69  femme  droite
1    46  homme  droite
2    89  homme  droite
3    14  homme  droite
4    74  homme  droite
5     5  femme  gauche
6    66  femme  droite
7    73  homme  gauche
8    99  homme  gauche
9    17  homme  gauche
    ...    ...     ...

[100 rows x 3 columns]

L’affichage par défaut depuis la version 0.13 est en mode ‘truncate’ où la fin de la DataFrame est tronquée suivant la hauteur de votre terminal, mais ça peut se changer via les divers paramètres à regarder sous pd.options.display.

Là donc nous avons une DataFrame de 3 colonnes (plus un index), chaque colonne étant en réalité une Serie :

In [20]: type(df['Age'])
Out[20]: pandas.core.series.Series

La sélection peut se faire de plusieurs manières, à chacun de choisir sa préférée (moi c’est Dafnée avec ses gros nénés). Ainsi pour avoir les 3 premières lignes des âges

In [21]: df['Age'][0:3]
Out[21]: 
0    69
1    46
2    89
Name: Age, dtype: int64

In [22]: df[0:3]['Age']
Out[22]: 
0    69
1    46
2    89
Name: Age, dtype: int64

In [23]: df.Age[0:3]
Out[23]: 
0    69
1    46
2    89
Name: Age, dtype: int64

In [24]: df.loc[0:3, 'Age']
Out[24]: 
0    69
1    46
2    89
3    14
Name: Age, dtype: int64

et oui, les noms de colonnes peuvent aussi être utilisés comme des attributs de la DataFrame. Pratique (qu’on n’attend pas).

L’une des forces de Pandas est de nous proposer tout un tas de solutions pour répondre à des questions existentielles tel que “quel est l’âge moyen par genre et par latéralité?”. Comme en SQL où la réponse sortirait du fondement d’une clause GROUP BY et d’une fonction d’agrégation, il en va de même ici :

In [25]: df.groupby(['Genre','Lateral']).aggregate(np.mean)
Out[25]: 
                     Age
Genre Lateral           
femme droite   45.476190
      gauche   49.208333
homme droite   41.571429
      gauche   55.823529

[4 rows x 1 columns]

OMG! c’est quoi c’t’index de malade? Un MultiIndex jeune padawan, qui te permettra d’organiser tes données par catégorie/niveau, et d’y accèder par le paramètre level dans pas mal de fonctions, mais ça je te laisse le découvrir par toi-même. Je ne vais pas non plus m’étendre plus sur toutes les possibilités offertes par les DataFrame, il y a tellement à dire qu’il faudrait plusieurs articles pour en faire le tour. Juste conclusionner sur la facilité d’intégration Pandas/matplotlib en vous disant que les Series et DataFrame ont une fonction plot permettant directement de visualiser les données, et ça, c’est juste jouissif.

Datetime dans les index

Je vous avez dit qu’on reviendrait sur les indexes, et là c’est pour rentrer dans le lourd (mais non pas toi Carlos). Pandas donc supporte l’indexation sur les dates, en reprenant et en élargissant les possibilités offertes par feu le module scikits.timeseries.
Prenons l’exemple de données (complètement bidons) fournies par un capteur à intervalle régulier sur un pas de temps horaire:

In [26]: dtindex=pd.date_range(start='2014-04-28 00:00', periods=96, freq='H')

In [27]: data=np.random.random_sample(96)*50

In [28]: df=pd.DataFrame(data, index=dtindex, columns=['mesure'])

In [29]: df.head()
Out[29]: 
                        mesure
2014-04-28 00:00:00  49.253929
2014-04-28 01:00:00   1.910280
2014-04-28 02:00:00   7.534761
2014-04-28 03:00:00  39.416415
2014-04-28 04:00:00  44.213409

[5 rows x 1 columns]

In [30]: df.tail()
Out[30]: 
                        mesure
2014-05-01 19:00:00  25.291453
2014-05-01 20:00:00  26.520291
2014-05-01 21:00:00  33.459766
2014-05-01 22:00:00  44.521813
2014-05-01 23:00:00  28.486003

[5 rows x 1 columns]

dtindex est un DatetimeIndex initialisé au 28 avril 2014 à 0 heure comportant 96 périodes de fréquence horaire, soit 4 jours. La fonction date_range peut aussi prendre en arguments des objets datetime purs au lieu de chaine de caractère (manquerait plus que ça…), et le nombre de périodes peut être remplacé par une date de fin.
Si l’on veut calculer, disons le maximum (horaire) par jour, rien de plus simple, il suffit de “resampler” en données journalières (‘D’ pour Day) et de dire comment aggréger le tout:

In [31]: df.resample('D', how=np.max)
Out[31]: 
               mesure
2014-04-28  26.298282
2014-04-29  28.385418
2014-04-30  26.723353
2014-05-01  24.106092

[4 rows x 1 columns]

Mais on peut aussi convertir en données quart-horaire (upsampling) en remplissant les données manquantes par celles de l’heure fixe:

In [32]:  df[:3].resample('15min', fill_method='ffill')
Out[32]: 
                        mesure
2014-04-28 00:00:00  49.253929
2014-04-28 00:15:00  49.253929
2014-04-28 00:30:00  49.253929
2014-04-28 00:45:00  49.253929
2014-04-28 01:00:00   1.910280
2014-04-28 01:15:00   1.910280
2014-04-28 01:30:00   1.910280
2014-04-28 01:45:00   1.910280
2014-04-28 02:00:00   7.534761

[9 rows x 1 columns]

Cependant, Pandas propose aussi d’autres possibilités non dépendantes des DatetimeIndex mais qu’il est bon de connaître, notamment celle pour remplacer les données manquantes avec fillna ou celle pour interpoler entre les données valides avec interpolate

In [52]:  df[:3].resample('15min')
Out[52]: 
                        mesure
2014-04-28 00:00:00  49.253929
2014-04-28 00:15:00        NaN
2014-04-28 00:30:00        NaN
2014-04-28 00:45:00        NaN
2014-04-28 01:00:00   1.910280
2014-04-28 01:15:00        NaN
2014-04-28 01:30:00        NaN
2014-04-28 01:45:00        NaN
2014-04-28 02:00:00   7.534761

[9 rows x 1 columns]

In [53]:  df[:3].resample('15min').fillna(df.mean())
Out[53]: 
                        mesure
2014-04-28 00:00:00  49.253929
2014-04-28 00:15:00  26.378286
2014-04-28 00:30:00  26.378286
2014-04-28 00:45:00  26.378286
2014-04-28 01:00:00   1.910280
2014-04-28 01:15:00  26.378286
2014-04-28 01:30:00  26.378286
2014-04-28 01:45:00  26.378286
2014-04-28 02:00:00   7.534761

[9 rows x 1 columns]

In [54]:  df[:3].resample('15min').interpolate()
Out[54]: 
                        mesure
2014-04-28 00:00:00  49.253929
2014-04-28 00:15:00  37.418016
2014-04-28 00:30:00  25.582104
2014-04-28 00:45:00  13.746192
2014-04-28 01:00:00   1.910280
2014-04-28 01:15:00   3.316400
2014-04-28 01:30:00   4.722520
2014-04-28 01:45:00   6.128641
2014-04-28 02:00:00   7.534761

[9 rows x 1 columns]

Voilà, j’espère que vous aurez plaisir à travailler avec cette librairie, il manquait vraiment un outil de cette trempe en Python pour l’analyse de données et je pense qu’on n’a plus trop grand chose à envier maintenant par rapport à des langages spécilisés. Je n’ai pas parlé de Panel qui est le passage à la troisième dimension, ni des possibilités d’export, notamment la df.to_html que je vous laisse le soin de découvrir.

A plus, et amusez vous bien avec votre bambou.

\o/

]]>
http://sametmax.com/le-pandas-cest-bon-mangez-en/feed/ 7 10113
NoSQL : arrêtons de dire n’importe quoi http://sametmax.com/nosql-arretons-de-dire-nimporte-quoi/ http://sametmax.com/nosql-arretons-de-dire-nimporte-quoi/#comments Sat, 22 Mar 2014 10:26:06 +0000 http://sametmax.com/?p=9844 J’ai regardé le mouvement NoSQL évoluer au fil des années. On y retrouve à peu près tout ce qui fait l’informatique depuis que le monde IT est monde : brillance et troll, hype et génie, utile et gadget, buzz et fact, sam et max, etc.

De plus on peut mettre n’importe quoi sous le label NoSQL, et du coup ça a été fait. En fait un fichier est déjà une base de données NoSQL :)

Mais rant mise à part, des projets comme redis, riak, elastic search ou mongodb changent vraiment la donne.

Malheureusement, tout comme d’autres technos du moment (prog asychrone, tout-http, pre-processeurs, generateurs…), les gens ont tendance à l’utiliser comme la barre de fer, la silver bullet, le passe-partout, le tournevis sonique, bref, le truc à tout faire.

L’adage populaire dit “quand on a un bon marteau, tous les problèmes ressemblent à des clous”. Or, je constate qu’au dessus de ça, les dev appliquent aussi souvent le dicton préféré d’un de mes colocs : “arrête de taper si fort, prend un plus gros marteau”.

Ca donne du NoSQL utilisé partout, pour tout, brandi comme LA solution, vendu à des débutants comme une panacée de traitement d’informations. Zob, vous vous doutez bien que ça pose problème, non ?

Anti-fact 1 : NoSql, c’est plus facile pour démarrer

Il n’existe pas à l’heure actuelle de base NoSQL embarquée qui arrive à la cheville de SQLite : ça marche partout, dans tous les langages, sans rien à avoir installer ou configurer pour la plupart des langages.

Dès que vous demandez à un débutant d’installer un truc, vous rajoutez une barrière d’entrée énorme.

De plus, il y a beaucoup, beaucoup, beaucoup plus d’hébergeurs qui fournissent du SQL que du NoSQL en solution par défaut. Et comme toute les technos legacy, il y a 100 fois plus de doc.

Enfin, il y a la fameuse question du “quoi” ? Quel système allez-vous installer ? Couch ? Casssandra ? Mongo ? On parle de NoSQL, ou de schemaless ? C’est pas la même chose ? Memcache et Redis, c’est que pour le cache ? Elastic Search, c’est que pour la recherche de texte ? Les données géographiques, je les mets dans quoi, mongo ou un GIS spécialisé ? Attends, j’ai entendu parler d’une super bdd de graph…

L’abondance de solutions, le manque de recul et les informations contradictoires disponibles rendent non seulement le choix difficile, mais en plus hasardeux. Car contrairement au monde du SQL, se gourrer en NoSQL peut vous pourir toute votre archi.

Souvenez-vous qu’il est beaucoup plus difficile de migrer son système de base de données NoSQL d’une solution à une autre car il n’y a pas ce petit détail en commun entre les produits : le SQL justement.

Anti-fact 2 : Avec NoSql, pas besoin de réfléchir à son modèle de données

Je crois que c’est ce qui me fait le plus grincer des dents. Les gens qui disent qu’on peut tout mettre dedans, hop, et on verra plus tard. J’ai vu les pires modèles de données possibles stockés en MongoDb ou Redis, parceque les gars qui avaient travaillé dessus avait juste dumpé leurs données sans réfléchir.

Une base NoSQL ne vous oblige pas à formaliser votre schéma, mais ça ne veut CERTAINEMENT PAS dire qu’il ne faut pas le faire. L’auteur de Redis a très bien expliqué le problème (je graisse pour donner une effet dramatique et puissant au message) :

Redis is not the kind of system where you can insert data and then argue about how to fetch those data in creative ways. Not at all, the whole idea of its data model, and part of the fact that it will be so fast to retrieve your data, is that you need to think in terms of organising your data for fetching. You need to design with the query patterns in mind.

C’est vrai pour tout système dans lequel on met ses données, SQL, NoSQL, fichier, mémoire, le tiroir de votre bureau…

Il faut penser au type des données, leurs formats, les relations entre les éléments, comment et à quelle fréquence vous allez les écrire, les lire, garantir la consistence de leurs relations et leur fraicheur (ou pas d’ailleurs, mais il faut en faire le choix). Les bases SQL sont contraignantes parce qu’elles vous obligent à penser, dès le début, en ces termes.

C’est vrai, vous êtes de grandes personnes, a priori vous savez ce que vous faites, vous n’avez pas besoin qu’on vous FORCE à le faire. C’est pour ça que j’aime le typage dynamique. Je ne veux pas que tu me demandes mes papiers pour déclarer une variable, je sais ce que je fais.

Seulement il faut le faire, et ce n’est souvent pas fait. Pire, le modèle n’est généralement jamais formalisé NULLE PART. Un schéma, c’est une doc. Sans doc, le coût d’entrée dans votre projet est élevé, sa maintenance est galère, le potentiel de bug lors de l’évolution est plus grand. Mais une doc c’est chiant à écrire et à tenir à jour.

C’est un des intéressants effets secondaires des ORMs : les classes de définition sont le modèle documenté dans sa structure, ses relations, ses limites, ses contraintes, ses tests et vérifications, etc. La doc par le code, j’adore.

Anti-fact 3 : NoSql, c’est plus performant

A chaque fois qu’on lit “x est plus performant que z”, il faut faire une pause et réfléchir deux minutes. Généralement il y a un piège.

Les performances, ça dépend toujours du contexte. Par exemple, Redis est plus performant à la lecture et l’écriture, mais les données doivent tenir en RAM, sinon ça bouffe sur la mémoire virtuelle. Autre chose, Redis est très lent à démarrer sur des gros jeux de données (ça peut aller à plusieurs minutes si vous avez des Go). MongoDB doit normalement pouvoir tenir une augmentation de charge de manière prédictive en rajoutant des noeuds. Mais sur un seul noeud, c’est toujours moins performant qu’un PostGres. Et 0.01 % des sites ont besoin de plus d’un serveur.

Par ailleurs, les performances sont très dépendantes de l’anti-fact 2. Il faut créer les bons index, avoir un cache correctement ajusté, faire des requêtes intelligentes. Pour tous les systèmes.

Bref, encore une fois, NoSQL n’est pas une techno magique. Il est contre-productif, et j’ai envie de dire même irrespectueux envers ses collègues, de la vendre comme telle.

Anti-fact 4 : NoSQL remplace le SQL

Twitter tourne sur MySQL ET Memcache.

Stackoverflow utiliser SQL Server 2008 ET Redis.

Il y a carrément des sites qui utilisent PostGres et MongoDb en parallèle. En fait, il y a des outils pour les faire collaborer.

Nous sur notre plus gros site on utilise Redis pour les sessions, les compteurs, les crawlers, les queues et passer des données entre process. Pas juste pour le cache. On utilise PostGres pour les données complexes avec des queries lourdes. Et on utilise Solr pour le moteur de recherche.

Les bases NoSQL sont des nouveaux outils, qui sont mieux adaptés à CERTAINS usages ou à CERTAINS contextes. Pas tout, tout le temps, partout. C’est un outil en plus, pas un obligatoire remplaçant.

Par ailleurs, on peut utiliser PostGres comme une base de données clé-valeur ou JSON, on peut mettre SQLite complètement en mémoire vive, on peut utiliser MySQL comme un moteur de recherche de texte… Multiplier les points of failures dans une archi n’est pas toujours une bonne idée. Ces outils qu’on considère comme de l’histoire ancienne ont beaucoup plus de ressource que vous ne l’imaginez. Ils sont ultra performants. Des années et des années d’optimisation.

Le monde de la tech n’est jamais lisse. Jamais.

]]>
http://sametmax.com/nosql-arretons-de-dire-nimporte-quoi/feed/ 39 9844
MySQL: créer un utilisateur avec une base de données à son nom sur laquelle il a tous les droits http://sametmax.com/mysql-creer-un-utilisateur-avec-une-table-a-son-nom-sur-laquelle-il-a-tous-les-droits/ http://sametmax.com/mysql-creer-un-utilisateur-avec-une-table-a-son-nom-sur-laquelle-il-a-tous-les-droits/#comments Mon, 15 Oct 2012 15:07:09 +0000 http://sametmax.com/?p=2575 Le truc classique, qu’on fait tout le temps, et qu’on oublie toujours comment on a fait la dernière fois.

Vu qu’on a pas toujours PhpMyAdmin installé…

CREATE DATABASE nom_db;
GRANT ALL PRIVILEGES ON nom_db.* TO "nom_utilisateur"@"localhost" IDENTIFIED BY 'mot_de_passe';
FLUSH PRIVILEGES;
]]>
http://sametmax.com/mysql-creer-un-utilisateur-avec-une-table-a-son-nom-sur-laquelle-il-a-tous-les-droits/feed/ 11 2575
Log des requêtes SQL faites par l’ORM Django en temps réel http://sametmax.com/log-des-requetes-sql-faites-par-lorm-django-en-temps-reel/ Fri, 16 Mar 2012 17:30:01 +0000 http://sametmax.com/?p=262 django-debug-toolbar ou connection.queries pour suivre les requêtes SQL de l'ORM Django.]]> Même en recherchant sur les sites spécialisés en anglais, je tombe toujours sur des solutions basées sur la django-debug-toolbar, l’affichage de connection.queries ou l’usage de devserver. Ça a son utilité (surtout la fabuleuse django-debug-toolbar), mais c’est compliqué, long à mettre en oeuvre, et pas très pratique.

Il y a beaucoup plus simple, et le plus beau, c’est que Django le propose en natif.

Par default, Django loggue toutes les requêtes SQL dans django.db.backends si DEBUG = True. Il suffit donc de configurer le logger, dans votre settings.py:

import os
import tempfile

LOGGING = {
    'version': 1,
    'disable_existing_loggers': False,
    'formatters': {
        'verbose': {
            'format': '%(levelname)s %(asctime)s %(module)s %(process)d %(thread)d %(message)s'
        },
        'simple': {
            'format': '%(levelname)s %(message)s'
        },
    },
    'handlers': {
        'tempfile': {
            'level': 'DEBUG',
            'class': 'logging.handlers.RotatingFileHandler',
            'formatter': 'verbose',
            'filename': os.path.join(tempfile.gettempdir(), 'django-debug.log'),
        },
        'console':{
            'level':'DEBUG',
            'class':'logging.StreamHandler',
            'formatter': 'simple'
        },
    },
    'loggers': {
        'django.db.backends': {
            'handlers': ['console', 'tempfile'],
            'level': 'DEBUG',
            'propagate': True,
        },
    },
}

Lancez ./manage.py shell:

In [1]: m = Table.objects.filter(title__isnull=False)

In [2]: m
DEBUG (0.018) SELECT "table"."id", "table"."title", "table"."original_title" FROM "table" WHERE "table"."title" IS NOT NULL LIMIT 21; args=()
Out[2]: [< Table object >, < Table object >, < Table object >, < Table object >, < Table object >, < Table object >, < Table object >, < Table object >, < Table object >, < Table object >, < Table object >, < Table object >, < Table object >, < Table object >, < Table object >, < Table object >, < Table object >, < Table object >, < Table object >, < Table object >, '...(remaining elements truncated)...']

Une version plus verbeuse sera aussi logguée dans un fichier temporaire. Par exemple, sous Linux il sera dans /tmp/django-debug.log.

Toutes les requêtes faites par le site Web seront également affichées dans le terminal sur lequel tourne le serveur, et dans le fichier temporaire.

Une fois que vous maitrisez le concept, je vous recommande plutot de mettre le logger django.db.backends dans votre local_settings.py, histoire d’éviter de l’avoir en prod :

LOGGING['loggers'].update({
        'django.db.backends': {
            'handlers': ['console', 'tempfile'],
            'level': 'DEBUG',
            'propagate': True,
        },
    }
)

Autre bénéfice, il suffit de commenter cette partie quand on ne souhaite plus afficher autant d’informations.

]]>
262