Serveur d'actes et états patient¶
Ces deux messages suivent les mêmes principes que le PMSI : un fichier par événement,
mêmes options (entete, xmlns, version, validate_xsd, filename, encoding,
colmap), colonnes-chemins à l'écriture et extra à la lecture.
Colonnes communes de evenements¶
| groupe | colonnes |
|---|---|
| patient | ipp, ipp_recepteur, sexe, nom_usuel, nom_naissance, prenom, date_naissance |
| venue (facultative) | nda, nda_recepteur, venue_date_entree, venue_heure_entree, venue_mode_entree, venue_uf_responsable, venue_date_sortie, venue_heure_sortie |
| intervention (facultative) | intervention_id, intervention_date_debut, intervention_heure_debut, intervention_date_fin, intervention_heure_fin, intervention_uf |
| demande (facultative) | demande_date, demande_heure, demande_uf |
| repère des états patient | date_observation, heure_observation ; ou recueil_id, recueil_date, recueil_heure, recueil_uf |
| en-tête | voir En-tête du message |
Obligatoires : evt_id, ipp, sexe, date_naissance. La venue n'est écrite que si
nda est renseigné, l'intervention que si intervention_id l'est.
Serveur d'actes : evenementsServeurActes¶
hx.write_evenements_serveur_actes(evenements, actes, entete=..., out_dir="sortie/")
t = hx.read_evenements_serveur_actes("sortie/") # {"evenements", "actes"}
Table actes : une ligne par acteCCAM. Les colonnes-chemins sont relatives à acteCCAM.
| colonne | élément / attribut XML | |
|---|---|---|
acte_id |
acteCCAM/identifiant/emetteur |
obligatoire |
code, extension_pmsi |
codeActe, codeActeExtensionPMSI |
code obligatoire |
activite, phase |
codeActivite, codePhase |
obligatoires |
date_execution, heure_execution |
execute/date, execute/heure |
date obligatoire |
medecin |
executant/medecins/medecinExecutant/medecin/identification/code |
obligatoire |
medecin_principal |
medecinExecutant/@principal (défaut : oui) |
|
uf |
executant/uniteFonctionnelle/code |
|
quantite |
quantite (nombre5, chaîne de chiffres) |
|
action |
@action (défaut : creation) |
|
facturable, valide |
@facturable, @valide |
|
modificateurs |
modificateurs/modificateur (ex. "7J") |
|
association_non_prevue, extension_documentaire |
codeAssociationNonPrevue, codeExtensionDocumentaire |
Les actes NGAP, LPP et UCD ne sont pas encore pris en charge par la couche tabulaire ; ils restent accessibles par le dictionnaire.
États patient : evenementsServeurEtatsPatient¶
hx.write_evenements_etat_patient(evenements, diagnostics, entete=..., out_dir="sortie/")
t = hx.read_evenements_etat_patient("sortie/") # {"evenements", "diagnostics"}
Ce message n'a pas de nœud « événement » : les colonnes-chemins de evenements sont
relatives à la racine du message.
Le XSD impose un repère, au choix : une intervention (colonnes intervention_*), une
date d'observation (colonnes date_observation, heure_observation) ou un recueil
(colonnes recueil_id, recueil_date, recueil_heure, recueil_uf). Le recueil est le plus
précis : il porte l'UF où les états ont été observés, qui permet au récepteur de retrouver le
RUM. Le contenu est lui aussi un choix exclusif : des
diagnostics, ou un IGS2, ou des dépendances… En 2.4, un élément <divers/> est
obligatoire ; il est émis automatiquement.
Table diagnostics : une ligne par diagnostic. Les colonnes-chemins sont relatives à diagnostic.
| colonne | élément / attribut XML | |
|---|---|---|
type |
diagnostic/@type : dp, dr, da, ds, dd, fpc, mm, ae (mis en minuscules) |
obligatoire |
code |
codeCim10 (point retiré) |
obligatoire |
date_action |
dateAction |
|
action |
@action (défaut : creation) |
États patient : IGS2 détaillé¶
L'IGS2 détaillé passe par un message états patient : un élément <igs2> dont chaque item
est un attribut qui porte une classe (age="60A69", frequenceCardiaque="120A159"…),
plus le score global valeurGlobale (de 0 à 194). Aucune fonction dédiée n'est nécessaire :
les colonnes-chemins igs2/@age, igs2/valeurGlobale… donnent accès à tout
l'élément.
Le script examples/exemple_igs2.py part des pires valeurs mesurées pendant les 24 premières
heures. Il en déduit la classe et les points de chaque item selon la grille IGS2 de la SFAR
(Le Gall, 1993), puis écrit un fichier par patient, en version 2.00, validé contre le XSD.
Points imposés par le XSD :
- un seul
<igs2>par message, donc un fichier par patient ; - un repère obligatoire : ici le
recueil(colonnesrecueil_id,recueil_date,recueil_uf), qui donne la date et l'UF où les états ont été observés ; le récepteur retrouve ainsi le RUM, même un jour de mutation. Une simple date (date_observation) ou une intervention sont aussi possibles ; - un contenu exclusif : un message porte soit des diagnostics, soit un IGS2, soit des dépendances…, jamais deux à la fois ;
- en version 2.4 (pas en 2.00), un élément
<divers/>obligatoire, émis automatiquement.
La table de contrôle produite par le script (classe et points par item) :
| item | classe_IGS240001 | classe_IGS240002 | points_IGS240001 | points_IGS240002 |
|---|---|---|---|---|
| age | 60A69 | superieurEgalA80 | 12 | 18 |
| frequenceCardiaque | 120A159 | 70A119 | 4 | 0 |
| tensionArterielleSystolique | 70A99 | superieurEgalA200 | 5 | 2 |
| temperature | inferieurA39 | superieurEgalA39 | 0 | 3 |
| paO2_FIO2 | 100A199 | rien | 9 | 0 |
| diurese | "0,5A0,999" | superieurA1 | 4 | 0 |
| ureeSanguine | "10A29,9" | inferieurA10 | 6 | 0 |
| leucocytes | "1A19,9" | superieurEgalA20 | 0 | 3 |
| kaliemie | "3A4,9" | superieurEgalA5 | 0 | 3 |
| natremie | 125A144 | superieurEgalA145 | 0 | 1 |
| HCO3 | 15A19 | superieurEgalA20 | 3 | 0 |
| bilirubine | "inferieurA68,4" | "superieurA102,6" | 0 | 9 |
| scoreGlasgow | 11A13 | 14A18 | 5 | 0 |
| maladieChronique | aucune | cancerMetastatique | 0 | 9 |
| modeAdmission | medecine | chirUrgente | 6 | 8 |
Le code complet :
# IGS2 détaillé dans un message états patient (evenementsServeurEtatsPatient).
# Des mesures brutes (pires valeurs des 24 premières heures), on déduit pour chaque item
# la classe attendue par le XSD (attribut de <igs2>) et ses points, puis le score global.
# Un fichier par patient : l'élément <igs2> n'est pas répétable dans un message.
from datetime import date, datetime
import polars as pl
import pyhprimxml as hx
entete = {
"version": "2.00", # version HPRIM XML des messages
"emetteur": "REA",
"destinataire": "DIM",
"date_heure_production": datetime(2024, 3, 6, 9, 0),
}
# Pires valeurs des 24 premières heures, une ligne par patient
mesures = pl.DataFrame({
"evt_id": ["IGS240001", "IGS240002"],
"age": [67, 82],
"frequence_cardiaque": [128, 105], # /min
"pas": [85, 210], # pression artérielle systolique, mmHg
"temperature": [38.2, 39.4], # °C
"ventile": [True, False], # ventilation mécanique ou CPAP
"pao2_fio2": [180, None], # mmHg, si ventilé
"diurese": [0.8, 1.6], # L/24 h
"uree": [18.0, 8.0], # mmol/L
"leucocytes": [14.0, 22.0], # 10³/mm³
"kaliemie": [4.1, 5.4], # mmol/L
"natremie": [132, 147], # mmol/L
"hco3": [17, 22], # mmol/L
"bilirubine": [25.0, 110.0], # µmol/L
"ictere": [False, True], # la bilirubine ne compte que si ictère
"glasgow": [13, 15],
"maladie_chronique": ["aucune", "cancerMetastatique"],
"mode_admission": ["medecine", "chirUrgente"],
})
# Barème IGS2 : attribut XSD -> (mesure, bornes, [(classe XSD, points)])
# Source : grille et définitions de la SFAR, https://sfar.org/scores/igs2.php
# (Le Gall JR et coll., JAMA 1993; 270: 2957-2963). Recueil sur les 24 heures suivant
# l'admission : valeur la plus défavorable de chaque item ; Glasgow estimé avant sédation ;
# diurèse ramenée à 24 h si le séjour est plus court ; en cas d'arrêt cardiaque, FC 0,
# PAS 0 et Glasgow < 6. Unités : urée en mmol/L (seuils SFAR en g/L : 0,6 et 1,8),
# leucocytes en 10³/mm³, bilirubine en µmol/L (seuils en mg/L : 40 et 60).
# Les bornes sont fermées à gauche : 40 tombe dans « 40A59 ».
BAREME = {
"age": ("age", [40, 60, 70, 75, 80],
[("inferieurA40", 0), ("40A59", 7), ("60A69", 12), ("70A74", 15),
("75A79", 16), ("superieurEgalA80", 18)]),
"frequenceCardiaque": ("frequence_cardiaque", [40, 70, 120, 160],
[("inferieurA40", 11), ("40A69", 2), ("70A119", 0), ("120A159", 4),
("superieurEgalA160", 7)]),
"tensionArterielleSystolique": ("pas", [70, 100, 200],
[("inferieurA70", 13), ("70A99", 5), ("100A199", 0), ("superieurEgalA200", 2)]),
"temperature": ("temperature", [39], [("inferieurA39", 0), ("superieurEgalA39", 3)]),
"paO2_FIO2": ("pao2_fio2", [100, 200],
[("inferieurA100", 11), ("100A199", 9), ("superieurEgalA200", 6)]),
"diurese": ("diurese", [0.5, 1.0],
[("inferieurA0,5", 11), ("0,5A0,999", 4), ("superieurA1", 0)]),
"ureeSanguine": ("uree", [10, 30],
[("inferieurA10", 0), ("10A29,9", 6), ("superieurA30", 10)]),
"leucocytes": ("leucocytes", [1, 20],
[("inferieurA1", 12), ("1A19,9", 0), ("superieurEgalA20", 3)]),
"kaliemie": ("kaliemie", [3, 5],
[("inferieurA3", 3), ("3A4,9", 0), ("superieurEgalA5", 3)]),
"natremie": ("natremie", [125, 145],
[("inferieurA125", 5), ("125A144", 0), ("superieurEgalA145", 1)]),
"HCO3": ("hco3", [15, 20], [("inferieurA15", 6), ("15A19", 3), ("superieurEgalA20", 0)]),
"bilirubine": ("bilirubine", [68.4, 102.6],
[("inferieurA68,4", 0), ("68,4A102,5", 4), ("superieurA102,6", 9)]),
"scoreGlasgow": ("glasgow", [6, 9, 11, 14],
[("inferieurA6", 26), ("6A8", 13), ("9A10", 7), ("11A13", 5), ("14A18", 0)]),
}
POINTS_QUALITATIFS = {
"maladieChronique": ("maladie_chronique",
{"aucune": 0, "cancerMetastatique": 9, "maladieHematologiqueMaligne": 10, "sida": 17}),
"modeAdmission": ("mode_admission", {"chirProgrammee": 0, "medecine": 6, "chirUrgente": 8}),
}
def classer(mesure: str, bornes: list, libelles: list) -> pl.Expr:
"""Classe d'une mesure : libelles[i] si bornes[i-1] <= mesure < bornes[i]."""
expr = pl.lit(libelles[-1])
for borne, libelle in reversed(list(zip(bornes, libelles))):
expr = pl.when(pl.col(mesure) < borne).then(pl.lit(libelle)).otherwise(expr)
return pl.when(pl.col(mesure).is_null()).then(None).otherwise(expr)
classes, points = [], []
for attr, (mesure, bornes, cl) in BAREME.items():
classe = classer(mesure, bornes, [c for c, _ in cl])
if attr == "paO2_FIO2": # ni ventilé ni sous CPAP : classe « rien », 0 point
classe = pl.when(pl.col("ventile")).then(classe).otherwise(pl.lit("rien"))
if attr == "bilirubine": # patient non ictérique : classe la plus basse, 0 point
classe = pl.when(pl.col("ictere")).then(classe).otherwise(pl.lit("inferieurA68,4"))
classes.append(classe.alias(attr))
points.append(pl.col(attr).replace_strict(dict(cl) | {"rien": 0}, return_dtype=pl.Int64)
.alias(f"points_{attr}"))
for attr, (mesure, bareme) in POINTS_QUALITATIFS.items():
classes.append(pl.col(mesure).alias(attr))
points.append(pl.col(mesure).replace_strict(bareme, return_dtype=pl.Int64)
.alias(f"points_{attr}"))
igs2 = (
mesures.with_columns(classes)
.with_columns(points)
.with_columns(valeurGlobale=pl.sum_horizontal(pl.col("^points_.*$")))
)
attributs = list(BAREME) + list(POINTS_QUALITATIFS)
# Patient et venue, une ligne par message
patients = pl.DataFrame({
"evt_id": ["IGS240001", "IGS240002"],
"ipp": ["190101337", "190101338"], "sexe": ["M", "F"],
"date_naissance": [date(1956, 5, 14), date(1941, 11, 2)],
"nda": ["240000123", "240000124"], "venue_date_entree": [date(2024, 3, 5)] * 2,
# repère du message : le recueil, avec sa date et l'UF de réanimation où les états ont
# été observés (le récepteur retrouve ainsi le RUM, même un jour de mutation)
"recueil_id": ["REC240001", "REC240002"],
"recueil_date": [date(2024, 3, 6)] * 2,
"recueil_uf": ["4501"] * 2,
})
# L'IGS2 passe par des colonnes-chemins relatives à la racine du message
evenements = patients.join(
igs2.select(
"evt_id",
pl.lit("creation").alias("igs2/@action"),
*[pl.col(a).alias(f"igs2/@{a}") for a in attributs],
pl.lit(datetime(2024, 3, 6, 8, 0)).alias("igs2/dateAction"),
pl.col("valeurGlobale").alias("igs2/valeurGlobale"),
),
on="evt_id",
)
fichiers = hx.write_evenements_etat_patient(
evenements, None, # pas de table de diagnostics ici
entete=entete,
out_dir="sortie/igs2", # -> sortie/igs2/IGS240001.xml, IGS240002.xml
xmlns=None,
)
# Contrôle : une ligne par item, avec la classe et les points de chaque patient
controle = (
igs2.unpivot(index="evt_id", on=attributs, variable_name="item", value_name="classe")
.join(igs2.unpivot(index="evt_id", on=[f"points_{a}" for a in attributs],
variable_name="item", value_name="points")
.with_columns(pl.col("item").str.strip_prefix("points_")),
on=["evt_id", "item"], maintain_order="left")
.pivot(on="evt_id", index="item", values=["classe", "points"])
)
Le fichier produit pour le premier patient (sortie/igs2/IGS240001.xml) :
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<evenementsServeurEtatsPatient version="2.00">
<enteteMessage>
<identifiantMessage>IGS240001</identifiantMessage>
<dateHeureProduction>2024-03-06T09:00:00</dateHeureProduction>
<emetteur>
<agents>
<agent categorie="application">
<code>REA</code>
</agent>
</agents>
</emetteur>
<destinataire>
<agents>
<agent categorie="application">
<code>DIM</code>
</agent>
</agents>
</destinataire>
</enteteMessage>
<patient>
<identifiant>
<emetteur>
<valeur>190101337</valeur>
</emetteur>
</identifiant>
<personnePhysique sexe="M">
<dateNaissance>
<date>1956-05-14</date>
</dateNaissance>
</personnePhysique>
</patient>
<venue>
<identifiant>
<emetteur>
<valeur>240000123</valeur>
</emetteur>
</identifiant>
<entree>
<dateHeureOptionnelle>
<date>2024-03-05</date>
</dateHeureOptionnelle>
</entree>
</venue>
<recueil>
<identifiant>
<emetteur>REC240001</emetteur>
</identifiant>
<date>2024-03-06</date>
<uniteFonctionnelle>
<code>4501</code>
</uniteFonctionnelle>
</recueil>
<igs2 action="creation" modeAdmission="medecine" maladieChronique="aucune" scoreGlasgow="11A13" age="60A69" tensionArterielleSystolique="70A99" frequenceCardiaque="120A159" temperature="inferieurA39" paO2_FIO2="100A199" diurese="0,5A0,999" ureeSanguine="10A29,9" leucocytes="1A19,9" kaliemie="3A4,9" natremie="125A144" HCO3="15A19" bilirubine="inferieurA68,4">
<dateAction>2024-03-06T08:00:00</dateAction>
<valeurGlobale>54</valeurGlobale>
</igs2>
</evenementsServeurEtatsPatient>
Source du barème
Grille et définitions de la SFAR (https://sfar.org/scores/igs2.php), d'après
Le Gall JR et coll., JAMA 1993; 270: 2957-2963. Les 15 items, seuils et points du
script sont vérifiés contre cette grille par un test. Elle lève aussi les ambiguïtés
des classes du XSD : 14A18 correspond à un Glasgow de 14 à 15, superieurA1 à une
diurèse d'au moins 1 L. Les règles de recueil (pires valeurs des 24 premières heures,
Glasgow avant sédation, bilirubine seulement en cas d'ictère…) restent celles de
votre service.