presentation-projet-cloche/presentation.md
François Pelletier 23b75b0a0f premiere version
2021-08-19 22:40:22 -04:00

16 KiB

title author
Nettoyage et intégration de données hétéroclites. francois@francoispelletier.org

Le projet

Le projet "cloche" est né suite au dépôt du projet de loi 61 au printemps 2020. Il cherche à établir un diagnostic de notre système de santé pour les personnes âgées et du système d'éducation à l'aide de données ouvertes, afin de suggérer les meilleurs endroits pour construire des nouvelles résidences et écoles.

Pourquoi : Développer de nouvelles compétences. Aider notre gouvernement qui en arrache.

Infrastructure

  • Logiciels :
    • Python
    • PostgreSQL et PostGIS
    • pgAdmin
    • RStudio
  • J'utilise des conteneurs Docker pour avoir une installation standardisée le plus possible.
  • Les conteneurs sont assemblés avec docker-compose

Voici mon installation : docker-compose.yml

Acquisition des données

J'essaie autant que possible de télécharger les données de manière reproductible avec des scripts bash. Les données sont acquises depuis différents sites du gouvernement, des sites GitHub et aussi via des demandes d'accès à l'information.

Capacités des installations - Préparation des données

*Premier exemple *: Source : Capacités des installations

Premièrement, on télécharge les données

wget https://www.donneesquebec.ca/recherche/fr/dataset/e442a663-e069-4209-a4ba-7bd60f6c5fb4/resource/7038bb19-ad37-49ae-b2d0-aec844d41970/download/gouvernement-ouvert-20200702.csv \
--output-document=capacite_services_installation_20200702.csv

Puis, on les lit dans Python avec Pandas

capacite_services_installation_20200702 =\
pd.read_csv("/raw/donneesquebec/capacite_services_installation_20200702.csv")

Voici un exemple de données :

Code :

capacite_services_installation_20200702.iloc[0].to_dict()

Résultat :

{'RSS_Etablissement' : '01 - Bas-Saint-Laurent',
 'RTS_Etablissement' : '011 - RTS du Bas-Saint-Laurent',
 'Code_Etablissement_(Regroupes_inclus_dans_CISSS/CIUSSS)' : 11045119.0,
 'Nom_Etablissement_(Regroupes_inclus_dans_CISSS/CIUSSS)' : 'CISSS DU BAS-SAINT-LAURENT',
 'Code_Etablissement' : 11045119,
 'Nom_Etablissement' : 'CISSS DU BAS-SAINT-LAURENT',
 'Statut' : 'Public',
 'Mode_financement' : 'Budget',
 'Territoire_CLSC_Installation' : '01161 - La Mitis',
 'Code_Installation' : 51216281,
 'Nom_Installation' : 'SERVICES PROFESSIONNELS - MRC MITIS',
 'MCT-Capacite/Service_Installation' : 'CRDI ',
 'Unite_mesure_Installation' : "Place(s) en centre d'activités de jour",
 'Capacite(C)_Service (S)_Installation' : 'C',
 'Capacite_Installation' : '20',
 'Date_extraction' : 'Produit le 2020-07-02'}

Capacités des installations - séparer une colonne avec une expression régulière

On remarquera que le champ RSS_Etablissement contient deux données : le code numérique de la région administrative et le nom de la région.

Nous allons préférer utiliser un identifiant numérique, alors on sépare le champ en deux colonnes en utilisant la fonction extract qui prend en argument une expression régulière :

rss_mod = capacite_services_installation_20200702['RSS_Etablissement']\
.str\
.extract(r'(?P<RSS>[0-9]+)\s\-\s(?P<RSS_nom>.+$)')

Remarquez la syntaxe ?P<> qui débute un groupe de capture délimité par des parenthèses. Ceci permet de nommer les groupes, qui deviendront des colonnes d'un DataFrame.


*Conseil *: Pour avoir le détail d'une expression régulière, j'utilise le site regex101.com

Site regex101.com, volet gauche Site regex101.com, volet droite


On remplace ensuite les valeurs manquantes pour éviter les erreurs :

rss_mod.fillna(0, inplace=True)

On importe ces nouvelles colonnes dans le DataFrame principal

capacite_services_installation_20200702['RSS'] = rss_mod['RSS'].astype(int)
capacite_services_installation_20200702['RSS_nom'] = rss_mod['RSS_nom']

On supprime la colonne originale

del capacite_services_installation_20200702['RSS_Etablissement']

Capacités des installations - nettoyer une colonne

La colonne "Date extration" contient une date, précédée de la chaine "Produit le ".

Nous souhaitons convertir ce champ en date.

On supprime la chaine de caractère superflue avec la méthode replace, puis on convertit la colonne au format date avec la fonction to_datetime de Pandas, avec les paramètres par défaut, puisque la date est déjà en format ISO 8601.

capacite_services_installation_20200702['Date_extraction']=\
pd.to_datetime(capacite_services_installation_20200702['Date_extraction']\
               .str\
               .replace(r'Produit le ',''))

*Conseil *: Convertissez toujours vos dates en ISO 8601. Vous allez apprendre à aimer les dates !


Capacités des installations - Caractère vers numérique

Vous retrouverez parfois des manières originales de marquer les valeurs manquantes. Souvent, celles-ci auront pour conséquence qu'une colonne numérique sera lue comme une colonne caractère. Vous voulez donc la convertir en valeur numérique.

capacite_services_installation_20200702['Capacite_Installation']=\
capacite_services_installation_20200702['Capacite_Installation']\
.str\
.replace('n.a.','0')\
.astype(int)

Capacités des installations - Chargement des données

Comme j'écris dans une base de données PostgreSQL, j'utilise le module psycopg2.

Je me suis fait quelques fonctions pour aider

J'ai aussi utilisé une technique d'importation rapide qui utilise le module de bas niveau StringIO, emprunté de la page GitHub de Naysan Saran. Merci !

Pour charger les données dans une table de la base de données PostgreSQL, je crée le DDL à la main

TODO : générer le string create_statement automatiquement !

ps_table.table_if_not_exists(conn=conn,
                            table="msss_gouv_qc_ca.capacite_services_installation",
                            create_statement="""
                            id integer not null,
                            Code_Etablissement_Reg integer,
                            Nom_Etablissement_Reg text,
                            Code_Etablissement integer,
                            Nom_Etablissement text,
                            Statut text,
                            Mode_financement text,
                            Code_Installation integer,
                            Nom_Installation text,
                            MCT_Capacite_Service text,
                            Unite_mesure_Installation text,
                            Capacite_Service text,
                            Capacite_Installation integer,
                            Date_extraction date,
                            RSS integer,
                            RSS_nom text,
                            RTS integer,
                            RTS_nom text,
                            Territoire_CLSC integer,
                            Territoire_CLSC_nom text
                            """)

Ensuite, je charge les données dans la table que j'ai créée

cfs.copy_from_stringio(conn=conn,
                       df=capacite_services_installation_20200702,
                       table="msss_gouv_qc_ca.capacite_services_installation")

Effectif du réseau de la santé - Préparation des données

*Second exemple *: Source : Portrait des effectifs du réseau de la santé et des services sociaux

Premièrement, on télécharge les données

wget https://www.donneesquebec.ca/recherche/fr/dataset/25dabc10-e61a-491c-a0de-98b4d788b4d2/resource/36a2b8b4-899c-408d-92bb-5ae9383eb89c/download/a---donneesparcategoriesetsous-categoriesdepersonnelconcernant.csv \
--output-document=a_categorie_ss_personnel_etc_emploi.csv

Puis, on les lit dans Python avec Pandas, en utilisant la colonne NUMEROTATION comme index.

a_categorie_ss_personnel_etc_emploi =\
pd.read_csv("/raw/msss_gouv_qc_ca/a_categorie_ss_personnel_etc_emploi.csv",
            index_col="NUMEROTATION")

Exemple de donnes :

Code :

a_categorie_ss_personnel_etc_emploi.iloc[0].to_dict()

Résultat :

{'VARIABLES' : "Nombre de personnes en emploi au 31 mars de l'année",
 'CATEGORIE_PERSONNEL' : '11 - Infirmière',
 '2019_TOTAL_EFFECTIF' : '31 296',
 '2018_TOTAL_EFFECTIF' : '31 778',
 '2017_TOTAL_EFFECTIF' : '32 310',
 '2016_TOTAL_EFFECTIF' : '33 434',
 '2015_TOTAL_EFFECTIF' : '33 805',
 '2014_TOTAL_EFFECTIF' : '35 011',
 '2013_TOTAL_EFFECTIF' : '35 114',
 '2012_TOTAL_EFFECTIF' : '35 275',
 '2011_TOTAL_EFFECTIF' : '35 670',
 '2010_TOTAL_EFFECTIF' : '36 087',
 'TITRE_RAPPORT' : 'Données par catégories et sous-catégories de personnel concernant',
 'DATE_PRODUCTION' : '2019-12-01',
 'SOURCE' : 'Banque de données sur les cadres et salariés du réseau de la santé et des services sociaux (R25)',
 'PRODUIT_PAR' : 'Direction générale du personnel réseau et ministériel (DGPRM) du MSSS'}

Effectif du réseau de la santé - Correction d'un nom de colonne

On doit corriger le nom d'une colonne qui contient un tiret

a_categorie_ss_personnel_etc_emploi = a_categorie_ss_personnel_etc_emploi\
.rename(columns={"CATEGORIE_SOUS-CATEGIE_PERSONNEL":"CATEGORIE_PERSONNEL"})

Effectif du réseau de la santé - Format large vers format long

Les données des effectifs sont disposées selon le format "large" dans le fichier CSV, on désire donc les ramener sur le format "long" pour faciliter les traitements futurs en ayant des données normalisées.

*Conseil *: La forme normale de Boyce-Codd est la représentation de données la plus courante dans les bases de données relationnelles. C'est un des concepts les plus importants en informatique qui a permis l'essor des bases de données et de la science des données. Vous devriez toujours chercher à stocker vos données sous cette forme.

a_categorie_ss_personnel_etc_emploi_m = pd.melt(frame=a_categorie_ss_personnel_etc_emploi,
                id_vars=['VARIABLES',
                         'CATEGORIE_PERSONNEL',
                         'TITRE_RAPPORT',
                         'DATE_PRODUCTION',
                         'SOURCE',
                         'PRODUIT_PAR'],
                value_vars=['2019_TOTAL_EFFECTIF',
                         '2018_TOTAL_EFFECTIF',
                         '2017_TOTAL_EFFECTIF',
                         '2016_TOTAL_EFFECTIF',
                         '2015_TOTAL_EFFECTIF',
                         '2014_TOTAL_EFFECTIF',
                         '2013_TOTAL_EFFECTIF',
                         '2012_TOTAL_EFFECTIF',
                         '2011_TOTAL_EFFECTIF',
                         '2010_TOTAL_EFFECTIF'],
                var_name='ANNEE',
                value_name='TOTAL_EFFECTIF'
               )

On extrait ensuite l'année de la colonne ANNEE:

a_categorie_ss_personnel_etc_emploi_m['ANNEE']=a_categorie_ss_personnel_etc_emploi_m['ANNEE']\
.str\
.extract(r'(?P<annee>[0-9]+)_TOTAL_EFFECTIF')\
.astype(int)

Éducation - Indices de défavorisation

Source : Éducation - Indices de défavorisation

Téléchargement des données :

wget http://www.education.gouv.qc.ca/fileadmin/site_web/documents/PSG/statistiques_info_decisionnelle/Indices-defavorisation-2018-2019.xlsx \
--output-document=Indices-defavorisation-2018-2019.xlsx

Nous avons ici affaire à des chiffriers Excel qui contiennent des données qui ne sont pas directement exploitables. Nous allons donc faire du "Excel Mining" avec le module xlrd (Site web).

Capture d'écran du fichier Excel Indices-defavorisation-2018-2019.xlsx

La grosse fonction qui fait tout !

TODO : Refactoriser en plusieurs petites fonctions

def indices_ecoles(loc,periode) :
    wb = xlrd.open_workbook(loc)
# Décompte du nombre de feuilles
    nb_sheets = wb.nsheets
# Liste pour contenir chacune des écoles
    liste_ecoles = []
# Boucle sur chacune des feuilles (commissions scolaires)
    for sheet_no in range(nb_sheets):

        sheet = wb.sheet_by_index(sheet_no)
        # Nous faisons l'extraction de certaines métadonnées
        meta = {
            'periode':periode,
            'numero_cs':sheet.cell_value(1, 0),
            'nom_cs':sheet.cell_value(1, 1)
        }
        # On itère ensuite sur chaque ligne pour identifier
        # les sections avec les écoles primaires et secondaires
        for i in range(sheet.nrows) :
            if sheet.cell_value(i, 1) == 'Écoles secondaires':
                type_ecole = ['secondaire']
            if sheet.cell_value(i, 1) == 'Écoles primaires':
                type_ecole = ['primaire']
            # Le code à 6 chiffres est un code d'école
            res = re.search(r'\d{6}', str(sheet.cell_value(i, 0)))
            if res != None :
                if sheet.cell_value(i, 0) != meta['numero_cs']:
                    all_values = list(meta.values())+type_ecole+sheet.row_values(i)
                    all_values = [str(x).strip() for x in all_values]
                    liste_ecoles.append(all_values)
    df_ecoles = pandas.DataFrame(liste_ecoles,
                                columns=['periode',
                                         'numero_cs',
                                         'nom_cs',
                                         'type_ecole',
                                         'code_ecole',
                                         'nom_ecole',
                                         'indice_seuil_faible_revenu',
                                         'rang_decile_sfr',
                                         'indice_milieu_socio_econo',
                                         'rang_decile_imse',
                                         'nb_eleves'])
    df_ecoles[['numero_cs','code_ecole']] = df_ecoles[['numero_cs','code_ecole']].applymap(lambda x : x.replace(r'.0',''))

    return df_ecoles

On appelle la fonction

df_ecoles_2019 = indices_ecoles(loc[2],'2018-2019')

Et nous avons des données nettoyées !

Données nettoyées pour l'année 2019

Pour les curieux

J'ai fait un web scraping de la base de données K10 du MSSS : Registre des résidences privées pour aînés avec scrapy.

Conclusion

Bientôt ...

  • Extraction OCR de fichiers Excel imprimés, faxés puis numérisés (AKA demande d'accès à l'information) avec le module de vision par ordinateur OpenCV et Tesseract OCR
  • Des statistiques descriptives
  • Des modèles

Suivez mon projet sur ma page Gitea