Lecture de fichiers hprimXML avec python

Author

gpr

Published

August 16, 2024


Bibliothèques utilisées
  • lxml : pour enlever le namespace xmlns avec un script xslt
  • xmltodict : pour convertir le xml en dict python
  • json : pour encapsuler le dict en json pour la lecture avec pl.read_json
  • polars : pour les étapes de déstructuration de la donnée hiérarchique issue des XML
  • io, os, glob : pour les étapes de manipulations de fichiers dans le système
Ressources
  • Le script pour le namespace xslt est issu de la ressource ici
  • La fonction d’applatissement récursif des struct/list en polars est issue d’ici


Contexte / exemple de fichiers

Le contenu des fichiers nous intéresse. Leur lecture n’est pas des plus aisée puisque le contenu est très hiérarchique, voire dynamique en fonction de la prise en charge du patient.

f = open('pyhprimxml/data/xml_tests/actes/00001.xml', 'r')

print(f.read()[0:752] + '\n... ... ... ...\n... ... ... ...\n')
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<evenementsServeurActes acquittementAttendu="oui" version="2.00" xmlns="http://www.hprim.org/hprimXML">
    <enteteMessage>
        <identifiantMessage>00001_acte</identifiantMessage>
        <dateHeureProduction>2024-01-01T01:01:01</dateHeureProduction>
        <emetteur>
            <agents>
                <agent categorie="application">
                    <code>TEST</code>
                </agent>
            </agents>
        </emetteur>
        <destinataire>
            <agents>
                <agent categorie="application"/>
            </agents>
        </destinataire>
        <commentaireMessage>HPRIM-TEST</commentaireMessage>
    </enteteMessage>
    <evenementServeurActe>
        <patient>
            <identifiant>
                <emetteur>
                    <valeur>123456789</valeur>
                </emetteur>
                <recepteur>
                    <valeur>123456789</valeur>
... ... ... ...
... ... ... ...
from pyhprimxml import read_hprimxml
from pyhprimxml import recursively_flatten
from pyhprimxml import flatten
from pyhprimxml import unpack


Évènement evenementsServeurActes

On lit un fichier exemple ici avec des données fictives.

La fonction read_hprimxml retourne un dictionnaire avec deux objets :

  • le nom de l’évènement contenu dans le fichier
  • un dataframe polars qui contient les éléments XML du niveau inférieur à evenementsServeurActes.
input_file = 'pyhprimxml/data/xml_tests/actes/00001.xml'
read_hprimxml(input_file)
{'type_evenement': ['evenementsServeurActes'],
 'message': shape: (1, 5)
 ┌─────────────────────┬─────────┬───────────────────────────┬──────────────────────────┬───────────┐
 │ acquittementAttendu ┆ version ┆ enteteMessage             ┆ evenementServeurActe     ┆ source_id │
 │ ---                 ┆ ---     ┆ ---                       ┆ ---                      ┆ ---       │
 │ str                 ┆ str     ┆ struct[5]                 ┆ struct[4]                ┆ str       │
 ╞═════════════════════╪═════════╪═══════════════════════════╪══════════════════════════╪═══════════╡
 │ oui                 ┆ 2.00    ┆ {"00001_acte","2024-01-01 ┆ {{{{"123456789"},{"12345 ┆ 00001.xml │
 │                     ┆         ┆ T01:0…                    ┆ 6789"}…                  ┆           │
 └─────────────────────┴─────────┴───────────────────────────┴──────────────────────────┴───────────┘}

Focus sur l’entête du message

On peut applatir le contenu de l’élément (ici enteteMessage) de deux manières, la première avec flatten permet de préfixer les objets enfants de l’élément par le nom de l’objet parent (enteteMessage).

C’est au choix selon que l’on veuille de noms de colonnes très informatifs ou non, selon ce que l’on veut réaliser ensuite.

flatten(
    read_hprimxml(input_file)['message']
    .select('source_id', 'enteteMessage')
)
shape: (1, 6)
source_id enteteMessage.identifiantMessage enteteMessage.dateHeureProduction enteteMessage.emetteur enteteMessage.destinataire enteteMessage.commentaireMessage
str str str struct[1] struct[1] str
"00001.xml" "00001_acte" "2024-01-01T01:01:01" {{{"application","TEST"}}} {{{"application"}}} "HPRIM-TEST"

La deuxième avec unpack au travers d’un pipe polars permet de “déliver les objets enfants de l’élément de l’élément parent.

(
    read_hprimxml(input_file)['message']
    .select('source_id', 'enteteMessage')
    .pipe(unpack, 'enteteMessage')
)
shape: (1, 6)
source_id identifiantMessage dateHeureProduction emetteur destinataire commentaireMessage
str str str struct[1] struct[1] str
"00001.xml" "00001_acte" "2024-01-01T01:01:01" {{{"application","TEST"}}} {{{"application"}}} "HPRIM-TEST"

Dans les deux cas, on remarque qu’il reste des éléments dont la structure est encore sous forme de dictionnaire, hiérarchique. Et dans les deux cas on peut leur appliquer la fonction récursive d’applatissement (qui viendra ici préfixer les noms de colonnes en conséquence) :

recursively_flatten(
    read_hprimxml(input_file)['message']
    .select('source_id', 'enteteMessage')
    .pipe(unpack, 'enteteMessage')
)
shape: (1, 7)
source_id identifiantMessage dateHeureProduction emetteur.agents.agent.categorie emetteur.agents.agent.code destinataire.agents.agent.categorie commentaireMessage
str str str str str str str
"00001.xml" "00001_acte" "2024-01-01T01:01:01" "application" "TEST" "application" "HPRIM-TEST"
import polars as pl
with pl.Config(fmt_str_lengths=50):
    print(recursively_flatten(
        read_hprimxml(input_file)['message']
        .select('source_id', 'enteteMessage')
    ).unpivot(index = 'source_id', variable_name = 'element_name', value_name = 'value'))
shape: (6, 3)
┌───────────┬───────────────────────────────────────────────────┬─────────────────────┐
│ source_id ┆ element_name                                      ┆ value               │
│ ---       ┆ ---                                               ┆ ---                 │
│ str       ┆ str                                               ┆ str                 │
╞═══════════╪═══════════════════════════════════════════════════╪═════════════════════╡
│ 00001.xml ┆ enteteMessage.identifiantMessage                  ┆ 00001_acte          │
│ 00001.xml ┆ enteteMessage.dateHeureProduction                 ┆ 2024-01-01T01:01:01 │
│ 00001.xml ┆ enteteMessage.emetteur.agents.agent.categorie     ┆ application         │
│ 00001.xml ┆ enteteMessage.emetteur.agents.agent.code          ┆ TEST                │
│ 00001.xml ┆ enteteMessage.destinataire.agents.agent.categorie ┆ application         │
│ 00001.xml ┆ enteteMessage.commentaireMessage                  ┆ HPRIM-TEST          │
└───────────┴───────────────────────────────────────────────────┴─────────────────────┘

Focus sur le contenu du message

(
    read_hprimxml(input_file)['message']
    .select('source_id', 'evenementServeurActe')
    .pipe(unpack, 'evenementServeurActe')
)
shape: (1, 5)
source_id patient venue intervention actesCCAM
str struct[2] struct[3] struct[5] struct[1]
"00001.xml" {{{"123456789"},{"123456789"}},{"M","SANDWICK",{"JOHN"},{"1970-01-01"}}} {"non",{{"987654321"},{"987654321"}},{{"2024-01-01","08:00:00"},{"4321"}}} {{"000000000001"},{"2024-01-01","08:10:00"},{"2024-01-01","09:00:00"},{"4321"},{{"2023-12-25","02:30:00"},{"4321"}}} {[{"creation","oui","oui",{"121212"},"EBLA003","1","0",{"2024-01-01","08:10:00"},{{{"oui",{{"01234567"}}}},{"4321"}},"1",{"","",""}}, {"creation","oui","oui",{"121213"},"YYYY033","1","0",{"2024-01-01","08:10:00"},{{{"oui",{{"01234567"}}}},{"4321"}},"1",{"","",""}}, … {"creation","oui","oui",{"121215"},"NFKA006","1","0",{"2024-01-01","08:10:00"},{{{"oui",{{"01234567"}}}},{"4321"}},"1",{"","",""}}]}

Donnée patient

with pl.Config(fmt_str_lengths=50):
    print(
    recursively_flatten(
        read_hprimxml(input_file)['message']
        .select('source_id', 'evenementServeurActe')
        .unnest('evenementServeurActe')
        .select('source_id', 'patient')
    )
    .unpivot(index = 'source_id', variable_name = 'element_name', value_name = 'value')
    )
shape: (6, 3)
┌───────────┬─────────────────────────────────────────────┬────────────┐
│ source_id ┆ element_name                                ┆ value      │
│ ---       ┆ ---                                         ┆ ---        │
│ str       ┆ str                                         ┆ str        │
╞═══════════╪═════════════════════════════════════════════╪════════════╡
│ 00001.xml ┆ patient.identifiant.emetteur.valeur         ┆ 123456789  │
│ 00001.xml ┆ patient.identifiant.recepteur.valeur        ┆ 123456789  │
│ 00001.xml ┆ patient.personnePhysique.sexe               ┆ M          │
│ 00001.xml ┆ patient.personnePhysique.nomUsuel           ┆ SANDWICK   │
│ 00001.xml ┆ patient.personnePhysique.prenoms.prenom     ┆ JOHN       │
│ 00001.xml ┆ patient.personnePhysique.dateNaissance.date ┆ 1970-01-01 │
└───────────┴─────────────────────────────────────────────┴────────────┘

Donnée venue

with pl.Config(fmt_str_lengths=50):
    print(
    recursively_flatten(
        read_hprimxml(input_file)['message']
        .select('source_id', 'evenementServeurActe')
        .unnest('evenementServeurActe')
        .select('source_id', 'venue')
    )
    .unpivot(index = 'source_id', variable_name = 'element_name', value_name = 'value')
    )
shape: (6, 3)
┌───────────┬─────────────────────────────────────────────────┬────────────┐
│ source_id ┆ element_name                                    ┆ value      │
│ ---       ┆ ---                                             ┆ ---        │
│ str       ┆ str                                             ┆ str        │
╞═══════════╪═════════════════════════════════════════════════╪════════════╡
│ 00001.xml ┆ venue.prive                                     ┆ non        │
│ 00001.xml ┆ venue.identifiant.emetteur.valeur               ┆ 987654321  │
│ 00001.xml ┆ venue.identifiant.recepteur.valeur              ┆ 987654321  │
│ 00001.xml ┆ venue.entree.dateHeureOptionnelle.date          ┆ 2024-01-01 │
│ 00001.xml ┆ venue.entree.dateHeureOptionnelle.heure         ┆ 08:00:00   │
│ 00001.xml ┆ venue.entree.uniteFonctionnelleResponsable.code ┆ 4321       │
└───────────┴─────────────────────────────────────────────────┴────────────┘

Donnée intervention

with pl.Config(fmt_str_lengths=50):
    print(
    recursively_flatten(
        read_hprimxml(input_file)['message']
        .select('source_id', 'evenementServeurActe')
        .unnest('evenementServeurActe')
        .select('source_id', 'intervention')
    )
    .unpivot(index = 'source_id', variable_name = 'element_name', value_name = 'value')
    )
shape: (9, 3)
┌───────────┬──────────────────────────────────────────────┬──────────────┐
│ source_id ┆ element_name                                 ┆ value        │
│ ---       ┆ ---                                          ┆ ---          │
│ str       ┆ str                                          ┆ str          │
╞═══════════╪══════════════════════════════════════════════╪══════════════╡
│ 00001.xml ┆ intervention.identifiant.emetteur            ┆ 000000000001 │
│ 00001.xml ┆ intervention.debut.date                      ┆ 2024-01-01   │
│ 00001.xml ┆ intervention.debut.heure                     ┆ 08:10:00     │
│ 00001.xml ┆ intervention.fin.date                        ┆ 2024-01-01   │
│ 00001.xml ┆ intervention.fin.heure                       ┆ 09:00:00     │
│ 00001.xml ┆ intervention.uniteFonctionnelle.code         ┆ 4321         │
│ 00001.xml ┆ intervention.demande.datePrescription.date   ┆ 2023-12-25   │
│ 00001.xml ┆ intervention.demande.datePrescription.heure  ┆ 02:30:00     │
│ 00001.xml ┆ intervention.demande.uniteFonctionnelle.code ┆ 4321         │
└───────────┴──────────────────────────────────────────────┴──────────────┘

Données actes CCAM

recursively_flatten(
    read_hprimxml(input_file)['message']                
        .select('source_id', 'evenementServeurActe')
        .pipe(unpack, 'evenementServeurActe')
        .select('source_id', 'actesCCAM')
        .unnest('actesCCAM')
        .pipe(unpack, 'acteCCAM')
)
shape: (4, 17)
source_id action facturable valide identifiant.emetteur codeActe codeActivite codePhase execute.date execute.heure executant.medecins.medecinExecutant.principal executant.medecins.medecinExecutant.medecin.identification.code executant.uniteFonctionnelle.code quantite radiotherapie.nombreFaisceau radiotherapie.typeDosimetrie radiotherapie.typeMachine
str str str str str str str str str str str str str str str str str
"00001.xml" "creation" "oui" "oui" "121212" "EBLA003" "1" "0" "2024-01-01" "08:10:00" "oui" "01234567" "4321" "1" "" "" ""
"00001.xml" "creation" "oui" "oui" "121213" "YYYY033" "1" "0" "2024-01-01" "08:10:00" "oui" "01234567" "4321" "1" "" "" ""
"00001.xml" "creation" "oui" "oui" "121214" "EPLF002" "1" "0" "2024-01-01" "08:10:00" "oui" "01234567" "4321" "1" "" "" ""
"00001.xml" "creation" "oui" "oui" "121215" "NFKA006" "1" "0" "2024-01-01" "08:10:00" "oui" "01234567" "4321" "1" "" "" ""


Évènement evenementsServeurEtatsPatient

De la même manière quand on lit un message de type evenenementsServeurEtatsPatient, on obtient :

input_file = 'pyhprimxml/data/xml_tests/etat_patient/00001.xml'
read_hprimxml(input_file)
{'type_evenement': ['evenementsServeurEtatsPatient'],
 'message': shape: (1, 8)
 ┌────────────┬─────────┬────────────┬────────────┬────────────┬────────────┬───────────┬───────────┐
 │ acquitteme ┆ version ┆ enteteMess ┆ patient    ┆ venue      ┆ interventi ┆ Diagnosti ┆ source_id │
 │ ntAttendu  ┆ ---     ┆ age        ┆ ---        ┆ ---        ┆ on         ┆ cs        ┆ ---       │
 │ ---        ┆ str     ┆ ---        ┆ struct[3]  ┆ struct[3]  ┆ ---        ┆ ---       ┆ str       │
 │ str        ┆         ┆ struct[5]  ┆            ┆            ┆ struct[2]  ┆ struct[1] ┆           │
 ╞════════════╪═════════╪════════════╪════════════╪════════════╪════════════╪═══════════╪═══════════╡
 │ non        ┆ 2.00    ┆ {"00001_es ┆ {"non",{{" ┆ {"non",{{" ┆ {{"0000000 ┆ {[{"creat ┆ 00001.xml │
 │            ┆         ┆ ep","2024- ┆ permanent" ┆ 987654321" ┆ 00001"},{" ┆ ion","dp" ┆           │
 │            ┆         ┆ 01-01T01:0 ┆ ,"local"," ┆ },{"987654 ┆ 4321"}}    ┆ ,"2024-01 ┆           │
 │            ┆         ┆ …          ┆ …          ┆ …          ┆            ┆ -01…      ┆           │
 └────────────┴─────────┴────────────┴────────────┴────────────┴────────────┴───────────┴───────────┘}

Donnée patient

import polars as pl
with pl.Config(fmt_str_lengths=50):
    print(
        recursively_flatten(
        read_hprimxml(input_file)['message']                
            .select('source_id', 'patient')
            .pipe(unpack, 'patient')
        ).unpivot(index = 'source_id', variable_name = 'element_name', value_name = 'value')
    )
shape: (13, 3)
┌───────────┬─────────────────────────────────────┬────────────┐
│ source_id ┆ element_name                        ┆ value      │
│ ---       ┆ ---                                 ┆ ---        │
│ str       ┆ str                                 ┆ str        │
╞═══════════╪═════════════════════════════════════╪════════════╡
│ 00001.xml ┆ confidentiel                        ┆ non        │
│ 00001.xml ┆ identifiant.emetteur.etat           ┆ permanent  │
│ 00001.xml ┆ identifiant.emetteur.portee         ┆ local      │
│ 00001.xml ┆ identifiant.emetteur.referent       ┆ non        │
│ 00001.xml ┆ identifiant.emetteur.valeur         ┆ 123456789  │
│ …         ┆ …                                   ┆ …          │
│ 00001.xml ┆ identifiant.recepteur.valeur        ┆ 123456789  │
│ 00001.xml ┆ personnePhysique.sexe               ┆ M          │
│ 00001.xml ┆ personnePhysique.nomUsuel           ┆ SANDWICK   │
│ 00001.xml ┆ personnePhysique.prenoms.prenom     ┆ JOHN       │
│ 00001.xml ┆ personnePhysique.dateNaissance.date ┆ 1970-01-01 │
└───────────┴─────────────────────────────────────┴────────────┘

Donnée diagnostics (CIM-10)

(
    read_hprimxml(input_file)['message']                
    .select('source_id', 'Diagnostics')
    .pipe(unpack, 'Diagnostics')
    .pipe(unpack, 'diagnostic')
)
shape: (2, 5)
source_id action type dateAction codeCim10
str str str str str
"00001.xml" "creation" "dp" "2024-01-01T08:10:00" "Z511"
"00001.xml" "creation" "dr" "2024-01-01T08:10:00" "C10"


Évènement evenementsPMSI

Les évènements PMSI sont plus complexes hiérachiquement car ils imbriquent la donnée PMSI sous son format réglementaire : les éléments actes et diagnostics sont saisis dans des éléments rum, eux-mêmes enfants de l’élément rss.

input_file = 'pyhprimxml/data/xml_tests/pmsi/00001.xml'
read_hprimxml(input_file)
{'type_evenement': ['evenementsPMSI'],
 'message': shape: (1, 5)
 ┌─────────────────────┬─────────┬───────────────────────────┬──────────────────────────┬───────────┐
 │ acquittementAttendu ┆ version ┆ enteteMessage             ┆ evenementPMSI            ┆ source_id │
 │ ---                 ┆ ---     ┆ ---                       ┆ ---                      ┆ ---       │
 │ str                 ┆ str     ┆ struct[6]                 ┆ struct[3]                ┆ str       │
 ╞═════════════════════╪═════════╪═══════════════════════════╪══════════════════════════╪═══════════╡
 │ oui                 ┆ 2.00    ┆ {"reel","00001_pmsi","202 ┆ {{"non","non",{{"1234567 ┆ 00001.xml │
 │                     ┆         ┆ 4-01-…                    ┆ 89"},{…                  ┆           │
 └─────────────────────┴─────────┴───────────────────────────┴──────────────────────────┴───────────┘}

Unité médicale et diagnostics

C’est dans l’élément rum que l’on trouve les informations sur l’unité médicale fréquentée (UF) ainsi que tous les éléments relatifs à la saisie du PMSI (y compris qui a réalisé l’acte, et des éléments de facturation de la CCAM).

Ici on déplie les noeuds xml unité médicale et diagnostics

recursively_flatten(
    read_hprimxml(input_file)['message']                
        .select('source_id', 'evenementPMSI')
        .pipe(unpack, 'evenementPMSI')
        .select('source_id', 'rss')
        .pipe(unpack, 'rss')
        .pipe(unpack, 'rum')
        .select('uniteMedicale','diagnostics', 'source_id')
        #.pipe(unpack, 'diagnostics')
)
shape: (2, 7)
uniteMedicale.code uniteMedicale.entree.mode uniteMedicale.entree.date uniteMedicale.entree.heure diagnostics.diagnosticPrincipal.codeCim10 diagnostics.diagnosticRelie.codeCim10 source_id
str str str str str str str
"4321" "8" "2024-01-01" "08:00:00" "Z511" "C10" "00001.xml"
"5000" "8" "2024-01-01" "09:00:00" "Z743" null "00001.xml"

Unité médicale et actes

Ici on déplie les actes et les unités médicales. Les actes sont bien rattachés à leur saisie dans un passage dans une unité médicale.

recursively_flatten(
    read_hprimxml(input_file)['message']                
        .select('source_id', 'evenementPMSI')
        .unnest('evenementPMSI')
        .select('source_id', 'rss')
        .unnest('rss')
        .pipe(unpack, 'rum')
        .select('uniteMedicale','actes', 'source_id')
        .unnest('actes')
        .pipe(unpack, 'acte')
        .pipe(unpack, 'CCAM')
)
shape: (5, 12)
uniteMedicale.code uniteMedicale.entree.mode uniteMedicale.entree.date uniteMedicale.entree.heure externe remboursementExceptionnel codeActe codePhase codeActivite quantite dateRealisation source_id
str str str str str str str str str str str str
"4321" "8" "2024-01-01" "08:00:00" "non" "non" "EBLA003" "0" "1" "1" "2024-01-01T08:10:00" "00001.xml"
"4321" "8" "2024-01-01" "08:00:00" "non" "non" "EPLF002" "0" "1" "1" "2024-01-01T08:10:00" "00001.xml"
"4321" "8" "2024-01-01" "08:00:00" "non" "non" "NFKA006" "0" "1" "1" "2024-01-01T08:10:00" "00001.xml"
"4321" "8" "2024-01-01" "08:00:00" "non" "non" "YYYY033" "0" "1" "1" "2024-01-01T08:10:00" "00001.xml"
"5000" "8" "2024-01-01" "09:00:00" "non" "non" "QZRP004" "0" "1" "1" "2024-01-01T09:10:00" "00001.xml"

Médecin responsable du passage dans l’unité

recursively_flatten(
    read_hprimxml(input_file)['message']                
        .select('source_id', 'evenementPMSI')
        .unnest('evenementPMSI')
        .select('source_id', 'rss')
        .unnest('rss')
        .pipe(unpack, 'rum')
        .select('medecinResponsable', 'uniteMedicale','source_id')
        .unnest('medecinResponsable')
)
shape: (2, 8)
identification.code personne.nomUsuel personne.prenoms.prenom uniteMedicale.code uniteMedicale.entree.mode uniteMedicale.entree.date uniteMedicale.entree.heure source_id
str str str str str str str str
"01234567" "HOBBES" "MARC-JACQUES" "4321" "8" "2024-01-01" "08:00:00" "00001.xml"
"01234567" "HOBBES" "MARC-JACQUES" "5000" "8" "2024-01-01" "09:00:00" "00001.xml"


Boucle sur un ensemble de fichiers hprimXML


%%time
import glob
resu = pl.DataFrame()
for f in glob.glob('raw_input/2024_08_02/actes/actes/*.xml'):
    #print(f)
    resu = pl.concat([resu, 
                      recursively_flatten(
                          read_hprimxml(f)['message']
                          #.lazy()
                          .select('source_id', 'evenementServeurActe')
                          .unnest('evenementServeurActe')
                          .select('source_id', 'actesCCAM')
                          .unnest('actesCCAM')
                          .pipe(unpack, 'acteCCAM')
                          #.collect()
                      )
                     ], how = 'diagonal')
CPU times: user 5.48 s, sys: 2.13 s, total: 7.61 s
Wall time: 5.26 s
resu.group_by('codeActe').agg(pl.col('execute.date').len().alias('nb acte ccam')).head()
shape: (5, 2)
codeActe nb acte ccam
str u32
"EBLA003" 192
"EPLF002" 107
"YYYY033" 2
"QZRP004" 1
"NFKA006" 100