Après presque une année d’absence pour des diverses raisons, je me suis dit qu’il est temps de retourner à mon blog et d’ajouter des nouveaux articles et surtout des tutoriels qu’on ne trouve pas facilement sur le net. Ceci dit, je vais vous parler aujourd’hui de comment faire pour utiliser Pandas et Django ORM pour aboutir aux memes résultats. Je tiens à noter qu’on va ignorer les points de différences et similitudes entre les deux outils et on va se consacrer uniquement, au moins dans ce tutoriel, à lire un document EXCEL (.XLSX), de remplir une base de donnée SQLite, de la lire et d’effecter quelques opérations avec les données enregistrés dans cette base de donnée.

Toutefois, avant de commencer, je tiens à vous inviter à créer vos environnements virtuels de Python pour ne pas altérer une autre applications que vous développer/utiliser. Et surtout, car c’est une bonne pratique de séparer les environnements :D .

Donc, commençons:

Création d’un projet Django:

──╼ [>] django-admin startproject django_pandas
──╼ [>] cd django_pandas
──╼ [>] python manage.py startapp example_project

Ensuite, on va modifier un peu la structure de notre projet Django. Mais avant tout, voici la structure des dossier et fichier qu’on devra avoir pour bien suivre ce tutoriel:

└──╼ [>] tree django_pandas/ -I "*.pyc|__pycache__"
django_pandas/
├── db.sqlite3
├── django_pandas
│   ├── __init__.py
│   ├── settings.py
│   └── wsgi.py
├── example_project
│   ├── apps.py
│   ├── dataset.xlsx
│   ├── __init__.py
│   ├── migrations
│   │   ├── 0001_initial.py
│   │   └── __init__.py
│   ├── models.py
│   └── process.py
├── manage.py
└── requirements.txt

3 directories, 13 files

Ceci dit, notre fichier settings, au minimum, devra ressembler à cet exemple:

#
# Settings file
#

import os

BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))


# Quick-start development settings - unsuitable for production
# See https://docs.djangoproject.com/en/2.2/howto/deployment/checklist/

# SECURITY WARNING: keep the secret key used in production secret!
SECRET_KEY = 'SECRET_KEY'


# Application definition

INSTALLED_APPS = [
    'example_project'
]

WSGI_APPLICATION = 'django_pandas.wsgi.application'

# Database
# https://docs.djangoproject.com/en/2.2/ref/settings/#databases

DATABASES = {
    'default': {
        'ENGINE': 'django.db.backends.sqlite3',
        'NAME': os.path.join(BASE_DIR, 'db.sqlite3'),
    }
}

Ensuite nos modèles sont:

#
# Déclaration des modèles de l'application example_project
#


from django.db import models


class Manager(models.Model):
    """Chaque employé a un manager"""
    name = models.CharField(max_length=200, verbose_name='Manager')

    def __str__(self):
        return self.name

    class Meta:
        db_table = 'manager'
        verbose_name = 'Manager'
        verbose_name_plural = 'Managers'


class Job(models.Model):
    """Chaque développeur a un job"""
    name = models.CharField(max_length=200, verbose_name='Job')

    def __str__(self):
        return self.name

    class Meta:
        db_table = 'job'
        verbose_name = 'Job'
        verbose_name_plural = 'Jobs'


class Developer(models.Model):
    """Model des développeurs"""
    name = models.CharField(max_length=200, verbose_name='Name')
    age = models.IntegerField(default=0, verbose_name='Age')
    job = models.ForeignKey(
        Job,
        verbose_name='Job',
        related_name='dev_job',
        null=True,
        blank=True,
        on_delete=models.DO_NOTHING
    )
    manager = models.ForeignKey(
        Manager,
        verbose_name='Manager',
        related_name='dev_manager',
        null=True,
        blank=True,
        on_delete=models.DO_NOTHING
    )
    date = models.DateField(verbose_name='Date')

    def __str__(self):
        return '{dev_name} - {job} <{manager}>'.format(
            dev_name=self.name,
            job=self.job,
            manager=self.manager
        )

    class Meta:
        db_table = 'developer'
        verbose_name = 'Developer'
        verbose_name_plural = 'Developers'

Ensuite, on va créer notre premier fichier de migrations:

──╼ [>] python manage.py makemigrations
Migrations for 'example_project':
  example_project/migrations/0001_initial.py
    - Create model Job
    - Create model Manager
    - Create model Developer

Et évidemment, appliquer ces changements dans la base de donnée

──╼ [>] python manage.py migrate
Operations to perform:
  Apply all migrations: example_project
Running migrations:
  Applying example_project.0001_initial... OK

Et pour un meilleur debug, il est préférable d’installer SQLite3-cli via:

──╼ [>] sudo apt install sqlite3

Maintenant, si on cherche si la migrations a bien eu lieu. On peut entrer dans le Database Shell que Django nous offre et voir les tableau (Ou bien exécuter une requete avec SQLite3 et vous devrez avoir les memes résultats)

──╼ [>] python manage.py dbshell
SQLite version 3.11.0 2016-02-15 17:29:24
Enter ".help" for usage hints.
sqlite> .tables
developer          django_migrations  job                manager          
sqlite>

Et si on essaie d’exécuter des requetes sur les tables de cette base de donnée, on va remarquer que notre table est vide. (Bien sur car on n’a rien insérer jusqu’au moment):

sqlite> select * from developer;
sqlite> select * from manager;
sqlite> select * from job;
sqlite> 

Les memes résultats avec Django Shell

──╼ [>] python manage.py shell
Python 3.6.8 (default, May  7 2019, 14:58:50) 
[GCC 5.4.0 20160609] on linux
Type "help", "copyright", "credits" or "license" for more information.
(InteractiveConsole)
>>> from example_project import models
>>> models.Manager.objects.all()
<QuerySet []>
>>> models.Job.objects.all()
<QuerySet []>
>>> models.Developer.objects.all()
<QuerySet []>
>>> 

Comment lire un document EXCEL (.XLSX) avec Python?

Pour répondre à cette question, j’ai ajouté un document EXCEL dans le repository du tutoriel sur github qui comporte 4 colonnes avec des données.

Et pour lire un document EXCEL avec Python, il y a plusieurs paquets qui nous facilitent la vie tel:

Par contre dans ce tutoriel on va utiliser openpyxl car on va travailler avec une récentre version de LibreOffice qui peut générer des documents EXCEL compatibles avec EXCEL 2003/2010

… Donc, voici une fonction qui permet de tout lire dans un tableur de document EXCEL. En théorie, ça permet de lire toute dans le document EXCEL, mais je n’ai pas creusé beaucoup avec les différentes formes qu’un document EXCEL peut avoir pour générer une fonction qui peut tout lire. Mais, si votre documents EXCEL est composé par des colonnes et des lignes, cette fonction fera l’affaire:

import openpyxl

def read_excel_file(file_path='', sheet_name='Sheet1'):
    """On va lire le document EXCEL avec openpyxl
    NB: On peut le lire directement avec Pandas
    Mais je préfère le lire avec openpyxl ...
    Parce que je veux controler ce que je fais :D"""
    wb = openpyxl.load_workbook(file_path)
    ws = wb[sheet_name]
    # Balayer les colonnes une par une et générer des sous listes
    # Qui vont contenir les noms des colonnes (première position)
    # ensuite chaque transposer les données des colonnes
    # pour créer des sous listes qui vont contenir les données de chaque ligne
    # sous forme d'une sous liste de la liste finale
    # Ceci dit:
    # colonnes = values[0]
    # lignes = values[1:]
    values = [[k.value for k in elm] for elm in zip(*ws.columns)]
    return values

Et pour insérer les données dans la base de données en utilisant Django ORM:


from django.db.models import transaction
from example_project import models

def insert_data(values):
    """Insertion des données dans la database"""
    with transaction.atomic():
        for name, age, job_name, date, manager_name in values:
            manager, _ = models.Manager.objects.get_or_create(
                name=manager_name
            )
            job, _ = models.Job.objects.get_or_create(name=job_name)
            developer, _ = models.Developer.objects.get_or_create(
                manager=manager,
                job=job,
                name=name,
                age=age,
                date=date
            )
            print(developer)

NB: On doit ajouter DJANGO_SETTINGS_MODULE path à nos variables d’environnement.

Et l’exécution de cette fonction va générer comme output:

──╼ [>] python process.py 
Alex - Python developer <Chiheb>
Jean - Go developer <Chiheb>
Janet - PHP developer <Ahmed>
Ali - Python developer <Chiheb>

Comment générer un DataFrame avec les données de la base de données?

Pour que ça soit possible, on doit installer Pandas en premier lieu, ensuite, on doit lire les données de notre base de données pour au final créer un DataFrame.

Pour lire les données de notre base de donnée:

from example_project import models

def read_data():
    """Read data from Database"""
    # Ceci sont les noms des colonnes de notre base de données
    names = [
        'id',
        'name',
        'age',
        'job__name',  # Ceci permet à Django ORM d'accèder au contenu du ForeignKey
        'manager__name',  # Permet à django ORM d'accèder au contenu du ForeignKey
        'date'
    ]
    # retourner une liste contenant par ordre les valeurs des noms de colonnes insérés
    return models.Developer.objects.all().values_list(*names)

Ensuite, pour créer un DataFrame des données lues de la base de données on peut faire:

from example_project import models
import pandas as pd

def create_df(data):
    """Create DataFrame from database"""
    # Ceci permet de générer une liste des verbose_name qu'on a utilisé
    # Dans la définition de nos models dans l'ORM de Django
    # Et dans notre cas, ces verbose_name sont identiques aux noms 
    # de colonnes du document EXCEL
    column_names = [
        field.verbose_name for field in models.Developer._meta.fields
    ]
    return pd.DataFrame(
        data,
        columns=column_names
    )

Et si on affiche le contenu du DataFrame on aura cette résultat:

──╼ [>] python process.py 
   ID   Name  Age               Job Manager        Date
0   1   Alex   22  Python developer  Chiheb  2019-01-05
1   2   Jean   24      Go developer  Chiheb  2019-03-04
2   3  Janet   23     PHP developer   Ahmed  2019-02-11
3   4    Ali   25  Python developer  Chiheb  2018-04-03

Faire des opérations sur la base de données et sur le DataFrame:

Arrivons, donc, au choses sérieuses. On va essayer de faire des opérations de calcul sur la base de données et on va essayer de faire la meme chose avec le DataFrame généré:

voici des exemples:

from django.db.models import Avg, Sum
from example_project import models

def pprint(dataframe=None, database=None, field='', header=''):
    """Pretty print data"""
    print('#' * 5, header, '#' * 5)
    if database:
        print("""Database: {field}: {data}""".format(
            field=field,
            data=database.get(field)
        ))
    if dataframe:
        print("""DataFrame: {field}: {data}""".format(
            field=field,
            data=dataframe
        ))
    print()


def operations(df):
    """Faire les memes opérations sur la database et sur le DataFrame"""
    # Average age
    db_avg = models.Developer.objects.all().aggregate(
        average=Avg('age')
    )
    df_avg = df['Age'].mean()
    pprint(
        dataframe=df_avg,
        database=db_avg,
        field='average',
        header='Average Age'
    )
    # Sum age
    db_sum = models.Developer.objects.all().aggregate(
        sum=Sum('age')
    )
    df_sum = df['Age'].sum()
    pprint(
        dataframe=df_sum,
        database=db_sum,
        field='sum',
        header='Sum Age'
    )
    # Average age uniquement pour Manager == Chiheb
    db_avg_chiheb = models.Developer.objects.filter(
        manager__name='Chiheb'
    ).aggregate(
        average=Avg('age')
    )
    df_avg_chiheb = df.loc[df['Manager'].isin(['Chiheb'])]['Age'].mean()
    pprint(
        dataframe=df_avg_chiheb,
        database=db_avg_chiheb,
        field='average',
        header='Average only chiheb'
    )
    # Average age uniquement pour job = Python developer
    db_avg_python = models.Developer.objects.filter(
        job__name='Python developer'
    ).aggregate(
        average=Avg('age')
    )
    df_avg_python = df.loc[df['Job'].isin(['Python developer'])]['Age'].mean()
    pprint(
        dataframe=df_avg_python,
        database=db_avg_python,
        field='average',
        header='Average Age only Python developer'
    )

Et on lance la fonction operations on aura comme affichage:

└──╼ [>] python process.py 
##### Average Age #####
Database: average: 23.5
DataFrame: average: 23.5

##### Sum Age #####
Database: sum: 94
DataFrame: sum: 94

##### Average only chiheb #####
Database: average: 23.666666666666668
DataFrame: average: 23.666666666666668

##### Average Age only Python developer #####
Database: average: 23.5
DataFrame: average: 23.5

Bien sur, on peut faire des opérations plus compliquées que ce qu’on a fait. Mais l’essentiel c’est de noter que ces deux outils sont hyper robustes et peuvent nous faire gagner beaucoup de temps lors de nos taches journalières.

Et pour ne pas oublier, voici l’URL de la repository github du tutoriel avec le fichier requirements, le fichier EXCEL et la base de donnée SQLite que j’ai utilisé pour l’élaboration des ces exemples.

Au plaisir de vous écrire une prochaine fois.