Recherche

Effacer le filtre
Article
Lorenzo Scalese · Oct 5, 2022

Utilisation de SUSHI pour la création de profils FHIR, partie 2

Bonjour, chers développeurs ! Cet article est le deuxième d'une série sur la façon d'utiliser SUSHI, un outil de création de profils FHIR, en tant que technologie associée à FHIR. Six mois se sont écoulés avant cette deuxième partie. Dans la précédente [partie 1](https://fr.community.intersystems.com/post/cr%C3%A9ons-un-profil-fhir-%C3%A0-laide-de-sushi-partie-1), nous avons abordé les questions suivantes : qu'est-ce que FHIR, qu'est-ce que le profil FHIR, qu'est-ce que FHIR Shorthand ? Et quel genre d'outil est SUSHI ? Que peut-il produire ? Avec des captures d'écran pour des exemples de résultats. Cet article présente un exemple d'utilisation réelle d'un profil créé avec SUSHI, dans lequel une **Extension** est ajoutée à une ressource Patient à l'aide de SUSHI, et un nouveau **SearchParameter** est défini pour l'élément de cette Extension, jusqu'à ce que le nouveau SearchParameter puisse être utilisé dans l'IRIS for Health's FHIR Repositoy jusqu'à ce que le nouveau SearchParameter puisse être utilisé. ## Mise à jour de SUSHI Je suis désolé de m'écarter du sujet principal, mais si vous êtes comme moi et que vous n'avez pas touché à SUSHI depuis un moment, vous devriez mettre à jour SUSHI. Au cours des six derniers mois, SUSHI a fait l'objet d'une importante mise à jour, et la version 2.0.0 est sortie en août. La dernière version au moment de la rédaction de cet article était [SUSHI 2.1.1](https://github.com/FHIR/sushi/releases). Comme décrit dans ce lien, la mise à jour est la commande suivante de même que l'installation. ```Bash $ npm install -g fsh-sushi ``` Vous pouvez vérifier la version en exécutant sushi -version. De même, l'outil IG Publisher, qui crée un ensemble de fichiers HTML pour le Guide de mise en œuvre sur la base des Profils générés par SUSHI, peut être mis à jour en exécutant la commande **_updatePublisher**. ## Création de fichiers FISH Tout d'abord, créez un projet en utilisant la commande ``sushi --init`` comme précédemment. Dans cet article, nous allons modifier le fichier **patient.fsh** généré par le modèle. Cette fois, nous ajouterons une extension de type **lieu de naissance**, qui est une chaîne de caractères String représentant le lieu de naissance du patient, et nous définirons également un SearchParameter pour ce lieu de naissance, afin de pouvoir effectuer une recherche par lieu de naissance du patient ! # Ajout d'Extension Tout d'abord, ajoutez la définition suivante pour ajouter Extension. Comme dans US Core et JP Core, le type Adresse est habituellement utilisé, mais ici il s'agit simplement du type String ``` Extension: BirthPlace Id: birthPlace Title: "出身地" Description: "生まれた場所をstring型で表現する" * ^url = "http://isc-demo/fhir/StructureDefinition/patient-birthPlace" * value[x] only string ``` Chaque élément correspond à la StructureDefinition d'Extension comme suit. Certains éléments sont placés à plusieurs endroits. Certaines informations, comme la version du fhir de base et la version de cette Extension elle-même, proviennent du fichier `sushi-config.yml`. | Entrée SUSHI | Entrée StructureDefinition correspondante | |:----------|:------------| | Extensions | nom | | Id | id | | Titre | title/differencial.element[id=Extension].short | | Desctiption | description/differencial.element[id=Extension].definition | | ^url | url//differencial.element[id=Extension.url].fixedUri | | valeur[x] | differencial.element[id=Extension.value[x]].type.code | La StructureDefinition réelle d'Extension générée. Il est difficile de créer cela à partir de rien et à la main, mais avec SUSHI, c'est relativement facile. ```json { "resourceType": "StructureDefinition", "id": "birthPlace", "url": "http://isc-demo/fhir/StructureDefinition/patient-birthPlace", "version": "0.1.0", "name": "BirthPlace", "title": "出身地", "status": "active", "description": "生まれた場所をstring型で表現する", "fhirVersion": "4.0.1", "mapping": [ { "identity": "rim", "uri": "http://hl7.org/v3", "name": "RIM Mapping" } ], "kind": "complex-type", "abstract": false, "context": [ { "type": "element", "expression": "Element" } ], "type": "Extension", "baseDefinition": "http://hl7.org/fhir/StructureDefinition/Extension", "derivation": "constraint", "differential": { "element": [ { "id": "Extension", "path": "Extension", "short": "出身地", "definition": "生まれた場所をstring型で表現する" }, { "id": "Extension.extension", "path": "Extension.extension", "max": "0" }, { "id": "Extension.url", "path": "Extension.url", "fixedUri": "http://isc-demo/fhir/StructureDefinition/patient-birthPlace" }, { "id": "Extension.value[x]", "path": "Extension.value[x]", "type": [ { "code": "string" } ] } ] } } ``` Les données d'Extension aux ressources Patient ajoutées avec cette Extension ressembleront à ceci. ```json "extension": [ { "url": "http://isc-demo/fhir/StructureDefinition/patient-birthPlace", "valueString": "鹿児島" } ], ``` # Ajout de SearchParamter Ensuite, ajoutez un **SearchParamter** afin de pouvoir rechercher des ressources en utilisant l'entrée Extension que vous venez d'ajouter comme clé, **mais seules les entrées (≒elements) définies dans le SearchParamter peuvent être recherchées**. Ceci est un peu différent des tables SQL. Le nom de SearchParamter est défini séparément du nom de l'élément, et certains éléments correspondent au nom de l'élément = nom de SearchParameter, comme le sexe dans la ressource Patient, alors que d'autres ne correspondent pas, comme le nom de l'élément = adresse. country -> nom de SearchParamter = Certains ne correspondent pas aux éléments structurés, comme address-country. Naturellement, les éléments ajoutés à l'extension ne sont pas des SearchParameters par défaut (car vous ne savez pas ce qui sera inclus), mais les extensions qui osent définir l'extension et définir une politique pour les stocker sont souvent des éléments importants. Ajoutez ce qui suit au fichier patient.fsh pour créer la définition de SearchParameter ``` Instance: BirthPlaceSearchParameter InstanceOf: SearchParameter Usage: #definition * url = "http://isc-demo/fhir/SearchParameter/patient-birthPlace" * version = "0.0.1" * name = "birthPlace" * status = #active * description = "出身地検索のパラメータ" * code = #birthPlace * base = #Patient * type = #string * expression = "Patient.extension.where(url='http://isc-demo/fhir/StructureDefinition/patient-birthPlace').value" * comparator = #eq ``` Voici la StructureDefinition générée par SearchParameter. Comme il s'agit d'une définition relativement simple, le mappage avec l'information SUSHI ci-dessus devrait être facile à comprendre. ```json { "resourceType": "SearchParameter", "id": "BirthPlaceSearchParameter", "url": "http://isc-demo/fhir/SearchParameter/patient-birthPlace", "version": "0.0.1", "name": "birthPlace", "status": "active", "description": "出身地検索のパラメータ", "code": "birthPlace", "base": [ "Patient" ], "type": "string", "expression": "Patient.extension.where(url='http://isc-demo/fhir/StructureDefinition/patient-birthPlace').value", "comparator": [ "eq" ] } ``` Les principales composantes de la définition de SearchParameter sont l' **expression** et le **comparateur**. L'élément **expression** décrit l'expression **FHIRPath** pour le SearchParameter cible. Si vous êtes intéressé par FHIRPath, veuillez vous référer à [cette page officielle](. html). Définition utilisée cette fois-ci `Patient.extension.where(url='http://isc-demo/fhir/StructureDefinition/patient-birthPlace').value"` Cette expression spécifie Patient.extension dans un ordre hiérarchique selon la structure Json de la ressource Patient, et réduit l'extension avec url=(omitted) par rapport aux multiples extensions qui peuvent exister, et spécifie la valeur de l'extension. comparator spécifie le type d'expressions de comparaison qui peuvent être utilisées. Pour plus d'informations, voir [ici](https://www.hl7.org/fhir/valueset-search-comparator.html). # Ajout de la définition d'Extension créée dans Patient Il y a un autre changement important : l'ajout de l'Extension BirthPlace créée dans la ressource Patient. Modifiez la définition de profil MyProfile dans la ressource Patient générée automatiquement à l'origine comme suit : les modifications apportées au paramètre Cardinality de l'élément Name ont été commentées. ```comparator spécifie le type d'expressions de comparaison qui peuvent être utilisées. Pour plus d'informations, voir [ici](https://www.hl7.org/fhir/valueset-search-comparator.html). Profile: MyPatient Parent: Patient Description: "An example profile of the Patient resource." //* name 1..* MS * extension contains BirthPlace named birthPlace 0..1 ``` L'Extension nommée "BirthPlace" qui a été ajoutée précédemment est ajoutée dans la ressource Patient avec le nom birthPlace dans le paramètre Cardinality 0..1. # Création de ressources pour des tests en plus de ce qui précède SUSHI vous permet également de créer des Instances de ressources qui peuvent être utilisées à des fins d'illustration ou autres. Vous pouvez également les utiliser à des fins de test. Vous pouvez également y inclure l'extension que vous venez de définir. ``` Instance: KamiExample InstanceOf: MyPatient Description: "Exemples de ressources Patient " * nom.family = "Yamada" * extension[BirthPlace].valueString = "Kagoshima" ``` Vous verrez quel type de données a été produit dans le test final. ## Essayons SUSHI ! Le fichier FSH est prêt ! Maintenant, utilisons la commande SUSHI pour générer chaque fichier de définition à partir du fichier fsh ! Exécutez la commande **sushi** et elle est réussie si deux Profils (Patient et Extension étendus) et deux Instances (SearchParameter et ressource de modèle) sont générés comme suit. ```PowerShell C:\Users\kaminaka\Documents\Work\FHIR\SUSHI\TestProject\MyProfileProject>sushi . info Running SUSHI v2.1.1 (implements FHIR Shorthand specification v1.2.0) info Arguments: info C:\Users\kaminaka\Documents\Work\FHIR\SUSHI\TestProject\MyProfileProject info No output path specified. Output to . info Using configuration file: C:\Users\kaminaka\Documents\Work\FHIR\SUSHI\TestProject\MyProfileProject\sushi-config.yaml info Importing FSH text... info Preprocessed 1 documents with 0 aliases. info Imported 2 definitions and 2 instances. info Checking local cache for hl7.fhir.r4.core#4.0.1... info Found hl7.fhir.r4.core#4.0.1 in local cache. info Loaded package hl7.fhir.r4.core#4.0.1 (node:27132) Warning: Accessing non-existent property 'INVALID_ALT_NUMBER' of module exports inside circular dependency (Use `node --trace-warnings ...` to show where the warning was created) (node:27132) Warning: Accessing non-existent property 'INVALID_ALT_NUMBER' of module exports inside circular dependency info Converting FSH to FHIR resources... info Converted 2 FHIR StructureDefinitions. info Converted 2 FHIR instances. info Exporting FHIR resources as JSON... info Exported 4 FHIR resources as JSON. info Assembling Implementation Guide sources... info Generated ImplementationGuide-myprofileproject.json info Assembled Implementation Guide sources; ready for IG Publisher. ╔════════════════════════ SUSHI RESULTS ══════════════════════════╗ ║ ╭───────────────┬──────────────┬──────────────┬───────────────╮ ║ ║ │ Profiles │ Extensions │ Logicals │ Resources │ ║ ║ ├───────────────┼──────────────┼──────────────┼───────────────┤ ║ ║ │ 1 │ 1 │ 0 │ 0 │ ║ ║ ╰───────────────┴──────────────┴──────────────┴───────────────╯ ║ ║ ╭────────────────────┬───────────────────┬────────────────────╮ ║ ║ │ ValueSets │ CodeSystems │ Instances │ ║ ║ ├────────────────────┼───────────────────┼────────────────────┤ ║ ║ │ 0 │ 0 │ 2 │ ║ ║ ╰────────────────────┴───────────────────┴────────────────────╯ ║ ║ ║ ╠═════════════════════════════════════════════════════════════════╣ ║ FSHing for compliments? Super job! 0 Errors 0 Warnings ║ ╚═════════════════════════════════════════════════════════════════╝ C:\Users\kaminaka\Documents\Work\FHIR\SUSHI\TestProject\MyProfileProject> ``` Les artefacts suivants ont été créés dans le dossier `fsh-generated\resource`. |nom de fichier | contenu | |:----------|:------------| | ImplementationGuide-myprofileproject.json | ImplemamtionGuide, qui résume l'ensemble des contenus présentés ici. | | StructureDefinition-MyPatient.json | StructureDefinition avec Extension ajoutée au Patient | | StructureDefinition-birthPlace.json | StructureDefinition contenant la définition d'Extension birthPlace | | SearchParameter-BirthPlaceSearchParameter.json | Fichier de définition de SearchParameter pour le lieu de naissance (birthPlace) | | Patient-KamiExample.json | Exemples d'instances de Patient | ## Importation et test du profil FHIR dans IRIS for Health # Application d'IRIS for Health au référentiel FHIR Dans l'article précédent, nous avons exécuté _updatePublisher pour générer un groupe de fichiers IG. Dans cet article, nous allons voir comment le fichier StructureDefinitino/SearchParameter peut être importé dans le référentiel FHIR d'IRIS for Health et faire l'objet d'une recherche avec le nouveau SearchParameter. Pour plus d'informations sur l'importation des profils FHIR, etc., veuillez vous référer à [l'article de la communauté des développeurs sur les profils FHIR](https://jp.community.intersystems.com/node/495321). Vous pouvez également vous référer à [cet article](https://jp.community.intersystems.com/node/480231) pour plus d'informations sur la façon de construire un référentiel FHIR. L'importation est ciblée sur les cinq fichiers qui viennent d'être générés. - StructureDefinition-MyPatient.json - StructureDefinition-birthPlace.json - SearchParameter-BirthPlaceSearchParameter.json Il y a trois fichiers. Copiez-les dans un autre dossier, et préparez également un fichier `package.json` pour gérer l'ensemble des informations du paquet. **package.json** ```json { "name": "SUSHI Demo", "title": "SUSHI Demo", "version": "0.0.1", "author": { "name": "ISC" }, "fhirVersions": [ "4.0.1" ], "bundleDependencies": false, "date": "20201208205547", "dependencies": { "hl7.fhir.r4.core": "4.0.1" }, "deprecated": false } ``` Vous pouvez modifier le nom, le titre, l'auteur, la date et d'autres éléments comme vous le souhaitez. (Remarque : lorsque chaque profil est modifié et réimporté dans IRIS, la version doit être modifiée (augmentée) en conséquence. (La version actuelle 2021.1 du référentiel FHIR n'a pas de fonction de suppression des profils, il faut donc veiller à ce que le nombre de profils n'augmente pas trop dans l'environnement de production, etc., en vérifiant le bon fonctionnement dans l'environnement de test et en ne les appliquant ensuite qu'un nombre minimum de fois dans l'environnement de production.) À partir du portail de gestion IRIS, allez Health -> FHIR Configuration -> Package Configuration et sélectionnez le dossier contenant les quatre fichiers ci-dessus dans Import Package, et vous verrez l'écran suivant. ![image](/sites/default/files/inline/images/sushi_part2_ss1.jpg) Cliquez sur Import pour terminer l'importation dans IRIS. Ensuite, créez un nouveau référentiel FHIR sur l'écran de configuration du serveur Server Configuration. (Vous pouvez également ajouter à un référentiel FHIR existant). ![image](/sites/default/files/inline/images/sushi_part2_ss2.jpg) ## Test de POSTMAN POSTEZ la ressource de test qui vient d'être générée par SUSHI. À des fins de vérification, il peut être préférable de générer des données qui incluent d'autres valeurs de birthPlace, ou une ressource Patient qui n'inclut pas de birthPlace en premier lieu. ![image](/sites/default/files/inline/images/sushi_part2_ss3.jpg) Si birthPlace a été correctement ajouté au SearchParameter dans le référentiel FHIR, la requête GET suivante devrait permettre de récupérer ces informations sur le patient ! ```http GET http://localhost:52785/csp/healthshare/sushi/fhir/r4/Patient?birthPlace=Kagoshima ``` Obtenez-vous maintenant les bons résultats ? Si le nouveau SearchParameter, birthPlace, n'a pas été ajouté correctement, la première réponse à la requête GET contiendra la ressource OperationOutcome suivante qui contient les informations d'erreur suivantes : "Le paramètre birthPlace n'a pas été reconnu. Vérifiez le message de réponse pour ce message. ```json { "resource": { "resourceType": "OperationOutcome", "issue": [ { "severity": "error", "code": "invalid", "diagnostics": "ParameterNotSupported", "details": { "text": "Unrecognized parameter 'birthPlace'. 鹿児島" } } ] }, "search": { "mode": "outcome" } }, ``` # Résumé Vous avez vu le processus de création d'un profil (StructureDefinition/SearchParameter) pour FHIR à l'aide de SUSHI et son importation dans le référentiel FHIR d'IRIS for Health pour étendre ses fonctionnalités. Dans ce cas, les éléments ajoutés à Extension ont été ajoutés à SearchParameter, mais il est également possible d'ajouter SearchParameter à des éléments qui existent dans la spécification standard FHIR mais qui ne sont pas encore des SearchParameters. Bien que le développement très flexible de FHIR permette d'étendre les fonctionnalités de cette manière, il est également important de partager des informations sur le type d'extensions réalisées pour assurer l'interopérabilité, c'est-à-dire de créer des guides de mise en œuvre, etc. Comme nous l'avons vu dans les parties 1 et 2 de cette série, SUSHI est un outil open source très unique et puissant qui couvre les deux côtés de la question. On espère que ces outils seront combinés avec IRIS for Health pour créer une nouvelle solution FHIR. Le fichier fsh SUSHI utilisé dans cet article et les fichiers modèles StructureDefinition/SearchParameter générés sont disponibles [ici](https://github.com/Intersystems-jp/FHIR_SUSHI).
Article
Irène Mykhailova · Juin 16, 2023

Tutoriel : Déploiement de votre application dockerisée sur AWS

Aujourd'hui, la plupart des applications sont déployées sur des services de cloud public. Cela présente de nombreux avantages, notamment des économies de ressources humaines et matérielles, la possibilité de se développer rapidement et à moindre coût, une plus grande disponibilité, une plus grande fiabilité, une évolutivité élastique et des options permettant d'améliorer la protection des actifs numériques. L'une des options les plus populaires est AWS. Nous pouvons y déployer nos applications à l'aide de machines virtuelles (service EC2), de conteneurs Docker (service ECS) ou de Kubernetes (service EKS). La première solution, au lieu d'utiliser Docker, emploie une machine virtuelle avec Windows ou Linux où vous pouvez installer votre serveur et déployer votre application. Cependant, la dernière correspond mieux aux applications à grande échelle avec de nombreuses instances Docker en cours d'exécution grâce à l'option auto-scale. La deuxième solution (ECS), en revanche, est le meilleur choix pour les applications de petite et moyenne échelle.Cet article vous montrera comment utiliser, configurer et exécuter des applications Docker sur AWS à l'aide du service ECS. ## Obtention d'un exemple d'application Docker à déployer Pour notre exemple, nous allons utiliser une application Docker prête du catalogue InterSystems Open Exchange. Pour commencer, suivez les étapes suivantes : 1. Assurez-vous que Git est installé. 2. Allez sur https://openexchange.intersystems.com/package/iris-rest-api-template. 3. Clonez/git pull le référentiel dans n'importe quel répertoire local:   git <span class="hljs-built_in">clone</span> git@github.com:intersystems-community/iris-rest-api-template.git Le modèle iris-rest-api-template est une application backend avec une base de données IRIS et une API REST d'IRIS écrite en ObjectScript. Nous allons déployer cette application sur le service AWS ECS. ## Obtention de vos références AWS Pour commencer, vous aurez besoin d'un compte AWS et d'un utilisateur disposant d'une clé d'accès. Pour ce faire, procédez comme suit : 1. Allez sur https://aws.amazon.com/console et cliquez sur le bouton de connexion en haut à droite Sign in : ![](/sites/default/files/inline/images/images/image-20230510101315-1.png)   2. Si vous disposez d'un compte AWS, il vous suffit de vous connecter avec celui-ci. Si vous n'en possédez pas, cliquez sur le bouton Create a new AWS account (créer un nouveau compte AWS). Après avoir complété votre profil, connectez-vous avec vos nouvelles données. 3. Dans le champ de recherche supérieur, écrivez IAM ("outil de gestion des identités et des accès AWS"), puis cliquez sur IAM : ![](/sites/default/files/inline/images/images/image-20230510101329-2.png)   4. Dans le menu de gauche, cliquez sur Users (utilisateurs) : ![](/sites/default/files/inline/images/images/image-20230510101354-3.png)   5. Cliquez sur le bouton Add users (ajouter des utilisateurs) : ![](/sites/default/files/inline/images/images/image-20230510101405-4.png)   6. Remplissez le champ qui est apparu avec les valeurs mentionnées ci-dessous : * Nom d'utilisateur : iris * Cochez la case Provide user access to the AWS Management Console (fournir un accès à l'utilisateur à la console de gestion AWS). * Choisissez I want to create an IAM user (Je veux créer un utilisateur IAM) * Sélectionnez Custom password (mot de passe personnalisé) et saisissez Iris@2023 * Décochez la case Users must create a new password at next sign-in (Les utilisateurs doivent créer un nouveau mot de passe lors de la prochaine connexion) * Cliquez sur le bouton Next (suivant)     ![](/sites/default/files/inline/images/images/image-20230510101424-5.png)   ![](/sites/default/files/inline/images/images/image-20230510101443-6.png)   7. Dans les options de permissions, choisissez Attach policies directly (attacher les politiques directement), sélectionnez AdministratorAccess (accès administrateur) et cliquez sur le bouton Next (suivant) : ![](/sites/default/files/inline/images/images/image-20230510101504-7.png)   8. Dans Review and Create, cliquez sur le bouton Create user (créer un utilisateur) dans le pied de page : ![](/sites/default/files/inline/images/images/image-20230510101513-8.png)   9. Cliquez sur le bouton Download .csv file (télécharger le fichier .csv) pour enregistrer les nouvelles références de l'utilisateur. 10. Dans la barre Search de recherche supérieure, recherchez IAM et cliquez sur IAM : ![](/sites/default/files/inline/images/images/image-20230510101533-9.png)   11. Dans le menu de gauche, sélectionnez Users (Utilisateurs) : ![](/sites/default/files/inline/images/images/image-20230510101551-10.png)   12. Cliquez sur le lien de l'utilisateur Iris : ![](/sites/default/files/inline/images/images/image-20230510101604-11.png)   13. Cliquez sur l'onglet Security Credentials (références de sécurité) : ![](/sites/default/files/inline/images/images/image-20230510101618-12.png)   14. Allez dans la sous-section Access keys (clés d'accès) (faites défiler l'écran pour la trouver) et cliquez sur le bouton Create access key (créer une clé d'accès) : ![](/sites/default/files/inline/images/images/image-20230510101633-13.png)   15. Sélectionnez Command Line Interface (interface de ligne de commande), cochez la case "I understand the above recommendation and want to proceed to create an access key" (Je comprends la recommandation ci-dessus et je souhaite procéder à la création d'une clé d'accès), puis cliquez sur le bouton Next (suivant) : ![](/sites/default/files/inline/images/images/image-20230510101651-14.png)   ![](/sites/default/files/inline/images/images/image-20230510101704-15.png)   16. Cliquez maintenant sur le bouton Create access key (créer une clé d'accès) : ![](/sites/default/files/inline/images/images/image-20230510101730-16.png)   17. Copiez votre clé d'accès et votre clé d'accès secrète dans un fichier sur votre ordinateur. Utilisez le bouton Télécharger le fichier .csv et enfin cliquez sur le bouton Done (terminé) : ![](/sites/default/files/inline/images/images/image-20230510101749-17.png)   ## Installation de l'outil AWS CLI et y attribuer l'utilisateur créé L'outil AWS CLI est utilisé pour tirer l'image Docker vers AWS ECR (c'est une sorte de Docker Hub pour les images Docker AWS). Pour l'installer, procédez comme suit : 1. Allez sur https://docs.aws.amazon.com/cli/latest/userguide/getting-started-install... et choisissez les instructions d'installation correspondant au système d'exploitation de votre ordinateur. 2. Après l'installation, si vous ne l'avez pas encore fait, suivez les étapes suivantes : a. Sur votre terminal, mettez :   aws configure b. Définissez la clé d'accès créée ci-dessus : ![](/sites/default/files/inline/images/images/image-20230510102005-18.png)   c. Définissez la clé secrète assemblée précédemment : ![](/sites/default/files/inline/images/images/image-20230510102013-19.png)   d. Ne modifiez pas les valeurs restantes. Acceptez simplement les valeurs par défaut : ##![](/sites/default/files/inline/images/images/image-20230510102020-20.png)   ## Téléchargement de votre application Docker sur l'ECR AWS 1. Dans le champ de recherche de la console AWS, recherchez ECR et sélectionnez Elastic Container Registry : ![](/sites/default/files/inline/images/images/image-20230510102053-21.png)   2. Cliquez sur le bouton Get Started (commencer) dans la section Create Repository (créer le référentiel) : ![](/sites/default/files/inline/images/images/image-20230510102103-22.png)   3. Dans Create Repository, mettez les valeurs suivantes : * Paramètres de visibilité : Public * Nom du référentiel : iris-repo iris-repo * Cliquez sur le bouton Create repository (créer le référentiel) ![](/sites/default/files/inline/images/images/image-20230510102122-23.png)   ![](/sites/default/files/inline/images/images/image-20230510102145-24.png)   4. Le référentiel est maintenant créé. Sélectionnez iris-repo et cliquez sur le bouton View push commands (afficher les commandes push) : ![](/sites/default/files/inline/images/images/image-20230510102337-25.png)   5. Copiez la valeur de l'URI du référentiel (deuxième colonne - URI) et stockez-la dans un fichier. Vous en aurez besoin plus tard au cours de cet article. 6. Exécutez les 4 commandes de la boîte de dialogue dans votre terminal dans le dossier où vous avez cloné le projet Git : ![](/sites/default/files/inline/images/images/image-20230510102356-26.png)   a. Première commande : connectez-vous avec l'utilisateur IRIS : ![](/sites/default/files/inline/images/images/image-20230510102416-27.png)   b. Deuxième commande : docker build -t iris-repo . ![](/sites/default/files/inline/images/images/image-20230510102434-28.png)        c. Troisième commande : docker tag iris-repo:latest public.ecr.aws/e7i6j8j1/iris-repo:latest ![](/sites/default/files/inline/images/images/image-20230510102451-29.png)   d. Dernière commande : docker push public.ecr.aws/e7i6j8j1/iris-repo:latest ![](/sites/default/files/inline/images/images/image-20230510102507-30.png)   Félicitations ! Votre projet Docker est maintenant une image Docker public sur AWS ECR.. ## Création de l'instance Docker sur AWS ECS pour votre nouvelle image AWS ECR Voici venue l'heure des dernières étapes. Nous allons créer une instance Docker fonctionnant sur AWS à ce stade. Pour ce faire, procédez comme suit 1. Accédez à la console AWS et recherchez ECS dans la barre de recherche supérieure. Cliquez ensuite sur le lien Elastic Container Service : ![](/sites/default/files/inline/images/images/image-20230510102539-31.png)   2. Dans le menu de gauche, sélectionnez Clusters : ![](/sites/default/files/inline/images/images/image-20230510102551-32.png)   3. Cliquez sur le bouton Create a cluster (créer un cluster) : ![](/sites/default/files/inline/images/images/image-20230510102604-33.png)   4. Sur Create Cluster, ajoutez la valeur iriscluster au champ Cluster name (nom du cluster). Acceptez les valeurs restantes pour les autres champs et cliquez sur le bouton Create (créer) : ![](/sites/default/files/inline/images/images/image-20230510102624-34.png)   ![](/sites/default/files/inline/images/images/image-20230510102637-35.png)   ![](/sites/default/files/inline/images/images/image-20230510102658-36.png)   5. Attendez quelques secondes, et vous aurez un nouveau cluster listé :     ![](/sites/default/files/inline/images/images/image-20230510102736-37.png)   6. Dans le menu de gauche, sélectionnez Task definitions (définitions de tâches) et allez à Create new task definition (créer une nouvelle définition de tâche) : ![](/sites/default/files/inline/images/images/image-20230510102754-38.png)   7. Dans Configure task definition and containers (configuration de la définition de la tâche et des conteneurs), définissez les valeurs indiquées ci-dessous et cliquez sur le bouton Next (suivant) : * Famille de définition des tâches : iristask * Détails du conteneur - Nom : irisrepo * Détails du conteneur - URI de l'image : URI que vous avez stocké dans un fichier lorsque vous avez créé l'image avec ECR. Dans mon cas, il s'agit de public.ecr.aws/e7i6j8j1/iris-repo * Port de Mappage – Port à conteneurs : 52773, Protocole : TCP. ![](/sites/default/files/inline/images/images/image-20230510102820-39.png)   ![](/sites/default/files/inline/images/images/image-20230510102827-40.png)   ![](/sites/default/files/inline/images/images/image-20230510102852-41.png)   8. Dans Configure environment, storage, monitoring, and tags (configuration de l'environnement, le stockage, la surveillance et les balises), modifiez la mémoire pour qu'elle soit de 4 Go. Le rôle de la tâche doit être modifié en ecsTaskExecutionRole, et Stockage - Quantité en 30. Pour les autres paramètres, acceptez les valeurs par défaut et cliquez sur le bouton Next (suivant) : ![](/sites/default/files/inline/images/images/image-20230510102922-42.png)   ![](/sites/default/files/inline/images/images/image-20230510102929-43.png)   ![](/sites/default/files/inline/images/images/image-20230510103004-44.png)   ![](/sites/default/files/inline/images/images/image-20230510103034-45.png)   ![](/sites/default/files/inline/images/images/image-20230510103039-46.png)   9. Dans Review (révision) and Create (créer), cliquez sur le bouton Create : ![](/sites/default/files/inline/images/images/image-20230510103146-47.png)   ![](/sites/default/files/inline/images/images/image-20230510103212-48.png)   ![](/sites/default/files/inline/images/images/image-20230510103235-49.png)   ![](/sites/default/files/inline/images/images/image-20230510103257-50.png)   ![](/sites/default/files/inline/images/images/image-20230510103341-51.png)     10. Cliquez sur le bouton Deploy > Run Task (déployer > exécuter une tâche) en haut de la page : ![](/sites/default/files/inline/images/images/image-20230510103412-52.png)   11. Dans Create (créer), définissez les valeurs mentionnées ci-dessous et cliquez sur le bouton Create : * Cluster existant : iriscluster * Options de calcul : Type de lancement Launch * Type d'application : Task (Tâche) 12. Développez la section Networking (mise en réseau) et choisissez : * Groupe de sécurité : sélectionnez Create a new security group (créer un nouveau groupe de sécurité) * Nom du groupe de sécurité : irissec * Description du groupe de sécurité : irissec * Règles d'entrée - Type : TCP personnalisé, plage de ports : 52773 ![](/sites/default/files/inline/images/images/image-20230510103454-53.png)   ![](/sites/default/files/inline/images/images/image-20230510103513-54.png)   ![](/sites/default/files/inline/images/images/image-20230510103536-55.png)   ![](/sites/default/files/inline/images/images/image-20230510103614-56.png)   ![](/sites/default/files/inline/images/images/image-20230510103650-57.png)   13. Attendez un certain temps pour voir l'état de la création (cliquez sur le bouton pour vérifier l'état actuel) : ![](/sites/default/files/inline/images/images/image-20230510103758-58.png)   14. . Lorsque l'état devient "Running" (en cours d'exécution), cliquez sur le lien Task (tâche) : ![](/sites/default/files/inline/images/images/image-20230510103832-59.png)   15. Copiez l'IP public : ![](/sites/default/files/inline/images/images/image-20230510103856-60.png)   16. Ouvrez votre navigateur et tapez (dans mon cas, il s'agit de 54.226.128.138) : http://<public ip>:52773/csp/sys/%25CSP.Portal.Home.zen 17. Le portail de gestion IRIS (avec l'utilisateur _SYSTEM et le mot de passe SYS) est maintenant actif, et les services REST pour l'application fonctionnent également (authentification de base avec _SYSTEM et SYS) : ![](/sites/default/files/inline/images/images/image-20230510103925-61.png)   ![](/sites/default/files/inline/images/images/image-20230510103959-62.png)   ![](/sites/default/files/inline/images/images/image-20230510104024-63.png)   Vous avez réussi ! Vous avez maintenant votre IRIS sur AWS. N'OUBLIEZ PAS D'ARRÊTER LA TÂCHE, POUR NE PAS ÊTRE FACTURÉ. Pour ce faire, cliquez sur le bouton Stop : ![](/sites/default/files/inline/images/images/image-20230510104054-64.png)   Profitez-en !
Article
Guillaume Rongier · Avr 4, 2022

Les globales sont des épées magiques pour stocker des données. Arbres (partie 2)

![](/sites/default/files/inline/images/old-sword-small.jpg) ## 3. Variantes des structures lors de l'utilisation de globales Une structure, telle qu'un arbre ordonné, présente plusieurs cas particuliers. Examinons ceux qui ont une valeur pratique pour le travail avec les globales. ### 3.1 Cas particulier 1. Un nœud sans branches Les globales peuvent être utilisées non seulement comme une liste de données, mais aussi comme des variables ordinaires. Par exemple, pour créer un compteur :   Set ^counter = 0 ; setting counter Set id=$Increment(^counter) ; atomic incrementation En même temps, une globale peut avoir des branches outre sa valeur. L'un n'exclut pas l'autre. ### 3.2 Cas particulier 2. Un nœud et plusieurs branches En fait, il s'agit d'une base classique clé-valeur. Et si nous enregistrons des tuples de valeurs au lieu de valeurs, nous obtiendrons une table ordinaire avec une clé primaire. ![](/sites/default/files/inline/images/key_value_table.png) Afin d'implémenter une table basé sur des globales, nous devrons former des chaînes de caractères à partir des valeurs des colonnes, puis les enregistrer dans une globale par la clé primaire. Afin de pouvoir diviser la chaîne en colonnes lors de la lecture, nous pouvons utiliser ce qui suit : 1. Caractère de délimitation. Set ^t(id1) = "col11/col21/col31" Set ^t(id2) = "col12/col22/col32" 2. Un schéma fixe, dans lequel chaque champ occupe un nombre particulier d'octets. C'est ainsi qu'on procède généralement dans les bases de données relationnelles. 3. Une fonction spéciale [$LB](http://docs.intersystems.com/latest/csp/docbook/DocBook.UI.Page.cls?KEY=RCOS_flistbuild) (introduite dans Caché) qui compose une chaîne de caractères à partir de valeurs. Set ^t(id1) = $LB("col11", "col21", "col31") Set ^t(id2) = $LB("col12", "col22", "col32") Ce qui est intéressant, c'est qu'il n'est pas difficile de faire quelque chose de similaire aux clés étrangères dans les bases de données relationnelles en utilisant des globales. Appelons ces structures des index globaux. Un index global est un arbre supplémentaire permettant d'effectuer des recherches rapides sur des champs qui ne font pas partie intégrante de la clé primaire de la globale principale. Vous devez écrire un code supplémentaire pour le remplir et l'utiliser. Nous créons un index global basé sur la première colonne. Set ^i("col11", id1) = 1 Set ^i("col12", id2) = 1 Pour effectuer une recherche rapide par la première colonne, vous devrez regarder dans la ^i globale et trouver les clés primaires (id) correspondant à la valeur nécessaire dans la première colonne. Lors de l'insertion d'une valeur, nous pouvons créer à la fois des valeurs et des index globaux pour les champs nécessaires. Pour plus de fiabilité, nous allons l'intégrer dans une transaction. TSTART Set ^t(id1) = $LB("col11", "col21", "col31") Set ^i("col11", id1) = 1 TCOMMIT Plus d'informations sont disponibles ici [making tables in M using globals and emulation of secondary keys.](http://gradvs1.mgateway.com/download/extreme1.pdf) Ces tables fonctionneront aussi rapidement que dans les bases de données traditionnelles (ou même plus rapidement) si les fonctions d'insertion/mise à jour/suppression sont écrites en COS/M et compilées. J'ai vérifié cette affirmation en appliquant un grand nombre d'opérations INSERT et SELECT à une seule table à deux colonnes, en utilisant également les commandes TSTART et TCOMMIT (transactions). Je n'ai pas testé de scénarios plus complexes avec des accès concurrents et des transactions parallèles. Sans utiliser de transactions, la vitesse d'insertion pour un million de valeurs était de 778 361 insertions/seconde. Pour 300 millions de valeurs, la vitesse était de 422 141 insertions/seconde. Lorsque des transactions ont été utilisées, la vitesse a atteint 572 082 insertions/seconde pour 50 millions de valeurs. Toutes les opérations ont été exécutées à partir du code M compilé. J'ai utilisé des disques durs ordinaires, pas des SSD. RAID5 avec Write-back. Le tout fonctionnant sur un processeur Phenom II 1100T. Pour effectuer le même test pour une base de données SQL, il faudrait écrire une procédure stockée qui effectuerait les insertions en boucle. En testant MySQL 5.5 (stockage InnoDB) avec la même méthode, je n'ai jamais obtenu plus de 11K insertions par seconde. En effet, l'implémentation de tables avec des globales est plus complexe que de faire la même chose dans des bases de données relationnelles. C'est pourquoi les bases de données industrielles basées sur les globales ont un accès SQL pour simplifier le travail avec les données tabulaires. En général, si le schéma de données ne change pas souvent, que la vitesse d'insertion n'est pas critique et que l'ensemble de la base de données peut être facilement représenté par des tables normalisées, il est plus facile de travailler avec SQL, car il offre un niveau d'abstraction plus élevé. Dans ce cas, je voulais montrer que les globales peuvent être utilisées comme un constructeur pour créer d'autres bases de données. Comme le langage assembleur qui peut être utilisé pour créer d'autres langages. Et voici quelques exemples d'utilisation des globales pour créer des contreparties de [key-values, lists, sets, tabular, document-oriented DB's.](http://gradvs1.mgateway.com/docs/nosql_in_globals.pdf) Si vous devez créer une base de données non standard avec un minimum d'efforts, vous devriez envisager d'utiliser les globales. ### 3.3 Cas particulier 3. Un arbre à deux niveaux dont chaque nœud de deuxième niveau a un nombre fixe de branches Vous l'avez probablement deviné : il s'agit d'une implémentation alternative des tables utilisant des globales. Comparons-la avec la précédente. Tables dans un arborescence deux niveaux vs. Tables dans un arborescence mono niveau. Cons Pros Insertions plus lentes, car le nombre de nœuds doit être égal au nombre de colonnes. Une plus grande consommation d'espace sur le disque dur, car les index globaux (comme les index de table) avec les noms de colonne occupent de l'espace sur le disque dur et sont dupliqués pour chaque ligne.   Un accès plus rapide aux valeurs de certaines colonnes, puisque vous n'avez pas besoin d'analyser la chaîne de caractères. D'après mes tests, c'est 11,5 % plus rapide pour 2 colonnes et encore plus rapide pour plus de colonnes. Il est plus facile de modifier le schéma de données et de lire le code. **Conclusion:** Rien d'extraordinaire. Les performances étant l'un des principaux avantages des globales, il n'y a pratiquement aucun intérêt à utiliser cette approche, car il est peu probable qu'elle soit plus rapide que les tables ordinaires des bases de données relationnelles. ### 3.4 Cas général. Arbres et clés ordonnées Toute structure de données qui peut être représentée comme un arbre s'adapte parfaitement aux globales. #### 3.4.1 Objets avec des sous-objets ![](/sites/default/files/inline/images/json_opt.png) C'est dans ce domaine que les globales sont traditionnellement utilisées. Il existe de nombreuses maladies, médicaments, symptômes et méthodes de traitement dans le domaine médical. Il est irrationnel de créer une table avec un million de champs pour chaque patient, d'autant plus que 99% d'entre eux seront vides. Imaginez une base de données SQL composée des tables suivants : " Patient " ~ 100 000 champs, " Médicament " 100 000 champs, " Thérapie " 100 000 champs, " Complications " 100 000 champs et ainsi de suite. Comme alternative, vous pouvez créer une BD avec des milliers de tableaux, chacun pour un type de patient particulier (et ils peuvent aussi se superposer !), un traitement, un médicament, ainsi que des milliers de tables pour les relations entre ces tables. Les globales s'adaptent parfaitement aux soins de santé, puisqu'elles permettent à chaque patient de disposer d'un dossier complet, de la liste des thérapies, des médicaments administrés et de leurs effets, le tout sous la forme d'un arbre, sans gaspiller trop d'espace disque en colonnes vides, comme ce serait le cas avec les bases de données relationnelles. **Les globales fonctionnent bien pour les bases de données contenant des données personnelles**, lorsque la tâche consiste à accumuler et à systématiser le maximum de données personnelles diverses sur un client. C'est particulièrement important dans les domaines de la santé, de la banque, du marketing, de l'archivage et autres. Il est évident que SQL permet également d'émuler un arbre en utilisant seulement quelques tables ([EAV](https://en.wikipedia.org/wiki/Entity%E2%80%93attribute%E2%80%93value_model), [1](https://en.wikipedia.org/wiki/Hierarchical_database_model),[2](http://mikehillyer.com/articles/managing-hierarchical-data-in-mysql/),[3](https://stackoverflow.com/questions/4048151/what-are-the-options-for-storing-hierarchical-data-in-a-relational-database),[4](https://www.simple-talk.com/sql/performance/the-performance-of-traversing-a-sql-hierarchy/),[5](http://moinne.com/blog/ronald/mysql/manage-hierarchical-data-with-mysql-stored-procedures),[6](https://coderwall.com/p/ohomlg/nested-set-model-the-best-approach-to-deal-with-hierarchical-data), [7](http://tdan.com/modeling-hierarchies/5400),[8](https://www.sitepoint.com/hierarchical-data-database/)), mais c'est beaucoup plus complexe et plus lent. En fait, nous devrions écrire une globale basé sur des tables et cacher toutes les routines liées aux tables sous une couche d'abstraction. Il n'est pas correct d'émuler une technologie de niveau inférieur (les globales) à l'aide d'une technologie de niveau supérieur (SQL). C'est tout simplement injustifié. Ce n'est pas un secret que la modification d'un schéma de données dans des tableaux gigantesques (ALTER TABLE) peut prendre un temps considérable. MySQL, par exemple, effectue l'opération ALTER TABLE ADD|DROP COLUMN en copiant toutes les données de l'ancienne tableau vers la nouvelle (je l'ai testé sur MyISAM et InnoDB). Cela peut bloquer une base de données de production contenant des milliards d'enregistrements pendant des jours, voire des semaines. **Si nous utilisons des globales, la modification de la structure des données ne nous coûte rien** Nous pouvons ajouter de nouvelles propriétés à n'importe quel objet, à n'importe quel niveau de la hiérarchie et à n'importe quel moment. Les changements qui nécessitent de renommer les branches peuvent être appliqués en arrière-plan avec la base de données en fonctionnement. Par conséquent, lorsqu'il s'agit de stocker des objets comportant un grand nombre de propriétés facultatives, les globales fonctionnent parfaitement. Je vous rappelle que l'accès à l'une des propriétés est instantané, puisque dans une globale, tous les chemins sont un B-Arbre. Dans le cas général, les bases de données basées sur des globales sont un type de bases de données orientées documents qui supportent le stockage d'informations hiérarchiques. Par conséquent, les bases de données orientées documents peuvent concurrencer efficacement les globales dans le domaine du stockage des cartes médicales. Mais ce n'est pas encore le cas. Prenons MongoDB, par exemple. Dans ce champ, il perd face aux globales pour les raisons suivantes : Taille du document. L'unité de stockage est un texte au format JSON (BSON, pour être exact) dont la taille maximale est d'environ 16 Mo. Cette limitation a été introduite dans le but de s'assurer que la base de données JSON ne devienne pas trop lente lors de l'analyse syntaxique, lorsqu'un énorme document JSON y est enregistré et que des valeurs de champ particulières sont traitées. Ce document est censé contenir des informations complètes sur un patient. Nous savons tous à quel point les cartes de patient peuvent être épaisses. Si la taille maximale de la carte est plafonnée à 16 Mo, cela permet de filtrer immédiatement les patients dont les cartes contiennent des IRM, des radiographies et d'autres documents. Une seule branche d'une entreprise mondiale peut contenir des gigaoctets, des pétaoctets ou des téraoctets de données. Tout est dit, mais laissez-moi vous en dire plus. Le temps nécessaire à la création/modification/suppression de nouvelles propriétés de la carte du patient. Une telle base de données devrait copier la carte entière dans la mémoire (beaucoup de données !), analyser les données BSON, ajouter/modifier/supprimer le nouveau nœud, mettre à jour les index, remballer le tout en BSON et sauvegarder sur le disque. Une globale n'aurait besoin que d'adresser la propriété nécessaire et d'effectuer l'opération nécessaire. La vitesse d'accès à des propriétés particulières. Si le document possède de nombreuses propriétés et une structure à plusieurs niveaux, l'accès à des propriétés particulières sera plus rapide car chaque chemin dans la globale est le B-Arbre. En BSON, vous devrez analyser linéairement le document pour trouver la propriété nécessaire. #### 3.3.2 Tables associatives Les tables associatives (même avec les tables imbriquées) fonctionnent parfaitement avec les globales. Par exemple, cette table PHP ressemblera à la première illustration en 3.3.1. $a = array( "name" => "Vince Medvedev", "city" => "Moscow", "threatments" => array( "surgeries" => array("apedicectomy", "biopsy"), "radiation" => array("gamma", "x-rays"), "physiotherapy" => array("knee", "shoulder") ) ); #### 3.3.3 Documents hiérarchiques : XML, JSON Ils peuvent également être facilement stockés dans des globales et décomposés de manières différentes. **XML** La méthode la plus simple pour décomposer le XML en globales consiste à stocker les attributs des balises dans les nœuds. Et si vous avez besoin d'un accès rapide aux attributs des attributs, nous pouvons les placer dans des branches séparées. <note id=5> <to>Alex</to> <from>Sveta</from> <heading>Reminder</heading> <body>Call me tomorrow!</body> </note> Dans COS, le code ressemblera à ceci : Set ^xml("note")="id=5" Set ^xml("note","to")="Alex" Set ^xml("note","from")="Sveta" Set ^xml("note","heading")="Reminder" Set ^xml("note","body")="Call me tomorrow!" **Note:** Pour XML, JSON et les tables associatives, vous pouvez imaginer un certain nombre de méthodes pour les afficher dans les globales. Dans ce cas particulier, nous n'avons pas reflété l'ordre des balises imbriquées dans la balise "note". Dans la globale **^xml**, les balises imbriquées seront affichés dans l'ordre alphabétique. Pour un affichage précis de l'ordre, vous pouvez utiliser le modèle suivant, par exemple : ![](/sites/default/files/inline/images/xml_sort.png) **JSON.** Le contenu de ce document JSON est présenté dans la première illustration de la section 3.3.1 : var document = { "name": "Vince Medvedev", "city": "Moscow", "threatments": { "surgeries": ["apedicectomy", "biopsy"], "radiation": ["gamma", "x-rays"], "physiotherapy": ["knee", "shoulder"] }, }; #### 3.3.4 Structures identiques liées par des relations hiérarchiques Exemples : structure des bureaux de vente, positions des personnes dans une structure MLM, base des débuts aux échecs. **Base de données des débuts.** Vous pouvez utiliser une évaluation de la force du mouvement comme valeur de l'indice de nœud d'une globale. Dans ce cas, vous devrez sélectionner une branche ayant le poids le plus élevé pour déterminer le meilleur déplacement. Dans la globale, toutes les branches de chaque niveau seront triées en fonction de la force du mouvement. ![](/sites/default/files/inline/images/debut.png) **La structure des bureaux de vente, des personnes dans une société MLM.** Les noeuds peuvent stocker certaines valeurs de cache reflétant les caractéristiques de la sous-arborescence entière. Par exemple, les ventes de cette sous-arborescence particulière. Nous pouvons obtenir des informations exactes sur les réalisations de n'importe quelle branche à tout moment. ![](/sites/default/files/inline/images/sales.png) ## 4. Situations où l'utilisation des globales est avantageuse La première colonne contient une liste de cas où l'utilisation des globales vous donnera un avantage considérable en termes de performance, et la seconde - une liste de situations où elles simplifieront le développement ou le modèle de données. Vitesse Commodité du traitement/de la présentation des données 1. Insertion [avec tri automatique à chaque niveau], [indexation par la clé primaire] 2. Suppression de sous-arbres 3. Objets comportant de nombreuses propriétés imbriquées auxquelles vous devez accéder individuellement 4. Structure hiérarchique avec possibilité de parcourir les branches enfant à partir de n'importe quelle branche, même inexistante 5. Parcours en profondeur de l'arbre 1. Objets/instances avec un grand nombre de propriétés/instances non requises [et/ou imbriquées] 2. Données sans schéma - de nouvelles propriétés peuvent souvent être ajoutées et d'anciennes supprimées 3. Vous devez créer une BD non standard.Bases de données de chemins et arbres de solutions 4. Lorsque les chemins peuvent être représentés de manière pratique sous forme d'arbre 5. On doit supprimer les structures hiérarchiques sans utiliser la récursion   Clause de non-responsabilité: cet article et les commentaires le concernant reflètent uniquement mon opinion et n'ont rien à voir avec la position officielle de la société InterSystems.
Article
Irène Mykhailova · Mai 9, 2022

Connaissez vos indexes

Cet article est le premier d'une série d'articles sur les indexes SQL. Partie 1 - Découvrez vos indexes Qu'est-ce qu'un index, en fait ? Imaginez la dernière fois où vous êtes allé à la bibliothèque. En général, les livres y sont classés par sujet (puis par auteur et par titre), et chaque étagère comporte une étiquette avec un code décrivant le sujet de ses livres. Si vous voulez collectionner des livres d'un certain sujet, au lieu de traverser chaque allée et de lire la couverture intérieure de chaque livre, vous pouvez vous diriger directement vers l'étagère étiquetée avec le sujet désiré et choisir vos livres. Un index SQL a la même fonction générale : améliorer les performances en donnant une référence rapide à la valeur des champs pour chaque ligne de la table. La mise en place d'index est l'une des principales étapes de la préparation de vos classes pour une performance SQL optimale. Dans cet article, nous allons examiner les questions suivantes : 1. Qu'est-ce qu'un index et pourquoi/quand dois-je l'utiliser ?2. Quels types d'indexes existent et pour quels scénarios sont-ils parfaitement adaptés ?3. Qu'est-ce qu'un index ?4. Comment le créer ? Et si j'ai des index, qu'est-ce que j'en fais ? Je vais me référer aux classes de notre schéma Sample. Celles-ci sont disponibles dans le stockage Github suivant, et elles sont également fournies dans l'espace de noms Samples dans les installations de Caché et Ensemble : https://github.com/intersystems/Samples-Data Les principes de base Vous pouvez indexer chaque propriété persistante et chaque propriété qui peut être calculée de manière fiable à partir de données persistantes. Disons que nous voulons indexer la propriété TaxID dans Sample.Company. Dans Studio ou Atelier, nous ajouterions ce qui suit à la définition de la classe : Index TaxIDIdx On TaxID; L'instruction SQL DDL équivalente ressemblerait à ceci : CREATE INDEX TaxIDIdx ON Sample.Company (TaxID); La structure globale de l'index par défaut est la suivante : ^Sample.CompanyI("TaxIDIdx ",<TaxIDValueAtRowID>,<RowID>) = "" Notez qu'il y a moins d'index inférieurs à lire que de champs dans une globale de données typique. Considérons la requête SELECT Name,TaxID FROM Sample.Company WHERE TaxID = 'J7349' C'est logiquement simple et le plan de requête pour l'exécution de cette requête le reflète : Ce plan indique essentiellement que nous vérifions l'index global pour les lignes avec la valeur TaxID donnée, puis nous nous référons à la globale de données ("carte principale") pour récupérer la ligne correspondante. Considérons maintenant la même requête sans index sur TaxIDX. Le plan de requête résultant est, comme prévu, moins efficace : Sans index, l'exécution de la requête sous-jacente d'IRIS repose sur la lecture en mémoire et l'application de la condition de la clause WHERE à chaque ligne de la table. Et comme nous ne nous attendons logiquement pas à ce qu'une société partage TaxID, nous faisons tout ce travail pour une seule ligne ! Bien sûr, avoir des indexes signifie avoir des données d'index et de ligne sur le disque. En fonction de ce sur quoi nous avons une condition et de la quantité de données que notre table contient, cela peut s'avérer avoir ses propres défis lorsque nous créons et alimentons un index. Alors, quand ajoutons-nous un index à une propriété ? Dans le cas général, nous avons fréquemment à remettre une propriété en état. Des exemples sont des informations d'identification telles que le SSN d'une personne ou un numéro de compte bancaire. Vous pouvez également considérer les dates de naissance ou les fonds d'un compte. Pour en revenir à Sample.Company, la classe bénéficierait peut-être de l'indexation de la propriété Revenue si nous voulions collecter des données sur les organisations à hauts revenus. À l'inverse, les propriétés sur lesquelles il est peu probable que nous remettions des conditions sont moins appropriées pour être indexées : disons un slogan ou une description d'entreprise. Facile - sauf qu'il faut aussi considérer quel type d'index est le meilleur ! Types d'indexes Il existe six principaux types d'index que je vais aborder ici : standard, bitmap, compound, collection, bitslice et data. Je vais également aborder brièvement les index iFind, qui sont basés sur les flux. Il y a des chevauchements possibles ici et nous avons déjà abordé les indexes standards avec l'exemple ci-dessus. Je vais présenter des exemples sur la façon de créer des indexes dans votre définition de classe, mais l'ajout de nouveaux index à une classe est plus complexe que le simple ajout d'une ligne dans votre définition de classe. Nous aborderons des considérations supplémentaires dans la partie suivante. Prenons l'exemple de Sample.Person. Notez que Person a une sous-classe Employee, ce qui sera utile pour comprendre certains exemples. Employee partage son stockage global de données avec Person, et tous les indexes de Person sont hérités par Employee - ce qui signifie qu'Employee utilise l'index global de Person pour ces indexes hérités. Si vous n'êtes pas familier avec ces classes, voici un aperçu général de celles-ci : Person a les propriétés SSN, DOB, Name, Home (un objet d'adresse intégré contenant l'état et la ville), Office (également une adresse), et la collection de listes FavoriteColors. Employee a une propriété supplémentaire Salary (que j'ai moi-même définie). Standard Index DateIDX On DOB; J'utilise ici le terme "standard" pour désigner les indexes qui stockent la valeur brute d'une propriété (par opposition à une représentation binaire). Si la valeur est une chaîne de caractères, elle sera stockée sous une certaine collation - celle de SQLUPPER par défaut. Par rapport aux index bitmap ou bitslice, les indexes standard sont plus compréhensibles pour les humains et relativement faciles à maintenir. Nous avons un nœud global pour chaque ligne de la table. Voici comment DateIDX est stocké au niveau global. ^Sample.PersonI("DateIDX",51274,100115)="~Sample.Employee~" ; Date is 05/20/81 Notez que le premier index inférieur après le nom de l'index est la valeur de la date, le dernier index inférieur est l'ID de la personne ayant cette date de naissance, et la valeur stockée sur ce noeud global indique que cette personne est également membre de la sous-classe Sample.Employee. Si cette personne n'était membre d'aucune sous-classe, la valeur du noeud serait une chaîne vide. Cette structure de base sera cohérente avec la plupart des indexes non binaires, où les indexes sur plus d'une propriété créent plus d'indexes inférieurs dans la globale, et où le fait d'avoir plus d'une valeur stockée au nœud produit un objet $listbuild, par exemple : ^Package.ClassI(IndexName,IndexValue1,IndexValue2,IndexValue3,RowID) = $lb(SubClass,DataValue1,DataValue2) Bitmap - Une représentation binaire de l'ensemble des ID-codes correspondant à une valeur de propriété. Index HomeStateIDX On Home.State [ Type = bitmap]; Les indexes bitmap sont stockés par valeur unique, contrairement aux indexes standard, qui sont stockés par ligne. Pour aller plus loin dans l'exemple ci-dessus, disons que la personne avec l'ID 1 vit dans le Massachusetts, avec l'ID 2 à New York, avec l'ID 3 dans le Massachusetts et avec l'ID 4 à Rhode Island. HomeStateIDX est essentiellement stocké comme suit : ID 1 2 3 4 (…) (…) 0 0 0 0 - MA 1 0 1 0 - NY 0 1 0 0 - RI 0 0 0 1 - (…) 0 0 0 0 - Si nous voulions qu'une requête renvoie les données des personnes vivant en Nouvelle-Angleterre, le système effectue un bitwise OR sur les lignes pertinentes de l'index bitmap. On voit rapidement que nous devons charger en mémoire des objets Personne avec les ID 1, 3 et 4 au minimum. Les bitmaps peuvent être efficaces pour les opérateurs AND, RANGE et OR dans vos clauses WHERE. Bien qu'il n'y ait pas de limite officielle au nombre de valeurs uniques que vous pouvez avoir pour une propriété avant qu'un index bitmap soit moins efficace qu'un index standard, la règle générale est d'environ 10 000 valeurs distinctes. Ainsi, si un index bitmap peut être efficace pour un état des États-Unis, un index bitmap pour une ville ou un comté des États-Unis ne serait pas aussi utile. Un autre concept à prendre en compte est l'efficacité du stockage. Si vous prévoyez d'ajouter et de supprimer fréquemment des lignes de votre table, le stockage de votre index bitmap peut devenir moins efficace. Prenons l'exemple ci-dessus : supposons que nous ayons supprimé de nombreuses lignes pour une raison quelconque et que notre table ne contienne plus de personnes vivant dans des états moins peuplés tels que le Wyoming ou le Dakota du Nord. Le bitmap comporte donc plusieurs lignes contenant uniquement des zéros. D'un autre côté, la création de nouvelles lignes dans les grandes tables peut finir par devenir plus lente, car le stockage bitmap doit accueillir un plus grand nombre de valeurs uniques. Dans ces exemples, j'ai environ 150 000 lignes dans Sample.Person. Chaque nœud global stocke jusqu'à 64 000 ID, de sorte que l'index bitmap global à la valeur MA est divisé en trois parties : ^Sample.PersonI("HomeStateIDX"," MA",1)=$zwc(135,7992)_$c(0,(...)) ^Sample.PersonI("HomeStateIDX"," MA",2)=$zwc(404,7990,(…)) ^Sample.PersonI("HomeStateIDX"," MA",3)=$zwc(132,2744)_$c(0,(…)) Cas particulier : Bitmap étendu Un bitmap étendue, souvent appelé $<ClassName>, est un index bitmap sur les ID d'une classe - cela donne à IRIS un moyen rapide de savoir si une ligne existe et peut être utile pour les requêtes COUNT ou les requêtes sur les sous-classes. Ces indexes sont générés automatiquement lorsqu'un index bitmap est ajouté à la classe ; vous pouvez également créer manuellement un index bitmap d'étendue dans une définition de classe comme suit : Index Company [ Extent, SqlName = "$Company", Type = bitmap ]; Ou via le mot-clé DDL appelé BITMAPEXTENT : CREATE BITMAPEXTENT INDEX "$Company" ON TABLE Sample.Company Composés - Les indexes basés sur deux ou plusieurs propriétés Index OfficeAddrIDX On (Office.City, Office.State); Le cas général d'utilisation des index composés est le conditionnement de requêtes fréquentes sur deux propriétés ou plus. L'ordre des propriétés dans un index composé est important en raison de la manière dont l'index est stocké au niveau global. Le fait d'avoir la propriété la plus sélective en premier est plus efficace en termes de performances car cela permet d'économiser les lectures initiales du disque de l'index global ; dans cet exemple, Office.City est en premier car il y a plus de villes uniques que d'états aux États-Unis. Le fait d'avoir une propriété moins sélective en premier est plus efficace en termes d'espace. En termes de structure globale, l'arbre d'indexation serait plus équilibré si State était placé en premier. Pensez-y : chaque état contient de nombreuses villes, mais certains noms de ville n'appartiennent qu'à un seul état. Vous pouvez également vous demander si vous vous attendez à exécuter des requêtes fréquentes ne conditionnant qu'une seule de ces propriétés - cela peut vous éviter de définir un autre index. Voici un exemple de la structure globale des indexes composés : ^Sample.PersonI("OfficeAddrIDX"," BOSTON"," MA",100115)="~Sample.Employee~" Commentaires : Index composé ou index bitmap ? Pour les requêtes comportant des conditions sur plusieurs propriétés, vous pouvez également vous demander si des indexes bitmap séparés seraient plus efficaces qu'un seul index composé. Les opérations par bit sur deux indexes différents peuvent être plus efficaces à condition que les indexes bitmap conviennent à chaque propriété. Il est également possible d'avoir des indexes bitmap composés, c'est-à-dire des indexes bitmap dont la valeur unique est l'intersection de plusieurs propriétés sur lesquelles vous effectuez l'indexation. Considérez la table donnée dans la section précédente, mais au lieu des états, nous avons toutes les paires possibles d'un état et d'une ville (par exemple, Boston, MA, Cambridge, MA, même Los Angeles, MA, etc.), et les cellules obtiennent des 1 pour les lignes qui adhèrent aux deux valeurs. Collection - Les index basés sur les propriétés de la collection Nous avons ici la propriété FavoriteColors définie comme suit : Property FavoriteColors As list Of %String; Avec chacun des indexes suivants définis à titre de démonstration : Index fcIDX1 On FavoriteColors(ELEMENTS);Index fcIDX2 On FavoriteColors(KEYS); J'utilise ici le terme "collection" pour désigner plus largement les propriétés à cellule unique contenant plus d'une valeur. Les propriétés List Of et Array Of sont pertinentes ici, et si vous le souhaitez, même les chaînes de caractères délimitées. Les propriétés de la collection sont automatiquement analysées pour construire leurs indexes. Pour les propriétés délimitées, comme un numéro de téléphone, vous devez définir cette méthode, <PropertyName>BuildValueArray(value, .valueArray), explicitement. Compte tenu de l'exemple ci-dessus pour FavoriteColors, fcIDX1 ressemblerait à ceci pour une personne dont les couleurs préférées sont le bleu et le blanc : ^Sample.PersonI("fcIDX1"," BLUE",100115)="~Sample.Employee~" (…) ^Sample.PersonI("fcIDX1"," WHITE",100115)="~Sample.Employee~" fcIDX2 ressemblerait à : ^Sample.PersonI("fcIDX2",1,100115)="~Sample.Employee~" ^Sample.PersonI("fcIDX2",2,100115)="~Sample.Employee~" Dans ce cas, puisque FavoriteColors est une collection de listes, un index basé sur ses clés est moins utile qu'un index basé sur ses éléments. Veuillez vous référer à notre documentation pour des considérations plus approfondies sur la création et la gestion des indexes sur les propriétés des collections. Bitslice - Représentation en bitmap de la représentation en chaîne de bits des données numériques Index SalaryIDX On Salary [ Type = bitslice ]; //In Sample.Employee Contrairement aux indexes bitmap, qui contiennent des balises indiquant quelles lignes contiennent une valeur spécifique, les indexes bitslice convertissent d'abord les valeurs numériques de la décimale à la binaire, puis créent un bitmap sur chaque chiffre de la valeur binaire. Reprenons l'exemple ci-dessus et, par souci de réalisme, simplifions le salaire en unités de 1 000 dollars. Ainsi, si le salaire d'un employé est enregistré sous la forme 65, il est compris comme représentant 65 000 dollars. Disons que nous avons un employé avec l'ID 1 qui a un salaire de 15, l'ID 2 un salaire de 40, l'ID 3 un salaire de 64 et l'ID 4 un salaire de 130. Les valeurs binaires correspondantes sont : 15 0 0 0 0 1 1 1 1 40 0 0 1 0 1 0 0 0 64 0 1 0 0 0 0 0 0 130 1 0 0 0 0 0 1 0 Notre chaîne de bits s'étend sur 8 chiffres. La représentation bitmap correspondante - les valeurs d'indexes bitslice - est essentiellement stockée comme suit : ^Sample.PersonI("SalaryIDX",1,1) = "1000" ; La ligne 1 a une valeur à la place 1 ^Sample.PersonI("SalaryIDX",2,1) = "1001" ; Les lignes 1 et 4 ont des valeurs à la place 2 ^Sample.PersonI("SalaryIDX",3,1) = "1000" ; La ligne 1 a une valeur à la place 4 ^Sample.PersonI("SalaryIDX",4,1) = "1100" ; Les lignes 1 et 2 ont des valeurs à la place 8 ^Sample.PersonI("SalaryIDX",5,1) = "0000" ; etc… ^Sample.PersonI("SalaryIDX",6,1) = "0100" ^Sample.PersonI("SalaryIDX",7,1) = "0010" ^Sample.PersonI("SalaryIDX",8,1) = "0001" Notez que les opérations modifiant Sample.Employee ou les salaires dans ses lignes, c'est-à-dire les INSERTs, UPDATESs et DELETEs, nécessitent maintenant la mise à jour de chacun de ces nœuds globaux, ou bitslices. L'ajout d'un index bitslice à plusieurs propriétés d'une table ou à une propriété fréquemment modifiée peut présenter des risques pour les performances. En général, la maintenance d'un index bitslice est plus coûteuse que celle des indexes standard ou bitmap. Les indexes Bitslice sont hautement spécialisés et ont donc des cas d'utilisation spécifiques : les requêtes qui doivent effectuer des calculs agrégés, par exemple SUM, COUNT ou AVG. En outre, ils ne peuvent être utilisés efficacement que sur des valeurs numériques - les chaînes de caractères sont converties en un 0 binaire. Notez que si la table de données, et non les index, doit être lu pour vérifier la condition d'une requête, les indexes bitslice ne seront pas choisis pour exécuter la requête. Supposons que Sample.Person ne possède pas d'index sur Name. Si nous calculions le salaire moyen des employés portant le nom de famille Smith : SELECT AVG(Salary) FROM Sample.Employee WHERE Name %STARTSWITH 'Smith,' nous aurions besoin de lire des lignes de données pour appliquer la condition WHERE, et donc l'index bitslice ne serait pas utilisé en pratique. Des problèmes de stockage similaires se posent pour les indexes bitslice et bitmap sur les tables où des lignes sont fréquemment créées ou supprimées. Data - Index dont les données sont stockées dans leurs nœuds globaux. Index QuickSearchIDX On Name [ Data = (SSN, DOB, Name) ]; Dans plusieurs des exemples précédents, vous avez peut-être observé la chaîne “~Sample.Employee~” stockée comme valeur au niveau du noeud lui-même. Rappelez-vous que Sample.Employee hérite des indexes de Sample.Person. Lorsque nous effectuons une requête sur les employés en particulier, nous lisons la valeur aux nœuds d'index correspondant à notre condition de propriété pour vérifier que ladite personne est également un employé. On peut aussi définir explicitement les valeurs à stocker. Le fait d'avoir des données définies au niveau des nœuds globaux de l'index permet d'éviter la lecture de l'ensemble des données globales ; cela peut être utile pour les requêtes sélectives ou les requêtes ordonnées fréquentes. Considérons l'index ci-dessus comme un exemple. Si nous voulions extraire des informations d'identification sur une personne à partir de tout ou une partie de son nom (par exemple, pour rechercher des informations sur les clients dans une application de réception), nous pourrions avoir une requête telle que SELECT SSN, Name, DOB FROM Sample.Person WHERE Name %STARTSWITH 'Smith,J' ORDER BY Name Puisque les conditions de notre requête sur le nom et les valeurs que nous récupérons sont toutes contenues dans les nœuds globaux QuickSearchIDX, il nous suffit de lire notre I globale pour exécuter cette requête. Notez que les valeurs de données ne peuvent pas être stockées avec des indexes de bitmap ou de bitslice. ^Sample.PersonI("QuickSearchIDX"," LARSON,KIRSTEN A.",100115)=$lb("~Sample.Employee~","555-55-5555",51274,"Larson,Kirsten A.") iFind Indexes Vous en avez déjà entendu parler ? Moi non plus. Les indexes iFind sont utilisés sur les propriétés des flux, mais pour les utiliser vous devez spécifier leurs noms avec des mots-clés dans la requête. Je pourrais vous en dire plus, mais Kyle Baxter a déjà rédigé un article utile à ce sujet.
Article
Sylvain Guilbaud · Mars 31, 2023

Prédictions de Covid-19 ICU via ML vs. IntegratedML (Partie II)

Mots-clés:  IRIS, IntegratedML, apprentissage automatique, Covid-19, Kaggle  Continuation de la [précédente Partie I](https://community.intersystems.com/post/run-some-covid-19-icu-predictions-ml-vs-integratedml-part-i) ... Dans la partie I, nous avons parcouru les approches ML traditionnelles sur ce jeu de données Covid-19 sur Kaggle. Dans cette partie II, nous allons exécuter les mêmes données et la même tâche, dans sa forme la plus simple possible, à travers IRIS integratedML qui est une interface SQL agréable et élégante pour les options AutoML du backend. Cette interface utilise le même environnement.    ## Approche IntegratedML ? ### **Comment charger des données dans IRIS** [integredML-demo-template](https://openexchange.intersystems.com/package/integratedml-demo-template) a défini plusieurs façons de charger des données dans IRIS. Par exemple, je peux définir une classe IRIS personnalisée spécifique à ce fichier xls au format CSV, puis le charger dans un tableau IRIS. Cela permet un meilleur contrôle pour les volumes de données importants.  Cependant, dans cet article, j'opte pour une méthode simplifiée et légère, en me contentant de [charger le jeux des données dans un tableau IRIS via une fonction Python personnalisée que j'ai créée](https://community.intersystems.com/post/save-pandas-dataframe-iris-quick-note).  Cela nous permet de sauvegarder à tout moment les différentes étapes des dataframes brutes ou traitées dans IRIS, pour des comparaisons similaires avec l'approche ML précédente. def to_sql_iris(cursor, dataFrame, tableName, schemaName='SQLUser', drop_table=False ): """" Insertion dynamique d'un dataframe dans un tableau IRIS via SQL par "excutemany" Inputs: cursor: Curseur Python JDBC ou PyODBC à partir d'une connexion DB valide et établie dataFrame: Pandas dataframe tablename: Tableau SQL IRIS à créer, à insérer ou à modifier schemaName: IRIS schemaName, par défaut pour "SQLUser" drop_table: Si le tableau existe déjà, le supprimer et le recréer si True ; sinon, le sauvegarder et l'appliquer Output: True en cas de succès ; False en cas d'exception. """ if drop_table: try: curs.execute("DROP TABLE %s.%s" %(schemaName, tableName)) except Exception: pass try: dataFrame.columns = dataFrame.columns.str.replace("[() -]", "_") curs.execute(pd.io.sql.get_schema(dataFrame, tableName)) except Exception: pass curs.fast_executemany = True cols = ", ".join([str(i) for i in dataFrame.columns.tolist()]) wildc =''.join('?, ' * len(dataFrame.columns)) wildc = '(' + wildc[:-2] + ')' sql = "INSERT INTO " + tableName + " ( " + cols.replace('-', '_') + " ) VALUES" + wildc #print(sql) curs.executemany(sql, list(dataFrame.itertuples(index=False, name=None)) ) return True ### **Configuration de la connexion Python JDBC** import numpy as np import pandas as pd from sklearn.impute import SimpleImputer import matplotlib.pyplot as plt from sklearn.linear_model import LogisticRegression from sklearn.model_selection import train_test_split from sklearn.metrics import classification_report, roc_auc_score, roc_curve import seaborn as sns sns.set(style="whitegrid") import jaydebeapi url = "jdbc:IRIS://irisimlsvr:51773/USER" driver = 'com.intersystems.jdbc.IRISDriver' user = "SUPERUSER" password = "SYS" jarfile = "./intersystems-jdbc-3.1.0.jar" conn = jaydebeapi.connect(driver, url, [user, password], jarfile) curs = conn.cursor()   ### **Définition du point de départ des données** Pour les comparaisons à l'identique, j'ai commencé par le dataframe après les sélections de caractéristiques dans le post précédent (dans la section "Sélection de caractéristiques - Sélection finale"), où "DataS" est le dataframe exact que nous commençons ici. data = dataS data = pd.get_dummies(data) data.ÂGE_AU-DESSUS65 = data.ÂGE_AU-DESSUS65.astype(int) data.ICU = data.ICU.astype(int) data_new = data data_new   ÂGE_AU-DESSUS65 GENRE HTN AUTRES CALCIUM_MÉDIAN CALCIUM_MIN CALCIUM_MAX CRÉATININE_MÉDIANE CRÉATININE_MOYENNE CRÉATININE_MIN ... DIFFÉRENCE_DU_RYTHME_CARDIAQUE_REL DIFFÉRENCE_DE_TAUX_RESPIRATOIRE_REL DIFFÉRENCE_DE_TEMPÉRATURE_REL DIFFÉRENCE_DE_SATURATION_D'OXYGÈNE_REL USI FENÊTRE_0-2 FENÊTRE_2-4 FENÊTRE_4-6 FENÊTRE_6-12 FENÊTRE_AU-DESSUS_12 1 0.0 0.0 1.0 0.330359 0.330359 0.330359 -0.891078 -0.891078 -0.891078 ... -1.000000 -1.000000 -1.000000 -1.000000 1 1 1 0.0 0.0 1.0 0.330359 0.330359 0.330359 -0.891078 -0.891078 -0.891078 ... -1.000000 -1.000000 -1.000000 -1.000000 1 2 1 0.0 0.0 1.0 0.183673 0.183673 0.183673 -0.868365 -0.868365 -0.868365 ... -0.817800 -0.719147 -0.771327 -0.886982 1 3 1 0.0 0.0 1.0 0.330359 0.330359 0.330359 -0.891078 -0.891078 -0.891078 ... -0.817800 -0.719147 -1.000000 -1.000000 1 4 1 0.0 0.0 1.0 0.326531 0.326531 0.326531 -0.926398 -0.926398 -0.926398 ... -0.230462 0.096774 -0.242282 -0.814433 1 1 ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... 1920 1.0 0.0 1.0 0.330359 0.330359 0.330359 -0.891078 -0.891078 -0.891078 ... -1.000000 -1.000000 -1.000000 -1.000000 1 1921 1.0 0.0 1.0 0.244898 0.244898 0.244898 -0.934890 -0.934890 -0.934890 ... -1.000000 -1.000000 -1.000000 -1.000000 1 1922 1.0 0.0 1.0 0.330359 0.330359 0.330359 -0.891078 -0.891078 -0.891078 ... -1.000000 -1.000000 -1.000000 -1.000000 1 1923 1.0 0.0 1.0 0.330359 0.330359 0.330359 -0.891078 -0.891078 -0.891078 ... -1.000000 -1.000000 -1.000000 -1.000000 1 1924 1.0 0.0 1.0 0.306122 0.306122 0.306122 -0.944798 -0.944798 -0.944798 ... -0.763868 -0.612903 -0.551337 -0.835052 1 1925 lignes × 62 colonnes Ce qui précède indique que nous disposons de 58 caractéristiques sélectionnées plus 4 autres caractéristiques converties à partir de la colonne non numérique précédente ("FENÊTRE").     ### **Sauvegarder les données dans le tableau IRIS** Nous utilisons la fonction **to\_sql\_iris** ci-dessus pour sauvegarder les données dans le tableau IRIS "CovidPPP62" : iris_schema = 'SQLUser' iris_table = 'CovidPPP62' to_sql_iris(curs, data_new, iris_table, iris_schema, drop_table=True) df2 = pd.read_sql("SELECT COUNT(*) from %s.%s" %(iris_schema, iris_table),conn) display(df2)  Sauvegarder les données dans le tableau IRIS Aggregate_1 1925 Définissez ensuite le nom de la vue de formation, le nom du modèle et la colonne cible de la formation, qui est ici " USI ".   dataTable = iris_table dataTableViewTrain = dataTable + 'Train1' dataTablePredict = dataTable + 'Predict1' dataColumn = 'ICU' dataColumnPredict = 'ICUPredicted' modelName = "ICUP621" #choisir un nom - doit être unique du côté serveur Nous pouvons ensuite diviser les données en une Vue de formation (1700 lignes) et une Vue de test (225 lignes). Nous ne sommes pas obligés de faire cela dans Integrated ML ; c'est juste à des fins de comparaison avec l'article précédent. curs.execute("CREATE VIEW %s AS SELECT * FROM %s WHERE ID<=1700" % (dataTableViewTrain, dataTable)) df62 = pd.read_sql("SELECT * from %s" % dataTableViewTrain, conn) display(df62) print(dataTableViewTrain, modelName, dataColumn) CovidPPP62Train1 ICUP621 ICU   ### **Formation du modèle à l'aide de l'AutoML par défaut d'IntegratedML** curs.execute("CREATE MODEL %s PREDICTING (%s) FROM %s" % (modelName, dataColumn, dataTableViewTrain)) curs.execute("TRAIN MODEL %s FROM %s" % (modelName, dataTableViewTrain)) df3 = pd.read_sql("SELECT * FROM INFORMATION_SCHEMA.ML_TRAINED_MODELS", conn) display(df3)   NOM_DU_MODÈLE NOM_DU_MODÈLE_FORMÉ FOURNISSEUR HORODATAGE_FORMÉ TYPE_DU_MODÈLE MODÈLE_INFO 9 USIP621 USIP6212 AutoML 2020-07-22 19:28:16.174000 classification ModelType:Random Forest, Paquet:sklearn, Prob... Ainsi, nous pouvons voir que le résultat montre qu'IntegratedML a automatiquement choisi "ModelType" comme étant "Random Forest" (forêt aléatoire), et traite le problème comme une tâche de "Classification".  C'est exactement ce que nous avons obtenu après les longues comparaisons de modèles et les sélections par boîte à moustaches, ainsi que le long réglage des paramètres du modèle par quadrillage, etc. dans l'article précédent, n'est-ce pas ? **Remarque**: le SQL ci-dessus est le strict minimum selon la syntaxe d'IntegratedML. Je n'ai pas spécifié d'approche de formation ou de sélection de modèle, et je n'ai pas défini de plateforme de ML. Tout a été laissé à la décision de l'IML, qui a réussi à mettre en œuvre sa stratégie de formation interne, avant de se contenter d'un modèle raisonnable avec des résultats finaux corrects. Je dirais que cela a dépassé mes attentes.    Effectuons un rapide test de comparaison du modèle actuellement entraîné sur notre ensemble de test réservé.   ### **Prédiction des résultats sur la base de données de test** Nous avons utilisé 1700 lignes pour la formation. Ci-dessous, nous créons une vue des données de test avec les 225 lignes restantes, et nous exécutons SELECT PREDICT sur ces enregistrements. Nous sauvegarderons le résultat prédit dans '`dataTablePredict`', et le chargerons dans 'df62' en tant que data frame. dataTableViewTest = "SQLUSER.DTT621" curs.execute("CREATE VIEW %s AS SELECT * FROM %s WHERE ID > 1700" % (dataTableViewTest, dataTable)) curs.execute("DROP TABLE %s" % dataTablePredict ) curs.execute("Create Table %s (%s VARCHAR(100), %s VARCHAR(100))" % (dataTablePredict, dataColumnPredict, dataColumn)) curs.execute("INSERT INTO %s SELECT PREDICT(%s) AS %s, %s FROM %s" % (dataTablePredict, modelName, dataColumnPredict, dataColumn, dataTableViewTest)) df62 = pd.read_sql("SELECT * from %s ORDER BY ID" % dataTablePredict, conn) display(df62) Nous n'avons pas besoin de calculer manuellement sa matrice de confusion. Il s'agit simplement d'une comparaison : TP = df62[(df62['ICUPredicted'] == '1') & (df62['ICU']=='1')].count()['ICU'] TN = df62[(df62['ICUPredicted'] == '0') & (df62['ICU']=='0')].count()["ICU"] FN = df62[(df62['ICU'] == '1') & (df62['ICUPredicted']=='0')].count()["ICU"] FP = df62[(df62['ICUPredicted'] == '1') & (df62['ICU']=='0')].count()["ICU"] print(TP, FN, '\n', FP, TN) precision = (TP)/(TP+FP) recall = (TP)/(TP+FN) f1 = ((precision*recall)/(precision+recall))*2 accuracy = (TP+TN) / (TP+TN+FP+FN) print("Precision: ", precision, " Recall: ", recall, " F1: ", f1, " Accuracy: ", accuracy) 34 20 8 163 Précision: 0.8095238095238095 rappel: 0.6296296296296297 F1: 0.7083333333333334 Exactitude: 0.8755555555555555 Nous pouvons également utiliser la syntaxe IntegratedML pour obtenir sa matrice de confusion intégrée : # valider les données de test curs.execute("VALIDATE MODEL %s FROM %s" % (modelName, dataTableViewTest) ) df5 = pd.read_sql("SELECT * FROM INFORMATION_SCHEMA.ML_VALIDATION_METRICS", conn) df6 = df5.pivot(index='VALIDATION_RUN_NAME', columns='METRIC_NAME', values='METRIC_VALUE') display(df6) NOM_MÉTRIQUE Exactitude Mesure F Précision Rappel NOM_DE_L'EXÉCUTION_DE_LA_VALIDATION         USIP62121 0.88 0.71 0.81 0.63 ... ... ... ... ... Si l'on compare avec le "Résultat original" de la section " Exécuter une formation de base en LR " dans la partie I, le résultat ci-dessus présente un rappel de 63 % contre 57 %, et une exactitude de 88 % contre 85 %. Il s'agit donc d'un meilleur résultat avec IntegratedML.   ### **Former à nouveau IntegratedML sur des données de formation rééquilibrées via SMOTE** Le test ci-dessus a été effectué sur des données déséquilibrées, dans lesquelles le rapport entre les patients admis en USI et les patients non admis est de 1:3. Donc, comme dans l'article précédent, nous allons simplement effectuer un SMOTE pour que les données soient équilibrées, puis nous allons réexécuter le pipeline IML ci-dessus. 'X\_train\_res' and 'y\_train\_res' sont des dataframes après SMOTE de la Partie I précédente dans sa section " Exécuter une formation de base en LR ".  df_x_train = pd.DataFrame(X_train_res) df_y_train = pd.DataFrame(y_train_res) df_y_train.columns=['ICU'] df_smote = pd.concat([df_x_train, df_y_train], 1) display(df_smote) iris_schema = 'SQLUser' iris_table = 'CovidSmote' to_sql_iris(curs, df_smote, iris_table, iris_schema, drop_table=True) # sauvegarder ceci dans un nouveau tableau IRIS portant le nom spécifié df2 = pd.read_sql("SELECT COUNT(*) from %s.%s" %(iris_schema, iris_table),conn) display(df2)   Aggregate_1 2490 Le jeu de données comporte désormais 2490 lignes au lieu de 1700, car SMOTE a enrichi davantage d'enregistrements avec USI = 1. dataTable = iris_table dataTableViewTrain = dataTable + 'TrainSmote' dataTablePredict = dataTable + 'PredictSmote' dataColumn = 'ICU' dataColumnPredict = 'ICUPredictedSmote' modelName = "ICUSmote1" #choisir un nom - doit être unique du côté serveur curs.execute("CREATE VIEW %s AS SELECT * FROM %s" % (dataTableViewTrain, dataTable)) df_smote = pd.read_sql("SELECT * from %s" % dataTableViewTrain, conn) display(df_smote) print(dataTableViewTrain, modelName, dataColumn) CovidSmoteTrainSmote ICUSmote1 ICU curs.execute("CREATE MODEL %s PREDICTING (%s)  FROM %s" % (modelName, dataColumn, dataTableViewTrain)) curs.execute("TRAIN MODEL %s FROM %s" % (modelName, dataTableViewTrain)) df3 = pd.read_sql("SELECT * FROM INFORMATION_SCHEMA.ML_TRAINED_MODELS", conn) display(df3)   NOM_DU_MODÈLE NOM_DU_MODÈLE_FORMÉ FOURNISSEUR HORODATAGE_FORMÉ TYPE_DU_MODÈLE MODEL_INFO 9 USIP621 USIP6212 AutoML 2020-07-22 19:28:16.174000 classification ModelType:Random Forest, Paquet:sklearn, Prob... 12 USISmote1 USISmote12 AutoML 2020-07-22 20:49:13.980000 classification ModelType:Random Forest, Paquet:sklearn, Prob... Ensuite, nous préparons à nouveau un ensemble réservé de 225 lignes de données de test et nous exécutons le modèle reformé de SMOTE sur ces lignes : df_x_test = pd.DataFrame(X3_test) df_y_test = pd.DataFrame(y3_test) df_y_test.columns=['ICU'] df_test_smote = pd.concat([df_x_test, df_y_test], 1) display(df_test_smote) iris_schema = 'SQLUser' iris_table = 'CovidTestSmote' to_sql_iris(curs, df_test_smote, iris_table, iris_schema, drop_table=True) dataTableViewTest = "SQLUSER.DTestSmote225" curs.execute("CREATE VIEW %s AS SELECT * FROM %s" % (dataTableViewTest, iris_table)) curs.execute("Create Table %s (%s VARCHAR(100), %s VARCHAR(100))" % (dataTablePredict, dataColumnPredict, dataColumn)) curs.execute("INSERT INTO %s SELECT PREDICT(%s) AS %s, %s FROM %s" % (dataTablePredict, modelName, dataColumnPredict, dataColumn, dataTableViewTest)) df62 = pd.read_sql("SELECT * from %s ORDER BY ID" % dataTablePredict, conn) display(df62) TP = df62[(df62['ICUPredictedSmote'] == '1') & (df62['ICU']=='1')].count()['ICU'] TN = df62[(df62['ICUPredictedSmote'] == '0') & (df62['ICU']=='0')].count()["ICU"] FN = df62[(df62['ICU'] == '1') & (df62['ICUPredictedSmote']=='0')].count()["ICU"] FP = df62[(df62['ICUPredictedSmote'] == '1') & (df62['ICU']=='0')].count()["ICU"] print(TP, FN, '\n', FP, TN) precision = (TP)/(TP+FP) recall = (TP)/(TP+FN) f1 = ((precision*recall)/(precision+recall))*2 accuracy = (TP+TN) / (TP+TN+FP+FN) print("Precision: ", precision, " Recall: ", recall, " F1: ", f1, " Accuracy: ", accuracy) 45 15 9 156 Précision: 0.8333333333333334 Rappel: 0.75 F1: 0.7894736842105262 Exactitude: 0.8933333333333333 # valider les données d'essai à l'aide du modèle reformé de SMOTE curs.execute("VALIDATE MODEL %s FROM %s" % (modelName, dataTableViewTest) ) #Covid19aTest500, Covid19aTrain1000 df5 = pd.read_sql("SELECT * FROM INFORMATION_SCHEMA.ML_VALIDATION_METRICS", conn) df6 = df5.pivot(index='VALIDATION_RUN_NAME', columns='METRIC_NAME', values='METRIC_VALUE') display(df6) NOM_MÉTRIQUE Exactitude Mesure F Précision Rappel NOM_DE_L'EXÉCUTION_DE_LA_VALIDATION         USIP62121 0.88 0.71 0.81 0.63 USISmote122 0.89 0.79 0.83 0.75 Le résultat indique une amélioration significative du rappel de 75 % par rapport aux 63 % précédents, ainsi qu'une légère amélioration de l'exactitude et du score F1.   Plus notablement, ce résultat est conforme à notre "approche ML traditionnelle" dans l'article précédent, après une "sélection de modèle" intensive et un "réglage des paramètres par quadrillage", comme indiqué dans la section "Exécuter le modèle sélectionné en poursuivant "Ajustement des paramètres via la recherche par quadrillage" supplémentaire". Le résultat de l'IML n'est donc pas mauvais du tout.   ### **Changement de fournisseur H2O d'IntegratedML ** Nous pouvons modifier le fournisseur AutoML de l'IML d'une seule ligne, puis former à nouveau le modèle comme nous l'avons fait à l'étape précédente :    curs.execute("SET ML CONFIGURATION %H2O; ") modelName = 'ICUSmoteH2O' print(dataTableViewTrain) curs.execute("CREATE MODEL %s PREDICTING (%s) FROM %s" % (modelName, dataColumn, dataTableViewTrain)) curs.execute("TRAIN MODEL %s FROM %s" % (modelName, dataTableViewTrain)) df3 = pd.read_sql("SELECT * FROM INFORMATION_SCHEMA.ML_TRAINED_MODELS", conn) display(df3)   NOM_DU_MODÈLE NOM_DU_MODÈLE_FORMÉ FOURNISSEUR HORODATAGE_FORMÉ TYPE_DU_MODÈLE MODÈLE_INFO 12 USISmote1 USISmote12 AutoML 2020-07-22 20:49:13.980000 classification ModelType:Random Forest, Paquet:sklearn, Prob... 13 USIPPP62 USIPPP622 AutoML 2020-07-22 17:48:10.964000 classification ModelType:Random Forest, Paquet:sklearn, Prob... 14 USISmoteH2O USISmoteH2O2 H2O 2020-07-22 21:17:06.990000 classification Aucun # valider les données de test curs.execute("VALIDATE MODEL %s FROM %s" % (modelName, dataTableViewTest) ) #Covid19aTest500, Covid19aTrain1000 df5 = pd.read_sql("SELECT * FROM INFORMATION_SCHEMA.ML_VALIDATION_METRICS", conn) df6 = df5.pivot(index='VALIDATION_RUN_NAME', columns='METRIC_NAME', values='METRIC_VALUE') display(df6) NOM_MÉTRIQUE Exactitude Mesure F Précision Rappel NOM_DE_L'EXÉCUTION_DE_LA_VALIDATION         USIP62121 0.88 0.71 0.81 0.63 USISmote122 0.89 0.79 0.83 0.75 USISmoteH2O21 0.90 0.79 0.86 0.73 Les résultats semblent montrer que H2O AutoML a une précision légèrement supérieure, le même F1, mais un rappel légèrement inférieur. Cependant, notre objectif principal dans cette tâche de Covid19 USI est de minimiser les faux négatifs si nous le pouvons. Il semble donc que le changement de fournisseur pour H2O n'ait pas encore permis d'augmenter notre performance cible. J'aimerais certainement tester également le fournisseur DataRobot d'IntegratedML, mais je n'ai malheureusement pas encore de clé API de DataRobot, alors je vais la mettre de côté ici.   ## Récapitulatif: 1. **Performance** : Pour cette tâche spécifique de l'unité de soins intensifs de Covid-19, nos comparaisons de tests indiquent que les performances de l'IntegratedML d'IRIS sont au moins équivalentes ou similaires aux résultats de l'approche ML traditionnelle. Dans ce cas précis, IntegratedML a été capable de choisir automatiquement et correctement la stratégie d'entraînement interne, et a semblé établir le bon modèle, fournissant le résultat escompté. 2. **Simplicité** : IntegratedML a un processus beaucoup plus simplifié que les pipelines ML traditionnels. Comme indiqué ci-dessus, je n'ai plus besoin de me préoccuper de la sélection des modèles et l'ajustement des paramètres, etc. Je n'ai pas non plus besoin de la sélection des caractéristiques, si ce n'est à des fins de comparaison. De plus, je n'ai utilisé que la syntaxe minimale d'IntegratedML, comme indiqué dans le cahier de démonstration d'Integrated-demo-template. Bien sûr, le désavantage est que nous sacrifions les capacités de personnalisation et d'ajustement des outils courants de science des données via leurs pipelines traditionnels, mais c'est aussi plus ou moins vrai pour d'autres plateformes AutoML. 3. **Le prétraitement des données reste important** : Il n'y a malheureusement pas de solution miracle ; ou plutôt, cette solution miracle prendrait du temps. Spécifiquement pour cette tâche de Covid19 USI, les tests ci-dessus montrent que les données ont encore beaucoup d'importance pour l'approche actuelle d'IntegratedML : données brutes, caractéristiques sélectionnées avec données manquantes imputées, et données rééquilibrées avec suréchantillonnage SMOTE de base, elles ont toutes abouti à des performances significativement différentes. C'est vrai pour l'AutoML par défaut d'IML et son fournisseur H2O. J'imagine que DataRobot pourrait revendiquer une performance légèrement supérieure, mais cela doit être testé plus avant avec l'enveloppe SQL d'IntegratedML. **En bref, la normalisation des données est toujours importante dans IntegratedML.** 4. **Déployabilité** : Je n'ai pas encore comparé la déployabilité, la gestion de l'API, la surveillance et la facilité d'utilisation non fonctionnelle, etc.   ## Suivant 1. **Déploiements de modèles** : Jusqu'à présent, nous avons fait des démonstrations d'IA sur les radiographies pour le Covid-19 et des prédictions pour l'unité de soins intensifs pour le Covid-19 sur les signes vitaux et les observations. Pouvons-nous les déployer dans les piles de services Flask/FastAPI et IRIS, et exposer leurs capacités ML/DL de démonstration via des API REST/JSON ? Bien sûr, nous pouvons essayer de le faire dans le prochain article. Ensuite, nous pourrons ajouter d'autres capacités d'IA de démonstration au fil du temps, y compris des API NLP, etc. 2. **Interopérabilité de l'API enveloppée dans FHIR** : Nous disposons également d'un modèle FHIR, ainsi que d'une API native IRIS, etc. dans cette communauté de développeurs. Pourrions-nous transformer notre service d'IA de démonstration en SMART on FHIR apps, ou en services d'IA enveloppés dans FHIR selon les normes correspondantes - pourrions-nous essayer cela ? Et n'oubliez pas que dans la gamme de produits IRIS, nous avons également API Gateway, ICM avec support Kubernetes, et SAM etc. que nous pourrions également exploiter avec nos piles de démonstrations d'IA. 3. **Démonstration d'intégration avec HealthShare Clinical Viewer et/ou Trak etc** ? J'ai brièvement montré [une démonstration d'intégration du PACS Viewer d'un fournisseur d'IA tiers (pour les CT Covid-19) avec HealthShare Clinical Viewer](https://community.intersystems.com/post/run-some-covid-19-lung-x-ray-classification-and-ct-detection-demos), et nous pourrions peut-être terminer cette randonnée avec nos propres services de démonstration d'IA, dans divers domaines de spécialité au fil du temps.
Article
Guillaume Rongier · Sept 28, 2022

HL7v2 vers FHIR, c'est facile !

# Service Iris Healthtoolkit [![Vidéo](https://raw.githubusercontent.com/grongierisc/iris-healthtoolkit-service/main/misc/images/Cover.png)](https://youtu.be/lr2B7zSFkds "Video") Utilisation facile de HL7v2 vers FHIR, CDA vers FHIR, FHIR vers HL7v2 en tant que service. L'objectif de ce projet est de fournir une API REST capable de convertir facilement divers formats de santé. Publiez le format souhaité dans le corps REST, obtenez la réponse dans le nouveau format. * Version officielle : https://aws.amazon.com/marketplace/pp/prodview-q7ryewpz75cq2 :fire: * Vidéo : https://youtu.be/lr2B7zSFkds :tv: ## Installation Clonez ce référentiel ``` git clone https://github.com/grongierisc/iris-healthtoolkit-service.git ``` Docker ``` docker-compose up --build -d ``` ## Utilisation * Atteignez : http://localhost:32783/swagger-ui/index.html ## Détails de l'Api ### HL7 vers FHIR ``` POST http://localhost:32783/api/hl7/fhir ``` #### Exemple Saisie ```text MSH|^~\&||^^NPI|||20211105165829+0000||ADT^A01|66053,61109.396628|P|2.5.1|||AL|AL|||||PH_SS-Ack^SS Sender^2.16.840.1.114222.4.10.3^ISO EVN||202111051658|||||^^NPI PID|1||060a6bd5-5146-4b08-a916-009858997bd3^^^https://github.com/synthetichealth/synthea^~060a6bd5-5146-4b08-a916-009858997bd3^^^http://hospital.smarthealthit.org^MR~999-97-4582^^^&^SS~S99986284^^^&^DL~X84330364X^^^&^PPN||Masson^Livia^^^Mrs.^^||19920820|F|Simon^Livia^^||615 Avenue Lemaire^^Lyon^Auvergne-Rhone-Alpes^63000||^PRN^PH^^^555^286||||||||||||||||||||| PV1|1|O||424441002|||||||||||||||1^^^&&^VN|||||||||||||||||||||||||200812312325|20090101044004 PV2|||72892002^Grossesse normale^SCT OBX|1||8302-2^Taille du corps^LN||20101014002504^^|cm^^UCUM|||||F|||20101014 OBX|2||72514-3^Gravite de la douleur - 0-10 evaluation numerique verbale [Score] - Signaleee^LN||20101014002504^^|{score}^^UCUM|||||F|||20101014 OBX|3||29463-7^Poids corporel^LN||20101014002504^^|kg^^UCUM|||||F|||20101014 OBX|4||39156-5^Indice de masse corporelle^LN||20101014002504^^|kg/m2^^UCUM|||||F|||20101014 OBX|5||72166-2^Statut du tabagisme^LN||20171026002504^Ancien fumeur^SCT^^^^^^Ancien fumeur||||||F|||20171026 ``` Sortie ```json { "typeDeRessource": "Paquet", "type": "transaction", "saisie": [ { "demande": { "méthode": "POST", "url": "Organisation" }, "UrlComplète": "urn:uuid:347a0c88-e7fa-11ec-9601-0242ac1a0002", "ressource": { "typeDeRessource": "Organisation", "identifiant": [ { "valeur": "https://github.com/synthetichealth/synthea" } ] } }, { "demande": { "méthode": "POST", "url": "Organisation" }, "UrlComplète": "urn:uuid:34d03d1a-e7fa-11ec-9601-0242ac1a0002", "ressource": { "typeDeRessource": "Organisation", "identifiant": [ { "valeur": "http://hospital.smarthealthit.org" } ] } }, { "demande": { "méthode": "POST", "url": "Patient" }, "UrlComplète": "urn:uuid:36dd6e2a-e7fa-11ec-9601-0242ac1a0002", "ressource": { "typeDeRessource": "Patient", "adresse": [ { "ville": "Lyon", "ligne": [ "615 Avenue Lemaire" ], "codePostal": "63000", "région": "Auvergne-Rhone-Alpes" } ], "dateDeNaissance": "1992-08-20", "sex": "femme", "identifiant": [ { "assigner": { "référence": "urn:uuid:347a0c88-e7fa-11ec-9601-0242ac1a0002" }, "système": "https://github.com/synthetichealth/synthea", "valeur": "060a6bd5-5146-4b08-a916-009858997bd3" }, { "assigner": { "référence": "urn:uuid:34d03d1a-e7fa-11ec-9601-0242ac1a0002" }, "système": "http://hospital.smarthealthit.org", "type": { "codage": [ { "code": "MR", "système": "http://terminology.hl7.org/CodeSystem/v2-0203" } ], "texte": "MRN" }, "valeur": "060a6bd5-5146-4b08-a916-009858997bd3" }, { "extension": [ { "url": "http://intersystems.com/fhir/extn/sda3/lib/patient-number-i-s-o-assigning-authority", "valeurDeLigne": "&" } ], "type": { "codage": [ { "code": "SS" } ], "texte": "SS" }, "valeur": "999-97-4582" }, { "extension": [ { "url": "http://intersystems.com/fhir/extn/sda3/lib/patient-number-i-s-o-assigning-authority", "valeurDeLigne": "&" } ], "type": { "codage": [ { "code": "DL", "système": "http://terminology.hl7.org/CodeSystem/v2-0203" } ], "texte": "DL" }, "valeur": "S99986284" }, { "extension": [ { "url": "http://intersystems.com/fhir/extn/sda3/lib/patient-number-i-s-o-assigning-authority", "valeurDeLigne": "&" } ], "type": { "codage": [ { "code": "PPN", "système": "http://terminology.hl7.org/CodeSystem/v2-0203" } ], "texte": "PPN" }, "valeur": "X84330364X" } ], "nom": [ { "famille": "Simon", "prénom": [ "Livia" ], "texte": "Livia Simon" }, { "famille": "Masson", "prénom": [ "Livia" ], "préfixe": [ "Mrs." ], "texte": "Mrs. Livia Masson", "utilisation": "officiel" } ], "telecom": [ { "système": "téléphone", "utilisation": "domicile", "valeur": "(555) 286" } ] } }, { "demande": { "méthode": "POST", "url": "Visite" }, "UrlComplète": "urn:uuid:38cf2d40-e7fa-11ec-9601-0242ac1a0002", "ressource": { "typeDeRessource": "Visite", "class": { "code": "AMB", "système": "http://terminology.hl7.org/CodeSystem/v3-ActCode" }, "extension": [ { "url": "http://intersystems.com/fhir/extn/sda3/lib/encounter-encounter-type", "valeurDeLigne": "O" }, { "url": "http://intersystems.com/fhir/extn/sda3/lib/encounter-entered-on", "valeurDateHeur": "2008-12-31T23:25:00+00:00" }, { "url": "http://intersystems.com/fhir/extn/sda3/lib/encounter-to-time", "valeurDateHeur": "2009-01-01T04:40:04+00:00" } ], "identifiant": [ { "type": { "texte": "NuméroDeVisite" }, "utilisation": "officiel", "valeur": "1" } ], "période": { "lancement": "2008-12-31T23:25:00+00:00" }, "reasonCode": [ { "codage": [ { "code": "72892002", "affichage": "Grossesse normale", "système": "http://snomed.info/sct" } ] } ], "état": "inconnu", "sujet": { "référence": "urn:uuid:36dd6e2a-e7fa-11ec-9601-0242ac1a0002" }, "type": [ { "codage": [ { "code": "424441002" } ] } ] } }, { "demande": { "méthode": "POST", "url": "Observation" }, "UrlComplète": "urn:uuid:3a13745e-e7fa-11ec-9601-0242ac1a0002", "ressource": { "typeDeRessource": "Observation", "code": { "codage": [ { "code": "72166-2", "affichage": "Statut du tabagisme", "système": "http://loinc.org" } ] }, "effectiveDateHeure": "2017-10-26T00:00:00+00:00", "visite": { "référence": "urn:uuid:38cf2d40-e7fa-11ec-9601-0242ac1a0002" }, "extension": [ { "url": "http://intersystems.com/fhir/extn/sda3/lib/observation-encounter-number", "valeurDeLigne": "1" }, { "url": "http://intersystems.com/fhir/extn/sda3/lib/observation-observation-coded-value", "valeurConceptCodifiable": { "codage": [ { "code": "20171026002504", "affichage": "Ancien fumeur", "système": "http://snomed.info/sct" } ], "texte": "Ancien fumeur" } } ], "état": "final", "sujet": { "référence": "urn:uuid:36dd6e2a-e7fa-11ec-9601-0242ac1a0002" }, "valeurDeLigne": "Ancien fumeur" } }, { "demande": { "méthode": "POST", "url": "Observation" }, "UrlComplète": "urn:uuid:3b6212fc-e7fa-11ec-9601-0242ac1a0002", "ressource": { "typeDeRessource": "Observation", "category": [ { "codage": [ { "code": "Signes-vitaux", "affichage": "Signes vitaux", "système": "http://terminology.hl7.org/CodeSystem/observation-category" } ], "texte": "Signes vitaux" } ], "code": { "codage": [ { "code": "8302-2", "affichage": "Taille du corps", "système": "http://loinc.org" } ], "extension": [ { "url": "http://intersystems.com/fhir/extn/sda3/lib/code-table-detail-observation-observation-value-units", "valeurConceptCodifiable": { "codage": [ { "code": "cm", "système": "http://unitsofmeasure.org" } ] } } ] }, "effectiveDateHeure": "2010-10-14T00:00:00+00:00", "visite": { "référence": "urn:uuid:38cf2d40-e7fa-11ec-9601-0242ac1a0002" }, "extension": [ { "url": "http://intersystems.com/fhir/extn/sda3/lib/observation-encounter-number", "valeurDeLigne": "1" } ], "état": "final", "sujet": { "référence": "urn:uuid:36dd6e2a-e7fa-11ec-9601-0242ac1a0002" }, "valeurConceptCodifiable": { "codage": [ { "code": "20101014002504" } ] } } }, { "demande": { "méthode": "POST", "url": "Observation" }, "UrlComplète": "urn:uuid:3c8aba30-e7fa-11ec-9601-0242ac1a0002", "ressource": { "typeDeRessource": "Observation", "code": { "codage": [ { "code": "72514-3", "affichage": "Gravite de la douleur - 0-10 evaluation numerique verbale [Score] - Signaleee", "système": "http://loinc.org" } ], "extension": [ { "url": "http://intersystems.com/fhir/extn/sda3/lib/code-table-detail-observation-observation-value-units", "valeurConceptCodifiable": { "codage": [ { "code": "{score}", "système": "http://unitsofmeasure.org" } ] } } ] }, "effectiveDateHeure": "2010-10-14T00:00:00+00:00", "visite": { "référence": "urn:uuid:38cf2d40-e7fa-11ec-9601-0242ac1a0002" }, "extension": [ { "url": "http://intersystems.com/fhir/extn/sda3/lib/observation-encounter-number", "valeurDeLigne": "1" } ], "état": "final", "sujet": { "référence": "urn:uuid:36dd6e2a-e7fa-11ec-9601-0242ac1a0002" }, "valeurConceptCodifiable": { "codage": [ { "code": "20101014002504" } ] } } }, { "demande": { "méthode": "POST", "url": "Observation" }, "UrlComplète": "urn:uuid:3de455d0-e7fa-11ec-9601-0242ac1a0002", "ressource": { "typeDeRessource": "Observation", "category": [ { "codage": [ { "code": "signes -vitaux", "affichage": "Signes vitaux", "système": "http://terminology.hl7.org/CodeSystem/observation-category" } ], "texte": "Signes vitaux" } ], "code": { "codage": [ { "code": "29463-7", "affichage": "Poids corporel", "système": "http://loinc.org" } ], "extension": [ { "url": "http://intersystems.com/fhir/extn/sda3/lib/code-table-detail-observation-observation-value-units", "valeurConceptCodifiable": { "codage": [ { "code": "kg", "système": "http://unitsofmeasure.org" } ] } } ] }, "effectiveDateHeure": "2010-10-14T00:00:00+00:00", "visite": { "référence": "urn:uuid:38cf2d40-e7fa-11ec-9601-0242ac1a0002" }, "extension": [ { "url": "http://intersystems.com/fhir/extn/sda3/lib/observation-encounter-number", "valeurDeLigne": "1" } ], "état": "final", "sujet": { "référence": "urn:uuid:36dd6e2a-e7fa-11ec-9601-0242ac1a0002" }, "valeurConceptCodifiable": { "codage": [ { "code": "20101014002504" } ] } } }, { "demande": { "méthode": "POST", "url": "Observation" }, "UrlComplète": "urn:uuid:3f501418-e7fa-11ec-9601-0242ac1a0002", "ressource": { "typeDeRessource": "Observation", "code": { "codage": [ { "code": "39156-5", "affichage": "Indice de masse corporelle", "système": "http://loinc.org" } ], "extension": [ { "url": "http://intersystems.com/fhir/extn/sda3/lib/code-table-detail-observation-observation-value-units", "valeurConceptCodifiable": { "codage": [ { "code": "kg/m2", "système": "http://unitsofmeasure.org" } ] } } ] }, "effectiveDateHeure": "2010-10-14T00:00:00+00:00", "visite": { "référence": "urn:uuid:38cf2d40-e7fa-11ec-9601-0242ac1a0002" }, "extension": [ { "url": "http://intersystems.com/fhir/extn/sda3/lib/observation-encounter-number", "valeurDeLigne": "1" } ], "état": "final", "sujet": { "référence": "urn:uuid:36dd6e2a-e7fa-11ec-9601-0242ac1a0002" }, "valeurConceptCodifiable": { "codage": [ { "code": "20101014002504" } ] } } } ] } ``` ### FHIR vers HL7 ADT ``` POST http://localhost:32783/api/fhir/hl7/adt ``` #### Exemple ```json { "typeDeRessource": "Paquet", "type": "transaction", "saisie": [ { "demande": { "méthode": "POST", "url": "Organisation" }, "UrlComplète": "urn:uuid:347a0c88-e7fa-11ec-9601-0242ac1a0002", "ressource": { "typeDeRessource": "Organisation", "identifiant": [ { "valeur": "https://github.com/synthetichealth/synthea" } ] } }, { "demande": { "méthode": "POST", "url": "Organisation" }, "UrlComplète": "urn:uuid:34d03d1a-e7fa-11ec-9601-0242ac1a0002", "ressource": { "typeDeRessource": "Organisation", "identifiant": [ { "valeur": "http://hospital.smarthealthit.org" } ] } }, { "demande": { "méthode": "POST", "url": "Patient" }, "UrlComplète": "urn:uuid:36dd6e2a-e7fa-11ec-9601-0242ac1a0002", "ressource": { "typeDeRessource": "Patient", "adresse": [ { "ville": "Lyon", "ligne": [ "615 Avenue Lemaire" ], "codePostal": "63000", "région": "Auvergne-Rhone-Alpes" } ], "dateDeNaissance": "1992-08-20", "sex": "femme", "identifiant": [ { "assigner": { "référence": "urn:uuid:347a0c88-e7fa-11ec-9601-0242ac1a0002" }, "système": "https://github.com/synthetichealth/synthea", "valeur": "060a6bd5-5146-4b08-a916-009858997bd3" }, { "assigner": { "référence": "urn:uuid:34d03d1a-e7fa-11ec-9601-0242ac1a0002" }, "système": "http://hospital.smarthealthit.org", "type": { "codage": [ { "code": "MR", "système": "http://terminology.hl7.org/CodeSystem/v2-0203" } ], "texte": "MRN" }, "valeur": "060a6bd5-5146-4b08-a916-009858997bd3" }, { "extension": [ { "url": "http://intersystems.com/fhir/extn/sda3/lib/patient-number-i-s-o-assigning-authority", "valeurDeLigne": "&" } ], "type": { "codage": [ { "code": "SS" } ], "texte": "SS" }, "valeur": "999-97-4582" }, { "extension": [ { "url": "http://intersystems.com/fhir/extn/sda3/lib/patient-number-i-s-o-assigning-authority", "valeurDeLigne": "&" } ], "type": { "codage": [ { "code": "DL", "système": "http://terminology.hl7.org/CodeSystem/v2-0203" } ], "texte": "DL" }, "valeur": "S99986284" }, { "extension": [ { "url": "http://intersystems.com/fhir/extn/sda3/lib/patient-number-i-s-o-assigning-authority", "valeurDeLigne": "&" } ], "type": { "codage": [ { "code": "PPN", "système": "http://terminology.hl7.org/CodeSystem/v2-0203" } ], "texte": "PPN" }, "valeur": "X84330364X" } ], "nom": [ { "famille": "Simon", "prénom": [ "Livia" ], "texte": "Livia Simon" }, { "famille": "Masson", "prénom": [ "Livia" ], "préfixe": [ "Mrs." ], "texte": "Mrs. Livia Masson", "utilisation": "officiel" } ], "telecom": [ { "système": "téléphone", "utilisation": "domicile", "valeur": "(555) 286" } ] } }, { "demande": { "méthode": "POST", "url": "visite" }, "UrlComplète": "urn:uuid:38cf2d40-e7fa-11ec-9601-0242ac1a0002", "ressource": { "typeDeRessource": "visite", "class": { "code": "AMB", "système": "http://terminology.hl7.org/CodeSystem/v3-ActCode" }, "extension": [ { "url": "http://intersystems.com/fhir/extn/sda3/lib/encounter-encounter-type", "valeurDeLigne": "O" }, { "url": "http://intersystems.com/fhir/extn/sda3/lib/encounter-entered-on", "valeurDateHeur": "2008-12-31T23:25:00+00:00" }, { "url": "http://intersystems.com/fhir/extn/sda3/lib/encounter-to-time", "valeurDateHeur": "2009-01-01T04:40:04+00:00" } ], "identifiant": [ { "type": { "texte": "EncounterNumber" }, "utilisation": "officiel", "valeur": "1" } ], "période": { "lancement": "2008-12-31T23:25:00+00:00" }, "reasonCode": [ { "codage": [ { "code": "72892002", "affichage": "Grossesse normale", "système": "http://snomed.info/sct" } ] } ], "état": "inconnu", "sujet": { "référence": "urn:uuid:36dd6e2a-e7fa-11ec-9601-0242ac1a0002" }, "type": [ { "codage": [ { "code": "424441002" } ] } ] } }, { "demande": { "méthode": "POST", "url": "Observation" }, "UrlComplète": "urn:uuid:3a13745e-e7fa-11ec-9601-0242ac1a0002", "ressource": { "typeDeRessource": "Observation", "code": { "codage": [ { "code": "72166-2", "affichage": "Statut du tabagisme", "système": "http://loinc.org" } ] }, "effectiveDateHeure": "2017-10-26T00:00:00+00:00", "visite": { "référence": "urn:uuid:38cf2d40-e7fa-11ec-9601-0242ac1a0002" }, "extension": [ { "url": "http://intersystems.com/fhir/extn/sda3/lib/observation-encounter-number", "valeurDeLigne": "1" }, { "url": "http://intersystems.com/fhir/extn/sda3/lib/observation-observation-coded-value", "valeurConceptCodifiable": { "codage": [ { "code": "20171026002504", "affichage": "Ancien fumeur", "système": "http://snomed.info/sct" } ], "texte": "Ancien fumeur" } } ], "état": "final", "sujet": { "référence": "urn:uuid:36dd6e2a-e7fa-11ec-9601-0242ac1a0002" }, "valeurDeLigne": "Ancien fumeur" } }, { "demande": { "méthode": "POST", "url": "Observation" }, "UrlComplète": "urn:uuid:3b6212fc-e7fa-11ec-9601-0242ac1a0002", "ressource": { "typeDeRessource": "Observation", "category": [ { "codage": [ { "code": "signes-vitaux", "affichage": "Signes vitaux", "système": "http://terminology.hl7.org/CodeSystem/observation-category" } ], "texte": "Signes vitaux" } ], "code": { "codage": [ { "code": "8302-2", "affichage": "Taille du corps", "système": "http://loinc.org" } ], "extension": [ { "url": "http://intersystems.com/fhir/extn/sda3/lib/code-table-detail-observation-observation-value-units", "valeurConceptCodifiable": { "codage": [ { "code": "cm", "système": "http://unitsofmeasure.org" } ] } } ] }, "effectiveDateHeure": "2010-10-14T00:00:00+00:00", "visite": { "référence": "urn:uuid:38cf2d40-e7fa-11ec-9601-0242ac1a0002" }, "extension": [ { "url": "http://intersystems.com/fhir/extn/sda3/lib/observation-encounter-number", "valeurDeLigne": "1" } ], "état": "final", "sujet": { "référence": "urn:uuid:36dd6e2a-e7fa-11ec-9601-0242ac1a0002" }, "valeurConceptCodifiable": { "codage": [ { "code": "20101014002504" } ] } } }, { "demande": { "méthode": "POST", "url": "Observation" }, "UrlComplète": "urn:uuid:3c8aba30-e7fa-11ec-9601-0242ac1a0002", "ressource": { "typeDeRessource": "Observation", "code": { "codage": [ { "code": "72514-3", "affichage": "Gravite de la douleur - 0-10 evaluation numerique verbale [Score] - Signaleee", "système": "http://loinc.org" } ], "extension": [ { "url": "http://intersystems.com/fhir/extn/sda3/lib/code-table-detail-observation-observation-value-units", "valeurConceptCodifiable": { "codage": [ { "code": "{score}", "système": "http://unitsofmeasure.org" } ] } } ] }, "effectiveDateHeure": "2010-10-14T00:00:00+00:00", "visite": { "référence": "urn:uuid:38cf2d40-e7fa-11ec-9601-0242ac1a0002" }, "extension": [ { "url": "http://intersystems.com/fhir/extn/sda3/lib/observation-encounter-number", "valeurDeLigne": "1" } ], "état": "final", "sujet": { "référence": "urn:uuid:36dd6e2a-e7fa-11ec-9601-0242ac1a0002" }, "valeurConceptCodifiable": { "codage": [ { "code": "20101014002504" } ] } } }, { "demande": { "méthode": "POST", "url": "Observation" }, "UrlComplète": "urn:uuid:3de455d0-e7fa-11ec-9601-0242ac1a0002", "ressource": { "typeDeRessource": "Observation", "category": [ { "codage": [ { "code": "signes-vitaux", "affichage": "Signes vitaux", "système": "http://terminology.hl7.org/CodeSystem/observation-category" } ], "texte": "Signes vitaux" } ], "code": { "codage": [ { "code": "29463-7", "affichage": "Poids corporel", "système": "http://loinc.org" } ], "extension": [ { "url": "http://intersystems.com/fhir/extn/sda3/lib/code-table-detail-observation-observation-value-units", "valeurConceptCodifiable": { "codage": [ { "code": "kg", "système": "http://unitsofmeasure.org" } ] } } ] }, "effectiveDateHeure": "2010-10-14T00:00:00+00:00", "visite": { "référence": "urn:uuid:38cf2d40-e7fa-11ec-9601-0242ac1a0002" }, "extension": [ { "url": "http://intersystems.com/fhir/extn/sda3/lib/observation-encounter-number", "valeurDeLigne": "1" } ], "état": "final", "sujet": { "référence": "urn:uuid:36dd6e2a-e7fa-11ec-9601-0242ac1a0002" }, "valeurConceptCodifiable": { "codage": [ { "code": "20101014002504" } ] } } }, { "demande": { "méthode": "POST", "url": "Observation" }, "UrlComplète": "urn:uuid:3f501418-e7fa-11ec-9601-0242ac1a0002", "ressource": { "typeDeRessource": "Observation", "code": { "codage": [ { "code": "39156-5", "affichage": "Indice de masse corporelle", "système": "http://loinc.org" } ], "extension": [ { "url": "http://intersystems.com/fhir/extn/sda3/lib/code-table-detail-observation-observation-value-units", "valeurConceptCodifiable": { "codage": [ { "code": "kg/m2", "système": "http://unitsofmeasure.org" } ] } } ] }, "effectiveDateHeure": "2010-10-14T00:00:00+00:00", "visite": { "référence": "urn:uuid:38cf2d40-e7fa-11ec-9601-0242ac1a0002" }, "extension": [ { "url": "http://intersystems.com/fhir/extn/sda3/lib/observation-encounter-number", "valeurDeLigne": "1" } ], "état": "final", "sujet": { "référence": "urn:uuid:36dd6e2a-e7fa-11ec-9601-0242ac1a0002" }, "valeurConceptCodifiable": { "codage": [ { "code": "20101014002504" } ] } } } ] } ``` Sortie ```texte MSH|^~\&||^^NPI|||20220609134903+0000||^|66269,49743.388133779|P|2.5.1|||AL|AL|||||PH_SS-Ack^SS Sender^2.16.840.1.114222.4.10.3^ISO EVN||202206091349|||||^^NPI PID|1||060a6bd5-5146-4b08-a916-009858997bd3^^^https://github.com/s&&ISO^~060a6bd5-5146-4b08-a916-009858997bd3^^^http://hospital.smar&&ISO^MR~999-97-4582^^^&^SS~S99986284^^^&^DL~X84330364X^^^&^PPN||Masson^Livia^^^Mrs.^^||19920820|F|Simon^Livia^^||615 Avenue Lemaire^^^^||^PRN^PH^^^555^286||||||||||||||||||||| PV1|1|O||424441002|||||||||||||||1^^^&&^VN|||||||||||||||||||||||||200812312325| PV2|||72892002^Grossesse normale^SCT OBX|1||72166-2^Statut du tabagisme^LN||^^||||||F|||20171026 OBX|2||8302-2^Taille du corps^LN||^^||||||F|||20101014 OBX|3||72514-3^Gravite de la douleur - 0-10 evaluation numerique verbale [Score] - Signaleee^LN||^^||||||F|||20101014 OBX|4||29463-7^Poids corporel^LN||^^||||||F|||20101014 OBX|5||39156-5^Indice de masse corporelle^LN||^^||||||F|||20101014 ``` ### FHIR vers HL7 ORU ``` POST http://localhost:32783/api/fhir/hl7/oru ``` ### FHIR vers HL7 vxu ``` POST http://localhost:32783/api/fhir/hl7/vxu ``` ### CDA vers FHIR ``` POST http://localhost:32783/api/cda/fhir ``` #### Exemple ### Dépôt FHIR ``` GET http://localhost:32783/api/fhir/metadata ``` ## Format d'entrée HL7 pris en charge : * ADT_A01, ADT_A02, ADT_A03, ADT_A04, ADT_A05, ADT_A06, ADT_A07, ADT_A08, ADT_A09, ADT_A10, ADT_A11, ADT_A12, ADT_A13, ADT_A17, ADT_A18, ADT_A23, ADT_A25, ADT_A27, ADT_A28, ADT_A29, ADT_A30, ADT_A31, ADT_A34, ADT_A36, ADT_A39, ADT_A40, ADT_A41, ADT_A45, ADT_A47, ADT_A49, ADT_A50, ADT_A51, ADT_A60 * BAR_P12 * MDM_T02, MDM_T04, MDM_T08, MDM_T11 * OMP_O09 * ORM_O01 * ORU_R01 * PPR_PC1, PPR_PC2, PPR_PC3 * RDE_O11 * SIU_S12, SIU_S13, SIU_S14, SIU_S15, SIU_S16, SIU_S17, SIU_S26 * VXU_V04 ## Comment ça marche Ce projet fonctionne avec le diagramme pivot : SDA. Le SDA (Summary Document Architecture, Architecture du document de synthèse) est le format de données cliniques d'InterSystems. Les correspondances SDA FHIR peuvent être consultées [ici](https://docs.intersystems.com/irisforhealthlatest/csp/docbook/Doc.View.cls?KEY=HXFHIR_transforms), et celles de la CDA -> SDA [ici](https://docs.intersystems.com/irisforhealthlatest/csp/docbook/DocBook.UI.Page.cls?KEY=HXCDA). ![gif sda pivot](https://raw.githubusercontent.com/grongierisc/iris-healthtoolkit-service/main/misc/images/Gif_SDA_Pivot.gif)
Article
Lorenzo Scalese · Fév 27, 2023

OpenAPI Suite - Partie 1

Salut la communauté, J'aimerais vous présenter ma dernière application OpenAPI-Suite, c'est un ensemble d'outils permettant de générer du code ObjectScript à partir d'une specification OpenAPI version 3.0. L'application permet de: Générer les classes serveur REST. C'est assez similaire au code généré par ^%REST, la valeur ajoutée est le support de la version 3.0. Générer les classes pour un client HTTP. Générer une production cliente (business services, business operation, business process, Ens.Request, Ens.Response). Disposer d'une interface web pour générer et télécharger le code ou générer et compiler directement sur le serveur. Convertir les spécifications de version 1.x, 2.x en version 3.0. Aperçu OpenAPI-Suite est divisée en plusieurs packages et utilise différentes bibliothèques de la communauté des développeurs ainsi que des services REST publics. Vous pouvez voir sur le schéma ci-dessous, tous les packages développés et les bibliothèques et services web utilisés: Note: En cas de problème d'utilisation des services REST publics, il est possible de démarrer une instance docker du service de convertisseur et du validateur (nous verrons cela dans la partie 2). Quel est le rôle de chaque package? La suite OpenAPI a été conçue en différents pacakges pour faciliter la maintenance, les améliorations et extensions futures. Chaque package a un rôle. Jetons-y un coup d'oeil ! openapi-common-lib Il contient tout le code commun aux autres packages. Par exemple, openapi-client-gen et openapi-server-gen acceptent comme donnée en entrée: URL Chemin d'accès d'un fichier %Stream.Object %DynamicObject Format YAML Format JSON OpenAPI version 1.x, 2.x, 3.0.x. Cependant, seule la specification 3.0.x au format %DynamicObject peut être traité. Le code de transformation se trouve dans ce package. Il contient également divers utilitaires. swagger-converter-cli C'est une dépendance de openapi-common-lib et un client HTTP du service public REST converter.swagger.io afin de convertir les spécifications OpenAPI version 1.x ou 2.x en version 3.0. swagger-validator-cli C'est aussi une dépendance de openapi-common-lib, même si son nom est "validator", il n'est pas utilisé pour valider la spécification. converter.swagger.io fournit le service "parse" permettant de simplifier la structure d'une spécification OpenAPI. Par exemple: Il permet de créer une définition lorsqu'un objet est imbriqué et génère un "$ref" à la place. Cela réduit le nombre de cas à traiter dans l'algorithme de génération de code. openapi-client-gen Ce package est dédié à la génération de code côté client afin d'aider les développeurs à consommer les services REST. Il inclut la génération d'un simple HTTP client ou d'une production (business services, processes, ...). Il a été conçu intialement pour supporter swagger version 2.0, mais il a vient d'être complètement remanié pour supporter OpenAPI version 3.0. openapi-server-gen A l'opposé openapi-client-gen, ce package est dédié à la génération de code côté serveur. Il n'a aucun intérêt si vous avez besoin de générer un services REST à partir d'une spécification swagger 2.0. L'objectif de ce package est le support de la version 3.0. openapi-suite Il rassemble tous les packages mentionnés ci-dessus et fournit un API REST afin de: Générer le code et le compiler sur l'instance IRIS. Générer le code sans le compiler dans le but de le télécharger uniquement. Une interface web est également fournie pour consommer cette API REST et ainsi exploiter les fonctionnalités de la suite OpenAPI. Et les bibliothèques? Voici quelques bibliothèques existantes sur OpenExchange qui ont été utilisées dans ce développement : objectscript-openapi-definition Une bibliothèque utile pour générer les modèles à partir d'une spécification OpenAPI. C'est un élément très important de ce projet et je suis aussi un contributeur de ce projet. ssl-client Principalement utilisée pour créer une configuration "DefaultSSL" utilisées pour les requêtes HTTPS. yaml-utils Dans le cas de la spécification du format YAML, cette bibliothèque est utilisée pour la convertir au format JSON. Un must-have dans ce projet. D'ailleurs, elle a été initialement développée pour tester la spécification YAML avec openapi-client-gen version 1. io-redirect C'est une de mes bibliothèques, elle permet de rediriger les écritures vers un fichier, une variable, globale ou une chaîne de caractères. Elle est utilisée par le service REST pour garder un log des actions effectuées. Elle est inspirée par ce post de la communauté. Installation IPM La meilleure solution pour installation OpenAPI-Suite est d'utiliser IPM (zpm). Il y a beaucoup de pacakges et de dépendances. L'utilisation de IPM est très pratique et recommandée. zpm "install openapi-suite" ; optional ; zpm "install swagger-ui" Installation Docker ll n'y a rien de spécial, ce projet utilise le template intersystems-iris-dev-template git clone git@github.com:lscalese/openapi-suite.git cd openapi-suite docker-compose up -d Si vous obtenez une erreur lors du démarrage d'Iris, il peut s'agir d'un problème de permissions avec le fichier iris-main.log. Essayez ceci: touch iris-main.log && chmod 777 iris-main.log Note: ajouter les permissions RW pour l'utilisateur irisowner devrait être suffisant. Comment l'utiliser OpenAPI-Suite fournit une interface web pour "Générer et télécharger" ou "Générer et installer" le code. L'interface est disponible à l'adresse http://localhost:52796/openapisuite/ui/index.csp (*adapter avec votre numéro de port si nécessaire). C'est très simple, il suffit de remplir le formulaire : Nom du paquetage de l'application : c'est le package utilisé pour les classes générées. Il doit s'agir d'un nom de paquet inexistant. Sélectionnez ce que vous voulez générer : client HTTP, une Production ou serveur REST. Sélectionnez l'espace de nom où le code sera généré. Cela n'a d'importance si vous cliquez sur "Installer sur le serveur", sinon ce champs sera ignoré. Le nom de l'application Web est facultatif et n'est disponible que si vous sélectionnez "Server REST" pour la génération. Laissez ce champs vide si vous ne souhaitez pas créer d'application web liée à la classe de distribution REST générée. Le champ "OpenAPI specification" peut être une URL pointant vers la spécification ou un copier-coller de la spécification elle-même (dans ce cas, la spécification doit être au format JSON). Si vous cliquez sur le bouton "Télécharger uniquement", le code sera généré et téléchargé au formatXML, les classes seront ensuite supprimées du serveur. L'espace de nom utilisé pour stocker temporairement les classes générées est celui où OpenAPI-Suite est installé (par défaut IRISAPP si vous utilisez une installation docker). Cependant, si vous cliquez sur le bouton "Installer sur le serveur", le code sera généré et compilé et le serveur renverra un message JSON avec l'état de la génération et compilation du code. Par défaut, cette fonctionnalité est désactivée. Pour l'activer, il suffit d'ouvrir un terminal IRIS et de faire : Set ^openapisuite.config("web","enable-install-onserver") = 1 Explore the OpenAPI-suite REST API Le formulaire CSP utilise les services REST disponibles sur http://localhost:52796/openapisuite. Ouvrez swagger-ui http://localhost:52796/swagger-ui/index.html et explorez http://localhost:52796/openapisuite/_spec Il s'agit de la première étape vers la création d'une application front-end plus avancée avec le framework Angular plus tard. Generate code programmatically Bien sûr, il n'est pas obligatoire d'utiliser l'interface utilisateur, dans cette section nous verrons comment générer du code de manière programmatique et comment utiliser les services générés. Tous les extraits de code sont également disponibles dans la classe dc.openapi.suite.samples.PetStore. HTTP client Set features("simpleHttpClientOnly") = 1 Set sc = ##class(dc.openapi.client.Spec).generateApp("petstoreclient", "https://petstore3.swagger.io/api/v3/openapi.json", .features) Le premier argument est le package dans lequel les classes seront générées, assurez-vous donc de passer un nom de package valide. Le second argument peut être une URL pointant vers la spécification, un nom de fichier, un stream ou un %DynamicObject. "Features" est un array, seuls les clés suivantes sont disponibles: simpleHttpClientOnly: si égal à 1, seul un client HTTP simple sera généré sinon une production sera également générée (comportement par défaut). compile: Si égal à 0, le code généré ne sera pas compilé. Cela peut être utile si vous voulez générer du code uniquement pour l'exportation. Par défaut, compile est égal à 1. Vous trouverez ci-dessous un exemple d'utilisation du service "addPet" avec le client HTTP qui vient d'être généré: Set messageRequest = ##class(petstoreclient.requests.addPet).%New() Set messageRequest.%ContentType = "application/json" Do messageRequest.PetNewObject().%JSONImport({"id":456,"name":"Mittens","photoUrls":["https://static.wikia.nocookie.net/disney/images/c/cb/Profile_-_Mittens.jpg/revision/latest?cb=20200709180903"],"status":"available"}) Set httpClient = ##class(petstoreclient.HttpClient).%New("https://petstore3.swagger.io/api/v3","DefaultSSL") ; MessageResponse will be an instance of petstoreclient.responses.addPet Set sc = httpClient.addPet(messageRequest, .messageResponse) If $$$ISERR(sc) Do $SYSTEM.Status.DisplayError(sc) Quit sc Write !,"Http Status code : ", messageResponse.httpStatusCode,! Do messageResponse.Pet.%JSONExport() Click to show generated classes. Class petstoreclient.HttpClient Extends %RegisteredObject [ ProcedureBlock ] { Parameter SERVER = "https://petstore3.swagger.io/api/v3"; Parameter SSLCONFIGURATION = "DefaultSSL"; Property HttpRequest [ InitialExpression = {##class(%Net.HttpRequest).%New()} ]; Property SSLConfiguration As %String [ InitialExpression = {..#SSLCONFIGURATION} ]; Property Server As %String [ InitialExpression = {..#SERVER} ]; Property URLComponents [ MultiDimensional ]; Method %OnNew(Server As %String, SSLConfiguration As %String) As %Status { Set:$Data(Server) ..Server = Server Set:$Data(SSLConfiguration) ..SSLConfiguration = SSLConfiguration Quit ..InitializeHttpRequestObject() } Method InitializeHttpRequestObject() As %Status { Set ..HttpRequest = ##class(%Net.HttpRequest).%New() Do ##class(%Net.URLParser).Decompose(..Server, .components) Set:$Data(components("host"), host) ..HttpRequest.Server = host Set:$Data(components("port"), port) ..HttpRequest.Port = port Set:$$$LOWER($Get(components("scheme")))="https" ..HttpRequest.Https = $$$YES, ..HttpRequest.SSLConfiguration = ..SSLConfiguration Merge:$Data(components) ..URLComponents = components Quit $$$OK } /// Implement operationId : addPet /// post /pet Method addPet(requestMessage As petstoreclient.requests.addPet, Output responseMessage As petstoreclient.responses.addPet = {##class(petstoreclient.responses.addPet).%New()}) As %Status { Set sc = $$$OK $$$QuitOnError(requestMessage.LoadHttpRequestObject(..HttpRequest)) $$$QuitOnError(..HttpRequest.Send("POST", $Get(..URLComponents("path")) _ requestMessage.%URL)) $$$QuitOnError(responseMessage.LoadFromResponse(..HttpRequest.HttpResponse, "addPet")) Quit sc } ... } Class petstoreclient.requests.addPet Extends %RegisteredObject [ ProcedureBlock ] { Parameter METHOD = "post"; Parameter URL = "/pet"; Property %Consume As %String; Property %ContentType As %String; Property %URL As %String [ InitialExpression = {..#URL} ]; /// Use this property for body content with content-type = application/json.<br/> /// Use this property for body content with content-type = application/xml.<br/> /// Use this property for body content with content-type = application/x-www-form-urlencoded. Property Pet As petstoreclient.model.Pet; /// Load %Net.HttpRequest with this property object. Method LoadHttpRequestObject(ByRef httpRequest As %Net.HttpRequest) As %Status { Set sc = $$$OK Set httpRequest.ContentType = ..%ContentType Do httpRequest.SetHeader("accept", ..%Consume) If $Piece($$$LOWER(..%ContentType),";",1) = "application/json" Do ..Pet.%JSONExportToStream(httpRequest.EntityBody) If $Piece($$$LOWER(..%ContentType),";",1) = "application/xml" Do ..Pet.XMLExportToStream(httpRequest.EntityBody) If $Piece($$$LOWER(..%ContentType),";",1) = "application/x-www-form-urlencoded" { ; To implement. There is no code generation yet for this case. $$$ThrowStatus($$$ERROR($$$NotImplemented)) } Quit sc } } Class petstoreclient.responses.addPet Extends petstoreclient.responses.GenericResponse [ ProcedureBlock ] { /// http status code = 200 content-type = application/xml /// http status code = 200 content-type = application/json /// Property Pet As petstoreclient.model.Pet; /// Implement operationId : addPet /// post /pet Method LoadFromResponse(httpResponse As %Net.HttpResponse, caller As %String = "") As %Status { Set sc = $$$OK Do ##super(httpResponse, caller) If $$$LOWER($Piece(httpResponse.ContentType,";",1))="application/xml",httpResponse.StatusCode = "200" { $$$ThrowStatus($$$ERROR($$$NotImplemented)) } If $$$LOWER($Piece(httpResponse.ContentType,";",1))="application/json",httpResponse.StatusCode = "200" { Set ..Pet = ##class(petstoreclient.model.Pet).%New() Do ..Pet.%JSONImport(httpResponse.Data) Return sc } Quit sc } } Production Client Set sc = ##class(dc.openapi.client.Spec).generateApp("petstoreproduction", "https://petstore3.swagger.io/api/v3/openapi.json") Le premier argument est le nom du paquet, si vous testez la génération de code du client HTTP simple et la production du client, assurez-vous d'utiliser un nom de paquet différent. Le deuxième et le troisième suivent les mêmes règles que le client HTTP. Avant de tester, veuillez démarrer la production via le portail d'administration ou en utilisant cette commande dans un terminal IRIS: Do ##class(Ens.Director).StartProduction("petstoreproduction.Production") Vous trouverez ci-dessous un exemple d'utilisation du service "addPet", mais cette fois avec la production générée Set messageRequest = ##class(petstoreproduction.requests.addPet).%New() Set messageRequest.%ContentType = "application/json" Do messageRequest.PetNewObject().%JSONImport({"id":123,"name":"Kitty Galore","photoUrls":["https://www.tippett.com/wp-content/uploads/2017/01/ca2DC049.130.1264.jpg"],"status":"pending"}) ; MessageResponse will be an instance of petstoreclient.responses.addPet Set sc = ##class(petstoreproduction.Utils).invokeHostSync("petstoreproduction.bp.SyncProcess", messageRequest, "petstoreproduction.bs.ProxyService", , .messageResponse) Write !, "Take a look in visual trace (management portal)" If $$$ISERR(sc) Do $SYSTEM.Status.DisplayError(sc) Write !,"Http Status code : ", messageResponse.httpStatusCode,! Do messageResponse.Pet.%JSONExport() Maintenant, vous pouvez ouvrir la trace visuelle et voir les détails: Les classes générées dans les packages "model", "requests" et "responses" sont assez similaires au code généré pour un simple client HTTP. Les classes du package requests héritent de "Ens.Request" et les classes du package "responses" héritent de "Ens.Response". L'implémentation par défaut du "Business Operation" est très simple : Class petstoreproduction.bo.Operation Extends Ens.BusinessOperation [ ProcedureBlock ] { Parameter ADAPTER = "EnsLib.HTTP.OutboundAdapter"; Property Adapter As EnsLib.HTTP.OutboundAdapter; /// Implement operationId : addPet /// post /pet Method addPet(requestMessage As petstoreproduction.requests.addPet, Output responseMessage As petstoreproduction.responses.addPet) As %Status { Set sc = $$$OK, pHttpRequestIn = ##class(%Net.HttpRequest).%New(), responseMessage = ##class(petstoreproduction.responses.addPet).%New() $$$QuitOnError(requestMessage.LoadHttpRequestObject(pHttpRequestIn)) $$$QuitOnError(..Adapter.SendFormDataArray(.pHttpResponse, "post", pHttpRequestIn, , , ..Adapter.URL_requestMessage.%URL)) $$$QuitOnError(responseMessage.LoadFromResponse(pHttpResponse, "addPet")) Quit sc } ... } } Génération de serveur REST Set sc = ##class(dc.openapi.server.ServerAppGenerator).Generate("petstoreserver", "https://petstore3.swagger.io/api/v3/openapi.json", "/petstore/api") Le premier argument est le nom du package pour générer les classes. Le second suit les mêmes règles que le client HTTP. Le troisième argument n'est pas obligatoire, mais s'il est présent, une application web sera créée avec le nom donné (attention à bien donner un nom d'application web valide). La classe "petstoreserver.disp" (classe de dispatch %CSP.REST) ressemble à un code généré par ^%REST, effectue de nombreuses vérifications pour accepter ou rejeter la requête et appelle l'implémentation "ClassMethod" du service correspondant dans la classe "petstoreserver.impl". Pour les développeurs déjà familiarisé avec le code généré par ^%REST, la principale différence est l'argument passé à la méthode d'implémentation, il s'agit de l'objet "petstoreserver.requests". Exemple : Class petstoreserver.disp Extends %CSP.REST [ ProcedureBlock ] { Parameter CHARSET = "utf-8"; Parameter CONVERTINPUTSTREAM = 1; Parameter IgnoreWrites = 1; Parameter SpecificationClass = "petstoreserver.Spec"; /// Process request post /pet ClassMethod addPet() As %Status { Set sc = $$$OK Try{ Set acceptedMedia = $ListFromString("application/json,application/xml,application/x-www-form-urlencoded") If '$ListFind(acceptedMedia,$$$LOWER(%request.ContentType)) { Do ##class(%REST.Impl).%ReportRESTError(..#HTTP415UNSUPPORTEDMEDIATYPE,$$$ERROR($$$RESTContentType,%request.ContentType)) Quit } Do ##class(%REST.Impl).%SetContentType($Get(%request.CgiEnvs("HTTP_ACCEPT"))) If '##class(%REST.Impl).%CheckAccepts("application/xml,application/json") Do ##class(%REST.Impl).%ReportRESTError(..#HTTP406NOTACCEPTABLE,$$$ERROR($$$RESTBadAccepts)) Quit If '$isobject(%request.Content) Do ##class(%REST.Impl).%ReportRESTError(..#HTTP400BADREQUEST,$$$ERROR($$$RESTRequired,"body")) Quit Set requestMessage = ##class(petstoreserver.requests.addPet).%New() Do requestMessage.LoadFromRequest(%request) Set scValidateRequest = requestMessage.RequestValidate() If $$$ISERR(scValidateRequest) Do ##class(%REST.Impl).%ReportRESTError(..#HTTP400BADREQUEST,$$$ERROR(5001,"Invalid requestMessage object.")) Quit Set response = ##class(petstoreserver.impl).addPet(requestMessage) Do ##class(petstoreserver.impl).%WriteResponse(response) } Catch(ex) { Do ##class(%REST.Impl).%ReportRESTError(..#HTTP500INTERNALSERVERERROR,ex.AsStatus(),$parameter("petstoreserver.impl","ExposeServerExceptions")) } Quit sc } ... } Comme vous pouvez le voir, la classe de répartition appelle "LoadFromRequest" et "RequestValidate" avant d'appeler la méthode d'implémentation. Ces méthodes ont une implémentation par défaut, mais le générateur de code ne peut pas couvrir tous les cas. Actuellement, les cas les plus courants sont automatiquement traités comme les paramètres "query", "headers", "path" et body avec le type de contenu "application/json", "application/octet-stream", "application/xml" ou "multipart/form-data". Le développeur doit vérifier l'implémentation (par défaut, le générateur de code définit $$$ThrowStatus($$$ERROR($$$NotImplemented)) pour les cas non pris en charge). Example of request class : Class petstoreserver.requests.addPet Extends %RegisteredObject [ ProcedureBlock ] { Parameter METHOD = "post"; Parameter URL = "/pet"; Property %Consume As %String; Property %ContentType As %String; Property %URL As %String [ InitialExpression = {..#URL} ]; /// Use this property for body content with content-type = application/json.<br/> /// Use this property for body content with content-type = application/xml.<br/> /// Use this property for body content with content-type = application/x-www-form-urlencoded. Property Pet As petstoreserver.model.Pet; /// Load object properties from %CSP.Request object. Method LoadFromRequest(request As %CSP.Request = {%request}) As %Status { Set sc = $$$OK Set ..%ContentType = $Piece(request.ContentType, ";", 1) If ..%ContentType = "application/json"{ Do ..PetNewObject().%JSONImport(request.Content) } If ..%ContentType = "application/xml" { ; To implement. There is no code generation yet for this case. $$$ThrowStatus($$$ERROR($$$NotImplemented)) } If ..%ContentType = "application/x-www-form-urlencoded" { ; To implement. There is no code generation yet for this case. $$$ThrowStatus($$$ERROR($$$NotImplemented)) } Quit sc } /// Load object properties from %CSP.Request object. Method RequestValidate() As %Status { Set sc = $$$OK $$$QuitOnError(..%ValidateObject()) If ''$ListFind($ListFromString("application/json,application/xml,application/x-www-form-urlencoded"), ..%ContentType) { Quit:..Pet="" $$$ERROR(5659, "Pet") } If $IsObject(..Pet) $$$QuitOnError(..Pet.%ValidateObject()) Quit sc } } Comme pour l'utilisation de ^%REST, la classe "petstoreserver.impl" contient toutes les méthodes liées aux services que le développeur doit implémenter. Class petstoreserver.impl Extends %REST.Impl [ ProcedureBlock ] { Parameter ExposeServerExceptions = 1; /// Service implemntation for post /pet ClassMethod addPet(messageRequest As petstoreserver.requests.addPet) As %Status { ; Implement your service here. ; Return {} $$$ThrowStatus($$$ERROR($$$NotImplemented)) } ... } Short description of the generated packages Package name \ Class Name Type Description petstoreclient.model petstoreproduction.model Client-side and server-side Il contient tous les modèles. Ces classes étendent %JSON.Adaptor pour faciliter le chargement des objets à partir de JSON. Si une production est générée, ces classes étendent également %Persistent. petstoreclient.requests petstoreproduction.requests Client-side and server-side Objet utilisé pour initialiser facilement %Net.HttpRequest. Il existe une classe par opération définie dans la spécification.Dans le cas d'une production, ces classes étendent Ens.Request. Note: L'implémentation de cette classe est différente si elle est générée pour le côté serveur ou le côté client. Dans le cas du côté client, toutes les classes contienent la méthode "LoadHttpRequestObject" permettant de charger un "%Net.HttpRequest" à partir des propriétés de cette classe. Si les classes sont générées à des fins côté serveur, chaque classe contient une méthode "LoadFromRequest" permettant de charger l'instance à partir de l'objet "%request". petstoreclient.responses petstoreproduction.responses Client-side and server-side C'est l'opposé de "petstoreclient.requests". Elle permet de manipuler la réponse d'une requête %Net.HttpRequest. S'il s'agit d'une production, ces classes étendent Ens.Response. petstoreclient.HttpClient Client-side Contient toutes les méthodes pour exécuter les requêtes HTTP, il y a une méthode par opération définie dans la spécification OpenAPI. petstoreproduction. bo.Operation Client-side La classe Operation possède une méthode par opération définie dans la spécification OpenAPI. petstoreproduction.bp Client-side Deux business process par défaut sont définis : sync et async. petstoreproduction.bs Client-side Contient tous les services vides donc à implémenter par le développeur. petstoreproduction.Production Client-side Contient les paramètres de la production. petstoreserver.disp Server-side Classe de dispatch %CSP.REST. petstoreserver.Spec Server-side Cette classe contient la spécification OpenAPI dans un block XData. petstoreserver.impl Server-side It contains all empty methods related to operations defined in the OpenAPI specification. This is the class (extend %REST.Impl) where developers have to implement services. Elle contient toutes les méthodes vides liées aux opérations définies dans la spécification OpenAPI. Le développeur devra implémenter les méthodes. Statut du développement OpenAPI-Suite est encore un produit très jeune et a besoin d'être plus testé et amélioré. Le support d'OpenAPI 3 est partiel, l'implémentation pourrait être plus complète. Les tests ont été effectués avec la spécification publique https://petstore3.swagger.io/api/v3/openapi.json et deux autres relativement simples. Bien sûr, ce n'est pas suffisant pour couvrir tous les cas. Si vous avez des spécifications à partager, je serais heureux de les utiliser pour mes tests. Je pense que la base du projet est bonne et qu'il peut facilement évoluer, par exemple, être étendu pour supporter AsyncAPI. J'espère que vous apprécierez cette application et n'hésitez pas à laisser des commentaires. Merci pour votre attention.
Article
Lorenzo Scalese · Jan 9, 2023

Nouvelles extensions SDA de HealthShare

## **Création et utilisation des nouvelles extensions SDA pour le stockage d'éléments de données personnalisés** Dans HSCore 15.01, il existe une nouvelle façon de stocker les éléments de données personnalisés.  HealthShare peut désormais utiliser des extensions personnalisées pour de nombreux éléments SDA. Cet article a pour but de : 1. Montrer comment configurer votre système pour utiliser les extensions SDA. 2. Créer une nouvelle propriété d'extension SDA 3. Utiliser la nouvelle propriété d'extension SDA dans les transactions HL7 4. Interagir avec les nouvelles données 5. Montrer la nouvelle extension SDA utilisée dans une personnalisation du Rapport de résumé du patient (Patient Summary Report).       Note: Pour cet article, je me sers de build : HS-2016.1.1.108.0-hscore15.01_hsaa15_hspi15_hsviewer15.01_linkage15-b2136-win_x64 J'ai également créé le système en utilisant “d ##class(HS.Util.Installer).InstallBusDemo()”     ### Configurez votre système pour les extensions SDA   Cette section décrit comment configurer les extensions SDA pour un environnement HSCore 15.01.   **Création d'un nouvel espace de noms**   Dans le cadre des nouvelles extensions SDA, le nom de l'espace de noms personnalisé doit être **HSCUSTOM**. Vous pouvez l'ajouter en allant dans : Management Portal->System Administration->Configuration->System Configuration->Namespaces (Portail de gestion->Administration du système->Configuration->Configuration du système->Espaces de noms). Étapes pour créer un nouvel espace de noms : 1. Cliquez sur le bouton "Create New Namespace" (créer un nouvel espace de noms). 2. Entrez HSCUSTOM dans le champ “Name of the namespace” (Nom de l'espace de noms) (obligatoire). 3. Sélectionnez le bouton "Create New Database" (Créer une nouvelle base de données) Saisissez HSCUSTOM dans le champ "Enter the name of your database" (Entrez le nom de votre base de données). Pour le champ “Database directory” (Répertoire de la base de données) : Cliquez sur le bouton "Browse...". Créez un nouveau dossier/répertoire, j'ai nommé mon répertoire "HSCUSTOM", il se trouve dans le répertoire “mgr”. Cliquez sur le bouton “OK” Cliquez sur le bouton “Next” Acceptez les valeurs par défaut, et cliquez sur le bouton "Next". Créez une nouvelle ressource appelée %DB_HSCUSTOM et octroyez-lui des droits de lecture et d'écriture Cliquez sur le bouton "Finish" (terminer). 4.  De retour à l'écran "New Namespace" (nouvel espace de noms), cliquez sur le bouton "Save" (enregistrer). Cette procédure a pour effet de créer un nouvel espace de noms HSCUSTOM avec tous les mappings par défaut.   Exportation du paquet HS.Local Une des choses que nous devons faire est de copier les classes et le code de la base de données HSLIB vers la base de données HSCUSTOM. Vous pouvez le faire de plusieurs façons.  Je vais vous parler de la façon de le faire à partir de Studio ou de Terminal. Exportation à partir de Studio : 1. Connectez-vous à Studio 2. Changez l'espace de noms en espace de noms HSLIB (remarque : ceci peut être fait à partir de n'importe quel espace de noms qui a un paquetage HS mappé à HSLIB) 3. Allez dans le menu "Tools->Export" (outils - exportation). 4. Cliquez sur le bouton "Add" (ajouter) 5. Sélectionnez le dossier HS/Local, sélectionnez tout et cliquez sur le bouton "Open" (ouvrir). 6. Cela va tout sélectionner 7. Sélectionnez un fichier local ou distant pour exporter ces classes * Dans cet exemple, j'ai nommé le fichier "HSLocal.xml"       8.  Cliquez sur le bouton "OK".  Exportation à partir d'une session Terminal : 1. Connectez-vous à Terminal 2. Changez l'espace de noms en espace de noms HSLIB ( remarque : ceci peut être fait à partir de n'importe quel espace de noms qui a un paquetage HS mappé à HSLIB ) 3. HSLIB>d $system.OBJ.Export("HS.Local.*.cls","C:\Intersystems\Export\HSLocal.xml")   Ajoutez une nouvelle cartographie de paquet Le paquet "HS.Local" doit être référencé à partir du nouvel espace de noms HSCUSTOM.  Lorsque HSCUSTOM sera créé, "HS" sera mappé vers HSLIB.  Vous devrez ajouter "HS.Local" à l'espace de noms HSCUSTOM, car il est actuellement pointé vers HSLIB. Vous pouvez le faire manuellement via le Management Portal (Portail de gestion) ou de manière programmatique. Management Portal (Portail de gestion) : 1. Allez dans le Portail de gestion->Administration du système->Configuration->Configuration du système->Espaces de noms. 2. Recherchez HSCUSTOM dans la colonne des espaces de noms et sélectionnez le lien "Package Mapping". 3. Cliquez sur le bouton "New" 4. Sélectionnez **HSCUSTOM** dans le menu déroulant "Pakage Database Location" (location de la base de données de paquet) 5. Sélectionnez le bouton radio "Specify a new package” (Spécifier un nouveau paquet). 6. Entrez HS.Local dans le champ "Packaga Name" (Nom du paquet)          7. Cliquez sur le bouton "OK" Par programmation : Vous pouvez créer du code pour ajouter ce mappage à un espace de noms. 1. Déplacez-vous vers l'espace de noms %SYS * HSCUSTOM> ZN “%SYS” 2. Définissez la propriété de la base de données * %SYS> set tProperties("Database")="HSCUSTOM" 3. Créez le mappage * %SYS>w ##class(Config.MapPackages).Create("HSCUSTOM","HS.Local",.tProperties) _**Remarque:**_  Vous devrez le faire pour chaque espace de noms et instance qui est un espace de noms HealthShare, à l'exception des espaces de noms Library.   Il est important d'avoir ces mappages pour que les autres espaces de noms puissent accéder au code HSCUSTOM à utiliser dans leur traitement, comme les applications telles que Patient Index et Health Insight.   Importation du paquet HS.Local Maintenant que les paquets HS.Local pointent vers HSCUSTOM, vous pouvez déplacer les classes que nous avons exportées précédemment dans l'espace de noms HSCUSTOM. Vous pouvez le faire de plusieurs façons.  Je vais vous parler de la façon de le faire à partir de Studio ou de Terminal. Importation à partir de Studio: 1. Connectez-vous à Studio 2. Changez l'espace de noms en espace de noms HSCUSTOM. 3. Allez dans le menu Tools->Import Local (Outils->Importer Local). 4. Sélectionnez le fichier que vous avez exporté 5. Appuyez sur le bouton "Open" (ouvrir). 6. Vous devriez voir toutes les classes cochées et l'option "Compile Imported Items" (Compiler les éléments importés) cochée.      7.  Cliquez sur le bouton "OK".  Importation à partir d'une session Terminal : 1. Connectez-vous à Terminal 2. Changer l'espace de noms en HSCUSTOM 3. HSLIB>d $system.OBJ.Load("C:\Intersystems\Export\HSLocal.xml",”ck”)   Résumé Nous avons maintenant l'infrastructure pour les nouvelles extensions SDA personnalisées de HSCore 15.01.  Nous avons les classes HS.Local définies dans une nouvelle base de données HSCUSTOM et nous avons tous les espaces de noms qui pointent vers la localisation appropriée. Si vous avez plus d'une instance de cache, l'espace de noms HSCUSTOM et les mappages HS.Local doivent se trouver sur chaque instance qui exécute HealthShare.     ### Création d'une nouvelle extension SDA personnalisée Maintenant que nous avons les éléments nécessaires, nous allons créer une nouvelle propriété personnalisée. Nous allons commencer par créer une propriété personnalisée pour le Patient SDA (SDA du patient). En regardant les annotations HL7, il semble que "Veterans Military Status" (statut militaire des vétérans), qui est le PID, pièce 27, n'est pas utilisé dans SDA, alors essayons de créer ceci comme notre extension SDA personnalisée. Comme la pièce PID 27 est un champ d'entrée codé, nous allons montrer que les nouvelles extensions SDA personnalisées sont plus que la paire nom/valeur précédente, il s'agit maintenant d'un type de données plus complexe.  Dans cet exemple, nous créons un type de propriété personnalisé.   Edit HS.Local.SDA3.PatientExtension.cls Nous devons ajouter la nouvelle propriété à HS.Local.SDA3.PatientExtension.cls 1. Connectez-vous à Studio 2. Changez l'espace de noms en HSCUSTOM 3. Modifiez HS.Local.SDA3.PatientExtension.cls 4. Ajouter une classe personnalisée "Custom Class" * Cette classe représente un type de données complexe qui aura : * Champ de code * Description du Champ ![](data:image/png;base64,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)     5.   Ajoutez la propriété VeteransMilitaryStatus * Propriété VeteransMilitaryStatus En tant que CUSTOM.SDA3.CodeTableDetail.VeteransMilitaryStatus; ![](data:image/png;base64,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)       6.  Compilez HS.Local.SDA3.PatientExtension.cls       7.  Compilez la classe HS.SDA3.Patient       8.  Compilez la classe HS.Registry.Patient Cette propriété est maintenant disponible pour être ajoutée/modifiée/supprimée à partir du streamlet SDA. Utiliser la nouvelle propriété d'extension SDA dans les transactions HL7 Avant de pouvoir utiliser la propriété d'extension SDA, nous devons créer une nouvelle classe personnalisée qui étendra la classe HS.Gateway.HL7.HL7ToSDA3.  Ce code sera exécuté sur la passerelle EDGE. Voici un exemple de code de la nouvelle classe personnalisée : Class CUSTOM.Gateway.HL7.HL7ToSDA3 Extends HS.Gateway.HL7.HL7ToSDA3 [ Not ProcedureBlock ] { /// Méthode de rappel pour le traitement personnalisé du streamlet Patient ClassMethod OnPatient() { do ..write(cr_"<Extension>") set tVMSCode = $$$xml($g(^||d(s,27,1))) set tVMSDescription = $$$xml($g(^||d(s,27,2))) if tVMSCode'="" { do ..write(cr_"<VeteransMilitaryStatus>") do ..write(cr_""_tVMSCode_"") do ..write(cr_"<Description>"_tVMSDescription_"</Description>") do ..write(cr_"</VeteransMilitaryStatus>") } do ..write(cr_"</Extension>") Quit } Mise à jour de Production de Edge Gateway Ensemble. Modifiez l'opération : HS.Gateway.HL7.InboundProcess et changez le paramètre "HL7ToSDA3Class" pour utiliser la nouvelle classe que nous venons de créer. Cliquez sur le bouton "Apply" (appliquer) pour enregistrer les modifications.   Nous utilisons le message HL7 suivant :  (Notez que la pièce PID 27 a une valeur de "V^Veteran") MSH|^~\&||HC6|||||ADT^A04|||2.5 EVN|A04|20160711094500 PID|||STM123^^^HC6^MR||Bolton^George||19271014|M|||1 Memorial Drive^^Cambridge^MA^02142||||||||028345081||||||||V^Veteran   Nous traitons maintenant ce HL7 sur une passerelle Edge Gateway. Maintenant si nous regardons la trace, nous pouvons voir nos données dans le <Patient> SDA.![](data:image/png;base64,/9j/4AAQSkZJRgABAQEAYABgAAD/2wBDAAgGBgcGBQgHBwcJCQgKDBQNDAsLDBkSEw8UHRofHh0aHBwgJC4nICIsIxwcKDcpLDAxNDQ0Hyc5PTgyPC4zNDL/2wBDAQkJCQwLDBgNDRgyIRwhMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjL/wAARCAD5Al0DASIAAhEBAxEB/8QAHwAAAQUBAQEBAQEAAAAAAAAAAAECAwQFBgcICQoL/8QAtRAAAgEDAwIEAwUFBAQAAAF9AQIDAAQRBRIhMUEGE1FhByJxFDKBkaEII0KxwRVS0fAkM2JyggkKFhcYGRolJicoKSo0NTY3ODk6Q0RFRkdISUpTVFVWV1hZWmNkZWZnaGlqc3R1dnd4eXqDhIWGh4iJipKTlJWWl5iZmqKjpKWmp6ipqrKztLW2t7i5usLDxMXGx8jJytLT1NXW19jZ2uHi4+Tl5ufo6erx8vP09fb3+Pn6/8QAHwEAAwEBAQEBAQEBAQAAAAAAAAECAwQFBgcICQoL/8QAtREAAgECBAQDBAcFBAQAAQJ3AAECAxEEBSExBhJBUQdhcRMiMoEIFEKRobHBCSMzUvAVYnLRChYkNOEl8RcYGRomJygpKjU2Nzg5OkNERUZHSElKU1RVVldYWVpjZGVmZ2hpanN0dXZ3eHl6goOEhYaHiImKkpOUlZaXmJmaoqOkpaanqKmqsrO0tba3uLm6wsPExcbHyMnK0tPU1dbX2Nna4uPk5ebn6Onq8vP09fb3+Pn6/9oADAMBAAIRAxEAPwD1q78RzWt99lCNJK5laOOC0eVgiPtJJDDuR+dM/wCEkvf+fC+/8Fkn/wAXVP8A5nWH/r0u/wD0elblJsDP/wCEkvf+fC+/8Fkn/wAXR/wkl7/z4X3/AILJP/i60KSlcZQ/4SS9/wCfC+/8Fkn/AMXR/wAJJe/8+F9/4LJP/i60KKLgZ/8Awkl7/wA+F9/4LJP/AIuj/hJL3/nwvv8AwWSf/F1oUUXAz/8AhJL3/nwvv/BZJ/8AF0f8JJe/8+F9/wCCyT/4utCii4Gf/wAJJe/8+F9/4LJP/i6P+Ekvf+fC+/8ABZJ/8XWhRRcDOPiW9VSTY3wAGSf7Mk/+LqK08Xy39nDd2lteTW8yh45F0yTDKe4+etOb/US/7jfyrD8D/wDIi6H/ANeiU7gX/wDhJL3/AJ8L7/wWSf8AxdH/AAkl7/z4X3/gsk/+LrQopXAz/wDhJL3/AJ8L7/wWSf8AxdH/AAkl7/z4X3/gsk/+LrQoouBn/wDCSXv/AD4X3/gsk/8Ai6P+Ekvf+fC+/wDBZJ/8XWhRRcDP/wCEkvf+fC+/8Fkn/wAXR/wkl7/z4X3/AILJP/i60KKLgZ//AAkl7/z4X3/gsk/+Lo/4SS9/58L7/wAFkn/xdaFFFwM//hJL3/nwvv8AwWSf/F0f8JJe/wDPhff+CyT/AOLrQoouBn/8JJe/8+F9/wCCyT/4uj/hJL3/AJ8L7/wWSf8AxdaFFFwM/wD4SS9/58L7/wAFkn/xdH/CSXv/AD4X3/gsk/8Ai60KKLgZ/wDwkl7/AM+F9/4LJP8A4uj/AISS9/58L7/wWSf/ABdaFFFwM/8A4SS9/wCfC+/8Fkn/AMXR/wAJJe/8+F9/4LJP/i60KKLgZ/8Awkl7/wA+F9/4LJP/AIuj/hJL3/nwvv8AwWSf/F1oUUXAz/8AhJL3/nwvv/BZJ/8AF0f8JJe/8+F9/wCCyT/4utCii4Gf/wAJJe/8+F9/4LJP/i6P+Ekvf+fC+/8ABZJ/8XWhRRcDP/4SS9/58L7/AMFkn/xdH/CSXv8Az4X3/gsk/wDi60KKLgZ//CSXv/Phff8Agsk/+Lo/4SS9/wCfC+/8Fkn/AMXWhRRcDP8A+Ekvf+fC+/8ABZJ/8XR/wkl7/wA+F9/4LJP/AIutCii4Gf8A8JJe/wDPhff+CyT/AOLo/wCEkvf+fC+/8Fkn/wAXWhRRcDP/AOEkvf8Anwvv/BZJ/wDF0f8ACSXv/Phff+CyT/4utCii4Gf/AMJJe/8APhff+CyT/wCLo/4SS9/58L7/AMFkn/xdaFFFwM//AISS9/58L7/wWSf/ABdH/CSXv/Phff8Agsk/+LrQoouBn/8ACSXv/Phff+CyT/4uj/hJL3/nwvv/AAWSf/F1oUUXAz/+Ekvf+fC+/wDBZJ/8XR/wkl7/AM+F9/4LJP8A4utCii4Gf/wkl7/z4X3/AILJP/i6P+Ekvf8Anwvv/BZJ/wDF1oUUXAz/APhJL3/nwvv/AAWSf/F0f8JJe/8APhff+CyT/wCLrQoouBn/APCSXv8Az4X3/gsk/wDi6P8AhJL3/nwvv/BZJ/8AF1oUUXAz/wDhJL3/AJ8L7/wWSf8AxdH/AAkl7/z4X3/gsk/+LrQoouBn/wDCSXv/AD4X3/gsk/8Ai6P+Ekvf+fC+/wDBZJ/8XWhRRcDP/wCEkvf+fC+/8Fkn/wAXR/wkl7/z4X3/AILJP/i60KKLgZ//AAkl7/z4X3/gsk/+Lo/4SS9/58L7/wAFkn/xdaFFFwM//hJL3/nwvv8AwWSf/F0f8JJe/wDPhff+CyT/AOLrQoouBn/8JJe/8+F9/wCCyT/4uj/hJL3/AJ8L7/wWSf8AxdaFFFwMP/mdYf8Ar0u//R6VX1bxja6R4hh0eWDEksSyiWaZYUYFsbULcOw5JGRx0yabf3yaf4tgleGaXdb3MapCAWLG4XAwSPSodTt7HWJt9/omrzR7kdrcyL5LMhypKb8ZH6960VOUtUhXHj4h+GmKiO8nmZ3RYkitndpd+7aVAGSDsbmmXHxA0jFl9kmDm4mRX89Hj8pGdkJb5ThgUb5TjoeRVW10fSLOSzaDQdXUWcwmto/NTZEw3YwN/T5jwc0HSdCknun/AOEb1AvcXiXswDpzKuccb/u/Mx29Dk0/YT7BdGjN480C3tIrqae5jhmjaaFntmHmxAZMi+qgd/cetaGk+I9M1ye4j06d5xAzI8gQhNwOCAe/Wuc/sbSBHbJ/Yutj7IhS2JuFJhQjBRMvwuO3sPSrOlppmn6hPqFnpGpy3TRi2kmeVJCqA7tn3+OTnnnpR7CfYLo66isn+3H/AOgRqH5R/wDxdINeJXcNKvyB1I8vH/odHsKnYLo16KyRrjnppGoH6CP/AOLo/tx84/sjUMntiP8A+Lo9hU7BdGtRWT/bj/8AQH1H/vmP/wCLpf7cf/oD6j/3zH/8XR7Cp2C6NOb/AFEv+438q5jw1qNvpHww07UrssLe104TSFRk7VGeB61euvEKw2k0k+l6hFEEO6QohC5GMnDdPesLwrqVvc+A7DTpdJvLyH7ILecBE2NkcrywzwaXsam1gui+vjqzgu0s9TsrqyupYoJYIk/fmVZSQoG3o24YI/Wnr8QvDD+UV1EkSAHPlN8hIYhW9GOxuP8AEViX3h+0nS2Npp2swTxXFu5uJJFkl8qHO2JWL5Uc8e/PNWIdA0K2XbbeHNVgUxLE4jkUeaozjed+SeTz3zT9hU7BdGunjjRJLe1uA14kN2yrBJLauiyM33QMjv2PTpWfY/ESxu/sUs9tJaQXdp56JKGMzOZRGqKoGGyTwc59u9QyaJoUk1nM+garmyjhji/fLgLCcx5+fnB/PvTBoOgosI/sHVsQp5duTOuYR5gkGw7+CGAINHsJ9gujXHjzw800cQuptz7Qx8hsRFpDEA5x8p3grg960tG17T9ft3uNNkklgU4EjRlVbkjjPXkGuZGg6Eqso8O6qCwTefOXcxSUyhid/LbyST3rQ0tLLRp7uez0PVfPuypnlkZHZ9owufn7Cj2FTsF0dRRWV/bj/wDQH1H8o/8A4uk/t1v+gRqH5R//ABdHsKnYLo1qKyRrzEkDSb8kdQBHx/4/S/25J/0B9R/75j/+Lo9hU7BdGrRWV/bj/wDQH1H/AL5j/wDi6jPiSBN4ksb5JEKr5RjXcS33cfNzn+h9KTo1F0C6Nmisr+25P+gPqP5R/wDxdH9uP/0B9R/75j/+Lp+wqdgujVorJ/tx/wDoEah+Uf8A8XR/brFto0nUCeuAI8/+h0ewqdgujWorJGusemk6gccHAj/+Lpf7cf8A6BGo/lH/APF0ewqdgujVorK/tx/+gPqP/fMf/wAXSHXWUZbSdQA9SIx/7PR7Cp2C6Naisg6+R10q/GOufL/+Lp39uP8A9AfUfyj/APi6PYVOwXRq0Vlf24//AEB9R/75j/8Ai6ZJ4gEMbSTaVqMca8s5RCFHqcN0o9hU7BdGxRWT/wAJAjyOkOnX03lnazIibQcZxktz1FL/AG4//QH1H/vmP/4ukqNRq9gujVorK/tx/wDoD6j/AN8x/wDxdH9uP/0B9R/75j/+Lp+wqdgujVorK/tx/wDoD6j/AN8x/wDxdH9uP/0B9R/75j/+Lo9hU7BdGrRWV/bj/wDQH1H/AL5j/wDi6P7cf/oD6j/3zH/8XR7Cp2C6NWisr+3H/wCgPqP/AHzH/wDF0f24/wD0B9R/75j/APi6PYVOwXRq0Vlf24//AEB9R/75j/8Ai6P7cf8A6A+o/lH/APF0ewqdgujVorGbxLbx+YJbG+jkTb+7Ma7m3HAxhucnipP7cf8A6A+o/lH/APF0lRqPoF0atFZX9uP/ANAfUf8AvmP/AOLo/tx/+gPqP/fMf/xdP2FTsF0atFZX9uP/ANAfUf8AvmP/AOLo/tx/+gPqP/fMf/xdHsKnYLo1aKyv7cf/AKA+o/8AfMf/AMXR/bj/APQH1H/vmP8A+Lo9hU7BdGrRWV/bj/8AQH1H/vmP/wCLo/tx/wDoD6j/AN8x/wDxdHsKnYLo1aKyv7cf/oD6j/3zH/8AF0f24/8A0B9R/wC+Y/8A4uj2FTsF0atFZD+IPKjaSXStRSNRlm2IcD1wGo/4SCNpHSDT76fZjcUVMDIyBktzxS9jUvawXRr0Vlf24/8A0B9R/KP/AOLo/tx/+gPqP/fMf/xdP2FTsF0atFZX9uP/ANAfUf8AvmP/AOLo/tx/+gPqP/fMf/xdHsKnYLo1aKyv7cf/AKA+o/8AfMf/AMXR/bj/APQH1H/vmP8A+Lo9hU7BdGrRWV/bj/8AQH1H/vmP/wCLo/tx/wDoD6j/AN8x/wDxdHsKnYLo1aKyv7cf/oD6j/3zH/8AF0f24/8A0B9R/wC+Y/8A4uj2FTsF0atFZX9uP/0B9R/KP/4uli12OSV4msryGRAGKyqgyDnBB3YPQ0nRqJXaC6MfWDjxlphP/Tz/AOja2t6+o/Oub8VCdNctrmGKSQW6zvJsUkqvnAZwO3NZv9v+7/8AfJ/wr1MHQdSldMzk7M7beuRyPzri18JapCLsWmsR2rXF3Nc+bCWViH3lVbABwrMp6kHb2pv9v+7/APfJ/wAKP7f93/75P+FdLwLe7FzEkega1b6guoXet2kwjWZF+0O42CRQOvTgjgYH1NV9H8PajYS3VvBr9hP9piPmwhmYNuVEMm0HCkbTgjru56VS1u6XWtN+xvK8amaOQt5ZP3GDY/HFYlxp1y93PcW+sTwGQ4ASNgQmc7dw/PjvWMsFNPTX5j5jqtK8JXWm6xaXUWrWt29qiJJFMxzjyRGfujgcZGR9asv4UlTR7m182wX7XcG7u2XdEoYMCgTHAVVUAgg5/GuJNhcpNI0GtPbeY250iRgXYklmLH5gSSDgcZHSpmtLqSEqNeviZN5m3NIwYknZgZ+UDPIHXFJYOp2DmR1Vt4U8QIIo5fFLi2AjDLajyPlG3cECjC5w2CP73SpP+EZ14SpL/wAJK0jISoDlgWi3LtUsOVYqpDMOSTXJm31HzYWGuXWI5C7Y8wGT5sjPOBgcAAYx1rpzr/PV/wDvg/4VcMDJ76fMOY2dA0zVdOuruTUtZN9HKAIoy5Ij5J7j0OM55xk81u719R+dcT/b/u//AHyf8KP7f93/AO+T/hWqwTXUXMdNrzD/AIR3UuR/x6yd/wDZNZPw+YDwVY5I6t396yLzVJr6yntLeOWWaeNo0RUOSSMVT0C9uNI0O1tbmKWF9gdQ8ZGVbkEcVLwz51C/f9Auek719R+dKHUMDkcH1rh/+EgH95v++TR/wkA/vN/3yav6nLuLmKx8CzyswTV4J3QyHyy5xEzTtKvAB4GQCrAg4NWZfDWo6kljcRavpk3kQC2VxBmMBXUl1XkBjtZTjGOMEYrmrm3uJr69uYNSa1NzP5/7lHU5EYRdxBG7GM/Umq8NhfW6bIddmij3MwjjWQKCSx34z94bun3eORXN9Tn2/ErmR1f/AAiPiN5zdS+JFN2I5Y4pA0n7pX2HAyeeUbr03eop/wDY2tJcrC3jNRs2eZFJKS/PXng87Vx/wP1rk3t9SZ2Z/EVyMDEeBIMEZwx55PP04qu2iRXd552oam1wrHEnyNvkGCPvNnB57Yx2xR9SqdF+IcyO2tdE8WWt9aGTXRcIPNkmLO3lZAHlJgndjcWzj+EYzUNx4Gv5bg3C6rCZDcST+W4bywWmSXAxyPuEHr1FcybW/AHleIr1GIYs5aRiCS33QTjGCBg5xt4xQ9vqLSyOuu3EaugVYkaXan0ySSCeevc9qPqc+34hzI9IsNH8nxBe6zdfZRcTxrDGtuuAiDlsk8sxbv6ACtjevqPzrzPSLy60+a4e41GS5EoXaCr/ACkdcZzgH0rV/wCEgH95v++TW0MHK3YTkdvvX1H51hXzD/hIYDkcPBz/AN91i/8ACQD+83/fJpfMvLq3n1KK3meKGSI7ljOSFJ3EDqQNwziieGcFdvt+YJ3O23r6j86N6+o/OuJ/t/3f/vk/4Uf2/wC7/wDfJ/wq/qcu4cxteI9Ek1xbMQ3v2U27yMWXqd0bJgfi3PtWNpvg/UdNkl8jUrWNJ7f7PKwVmfkKpdGOChAUYXJXPPFJ/b/u/wD3yf8ACj+3/d/++T/hUPANu9w5iG38K63bLFb22v2FtEJI1kS3UozhVUMpxjJYBuGyRu61JbeEvEVnbQw23ibyo7e2SKGNWcoGXuwPUHGSMjGcdK5i60wTXUt3DevFcPevdgmJiqlkK9P73vnoKYtvqUarnxFOZVBHziQrtO/jG7/aXnr8orH6lUvt+I+ZHYjw34kEkIbxRI0SSkyHc4do+AFHOM4ByT3OaiHhzX9R0PT4dVv7aS4hnW6mEuSSw3DZnBXbtK9jzurjPI1wyxxrrty0QRg1xvfIyDhAueRyOTzx1rb0eWfTZpZbjUbu7d0VEEjOVQD2PGSe/WnHBzbs1YOY07vwHPeWf2R72yXN49ybwRFp3DBvkbIwQu4AeyjgV2WmxPaaXaW0zRGSGFY2MZO04GON3P51yX9v+7/98n/Cj+3/AHf/AL5P+FbLAtbMXMdtvX1H51V1Rx/ZF7yP+PeTv/smuT/t/wB3/wC+T/hUc+rTXdvJbQRzSyzKY0RI2JYngdqr6o1q2LmOo0VgLWfJH+vb+QrS3r6j864y5vJtIvbi2nR4y0nmJ8pwykDkHv0NR/8ACQD+83/fJqKeFc4KSe6G5anatNEhw0qKSCcFgOB1P0phvbQZzdW4x1zKvHp3rzPWlh1u7hnlndBFbzQgKhyfM2559MDBHfNYyeH7dLWVftSvcOUCu0B2KqtGxGBySTGOSeATiplhqqeiC6PaFmic4SVGJGcKwPFJ9oh8/wAjzU87Zv8AL3DdtzjOPTPevGptOuo5I3s7/ZhjH8oePETPJIclTkkM44GOlOlsNQMksya/KJ3j8v7Q6SeZjJI5zgYyOnHFT9Xq9h3R7L5sZUsHTaM5bcMDHWhZo3OFkRjgHhgeD0P0rxk6dOI5IYtbkjgczFkUPhjISSWGcHrjHTHvStpUkDltO1WS2LQxRM3ltuAj3YwRjjkYHoKf1ar2FdHs+4eoo3D1FeOyW2plWWLxDcLvYOWKyEhgWORzx1Ax0+WooItZe4aSbWrmJFlVlUSOfMxnLHrtPsOPaj6vU7Duj2fcPUfnSb19R+dedaTqDaZpsdtJdz3MgyXmlLsWY9eucD27Ve/4SAf3m/75NarCSa1FzG3qTD+3LUgjgwf+jTW5vX1H51xIku72KfUIbeaSOAxfMqHna+5sDqcA5OKP7f8Ad/8Avg/4VMcM5NpPZ/ogudtvX1H50b19R+dcT/b/ALv/AN8n/Cj+3/d/++T/AIVf1OXcOY7bevqPzo3r6j864n+3/d/++T/hQNfwQcvx/sn/AAo+py7hzHZ/aYMgefFk9BvHNC3MDttSeJmxnargnHrivHE0lbe8guk1HM1u5kjWSJioYytJ07D5iOO/NSaJpiaPq0N7Fe+YsUfl4EJyfkVfwHy5rFYarpoO6PYt6+o/OjevqPzrif7f93/74P8AhR/b/u//AHyf8K2+py7i5jtt6+o/OjevqPzrif7f93/75P8AhR/b/u//AHyf8KPqcu4cx198y/2fc8j/AFL9/Y1R0BgLWcEj/Wj/ANAWubl1mS4hkghSWSWRSiIqMSxIwAOKlnurjRrua3uI3j3bXQ7SQw2AHBHuCKh4ZqShfdP9AudpvX1H501riFGVXmjRnOFDOAWPt61xX/CQD+83/fJrC1oNqt/DdRXhtZI4/L80Rsz7c5wAeOuOeoxwacsJNLTUOY9R+0Q+f5Hmx+ds8zy9w3bc4zj0zxmpNw9RXjS2OofbReSa5KZ9uwuqyBgvXaDnpn1z14qRbW/KBJdduWUlMsjSoyqCcqpDfxDqTzms1h6vb8h3R6950Wzf5qbBwW3DA/GlWVGBKurYJBwc4PpXi0+mXlylzazavKLNkwmTId2Sxwy5xjJXnqcVYe3vm87y/EE0IkaRgEEn7vdu4HPI+Ycnn5RgihYaq+gXR7AZolba0iBuOCwzz0p+4eorxttMke4ad9XmZw0ZTejsAEkZ1DZOWxuwOeKVLXVAgz4kuhIrZRkWTC8gngnB4yOemaPq1Xt+QXR65Hf2k1zJbR3UDzx/fjVwWXHXI7dRU+4eo/OvFE0i5ikmmi1mSGWb77RLICwyCQWJLds/Wui0i9fTYZhNfXN1LNKXZ5S5AHYAHpgenWnDDVG7NWFzI9I3r6j86xdVIa/XBB/cr/6E1YX/AAkA/vN/3yav6Ql5rk9xLBCzxxoib2G0E5YkDPXt+dOrh3Tg5Ngnc62w/wCRvb/r2uP/AEeK6auZ0/8A5G9v+va4/wDR4rpq8I1CiiigAoqK5mW2tZbhwSsSF2A6kAZrG/4SaI2ok+xXHmKkEksZwNiynCnd0bHfBNACagP9Pl/D+VXNH/1Uv+8P5V5jb3TavY219Ne6tcXtzGJrkxX/ANnhiZuRGiqp6DA/qTW5ZabYzaHeXya/rdi1tnzluNQYrGcZBOOqkdCOtdMq1Bt4fnXNHddhulUUVUa0Z6LRXk6yTrDNJNq+pL5SoWH9sN8uWjBYtgjafMO312kZqwgaS21CRdS8RBraZowWv22RqGKh5Wz0OCeBkD86z5af834E6nqFFecaZpl5fSozanrklu0aljb6g+5GY4BJY4KkAtxyAV65qtpwe/1DSrb+0fEGy9MyyTxajI8aMm/AVwuCfk5zjGR60ctP+b8A1PSrv/jzn/65t/Ks62ybSDPP7pf5CuDkUm5aD+19WdFnlgkVdWZi4VlXC8Ab8NuIPQA/WoLyFrfzGs9U1K5giWL5E1U+bGWXPllc/M33cFeOehpctP8Am/ANT0j8P0o/D9K89mttt5LGmsarDGLZpYjNqRYsysQynawC9OM4yT17VDegxSWaWmp6tM01sZJFfVGXZJtJ284wBgZPvzt6k5af834Bqekfh+lH4fpXD6JpkerX6W48QaowNt5six6g6yRtkADaScg8nI9ueRXQP4LhSNm/tzxC20E4W/Yk/Tinyw/m/ANTRuv+PduO47VQQHev1Fc7c2NhNot5eWGta/Obfyxtlv2UBmIGGGMgjPIrENpeQsrHVdQHzhdyXjkqScAgNkHHXBGKuWIw2FnGhWnaUnZKz8v8+ppChVqRc4K6R6l3orM8OX82qeGdMv7nb59xbJJIVGAWI5OK06wlFxbT6EBRRRSAKdb86hH/ANcn/mtNp1v/AMhCP/rk/wDNaANGiiigAooooAKwdT/4/n+grerB1P8A4/m+grfD/GJ7F3R/9RJ/v/0rRrO0f/j3k/3/AOlaNRV+NgtgooorMYUnaloPSgDMH3QPaikHQUtABRRRQAVFc/8AHs/0qWorn/j3f6VUPiQGcOo+ta1ZI6j61rV0YnoTEKKKK5Sgo/CiigBqf8hK1/3ZP5CtWspP+Qja/wC7J/IVq0AFFFFABRRRQBgaj/x/yfh/Krmjfcm+oqnqP/H/ACfh/Krmjfcm+orsn/B+4lbmnRRRXGUFFFFABWVEf3SitWsmL/VigB9FFFABRRRQBHP/AKh/pWb2rSn/ANQ/0rN7V2Yb4WSzWHQfSigdBRXGUFFFFABRRRQBmaf/AMje3/Xtcf8Ao8V0F2xWznZSQRGxBHbiuf0//kb2/wCva4/9Hit+8/48bj/rm38qAItJYto9kzEsxgQkk5J+UVcqlo//ACBbH/r3T/0EVdoAiuYFubWW3ckLKhQkdQCMVit4Yh+yqgu5/O8uGKWXP+sSM5Ubfurz3AB5Nb9B6UAeMaDElp4esBFcWshaICaFrmOOSCUcOjK7DuK2ja6VbaHe31zdadcapJtMcC3oIVQQApw3J5JJ9/auh1fR9MuNUmln02zlkYjLvArE8dyRU+keHdEeOXdo2nHDDrap6fStVg8PSrTxsE1Oe7Xnv1NZYipOmqT2RxcN1oMseoW1y1naXYP+ipFKZAQHYAbidp4C8nHXNN1S4sLdmTT/ALBciOFFnczEZmD4dgC43KVBAx6g9BXo/wDwjehf9AXTf/AVP8KP+Ea0L/oCab/4Cp/hW31h/wA8vvMLHmP/AAk2iCHP9j2yNtc7DqDkgjGAcHjj8yOKVvE3h6MyBdJtwqTBVZryRBJkgbVHZuTycLgda9N/4RvQ/wDoC6d/4Cp/hR/wjehf9AXTv/AVP8KHiZfzy+8LHJeH38J6rpn2m7hsbKbzGXyjfnPHf7w+laY0/wAEA7vP0/d6/buf/Qq1brw3of2SY/2Lp2RG3P2VPT6Vn23h/RDaQE6Np5JjXJ+zJ6D2o+sy/nl94WIzp/gg5zPp5zwc33X/AMeoNh4Jbrcaefrff/ZVY/4R7RP+gNp3/gKn+FH/AAj2if8AQG07/wABU/wo+sy/nl94WK4sPBIORcaeCBjIvu3/AH1Q9n4O8ttl3Yb8Hbu1A4z7/PVj/hHtE/6A2nf+Aqf4Uf8ACPaJ/wBAbTv/AAFT/Cj6zL+eX3hY5B9C0TTdL1KeLUrG7v5pIpg0d2MlgRu2ruxjA4ByaozalaOgBmRFDqzPI6qqqCCSTn0Fdxc+H9FEDY0bT88dLVP8KpxaFpSTI6aRZKwYEEWyZH6VlicJRx9aGKruTlB6arpb/LpY3pYmdGEqcbWZZ8IRyQ+DNFjlRkdbOPKsMEcZ5rap21s/dP5Um1vQ/lWcpc0nLuZCUUu1vQ/lRtb0P5VICU63/wCQhH/1yf8AmtJtb0P5UsAI1CPIP+qf+a0AaNFFFABRRRQAVg6n/wAfzfQVu1h6mD9ubg9BW+H+MUti5o//AB7yf7/9K0aztHyIJMgj5/6Vo1FX42C2CiiisxhQelFB6UAZY6ClpB0FLQAUUUUAFRXP/Hu/0qWorn/j3f6VUPiQGcOo+ta1ZI6j61rV0YnoTEKKKK5SgooooAan/IRtf92T+QrVrKT/AJCNr/uyfyFatABRRRQAUUUUAYGo/wDH/J+H8quaN9yb6iqeo/8AH/J+H8quaN9yb6iuyf8AB+4lbmnRRRXGUFFFFABWTF/qxWtWTF/qxQA+iiigAooooAjn/wBQ/wBKze1aU/8AqH+lZvauzDfCyWaw6UUdqK4ygooooAKKKKAMzT/+Rvb/AK9rj/0eK37z/jxuP+ubfyrA0/8A5G9v+va4/wDR4rfvP+PG4/65t/KgCHR/+QLY/wDXun/oIq7VLR/+QLY/9e6f+girtABRRRQBgaj/AMf8v4fyq5o3+ql/3h/Kqeo/8f8AL+H8quaN/qpf94fyrsqfwfuJW5p0UUVxlBRRRQBDd/8AHnP/ANc2/lWbbf8AHnB/1yX+QrSu/wDjzn/65t/Ks22/484P+uS/yFAEtFFFABVC81J7W8ECWjz/AOjvOxWRVKhSBjDYB6+oxir9Z9/o9rqU6vdGRo/KMTwhsK4LBhkj5gQQOhHvmgDldc10ajfWdhJHdwWr5do4pdklwSiNGpIwV5fke3WoP7GgtSJb2O4FvuAdoNSnLRZ4ycnkZ64xU3iXTLfTta029e8aCJ3k23FwcxwyKqiNSeuCFIyxJPrUcutafqEZtLnUdMt7eTiZ/tiPuXuqgevTJxTh9XtL2r/G2nl369zWTxKUPYp26273e5W1rS7e21LyLTUNSVFiBlHnTSBAWA3IQfmIGSV+lUHt50XBGolw0HmCO6nBQNGSwBJIYlgB6DPbrSatqegRa1PFaSaYLQRxlTFEjgZ/1hHyEMwHQFh9DVddV0aHUd5ksJtN8xAGe3j8xkBTcSiqCpwX55z2AIrzl7K3xS+5f5nRfEef4mpd6XINTlh83UrdBHlEivJJs8cNvJxkthdvbOe2ann0hrKTToZGu55HLpKI9QmPnER5B3BtsQL8c+vWudPiTS/sVxD9g0YzKpXzhCcoSoKtgcMecYXoRk0L4m04JkaRpJWFWQq8XzTnC4PHTHzE9j0HOKdqf80vuX/yQc2I8/xNyPTmTQrC5uWv0vZLwRTxG7mPybyMgK52jAHzHP61XjtdumzvNc6okwX91cJNOW3HZ8hTptDFhu9Fz71mDX7GDz7p9O0iVZAPJt8KRH0znChh365znttINlPFehCWZW0XSSyqNiIpyGJ+6SRjGATuHy0Wp/zS+5f/ACQc2I8/xLos7wfb4zcXTYUiC4F7chEfa7dOpHCdc8t1PSpbW1TzFMlzrUqvZlyhuJtyTZG0HBHB6/z24rIHifTJhCDpWiW2WBkfymk2gOAQR/uk9M9M12Rm8C5/1uif99JUydJbyl9y/wDkgTxHn+JylvbahJCjSXt+paFm/wCPmcYIJyzcdMYAwM56qRzXV6XoNtPpNpNfXF/FdPGGlT+05RtP/fVHneBf+e2i/wDfSUnm+BP+euif99JUynSf2pfcv/khr6x5/iWLjQtMt7aWb7RqkvlqW2RalKWbHYDfyawl0mz1aeWbTtQuTapsCsbqZyxKhjnLjGM4x7Vozv4JeB0hutEhkYYWT5G2n1x3rLsdR8PeHHuLSyuoZLV3WRXimRtzbQGLZYYJIJ/Gu/AfVvevJuXS9kunm9e3zMMRLFqPu3/E0/Crz6R4wGmfbJHtpYJGdZJWZSQEKsNxOD8xBwcHjjivRfPh/wCeqf8AfQrzDREsPEnjdJfIju7VLeRnVgrhOEVS2MgEkNgZzwa7z/hFtB/6BFn/AN+hWs7c75dv+Ar/AIhLWMXVvzW1082afnw/89Y/++hUlc9qXhnQ00u7dNJtFZYXIIiGQdprS0Mk+H9NJOSbWL/0EVKbvZkSjHl5osv0HpRQelMzMsdBS0g6CloAKKKKACorn/j3f6VLUVz/AMe7/Sqh8SAzh1H1rWrJHUfWtaujE9CYhRRRXKUFFFFADU/5CNr/ALsn8hWrWUn/ACEbX/dk/kK1aACiiigAooooAwNR/wCP+T8P5Vc0b7k31FU9R/4/5Pw/lVzRvuTfUV2T/g/cStzTooorjKCiiigArJi/1YrWrJi/1YoAfRRRQAUUUUARz/6h/pWb2rSn/wBQ/wBKze1dmG+Fks1h0ooHSiuMoKKKKACiiigDM0//AJG9v+va4/8AR4rfvP8AjxuP+ubfyrA0/wD5G9v+va4/9Hit+8/48bj/AK5t/KgCHR/+QLY/9e6f+girtUtH/wCQLY/9e6f+girtABRRRQBgaj/x/wAv4fyq5o3+ql/3h/Kqeo/8f8v4fyq5o3+ql/3h/Kuyp/B+4lbmnRRRXGUFFFFAEN3/AMec/wD1zb+VZtt/x5wf9cl/kK0rv/jzn/65t/Ks22/484P+uS/yFAEtFFFABRRRQBBeKGtXVgCpxkEZBrMS1t96/wCjw9R/yzFal1/x7t9RVBPvr9RXZQXuMlt3ND7LbZ/49oP+/a/4UfZbb/n2g/79r/hUveiuMu7Ivstt/wA+0H/ftf8ACj7Lbf8APtD/AN+x/hUtFAXZD9ltv+faH/v2P8KdBaWxv0Btof8AVP8A8sx6r7VJTrf/AJCEf/XJ/wCa0Bdlr7Fa/wDPtD/37FH2K0/59Yf+/YqeigLsg+xWn/PrD/37FH2K0/59Yf8Av2Knpkj+XGz4ztUnFAXZH9itP+fWH/v2KxNSs4PtrYtYsYH/ACzHp9Kj/wCEqmFtbvJpqrJNapdBftAK7GZQQDjORuHbnB+ted2sdjrNnHd3NhFe30hdrq7uppTly7YRVVgAFGBVqtRw8JV68rRWnff/AIYqEKlWXJDVnq+ixLFbSBY1Qb+gXHatOvLrHSPB0ul3pvdMS0vLdSSkN1IWkGMhowzck9MHPIrEbSNMS1ncQxvIlvuQfvDuPyY43AlyS4I6ADdgDrralVXtINtPVWX/AASJKUXyy3R7Dqn/ACCbz/rg/wD6Caj0Pjw/pv8A16xf+givMP7E0Z49WA020Oxm8hnv3ACchQi/xtgAkEkfNjP8NTaT4Y0CQxSSR2xhWPaY7i8MIYFyFYY5DbACR0y2OMcL2Mb9f/Af+CPm92x6xR2ryCz0bR5tR09H0+2NkbiZLqZrpomKjf5Z2s+VGAnIzkmkn0bQ/t8kcNtEY0vGRD5jhXTMe0El/u4Mvzjuo+hfso+f3f8ABJuenBGwPlP5Uuxv7p/KvKL3RtFdZXsLeNJFjiJt5XkAMhXkRtz8uSM7vflcZqa50TSBfYt9Ps5MwuqxC8dEVw7DezMeQRtIAYZ9MHNHso+f/gP/AAQueo7G/un8qNjf3T+VeWwaPocqaT5lhHDKYkF2GmYqz4UyEkPwADkAEZbI5xVnR9C8M3Nzp32u3iCyWW67WSZ41SYY6ZIwTzxuP070eyj5/wDgP/BC56Tsb+6fyqG5Rvsz/Kenp71z58KeAgpIisyccD7e3P8A4/XPS2ngq60vUGstNhivLZUXyp7tyRIxIIwH5x1yCQcirhQVufWy30/4ILV2OxCtkfKevpWpXkzeHtPgKv8AZ42w6jALqeSBwQ2QeetegeErqe88JaZPcytLM0OGkY5LYJGT78VzQxmHxtL22HldJ22tra5rVw9ShLlmbNFFFSZhRRRQA1P+Qja/7sn8hWrWUn/IRtf92T+QrVoAKKKKACiiigDA1H/j/k/D+VXNG+5N9RVPUf8Aj/k/D+VXNG+5N9RXZP8Ag/cStzTooorjKCiiigArJi/1YrWrJi/1YoAfRRRQAUUUUARz/wCof6Vm9q0p/wDUP9Kze1dmG+Fks1h0ooHQUVxlBRRRQAUUUUAZlh/yN7f9e1x/6PFP1fXbmCKSFNHvgrSrAZ3VPLCswUtw2cYPHH1plh/yN7f9e1x/6PFc5awxf8JneeWkLgi5aQhUYqRyMlUBXn1J/OgDvNH/AOQLY/8AXun/AKCKu1S0f/kC2P8A17p/6CKu0AFFFFAGBqP/AB/y/h/Krmjf6qX/AHh/Kqeo/wDH/L+H8quaN/qpf94fyrsqfwfuJW5p0UUVxlBRRRQBDd/8ec//AFzb+VZtt/x5wf8AXJf5CtK7/wCPOf8A65t/Ks22/wCPOD/rkv8AIUAS0UUUAFFFFAEN1/x7t9RVBPvr9RV+7/49mPQAjJrOSRN6/OnUfxCu3D/AyXua/eim748/6xP++hR5kf8Az0T/AL6FcVih1FN8yP8A56J/30KPMj/56J/30KLAOp1v/wAhCP8A65P/ADWo/Mj/AOeif99ClgkQahH+8T/VP/EPVaLAalFM86P/AJ6J/wB9Cjzo/wDnon/fQosA+muodSp6MCDSedH/AM9E/wC+hR50f/PRP++hRYDHXwvYLZx2+6cskaReeXHmFFIwpOMY+UA4AyBXl9nLY6PZR2txqNtaX0bOt3Z3geNkkDn5lIUggjB/XNe0ebH/AM9E/wC+hWFqcq/bmxIv3R/EKtUKWJhKhXjeLs+23/DlQqzpS54OzOJj1HwoNC1AS6zYT6pcxlY5RESIeMKELL2659ax5LvTHurp5dW01k2s0fkqUydigKi7QOSDy3QkkV65ozBreQhgRv7H2rSrVOnRSpwTSWiV/wDgEylKb5pbs8UuPEOk20MCwSWeVch3a3jlLK+0gfMOq4YEtxzxnNRjxBbfafPU6GXl8tJCLePykUF8lBjJGGBycHI6Yr2TVP8AkE3v/XB//QTUWhf8i/pv/XrF/wCgij26vb3v/Av+APk93mPIodZtpWhilvdFiSLy2WR7ZQzkKdwk2qcgnqAPQ+tLba7bQj7L9p0UxpHgXM1skjswBP8Ad/iO0HPSvbKKftl/e/8AAv8AgE2PEn8X2qs26DQl3Tjyglqj8Erw+BwgG45Hze1NOr2t/ZFbnUNDtpVI2iK22buHzllGf7mO3PIODXrgJ2jntS5PqaXt1/e/8C/4AWPI7nxZbIs7quibRIvkqlnG7YJA+fj7uOSQM56cVJceKreSQGBPD8MZRhta1Rm3bRye33s456dRXrGT60ZPqaft1/e/8C/4AWOB8P8AiDwpHZTjWLrS2uGlLJm1QhVIGANq8gHPX+VVJZvBFppmpCy1WxmvrlUcSyw7WMikkkELhc9ABgDAr0nJ9TUVyT9nfk9KuGI05NbPfX/gBazueUv4j02UBPtMKAspJMgbABBPC5JPsK9A8I281p4R0yG4jeKUQ5aNxhlySQCOxwelSgnI571q1yQwdDBUvY4dWTd9XfW1jariKleXNMKKKKRkFFFFADU/5CNr/uyfyFatZSf8hG1/3ZP5CtWgAooooAKKKKAMDUf+P+T8P5Vc0b7k31FU9R/4/wCT8P5Vc0b7k31Fdk/4P3Erc06KKK4ygooooAKyYv8AVitasmL/AFYoAfRRRQAUUUUARz/6h/pWb2rSn/1D/Ss3tXZhvhZLNbtRQOlFcZQUUUUAFFFFAGZYf8je3/Xtcf8Ao8U2Pw/f2eqX2oyatLLDLDJuhJY7j82OC20AAqOB/DTtP/5G9v8Ar2uP/R4rfvP+PG4/65t/KgCHR/8AkC2P/Xun/oIq7VLR/wDkC2P/AF7p/wCgirtABRRRQBgaj/x/y/h/Krmjf6qX/eH8qp6j/wAf8v4fyq5o3+ql/wB4fyrsqfwfuJW5p0UUVxlBRRRQBDd/8ec//XNv5Vm23/HnB/1yX+QrSu/+POf/AK5t/Ks22/484P8Arkv8hQBLRRRQAVkaxrqaNLunRDbpbtM7bsMSGCqq9uSepIxWvVd7OKS+ju2L+akTRYDYVlbkhh36UAef67qg8R3um2s8Ef2FkM6x796vvSMqXHQld7ccjPNOufCugabELuSytGjikUyCa2jKFNwDZwoPTNXPFelx6ZqFlqkEMxt/nWYorSCI4TyxtUEqmE28A4JFUpPFNhe7YJkkjhaRC7NbTuNoYE/KI+emK9fD+09jHk87/ec1Xm5tDcsdJ+Ht3AZn0rQ4FLHy1kMe5l7MR2z6VZ/sD4c/8+Xh/wDOP/Gm2XirwlYwGGN7towxKK+mTnYD/CP3fQdqs/8ACbeFP+nj/wAFU/8A8brzorGW1v8AiXHmtrY4iHSvDT38ZXTbRLYzthZ4Ychcrnee427iuO5AyaXStM0WW4tRcaNomGtFDm4EcSiXnezgA8ngAA984xXbf8Jt4U/6eP8AwVT/APxusvxD4r0G90WWDTzci4Z4yMadOhKh1LDOwdVBHUfWnbF+f4le95GBY+HvD07xma10yJlkLHzhCsbYUFlOBkqX4Ujnbk03UdK0YG+jsNJ0J3R4CjFYkULgeYIiR8/IwScEA/Smz6raXFs0KyXcM5fcLl7e6KCPb8sflqM5BxzntnJ6U+TxXJH9nW2+2qyM6u32N5GdXO7PzRYG3ABOO/HWi2L8/wAQ18izrek+FU1CaKy07TjamCMh7eCIlW3nfsY9Wxt4PbNV7jSvDsqQINP0y3ugknmSQ20ckJwOARkncT0I446HpUX/AAkeofapJAxDz7UmkGnyBAo3DKDysgfNkZ+bI5ph1u9u4ltrm7mjhiCmNv7On3khCDuZEByT35GcHBwRRbF+f4hr5DxpOi/Z7EvpejmVJFMkSiNTKpzw8mCBjjPA7jrSf2Xo39kzR3OkaSl6LmRle2ELr5fPlqvtu2g5GdvOc1Jc+MtYVZ5IlLDdIY400t8hcHaMmPk5x2HOecVY/wCEqvXlVTcyxREjLnSmZlTPoIcGT3Hy46DNFsX5/iHveRA2laK15EselaGbddSAnd1ijJgwv3RzlBl/m68D1rdvNH8HtcfZ9J8O6LeOqhpZcL5aZ6DKgkk4P5VkweL9S+2W5nZBb+cpmxpUpOzA3ADy+n3u+enHatnU/EXhe5uVnt7+9srgLsYrpU5V16jKmPqOcH3NbUFiOf372+ZMua2liHTZ9F0PxRpctnBbaLBcwXUV5CJAkbSRmMrnsSAxIPXDV2n/AAkuhf8AQZ0//wACU/xrk9FFhq3ifTZNLjubqysra5N1dXVs0YaWUx4HzqMn5T0GAAK7n7Ha/wDPtD/37FZYu/Orfj6s3p8vL725kal4j0N9Lu1XWLAs0DgAXKZJ2n3q9oX/ACL+m/8AXrF/6CKs/Y7X/n2h/wC/YqcAAYAwBXMk73ZcpR5eWIUHpRQelMzMsdBS0g6CloAKKKKACorn/j3f6VLUVz/x7v8ASqh8SAzh1H1rWrJHUfWtaujE9CYhRRRXKUFFFFADU/5CNr/uyfyFatZSf8hG1/3ZP5CtWgAooooAKKKKAMDUf+P+T8P5Vc0b7k31FU9R/wCP+T8P5Vc0b7k31Fdk/wCD9xK3NOiiiuMoKKKKACsmL/VitasmL/VigB9FFFABRRRQBHP/AKh/pWb2rSn/ANQ/0rN7V2Yb4WSzWHSigdKK4ygooooAKKKKAMzT/wDkb2/69rj/ANHit+8/48bj/rm38qwNP/5G9v8Ar2uP/R4rfvP+PG4/65t/KgCHR/8AkC2P/Xun/oIq7VLR/wDkC2P/AF7p/wCgirtABRRRQBgaj/x/y/h/Krmjf6qX/eH8qp6j/wAf8v4fyq5o3+ql/wB4fyrsqfwfuJW5p0UUVxlBRRRQBDd/8ec//XNv5Vm23/HnB/1yX+QrSu/+POf/AK5t/Ks22/484P8Arkv8hQBLRRRQAUUUUAQ3JIt2IOORVJHbevzHqO9Xbr/j3b6iqCffX6iuyh8DJe5r73z95vzo3v8A3m/Om96K4yh29/7zfnRvf+83502igB29/wC8350sDMdQjyxP7p+p91plOt/+QhH/ANcn/mtAGjRRRQAUUUUAFYepMwvmAYjgd63KwdT/AOP5voK3w/xiexd0gkwSZJPz/wBK0aztH/495P8Af/pWjUVfjYLYKKKKzGFB6UUHpQBljoKWkHQUtABRRRQAVBeyJFZyySOiIoyWdgAOfU1n6vqt1p86pbWi3WLeSZowTvbayqAuAf72TweBxXI6/ri32raOl3aWl5Z/NNFBlmjldovk37gM7Wyeg+ma0pJOeuy1+7ULN6I6QarpuR/xMbP/AL/p/jWr/a+l/wDQTsv/AAIT/GuFX7NZP9qutM0i4t0+aaL+zolAXuVIGQQOeSal11/Cr36f2dbWaxCHMjQQQ7HBPKrn7smMkN64HrRQxVDMIc+HvJJ2dkaVqFSg+Wppc7X+19L/AOgnZf8AgQn+NH9r6X/0E7L/AMCE/wAa84mk0wQ7IrXTZCFgLZghUkAPvVWAzuJ8vcT/ALWOKu3Y0FtTJeHSfJ8o4WyjjjTo/BJGd+7y+emM1v8AVn/LL7jG53X9r6X/ANBOy/8AAhP8aP7X0v8A6Cdl/wCBCf41xUw0C0TTYohot2yS7ZX+yx4dfKbmUkZxv2/d5x2NMhbQodBtxdQ6RJqC6gDIYLeF98Hm5OcouF2enOMe9H1Z/wAsvuC52yaxpY1C2P8AaVlgB8/6QnoPetL+29J/6Cll/wCBCf415dE+iLYXSTWdlMWB2mKGHzVk+UEq3aPJYgYzgHp0KwLo6y3is2lPBJGESV7CLzEbY3zKucDB2jjqe3ej6u/5ZfcFz1D+29J/6Cll/wCBCf40f23pP/QUsv8AwIT/ABry+zfS4p7eSay0N7YWrx7GWNn80su15Ay/XgMcDPeo7U6MpgQWtiJgrkSXcUJiOVxiQKCfvbmBweNo7nB9Xf8ALL7gueqf23pP/QUsv/AhP8aP7b0n/oKWX/gQn+NcdoZ8HJodmmqQaGb5YgJiYI2Jb1JCgE/hUGv6v4L0my+022laHcpgq2LdMox+6SMcrng46VdPBzqSUYwld+QOVjc1DVtMa+kI1KzI4/5eE9PrWloF1b3MU5t7iGYBhny5A2PriuA1W10W/wBXnOnw2a2caRhPs1vGqkkEn+Hk9KPDMSWPjrTYrdUjDrOjlEVS6+WGCttAyAwyM9MmuT69h6laWCV1OKu7rTRJ9/PsdDw1SNJVnazPWKKB0oqDEKKKKACsmL/VitasmL/VigB9FFFABRRRQBHP/qH+lZvatKf/AFD/AErN7V2Yb4WSzWHQUUdqK4ygooooAKKKKAMzT/8Akb2/69rj/wBHit+8/wCPG4/65t/KsDT/APkb2/69rj/0eK37z/jxuP8Arm38qAIdH/5Atj/17p/6CKu1S0f/AJAtj/17p/6CKu0AFFFFAGBqP/H/AC/h/Krmjf6qX/eH8qp6j/x/y/h/Krmjf6qX/eH8q7Kn8H7iVuadFFFcZQUUUUAQ3f8Ax5z/APXNv5Vm23/HnB/1yX+QrSu/+POf/rm38qzbb/jzg/65L/IUAS0UUUAFFFFAEN1/x7t9RVBPvr9RV+6/492+oqgn31+ortofw2S9zV70Ud6K4igooooAKdb/APIQj/65P/NabTrf/kIR/wDXJ/5rQBo0UUUAFFFFABWDqf8Ax/N9BW9WDqf/AB/N9BW+H+MUti7o/wDx7yf7/wDStGs7R/8Aj3k/3/6Vo1FX42C2CiiisxhQelFB6UAZY6ClpB0FLQAUUUUAVbnTrS7njmuIhI0aMgViSrK2Mhl6MOAeR2rmPFGi28V9Y6kLK6mtUaRLtbXLyRqY9sbInYKQOFHfODXY1V1K6t7KwlnuriKCFcZklcKo59TWlJ2mv630A4CG5sWnUXv9sT2qsC0cejTI0vsx5wPXHX2q/rPiW+uNXS/0y31eJIofJWBtNmG/dnc2cEAjC4yp6fnoDxRoIx/xO7D/AMCF/wAa0/8AhLfDv/Qf03/wKX/GtfqdHDx5aNOy8v8AhhzrTqu83c4K01LVLe5s5pLHWZFs3aSOL7JIAwZwxQkRj5vvZboeBgUf214gOmXOntFrjQMjRYNk+XUrxtJjJUAk5zksOmK73/hLfDv/AEH9O/8AApf8aP8AhLfDv/Qf07/wJX/Glyf3H/XyJv5nDf8ACQeJd8kgTV0dN8cCrYSbVQleoMfJ+Ugeme9WY9f1q2mivFTWJ5QZC9u2myIp3FO+w/3SfboOtdh/wlvh3/oP6d/4Er/jR/wlvh3/AKD+nf8AgSv+NHJ/cf8AXyD5nAx3mqTLDDcx6wNkzzhBYSnzHJLHbhAVByOucHkelXf+Em8XBCWh1hnMp2+XpjAICRyQUO5cZ4GCOvNdenizw79vtm/t7TsAPk/aV44HvWj/AMJh4a/6D+mf+BSf40cn9x/18gucBP4n8XSzSyR2+qwxureXCmmOdnzcAsU54xyBW/4f8Tmw0wwahp2vzT+dI+9tPlfIZiRg7Bgc9K6D/hMPDX/Qf0z/AMCk/wAaP+Ex8M/9B/TP/ApP8aORfyP+vkF/Mp/8JpZ/9AjXf/BVL/hWXrOq6PrsaR3mk+IiqBtijTZdoYjG7G3BI7Z6ZroP+Ew8Nf8AQf0z/wACk/xo/wCEw8Nf9B/TP/ApP8auDlCXNGLT/ryEcHrmsCfWprq2sdT2yIgKS6dOrKVB/wBk5BzU3hO2u9Q8WWl+tpcxwWqytLJNA0SjcgVVG4AsTyTgYAHWty/8VeH3vZGXXNPIOORcr6fWtbw3qun6itwLK+trkqRuEMocj64qHgqFKUsVGnabVm7vbRbbGrxFSUFSb91G+OlFFFc5AUUUUAFZMX+rFa1ZMX+rFAD6KKKACiiigCOf/UP9Kze1aU/+of6Vm9q7MN8LJZrdqKB0orjKCiiigAooooAzNP8A+Rvb/r2uP/R4rX1eS6isJGtoBP8AKRJGD8xXB+77+3esjT/+Rvb/AK9rj/0eK6agDK8PSXUmkwfaLfyEWJFiVvvkBcEsO2ewrVoooAKKKKAMDUf+P+X8P5Vc0b/VS/7w/lVPUf8Aj/l/D+VXNG/1Uv8AvD+VdlT+D9xK3NOiiiuMoKKKKAIbv/jzn/65t/Ks22/484P+uS/yFaV3/wAec/8A1zb+VZtt/wAecH/XJf5CgCWiiigAooooAhuv+PdvqKoJ99fqKv3X/Hu31FUE++v1FdtD+GyXuaveijvRXEUFFFFABTrf/kIR/wDXJ/5rTadb/wDIQj/65P8AzWgDRooooAKKKKACsHU/+P5voK3qwdT/AOP5voK3w/xiexd0f/j3k/3/AOlaNZ2j/wDHvJ/v/wBK0air8bBbBRRRWYwoPSig9KAMsdBS0g6CloAKKKKAMLxBq15pkge2AdEtZJWiKg7mDooJORhRuJPI6dRXI6/qU+o6ppgmffB5bXEStCEKM0WM4yeRyQcnGeDXo7W8L3EVw8SNNCCI5CPmTPXB7ZrlPGGjPJLa6lZWRlaN5PtIgUGR9yBVfBI3YIAIznB4rpwjSrRbBmZdW9vYQNdrvXyWVySSwwGGeO/Haup03V9DvLX7RNZW0SyMTCq2hY7OxYhcZPPA6VxxvtRmZUm0bUljLqXKWmWwCDxl/auttfEsdkJFt/CviBFkkMhUW64BPXA38fQVti4Vny2ff9B12pTTp6L0ND7d4c/594v/AACb/wCJrkPMD6j5sdpcwW/2kt5boHCLuTLZ53KVD/L2LDjvXT/8Ji//AELPiD/wGT/4uue1/WdZ1G/tbvTtJ1u1a0RjGrWikO7cHd8/QL04PJrk9nW7/wBfeZ+93/D/AIJBpdzN9rs/tEcIQW5jc3Nt5m1wzZLBCAzEbOMcDPI6GSzjje4/0syRbZ/MMhjYqccnCL/Ax2gKegDdM1iIPEPmKz6PrZVLhrmNCd2GJJwx43E/3uMDjBqz/aPizzrrZpWuxwyyNJ5SkAYdtzIpwSuCT8/UgYwM8Hs63f8Ar7xe93/D/gmjqstxcDUI9O8lS8UJimNrsQkA7xEApZcnb97PfnpVvWJ4JNSb7Jaym0Nsm8QwiMbxICwQkBgxTIyePpXNxXPi6IxRLY6ynkxCO1VI8BMR7SSM8j7vH1NX4brxRF5Fw1hr8tzFKJGjeBNj4jCcsOcnGPTnOM0eyrd/6+8Pe7/h/wAEstKZ4rWB4JRNGGEl1awLgfuwAGQ4DEsScnkY4xnNRZuP7PtUePfNFMsmwW5UMp8zKSOG3YUFM8tyP4qzvJ8R3aTRXul62qzTGeQrAsgL7cDCnAGP1znggVYk1HxxtkMdhrYJctHGI12qMEBcnk446nkjJ60eyrd/6+8Pe7/h/wAEuiaZNM1C3vLMXVzLcF4J7RCnlqQvQEKcA7iFzzjB60jTudTjlhspUtftW6SGePcCvADZU5Cn5iV5A+UbTzUMWoeLiyrLY+IUiOM7VVmRM5ZM8bm/2+COmO9Mj1HxqLqF3tNcMSzIzqIFyygAEdcY4PHfPXij2Vbv/X3h73f8P+CbGpXsN9fzRafHDb2kBCFo7ZQ8j4BP3l4ABHbnmqel3R/4T/Q7ZkjE6xXIaeOJYzLGUBCvtABIYVLrOpLd37XUOi+IbS5ZQsgNmjo+OhI3jnHcHpUnh3T5b3xLp99FYX8EVkkzXNzfokbSu6hVRFUnAABP+JNelpGj73b8bf11/wCBs5QcEktT0TtRRRXkkhRRRQAVkxf6sVrVkxf6sUAPooooAKKKKAI5/wDUP9Kze1aU/wDqH+lZvauzDfCyWaw6UUDoKK4ygooooAKKKKAMzT/+Rvb/AK9rj/0eK6auZ0//AJG9v+va4/8AR4rpqACiiigAooooAwNR/wCP+X8P5Vc0b/VS/wC8P5VT1H/j/l/D+VXNG/1Uv+8P5V2VP4P3Erc06KKK4ygooooAhu/+POf/AK5t/Ks22/484P8Arkv8hWld/wDHnP8A9c2/lWbbf8ecH/XJf5CgCWiiigAooooAhuv+PdvqKoJ99fqKv3X/AB7t9RVBPvr9RXbQ/hsl7mr3oo70VxFBRRRQAU63/wCQhH/1yf8AmtNp1v8A8hCP/rk/81oA0aKKKACiiigArB1P/j+b6Ct6sHU/+P5voK3w/wAYnsXdH/495P8Af/pWjWdo/wDx7yf7/wDStGoq/GwWwUUUVmMKD0ooPSgDLHQUtIOgpaACiiigAqjq2oWem6c9zfXMVvCCF3yNgEnoB6n2qrqt5cW+oxRxXRgjFpNMVEIkLspQAY6nqeARzXI63qep2fiXSVu5Yp7mEMyJ5ahIZXixnA6468k9+ea0pJc13stfuVws3ojUHjHw4CP+Jxb/AJN/hWmPG/hg9Nbtvyb/AArCGsanppa9+33Fx5fzyxTMGWQDqMY4OOmKva9cf2pqKSxaTc7Ug2hntZVeTnlHAQ5jI468FiccCpwuNoZjD2lCLaTt/WhrXw9TDySn1ND/AITbwz/0G7b8m/wo/wCE28MHprdt+Tf4VzlzFeC2ZRFdxxokBYy20pRjGHyGDIQFBZSPXYM4JyL94ZLjVTcPb3t5mErm509wRw42bFXBQllb1+QdetdPsH/I/v8A+AYXNT/hN/DH/QbtuOvDf4Uf8Jv4Yz/yG7b8m/wqhMzQxaZDp8OrLHZy7gZbJx5amJk+VQuHOWDYb061HAbi10C3sfJ1G5uIdQF15zWc0eU83eQeCSccYPHPtR7B/wAj+/8A4AXNFfG3hkX9s39s25AD54buB7Vf/wCE98Lf9Bu2/X/CuVhF1Fp1zbpY3E8c4IMUlrL5bt8o80/IfnOGb0Bx1p8FvLDLeFINXWC5jETw+RISw2MCS/l5JyQBnOAOvaj2D/kf3/8AAC/mdP8A8J94VH/Mbtv1/wAKP+E98Lf9Bq2/X/Cua04X+n3trd7LqRYbdoPsh02RUUMyngqoBPBydoyfao7eOVGh8zw+QiLKzKttMV3OCNoHl/6vn7vrzR7D+4/v/wCAF/M6j/hPvCoODrdt+Z/wo/4T/wAKf9By2/M/4UabqtlpmnQWUWnaqUhTbn7BIMnuenHParX/AAkdt/0DdV/8AJP8KPq7/kf3/wDAC/mc7feNfDUl47rrNuVOOm70+lWdL8c+GIUk361bDJGM7v8ACtc+I7bP/IO1T/wAf/ClHiO2/wCgdqn/AIASf4Vs4ScOXkf3/wDAF8yt/wAJ/wCFP+g5a/mf8KP+E/8ACn/QctfzP+FWv+Ejtv8AoG6r/wCAEn+FH/CR23/QN1X/AMAJP8Kx9g/5H9//AAB38yr/AMJ/4U/6Dlr+Z/wo/wCE/wDCn/QctfzP+FWv+Ejtv+gbqv8A4ASf4Uf8JHbf9A3Vf/ACT/Cj2D/kf3/8AL+ZV/4T/wAKf9By1/M/4Vnx+N/DAjAOt2w/76/wra/4SO2/6Buq/wDgBJ/hTf8AhI7b/oG6r/4ASf4UfV3/ACP7/wDgBfzMr/hN/C//AEHLX/x7/Cj/AITfwv8A9By1/wDHv8K1f+Ejtv8AoG6r/wCAEn+FH/CR23/QN1X/AMAJP8KPq7/kf3/8ALmV/wAJv4X/AOg5a/8Aj3+FH/Cb+F/+g5a/+Pf4Vq/8JHbf9A3Vf/ACT/Cj/hI7b/oG6r/4ASf4UfV3/I/v/wCAFzGm8beGGhcLrdsSRwAG/wAKof8ACYeHcf8AIXtx7kMB/Ko5PGlxe22uWMlvcxz2kKx+bBbOq7mZvmJ6pxgYPQ5rNmjEERkieVXQgqfNb1+tYYzGUssq06FaErzelmvL07nTQw0sRGUotaHo0UiSxJJG6vG6hldTkMD0INOrn/BAA8H2KgYVDKqgdABK2APaugpVI8s3HsYLYKKKKgAooooAzNP/AORvb/r2uP8A0eK6auZ0/wD5G9v+va4/9HiumoAKKKKACiiigDA1H/j/AJfw/lVzRv8AVS/7w/lVPUf+P+X8P5Vc0b/VS/7w/lXZU/g/cStzTooorjKCiiigCG7/AOPOf/rm38qzbb/jzg/65L/IVpXf/HnP/wBc2/lWbbf8ecH/AFyX+QoAlooooAKKKKAIbr/j3b6iqCffX6ir91/x7t9RVBPvr9RXbQ/hsl7mr3oo70VxFBRRRQAU63/5CEf/AFyf+a02nW//ACEI/wDrk/8ANaANGiiigAooooAKwdT/AOP5voK3qwdT/wCP5voK3w/xilsXdH/495P9/wDpWjWdo/8Ax7yf7/8AStGoq/GwWwUUUVmMKD0ooPSgDLHQUtIOgpaACiiigBjQxPNHM0SNLHnY5UFkz1weozXOeJ9IMlxZalb6ZHeLC0ourdGWOSZZE27gx6suB1P0NdNUVz/x7v8AStKUnGaYHBW6zRXUck3h7W7qJGDLDLNbAEjpuIbLfj+NLrp8T6jrn9pWPhy7t5BAI0Z7iLcpHmDjDZGd/UEYx0NdWOo+ta1aTp0aCUadNJeWn5DlUnUd5O55gLXxi9ncW9xo+oT/AGpSJmN+q5Yxom7AbBxtPHQ7snkVZW18Q/b2u5NH1fzRINjpcQiTyg33Wk37m+XjGcZ5r0aio54/yr8f8ybHnBk8dfZ2jGnan5hQqrm+X5G4+b73zbsHg8Ln5agdPHksTRS2OpSI4AYPeocjZtIIBAI5zj8816dRRzx/lX4/5geZRw+MrSW3tdP03UYbVVfEQuIYzJ3YnacAnOMjGMAjkmppF+I0lokK2F/DtBORexszE7flLFs4G04PU55716Kn/IRtf92T+QrVpc8f5V+P+YHmejHxfZa2dQv9B1K7XbIig38ZKqxUjjcFPQ9hjjrXTf8ACRa3/wBCfqP/AIEwf/F101FPnj/Kvx/zCxzP/CRa3/0J2o/+BMH/AMXR/wAJFrf/AEJ+o/8AgTB/8XXTUGj2kf5V+P8AmFjkZfF2qRSFH8I6kGHUfaIP/i6fB4p1ecMY/COpNg85uIP/AIuqWseK9Gt9WuIWupHeNtj+TbySqrAcjcqkZHcZ4p+meONAgSTzJ7tdxGP+Jfcf/EV1uj+75lT/AD/zJvruX/8AhItb/wChO1H/AMCYP/i6P+Ei1v8A6E7Uf/AmD/4uj/hYHhz/AJ+rv/wX3H/xukPxB8NjGbu6GeBnT7jn/wAcrHkn/wA+vz/zHddxf+Ei1v8A6E7Uf/AmD/4uj/hItb/6E7Uf/AmD/wCLoHxA8OHpdXR5xxp9x/8AEUn/AAsHw0QCLu5weh+wXHP/AI5RyT/59fn/AJhddxf+Ei1v/oTtR/8AAmD/AOLqIeKtXIyPCGpY/wCviD/4upF+IPhtvu3dyfpYXB/9kqhH420EIoNxd59P7PuP/iKOSf8Az6/P/MLruXP+Ep1f/oUNS/8AAiD/AOLo/wCEp1f/AKFDUv8AwIg/+Lqp/wAJx4fzj7Rd5/7B9x/8RQPHPh44xdXRz0xYT8/+OUck/wDn1+f+Y7ruW/8AhKdX/wChQ1L/AMCIP/i6P+Ep1f8A6FDUv/AiD/4uqg8c+HySBc3RI6/6BP8A/EUh8d+HgCftVzx1/wBAn4/8co5J/wDPr8/8wuu5RnlmGmXdpb+DdStUng8oslxAccls8vycsSSetYLJrko2SaJeFSRuC+SpPtkynH1wa6qXxvoDwuFuLsnBHGnz9f8AviqJ8X6MBky3ar3JsJwB+OyreFhiJKdeleUXpe/l5+RUK06aahKyZveG9Pn0rw7Z2dyU+0IrNKEOVDMxYgHuBnGfatWo7eeG6tori3lSWGVA6SIchgehBqSuSbcpNvcQUUUVIBRRRQBmaf8A8je3/Xtcf+jxXTVzOn/8je3/AF7XH/o8V01ABRRRQAUUUUAYGo/8f8v4fyq5o3+ql/3h/Kqeo/8AH/L+H8quaN/qpf8AeH8q7Kn8H7iVuadFFFcZQUUUUAQ3f/HnP/1zb+VZtt/x5wf9cl/kK0rv/jzn/wCubfyrNtv+POD/AK5L/IUAS0UUUAFFFFAEN1/x7t9RVBPvr9RV+6/492+oqgn31+ortofw2S9zV70Ud6K4igooooAKdb/8hCP/AK5P/NabTrf/AJCEf/XJ/wCa0AaNFFFABRRRQAVg6n/x/N9BW9WDqf8Ax/N9BW+H+MUti7o//HvJ/v8A9K0aztH/AOPeT/f/AKVo1FX42C2CiiisxhQelFB6UAZY6ClpB0FLQAUUUUAFRXP/AB7v9KlqK5/493+lVD4kBnDqPrWtWSOo+ta1dGJ6ExCiiiuUoKKKKAGp/wAhG1/3ZP5CtWspP+Qja/7sn8hWrQAUUUUANckKSOwrz3+3dZFqx+2y/Zhp6zi62LzLwfvbduMkpgc9sZ5r0SofstuIBD5EXlA5Eewbc5z0+vNAHj+lossLrJ8wVn2gk4GZZCfzNacHiC48OaXq8lrZXs4jaCRRCoZFycMDk8FhxwDVe40DXdJvJrW2s0uIw7FZkuUAdSzMuVYZVgGweSDjNamhxa/bW90jeGhdmaRHcvqMaAbOVwAvrz1qaODrwzSeMqNSpO1lzf4em3RnXOvSeFVJL3l5G7pXie7m0uCa40nU7iSVfMMlvboI8HkBfn5AHGe+Kr67qV5qlvBBD4e1AosyySefAAwA/uEMdrZ7+mamtr/xHZw+TbeDIIo9xbYmpoBknJ42+pNTf2z4q/6FGP8A8Gif/E16E1BybUFb/Ev8ziVzlRZastvHGmm6jGwEqtLHDhgGiEe4DdjcSNzE9+lJfzXMOm2NveRajE4fy0SWGNIl6gGIAg+YoIwT354rq/7Z8Vf9CjH/AODRP/iaydbi8R64IPO8MGJoCWQpqiYJOPvDbhhxyp4Peo5Y/wAq/wDAv+CFyG8Ex0XUYYdH1SCS6SQuot0jgBOMMQrbvlAHQ89+tLby30V9rUstlq9xLqVuqiGSBF8sjeOobOz5gBxnj1rBk8K+LMKiaY7gzGR3l1JC6567OOGPc9/SnW3hzWo7pLltBEpDlyHvosnkkDJBOATnnOeKOWH8i/8AAv8AghqaNmdTtL2GQ2GoC4RIgwSL5nACAqfmzsxExHuze9VJFudP8u/ubG8010D73hUJGXYEKFBI2L83IB5wOvWqsXhfxBGig6U7yBdrSm/iDMMuQvT7nznK98dqsf8ACPa02zzNCmJDbnMeqxoW4ACggZULg4A6ZIo5Y/yr/wACX+YalprPUpJ/OhtNWgiW/W9jiW1jcOd24lmyCT1wOQM9T2kvU1a/u5bmXQrhJ2kjZJIrcADaDkshJDnJOMnpgZGKypvDnid2nlXSy8khDbZdQj2AjjgDoQPukfd6c0+Xw94mnn86WwuGYKUC/wBpoqhdgQcDHOByc8+lHLD+Vf8AgS/zDU2LPUbrQNL1ZoLHUkeWb5GmjUx2xIABY5znnccg9sk9apnUNQsSbyPUbp5ohvbzZiyyY6hl6YPtjHarGj2+uaJpt5aP4WhuobnBm338SlvlCnOBznH4VUgttQguEk/4RmaZI23JDPrEZjBHTPyZIHuTXDi8Fiq9SnLDTUFF6q+/46+j0OvD16NOMlUjdvY3/AjbvBWntt2g+aQv90eY3FdFWX4d02XSPD9nYzyK80as0jJnbuZixxnsCcVqVvWalUk13ZyrYKKKKzGFFFFAGZp//I3t/wBe1x/6PFdNXM6f/wAje3/Xtcf+jxXTUAFFFFABRRRQBgaj/wAf8v4fyq5o3+ql/wB4fyqnqP8Ax/y/h/Krmjf6qX/eH8q7Kn8H7iVuadFFFcZQUUUUAQ3f/HnP/wBc2/lWbbf8ecH/AFyX+QrSu/8Ajzn/AOubfyrNtv8Ajzg/65L/ACFAEtFFFABRRRQBDdf8e7fUVQT76/UVfuv+PdvqKoJ99fqK7aH8Nkvc1e9FHeiuIoKKKKACnW//ACEI/wDrk/8ANabTrf8A5CEf/XJ/5rQBo0UUUAFFFFABWDqf/H830Fb1YOp/8fzfQVvh/jE9i7o//HvJ/v8A9K0aztH/AOPeT/f/AKVo1FX42C2CiiisxhQelFB6UAZY6ClpB0FLQAUUUUAFRXP/AB7v9KlqK5/493+lVD4kBnDqPrWtWSOo+ta1dGJ6ExCiiiuUoKKKKAGp/wAhG1/3ZP5CtWspP+Qja/7sn8hWrQAUUUUAFFFFAGBqP/H/ACfh/Krmj/cm+oqnqP8Ax/yfh/Krmjfcm+orsn/B+4lbmnRRRXGUFFFFABWTF/qxWtWTF/qxQA+iiigAooooAjn/ANRJ9Kze1aU/+of6Vm9q7MN8LJZrdqKB0FFcZQUUUUAFFFFAH//Z) ![](data:image/png;base64,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
Article
Sylvain Guilbaud · Avr 25

Sécurisation des interfaces HL7 avec des certificats SSL/TLS (X.509)

## TLS, qu'est-ce que c'est ? La TLS (Transport Layer Security ou "Sécurité de la couche de transport"), qui succède à SSL (Secure Sockets Layer ou "Couche de sockets sécurisée"), fournit de la sécurité (c'est-à-dire le chiffrement et l'authentification) sur une connexion TCP/IP. Si vous avez déjà remarqué le "s" sur les URLs "https" vous avez reconnu une connexion HTTP "sécurisée" par SSL/TLS. Dans le passé, seules les pages de connexion/autorisation sur le web utilisaient TLS, mais dans l'environnement hostile d'Internet d'aujourd'hui, les meilleures pratiques indiquent que nous devrions sécuriser toutes les connexions avec TLS. ## Pourquoi utiliser TLS? Alors, pourquoi mettre en œuvre TLS pour les connexions HL7 ? Alors que les violations de données, les rançongiciels et les vulnérabilités sont de plus en plus fréquents, chaque mesure que vous prenez pour renforcer la sécurité de ces précieuses sources de données devient plus cruciale. La TLS est une méthode éprouvée et bien comprise pour protéger les données en transit. TLS fournit deux fonctionnalités principales qui nous sont bénéfiques : 1) le chiffrement et 2) l'authentification. ### Chiffrement Le chiffrement transforme les données en cours de transfert de sorte que seules les deux parties engagées dans la communication peuvent lire/comprendre les informations échangées. Dans la plupart des cas, seules les applications impliquées dans la connexion TLS peuvent interpréter les données transférées. Cela signifie que les acteurs malveillants opérant sur les serveurs ou réseaux de communication ne pourront pas lire les données, même s'ils parviennent à capturer les paquets TCP bruts à l'aide d'un renifleur de paquets (wiretap, wireshark, tcpdump, etc.). ![Without TLS](/sites/default/files/inline/images/without_tls.gif "Without TLS") ### Authentification L'authentification garantit que chaque partie communique avec la partie prévue et non avec un imposteur. En s'appuyant sur l'échange de certificats (et la vérification de la preuve de propriété associée qui s'est produite lors d'un handshake TLS), lorsque vous utilisez TLS, vous pouvez être sûr que vous échangez des données avec une partie de confiance. Plusieurs attaques consistent à tromper un serveur pour qu'il communique avec un acteur malveillant en redirigeant le trafic vers le mauvais serveur (par exemple, l'emploi de DNS et d'ARP poisoning) Lorsque TLS est impliqué, les imposteurs doivent non seulement rediriger le trafic, mais aussi voler les certificats et les clés appartenant à la partie de confiance. L'authentification protège non seulement contre les attaques intentionnelles de pirates informatiques ou de acteurs malveillants, mais aussi contre les erreurs de configuration accidentelles qui pourraient envoyer des données vers le ou les mauvais systèmes. Par exemple, si vous attribuez accidentellement l'adresse IP d'une connexion HL7 à un serveur qui n'utilise pas le certificat attendu, la vérification de la négociation TLS échouera avant l'envoi de données vers ce mauvais serveur. #### Vérification d'hôte Lors de la vérification, les clients ont la possibilité d'effectuer une vérification d'hôte. Cette vérification compare l'adresse IP ou le nom d'hôte utilisé dans la connexion avec les adresses IP et les noms d'hôte intégrés dans le certificat. Si cette vérification est activée et que l'adresse IP/l'hôte de la connexion ne correspond pas à une adresse IP/un hôte figurant dans le certificat, le handshake TLS échouera. Vous trouverez les adresses IP et les noms d'hôte dans les champs X.509 « Subject » et « Subject Alternative Name » présentés ci-dessous. #### Preuve de la propriété d'un certificat avec une clé privée Pour prouver la propriété des certificats échangés avec TLS, vous devez également avoir accès à la clé privée liée à la clé publique intégrée au certificat. Nous ne discuterons pas de la cryptographie employée pour la preuve de propriété avec une clé privée, mais vous devez savoir que l'accès à la clé privée de votre certificat est nécessaire pendant le handshake TLS. #### TLS mutuel Pour la plupart des connexions https établies par votre navigateur web, seul le certificat d'authenticité du serveur web est vérifié. Normalement, les serveurs web n'authentifient pas le client avec des certificats. Au lieu de cela, la plupart des serveurs web s'appuient sur l'authentification du client au niveau de l'application (formulaires de connexion, cookies, mots de passe, etc.). Avec HL7, il est préférable que les deux côtés de la connexion soient authentifiés. Lorsque les deux côtés sont authentifiés, on parle de «TLS mutuel». Avec le TLS mutuel, le serveur et le client échangent leurs certificats et l'autre côté vérifie les certificats fournis avant de poursuivre la connexion et l'échange de données. ## X.509 Certificats ### X.509 Champs du certificat Pour fournir le cryptage et l'authentification, les informations sur la clé publique et l'identité de chaque partie sont échangées dans des certificats [X.509] (https://en.wikipedia.org/wiki/X.509). Vous trouverez ci-dessous certains champs courants d'un certificat X.509 qui nous intéresseront: - `Serial Number`: numéro unique à un CA qui identifie ce certificat spécifique - `Subject Public Key Info`: clé publique du propriétaire - `Subject`: nom distinctif (DN) du serveur/service représenté par ce certificat - Ce champ peut être vide si des "Subject Alternative Names" (noms alternatifs du sujet) sont fournis. - `Issuer`: nom distinctif (DN) du CA qui a émis/signé ce certifica - `Validity Not Before`: date de mise en vigueur de ce certificat - `Validity Not After`: date d'expiration de ce certificat - `Basic Constraints`: indique s'il s'agit d'un CA ou non - `Key Usage`: l'utilisation prévue de la clé publique fournie par ce certificat - Valeurs d'exemple: digitalSignature, contentCommitment, keyEncipherment, dataEncipherment, keyAgreement, keyCertSign, cRLSign, encipherOnly, decipherOnly - `Extended Key Usage`: utilisations supplémentaires prévues de la clé publique fournie par ce certificat - Valeurs d'exemple: serverAuth, clientAuth, codeSigning, emailProtection, timeStamping, OCSPSigning, ipsecIKE, msCodeInd, msCodeCom, msCTLSign, msEFS - Pour les connexions TLS mutuelles, les deux modes d'utilisation serverAuth et clientAuth sont nécessaires. - `Subject Key Identifier`: identifie la clé publique du sujet fournie par ce certificat - `Authority Key Identifier`: identifie la clé publique du fournisseur utilisée pour vérifier ce certificat - `Subject Alternative Name`: contient un ou plusieurs noms alternatifs pour ce sujet - Les noms `DNS` et les adresses `IP` sont des noms alternatifs fréquemment fournis dans ce champ. - `Subject Alternative Name` est parfois abrégé en `SAN`. - Le nom DNS ou l'adresse IP utilisés dans la connexion doivent figurer dans cette liste ou dans le `Common Name` du `Subject` pour que la vérification de l'hôte soit réussie. #### Noms distingués Les champs `Subject` (Sujet) et `Issuer` (Émetteur) d'un certificat X.509 sont définis comme des `Distinguished Names` (DN, Noms Distingués). Les noms distingués sont constitués de plusieurs attributs, chaque attribut ayant le format `=`. Voici une liste non exhaustive des attributs courants que l'on trouve dans les champs `Subject` et `Issuer` | Abréviation | Nom | Exemple | Remarques | |--------------|--------------------|-----------------------|-----------------------------| |CN |Nom commun | CN=server1.domain.com | Le nom de domaine complet (FQDN) d'un serveur/service | |C |Pays | C=US | Code pays à deux caractères |ST |État (ou province) | ST=Massachusetts | Nom complet de l'état/province |L |Localité | L=Cambridge | Ville, région, etc. |O |Organisation | O=Best Corporation | Nom de l'organisation |OU |Unité opérationnelle| OU=Finance | Départment, division, etc. Selon les exemples du tableau ci-dessus, le DN complet pour cet exemple serait `C=US, ST=Massachusetts, L=Cambridge, O=Best Corporation, OU=Finance, CN=server1.domain.com` Notez que le `Common Name` (nom commun) trouvé dans le `Subject` (sujet) est utilisé lors de la vérification de l'hôte et correspond normalement au nom de domaine complet (FQDN) du serveur ou du service associé au certificat. Les `Subject Alternative Names` (noms alternatifs du sujet) du certificat peuvent également être utilisés lors de la vérification de l'hôte. #### Expiration du certificat Les champs `Validity Not Before` (Validité avant la date) et `Validity Not After` (Validité après la date) du certificat fournissent une plage de dates entre lesquelles le certificat est valide Normalement, les certificats feuille ont une validité d'un ou deux ans (bien que les sites Web soient encouragés à réduire leurs délais d'expiration à des périodes beaucoup plus courtes). Les autorités de certification ont généralement un délai d'expiration de plusieurs années. L'expiration des certificats est une fonctionnalité TLS nécessaire mais peu pratique. Avant d'ajouter TLS à vos connexions HL7, assurez-vous d'avoir un plan pour remplacer les certificats avant leur expiration. Une fois qu'un certificat expire, vous ne pourrez plus établir de connexion TLS à l'aide de celui-ci. ### Formats de certificat X.509 Les champs de ces certificats X.509 (ainsi que d'autres) sont structurés suivant le format [ASN.1](https://en.wikipedia.org/wiki/ASN.1) et généralement enregistrés dans l'un des formats de fichier suivants : - [DER] (https://en.wikipedia.org/wiki/Distinguished_Encoding_Rules) (format binaire) - [PEM] (https://en.wikipedia.org/wiki/Privacy-Enhanced_Mail) (base64) Exemple d'encodage PEM d'un certificat X.509: ``` -----BEGIN CERTIFICATE----- MIIEVTCCAz2gAwIBAgIQMm4hDSrdNjwKZtu3NtAA9DANBgkqhkiG9w0BAQsFADA7 MQswCQYDVQQGEwJVUzEeMBwGA1UEChMVR29vZ2xlIFRydXN0IFNlcnZpY2VzMQww CgYDVQQDEwNXUjIwHhcNMjUwMTIwMDgzNzU0WhcNMjUwNDE0MDgzNzUzWjAZMRcw FQYDVQQDEw53d3cuZ29vZ2xlLmNvbTBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IA BDx/pIz8HwLWsWg16BG6YqeIYBGof9fn6z6QwQ2v6skSaJ9+0UaduP4J3K61Vn2v US108M0Uo1R1PGkTvVlo+C+jggJAMIICPDAOBgNVHQ8BAf8EBAMCB4AwEwYDVR0l BAwwCgYIKwYBBQUHAwEwDAYDVR0TAQH/BAIwADAdBgNVHQ4EFgQU3rId2EvtObeF NL+Beadr56BlVZYwHwYDVR0jBBgwFoAU3hse7XkV1D43JMMhu+w0OW1CsjAwWAYI KwYBBQUHAQEETDBKMCEGCCsGAQUFBzABhhVodHRwOi8vby5wa2kuZ29vZy93cjIw JQYIKwYBBQUHMAKGGWh0dHA6Ly9pLnBraS5nb29nL3dyMi5jcnQwGQYDVR0RBBIw EIIOd3d3Lmdvb2dsZS5jb20wEwYDVR0gBAwwCjAIBgZngQwBAgEwNgYDVR0fBC8w LTAroCmgJ4YlaHR0cDovL2MucGtpLmdvb2cvd3IyLzlVVmJOMHc1RTZZLmNybDCC AQMGCisGAQQB1nkCBAIEgfQEgfEA7wB2AE51oydcmhDDOFts1N8/Uusd8OCOG41p wLH6ZLFimjnfAAABlIMTadcAAAQDAEcwRQIgf6SEH+xVO+nGDd0wHlOyVTbmCwUH ADj7BJaSQDR1imsCIQDjJjt0NunwXS4IVp8BP0+1sx1BH6vaxgMFOATepoVlCwB1 AObSMWNAd4zBEEEG13G5zsHSQPaWhIb7uocyHf0eN45QAAABlIMTaeUAAAQDAEYw RAIgBNtbWviWZQGIXLj6AIEoFKYQW4pmwjEfkQfB1txFV20CIHeouBJ1pYp6HY/n 3FqtzC34hFbgdMhhzosXRC8+9qfGMA0GCSqGSIb3DQEBCwUAA4IBAQCHB09Uz2gM A/gRNfsyUYvFJ9J2lHCaUg/FT0OncW1WYqfnYjCxTlS6agVUPV7oIsLal52ZfYZU lNZPu3r012S9C/gIAfdmnnpJEG7QmbDQZyjF7L59nEoJ80c/D3Rdk9iH45sFIdYK USAO1VeH6O+kAtFN5/UYxyHJB5sDJ9Cl0Y1t91O1vZ4/PFdMv0HvlTA2nyCsGHu9 9PKS0tM1+uAT6/9abtqCBgojVp6/1jpx3sx3FqMtBSiB8QhsIiMa3X0Pu4t0HZ5j YcAkxtIVpNJ8h50L/52PySJhW4gKm77xNCnAhAYCdX0sx76eKBxB4NqMdCR945HW tDUHX+LWiuJX -----END CERTIFICATE----- ``` Comme vous pouvez le voir, l'encodage PEM ajoute -----BEGIN CERTIFICATE----- et -----END CERTIFICATE----- aux données ASN.1 du certificat encodées en base64. ## Établir la confiance avec les autorités de certification Sur l'Internet ouvert, il serait impossible pour votre navigateur Web de connaître et de faire confiance au certificat de chaque site Web. Il y en a tout simplement trop! Pour contourner ce problème, votre navigateur Web délègue la confiance à un ensemble prédéterminé d'autorités de certification (AC). Les autorités de certification sont des entités qui vérifient qu'une personne demandant un certificat pour un site Web ou un domaine est bien propriétaire et responsable du serveur, du domaine ou des activités commerciales associés à la demande de certificat. Une fois que l'autorité de certification a vérifié un propriétaire, elle est en mesure d'émettre le certificat demandé. Chaque autorité de certification est représentée par un ou plusieurs certificats X.509. Ces certificats CA sont utilisés pour signer tous les certificats émis par la CA. Si vous regardez dans le champ `Issuer` (Émetteur) d'un certificat X.509, vous trouverez une référence au certificat CA qui a créé et signé ce certificat. Si un certificat est créé sans autorité de certification, il est appelé certificat *auto-signé*. Vous savez qu'un certificat est auto-signé si les champs `Subject` (Sujet) et `Issuer` (Émetteur) du certificat sont identiques. En général, la CA crée un certificat `root` (racine) auto-signé avec une longue fenêtre d'expiration. Ce certificat racine est ensuite utilisé pour générer quelques autorités de certification `intermédiaires`, qui ont une fenêtre d'expiration légèrement plus courte. La CA racine sera sécurisée et rarement utilisée après la création des CA intermédiaires. Les CA intermédiaires seront utilisées pour émettre et signer les certificats `leaf` (feuille) au quotidien. Les CA intermédiaires sont créées au lieu d'utiliser directement la CA racine afin de minimiser l'impact en cas de violation ou de mauvaise gestion d'un certificat. Si une seule CA intermédiaire est compromise, l'entreprise aura toujours les autres CA disponibles pour continuer à fournir le service. ### Chaînes de certificats Un certificat de connexion et tous les certificats CA impliqués dans l'émission et la signature de ce certificat peuvent être organisés en une structure appelée *chaîne de certificats*. Cette chaîne de certificats (décrite ci-dessous) sera utilisée pour vérifier et approuver le certificat de connexion. Si vous suivez le certificat feuille d'une connexion jusqu'à la CA émettrice (en utilisant le champ `Issuer`) puis, à partir de cette CA, jusqu'à son émetteur (et ainsi de suite, jusqu'à ce que vous atteigniez un certificat racine auto-signé), vous aurez parcouru la chaîne de certificats. ![Construction d'une chaîne de certificats](/sites/default/files/inline/images/certificate_chain.gif "Building a Certificate Chain") ### Faire confiance à un certificat Votre navigateur Web et votre système d'exploitation conservent généralement une liste d'autorités de certification approuvées. Lors de la configuration d'une interface HL7 ou d'une autre application, vous dirigerez probablement votre interface vers un fichier CA-bundle contenant une liste de CA approuvées. Ce fichier contiendra généralement une liste d'un ou plusieurs certificats CA encodés au format PEM. Par exemple: ``` # Probablement, une CA intermédiaire -----BEGIN CERTIFICATE----- MIIDQTCCAimgAwIBAgITBmyfz5m/jAo54vB4ikPmljZbyjANBgkqhkiG9w0BAQsF ... rqXRfboQnoZsG4q5WTP468SQvvG5 -----END CERTIFICATE----- # Probablement, une CA racine -----BEGIN CERTIFICATE----- MIIDqDCCApCgAwIBAgIJAP7c4wEPyUj/MA0GCSqGSIb3DQEBBQUAMDQxCzAJBgNV ... WyH8EZE0vkHve52Xdf+XlcCWWC/qu0bXu+TZLg== -----END CERTIFICATE----- ``` Lorsque votre navigateur Web (ou l'interface HL7) tente d'établir une connexion TLS, il utilise cette liste de certificats CA de confiance pour déterminer s'il fait confiance au certificat échangé lors du handshake TLS. Le processus commence par le certificat racine et traverse la chaîne de certificats jusqu'au certificat CA suivant. Si le certificat CA n'est pas trouvé dans le magasin de confiance ou le fichier CA-bundle, le certificat racine *n'est pas* considéré comme fiable et la connexion TLS échoue. Si le certificat CA ou le fichier CA-bundle est trouvé dans le magasin de confiance, le processus continue en remontant la chaîne de certificats, en vérifiant que chaque CA se trouvant sur le chemin est dans le magasin de confiance. Une fois que le certificat CA racine au sommet de la chaîne est vérifié (ainsi que tous les certificats CA intermédiaires se trouvant sur le chemin), le processus peut approuver le certificat feuille du serveur. ![Détermination de la confiance](/sites/default/files/inline/images/determining_trust.gif "Determining Trust") ## Le handshake TLS Pour ajouter TLS à une connexion TCP/IP (comme un flux HL7), le client et le serveur doivent effectuer un handshake TLS après que la connexion TCP/IP a été établie. Ce handshake implique de s'accorder sur les chiffrements/méthodes de chiffrement, de s'accorder sur la version TLS, d'échanger des certificats X.509, de prouver la propriété de ces certificats et de valider que chaque partie fait confiance à l'autre. Les étapes principales d'un handshake TLS sont les suivantes: 1. Le client établit une connexion TCP/IP avec le serveur. 2. Le client lance le handshake TLS. 3. Le serveur envoie son certificat (et la preuve de sa propriété) au client. 4. Le client vérifie le certificat du serveur. 5. En cas de TLS mutuel, le client envoie son certificat (et la preuve de sa propriété) au serveur. 6. En cas de TLS mutuel, le serveur vérifie le certificat du client. 7. Le client et le serveur s'échangent des données encryptées. ![Handshake TLS](/sites/default/files/inline/images/tls_handshake.gif "TLS Handshake") ### 1. Le client établit une connexion TCP/IP avec le serveur. À l'étape n° 1, le client et le serveur effectuent un handshake TCP à la procédure de base « ternaire » [TCP 3-way handshake] (https://en.wikipedia.org/wiki/Transmission_Control_Protocol#Connection_establishment) pour établir une connexion TCP/IP entre eux. Dans un handshake à la procédure de base ternaire: 1. Le client envoie un paquet `SYN`. 2. Le serveur envoie un paquet `SYN-ACK`. 3. Le client envoie un paquet `ACK`. Une fois ce handshake terminé, la connexion TCP/IP est établie. L'étape suivante consiste à lancer le handshake TLS. ### 2. Le client lance le handshake TLS. Une fois la connexion TCP établie, l'une des parties doit agir en tant que client et lancer le handshake TLS. Généralement, le processus qui a initié la connexion TCP est également responsable du lancement du handshake TLS, mais cela peut être inversé dans de rares cas. Pour lancer le handshake TLS, le client envoie un message ClientHello au serveur. Ce message contient diverses options utilisées pour négocier les paramètres de sécurité de la connexion avec le serveur. ### 3. Le serveur envoie son certificat (et la preuve de sa propriété) au client. Après avoir reçu le message `ClientHello` du client, le serveur répond à son tour par un message `ServerHello`. Celui-ci inclut les paramètres de sécurité négociés. Après le message `ServerHello`, le serveur envoie également un message `Certificate` et `CertificateVerify` au client. Cela permet de partager la chaîne de certificats X.509 avec le client et de fournir la preuve de propriété de la clé privée associée au certificat. ### 4. Le client vérifie le certificat du serveur. Une fois que le client a reçu les messages `ServerHello`, `Certificate` et `CertificateVerify`, il vérifie que le certificat est valide et approuvé (en comparant les CAs aux fichiers CA-bundle approuvés, au magasin de certificats du système opérationnel ou au magasin de certificats du navigateur web). Le client effectue également toute vérification de l'hôte (voir ci-dessus) pour s'assurer que l'adresse de connexion correspond aux adresses/IP du certificat. ### 5. S'il s'agit d'une connexion TLS mutuelle, le client envoie son certificat (et la preuve de propriété) au serveur. S'il s'agit d'une connexion TLS mutuelle (déterminée par l'envoi d'un message `CertificateRequest` par le serveur), le client enverra un message `Certificate` incluant sa chaîne de certificats, puis un message `CertificateVerify` pour prouver qu'il est le propriétaire de la clé privée associée. ### 6. S'il s'agit d'une connexion TLS mutuelle, le serveur vérifie le certificat client. Là encore, s'il s'agit d'une connexion TLS mutuelle, le serveur vérifie que la chaîne de certificats envoyée par le client est valide et approuvée. ### 7. Le client et le serveur s'échangent des données encryptées. Si la négociation TLS se déroule sans erreur, le client et le serveur s'échangent des messages `Finished` (Terminé) pour achever la négociation. Après cela, les données encryptées peuvent être échangées entre le client et le serveur. ## Configuration de TLS sur les interfaces HL7 Félicitations d'être arrivé jusqu'ici ! Maintenant que vous savez *en quoi* consiste TLS, comment procéderiez-vous pour *mettre en œuvre* le protocole TLS sur vos connexions HL7 ? De manière générale, voici les étapes à suivre pour configurer TLS sur vos connexions HL7. 1. Choisissez une autorité de certification. 2. Créez une clé et une demande de signature de certificat. 3. Obtenez votre certificat auprès de votre CA. 4. Obtenez la chaîne de certificats pour votre pair. 5. Créez une configuration SSL pour la connexion. 6. Ajoutez la configuration SSL à l'interface, faites rebondir l'interface et vérifiez le flux de messages. ### 1. Choisissez une autorité de certification. La procédure que vous utiliserez pour obtenir un certificat et une clé pour votre serveur dépendra largement des politiques de sécurité de votre entreprise. Dans la plupart des cas, votre certificat sera signé par l'une des autorités de certification suivantes: 1. Votre certificat sera signé par une CA interne à l'entreprise. - C'est mon option préférée, car votre entreprise dispose déjà de l'infrastructure nécessaire pour gérer les certificats et les CAs. Il vous suffit de travailler avec l'équipe qui possède cette infrastructure pour obtenir votre propre certificat pour vos interfaces HL7. 2. Votre certificat sera signé par une CA publique. - Cette option est intéressante dans le sens où la CA publique dispose également de toute l'infrastructure nécessaire pour maintenir les certificats et les CAs. Cette option est sans doute exagérée pour la plupart des interfaces HL7, car les CA publiques fournissent généralement des certificats pour l'Internet ouvert ; les interfaces HL7 ont tendance à se connecter via un intranet privé, et non via l'Internet public. - L'obtention de certificats auprès d'une CA publique peut également entraîner des frai. 3. Votre certificat sera signé par une CA que vous créerez et maintiendrezvous-même. - Cette option peut vous convenir, mais malheureusement, cela signifie que vous supportez la charge de la maintenance et de la sécurisation de votre configuration CA et de votre logiciel. - Vous l'utilisez à vos risques et périls! - Cette option est la plus complexe. Préparez-vous à une courbe d'apprentissage abrupte. - Vous pouvez utiliser des progiciels open source éprouvés pour gérer votre CA et vos certificats. La suite OpenSSL est une excellente option. Les autres options sont EJBCA, step-ca et cfssl. ### 2. Créez une clé et une demande de signature de certificat. Après avoir choisi votre CA, l'étape suivante consiste à créer une clé privée et une demande de signature de certificat (`CSR`) . La manière dont vous générez la clé et la CSR dépendra de la politique de votre entreprise et de la CA choisie. Pour l'instant, nous allons simplement parler des étapes de manière générale. Lors de la génération d'une clé privée, la clé publique associée est également générée. La clé publique sera intégrée à votre CSR et à votre certificat signé. Ces deux clés seront utilisées pour prouver la propriété de votre certificat signé lors de l'établissement d'une connexion TLS. ATTENTION! Veillez à enregistrer votre clé privée dans un endroit sûr (de préférence dans un format protégé par un mot de passe). Si vous perdez cette clé, votre certificat ne sera plus utilisable. Si quelqu'un d'autre accède à cette clé, il pourra se faire passer pour votre serveur. La demande de signature de certificat inclura des informations sur votre serveur, votre entreprise, votre clé publique, la manière d'utiliser le certificat, etc. Elle inclura également la preuve que vous possédez la clé privée associée. Cette CSR sera ensuite fournie à votre CA pour générer et signer votre certificat. REMARQUE: lors de la création de la CSR, assurez-vous de demander une `Extended Key Usage` (utilisation étendue de la clé) à la fois pour `serverAuth` et `clientAuth`, si vous utilisez le TLS mutuel. La plupart des CA sont habituées à signer des certificats avec uniquement la clé `serverAuth`. Malheureusement, cela signifie que le certificat ne peut pas être utilisé comme certificat client dans une connexion TLS mutuelle. ### 3. Obtenez votre certificat auprès de votre CA. Après avoir créé votre clé et votre CSR, soumettez la CSR à votre autorité de certification. Après avoir effectué plusieurs vérifications, votre CA devrait être en mesure de vous fournir un certificat signé et la chaîne de certificats associée. Ce certificat et cette chaîne doivent être enregistrés au format PEM. Si la CA a fourni votre certificat dans un format différent, vous devrez le convertir à l'aide d'un outil tel qu'OpenSSL. ### 4. Obtenez la chaîne de certificats pour votre homologue. Les étapes précédentes étaient axées sur l'obtention d'un certificat pour votre serveur. Vous devriez pouvoir utiliser ce certificat (et la clé associée) avec chaque connexion HL7 vers/depuis ce serveur. Vous devrez également obtenir les chaînes de certificats pour chacun des systèmes/homologues auxquels vous vous connecterez. Les chaînes de certificats de chaque homologue devront être enregistrées dans un fichier au format PEM. Ce CA-bundle n'aura pas besoin de contenir les certificats feuille ; il doit uniquement contenir les certificats CA intermédiaires et racine. Veillez à fournir à votre homologue un CA-bundle contenant vos CA intermédiaires et racine. Cela lui permettra de faire confiance à votre certificat lorsque vous établirez une connexion. ### 5. Créez une configuration SSL pour la connexion. Dans Health Connect d'InterSystems, il vous faudra créer des configurations SSL client et serveur pour chaque système auquel votre serveur se connectera. Ces configurations SSL dirigeront vers le fichier CA-bundle du système associé et vers les fichiers clé et de certificat de votre serveur. Les configurations SSL client sont utilisées lors des opérations pour lancer le handshake TLS. Les configurations SSL serveur sont utilisées sur les services pour répondre aux handshakes TLS. Si un système dispose à la fois de services entrants et de services sortants, il faudra configurer à la fois une configuration SSL client et une configuration SSL serveur pour ce système. Pour créer une configuration SSL client: 1. Accédez à `System Administration > Security > SSL/TLS Configurations` (Administration système > Sécurité > Configurations SSL/TLS). 2. Appuyeze sur `Create New Configuration` (Créer une nouvelle configuration). 3. Donnez un `Configuration Name` (Nom de configuration) et une `Description` (Description) à votre configuration SSL. 4. Assurez-vous que votre configuration SSL est `Enabled` (Activée). 5. Choisissez `Client` comme `Type`. 6. Choisissez `Require` (Obligatoire) pour le champ `Server certificate verification` (Vérification du certificat serveur). Cela effectue une vérification de l'hôte sur la connexion. 7. Dirigez le champ `File` (Fichier) contenant le(s) certificat(s) CA de confiance vers le fichier CA-bundle contenant les CA intermédiaires et racines (au format PEM) du système auquel vous vous connectez. 8. Dirigez le champ `File` (Fichier) contenant le certificat de ce client vers le fichier contenant le certificat X.509 de votre serveur au format PEM. 9. Dirigez le champ `File` (Fichier) contenant la clé privée associée vers le fichier contenant la clé privée de votre certificat. 10. Le `Private key type` (type de clé privée) sera très probablement `RSA` (chiffrement RSA). Cela devrait correspondre au type de votre clé privée. 11. Si votre clé privée est protégée par un mot de passe (comme cela devrait être le cas), saisissez le mot de passe dans les champs `Private key password` (mot de passe de la clé privée) et `Private key password (confirm)` (confirmer le mot de passe de la clé privée). 12. Vous pouvez probablement laisser les autres champs à leurs valeurs par défaut. Pour créer une configuration de serveur SSL: 1. Go to `System Administration > Security > SSL/TLS Configurations`. 2. Appuyeze sur `Create New Configuration` (Créer une nouvelle configuration). 3. Donnez un `Configuration Name` (Nom de configuration) et une `Description` (Description) à votre configuration SSL. 4. Assurez-vous que votre configuration SSL est `Enabled` (Activée). 5. Choisissez `Server` comme `Type`. 6. Choisissez `Require` (Obligatoire) pour le champ `Client certificate verification` (Vérification du certificat client). Cela permettra de s'assurer que le TLS mutuel est exécuté. 7. Dirigez `File containing trusted Certificate Authority certificate(s)` (Fichier contenant le(s) certificat(s) de l'autorité de certification de confiance) vers le fichier CA-bundle contenant les CA intermédiaires et racines (au format PEM) du système auquel vous vous connectez. 8. Dirigez `File containing this server's certificate` (Fichier contenant le certificat de ce serveur) vers le fichier contenant le certificat X.509 de votre serveur au format PEM. 9. Dirigez `File containing associated private key` (Fichier contenant la clé privée associée) vers le fichier contenant la clé privée de votre certificat. 10. Le `Private key type` (type de clé privée) sera très probablement `RSA` (chiffrement RSA). Cela devrait correspondre au type de votre clé privée. 11. Si votre clé privée est protégée par un mot de passe (comme cela devrait être le cas), saisissez le mot de passe dans les champs `Private key password` (mot de passe de la clé privée) et `Private key password (confirm)` (confirmer le mot de passe de la clé privée). 12. Vous pouvez probablement laisser les autres champs à leurs valeurs par défaut. ![configuration de configuration SSL](/sites/default/files/inline/images/create_ssl_configs.gif "Creating SSL Configs") ### 6. Ajoutez la configuration SSL à l'interface, relancez l'interface et vérifiez le flux de messages. Une fois que vous avez créé les configurations SSL client et serveur, vous êtes prêt à activer TLS sur les interfaces. Pour chaque service ou opération, choisissez la configuration SSL associée dans le menu déroulant `Connection Settings > SSL Configuration` (Paramètres de connexion > Configuration SSL) qui se trouve dans l'onglet *Settings* (Paramètres) de l'interface. Après avoir relancé l'interface, vous verrez la connexion se rétablir. Lorsqu'un nouveau message est transféré, un statut `Completed` (Terminé) indique que TLS fonctionne. Si TLS ne fonctionne pas, la connexion sera interrompue à chaque tentative de message. Pour vous aider à déboguer les problèmes avec TLS, il se peut que vous ayez besoin d'utiliser des outils tels que tcpdump, Wireshark ou l'utilitaire s_client d'OpenSSL. ## Conclusion Nous avons fait une analyse très approfondie du sujet SSL/TLS. Il y a tellement d'autres informations qui n'ont pas été incluses dans cet article. J'espère que cet article vous a fourni un aperçu suffisant du fonctionnement de TLS pour que vous puissiez rechercher les détails et obtenir plus d'informations si nécessaire. Si vous recherchez une ressource approfondie sur TLS, consultez le site Web d'Ivan Ristić, [fiestyduck.com](https://www.feistyduck.com/) et son livre, [Bulletproof TLS and PKI](https://www.feistyduck.com/books/bulletproof-tls-and-pki/). J'ai trouvé que ce livre était une excellente ressource pour en savoir plus sur l'utilisation de TLS.
Article
Sylvain Guilbaud · Mars 29, 2023

Prédictions de Covid-19 ICU via ML vs. IntegratedML (Partie I)

Keywords:  IRIS, IntegratedML, apprentissage automatique, Covid-19, Kaggle  ## Objectif J'ai récemment remarqué un [jeu de données Kaggle](https://www.kaggle.com/S%C3%ADrio-Libanes/covid19/kernels) permettant de prédire si un patient Covid-19 sera admis en soins intensifs. Il s'agit d'un tableur de 1925 enregistrements comprenant 231 colonnes de signes vitaux et d'observations, la dernière colonne " USI " valant 1 pour Oui ou 0 pour Non. Ce jeu de données représente un bon exemple de ce que l'on appelle une tâche "traditionnelle de ML". Les données semblent avoir une quantité suffisante et une qualité relativement bonne. Il pourrait avoir de meilleures chances d'être appliqué directement sur le kit [IntegratedML demo](https://github.com/intersystems-community/integratedml-demo-template). Quelle serait donc l'approche la plus simple pour un test rapide basé sur les pipelines ML normaux par rapport à l'approche possible avec IntegratedML ?   ## Champ d'application Nous examinerons brièvement quelques étapes normales de ML, telles que : * Analyse des données (EDA) * Sélection des caractéristiques * Sélection du modèle * Ajustement des paramètres du modèle via le quadrillage Vs.  * Approches ML intégrées via SQL. Il est exécuté sur un serveur AWS Ubuntu 16.04 avec Docker-compose, etc.     ## Environnement Nous allons réutiliser l'environnement Docker de [integredML-demo-template](https://openexchange.intersystems.com/package/integratedml-demo-template): ![](https://user-images.githubusercontent.com/8899513/85151307-a0d1f280-b221-11ea-81d8-f0e11ca45d4c.PNG) Le fichier de bloc-notes suivant est exécuté sur "tf2jupyter", et IRIS avec IntegratedML sur "irismlsrv". Docker-compose fonctionne sur un AWS Ubuntu 16.04.   ## Données et tâches Le jeu de données contient 1925 enregistrements collectés auprès de 385 patients, chacun comportant exactement 5 enregistrements de rendez-vous. Sur ses 231 colonnes, une seule, "USI", constitue notre cible d'apprentissage et de prédiction, et les 230 autres colonnes pourraient toutes être utilisées comme entrées de quelque manière que ce soit. L'unité de soins intensifs a une valeur binaire de 1 ou 0. À l'exception de deux colonnes qui semblent être des chaînes catégorielles (présentées comme "objet" dans le cadre de données), toutes les autres sont numériques. import numpy as np import pandas as pd from sklearn.impute import SimpleImputer import matplotlib.pyplot as plt from sklearn.linear_model import LogisticRegression from sklearn.model_selection import train_test_split from sklearn.metrics import classification_report, roc_auc_score, roc_curve import seaborn as sns sns.set(style="whitegrid") import os for dirname, _, filenames in os.walk('./input'): for filename in filenames: print(os.path.join(dirname, filename)) ./input/datasets_605991_1272346_Kaggle_Sirio_Libanes_ICU_Prediction.xlsx   df = pd.read_excel("./input/datasets_605991_1272346_Kaggle_Sirio_Libanes_ICU_Prediction.xlsx") df   IDENTIFIANT_DE_VISITE_DU_PATIENT ÂGE_AU-DESSUS65 ÂGE_POURCENTAGE GENRE GROUPE DE MALADIES 1 GROUPE DE MALADIES 2 GROUPE DE MALADIES 3 GROUPE DE MALADIES 4 GROUPE DE MALADIES 5 GROUPE DE MALADIES 6 ... DIFFÉRENCE_DE_TEMPÉRATURE DIFFÉRENCE_DE SATURATION_D'OXYGÈNE DIFFÉRENCE_DE_TENSION_DIASTOLIQUE_REL DIFFÉRENCE_DE_TENSION_SISTOLIQUE_REL DIFFÉRENCE_DU_RYTHME_CARDIAQUE_REL DIFFÉRENCE_DE_TAUX_RESPIRATOIRE_REL DIFFÉRENCE_DE_TEMPÉRATURE_REL DIFFÉRENCE_DE_SATURATION_D'OXYGÈNE_REL FENÊTRE ICU 1 âge de 60-69 ans 0.0 0.0 0.0 0.0 1.0 1.0 ... -1.000000 -1.000000 -1.000000 -1.000000 -1.000000 -1.000000 -1.000000 -1.000000 0-2 1 1 âge de 60-69 ans 0.0 0.0 0.0 0.0 1.0 1.0 ... -1.000000 -1.000000 -1.000000 -1.000000 -1.000000 -1.000000 -1.000000 -1.000000 2-4 2 1 âge de 60-69 ans 0.0 0.0 0.0 0.0 1.0 1.0 ... NaN NaN NaN NaN NaN NaN NaN NaN 4-6 3 1 âge de 60-69 ans 0.0 0.0 0.0 0.0 1.0 1.0 ... -1.000000 -1.000000 NaN NaN NaN NaN -1.000000 -1.000000 6-12 4 1 âge de 60-69 ans 0.0 0.0 0.0 0.0 1.0 1.0 ... -0.238095 -0.818182 -0.389967 0.407558 -0.230462 0.096774 -0.242282 -0.814433 AU-DESSUS_12 1 ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... 1920 384 âge de 50-59 ans 1 0.0 0.0 0.0 0.0 0.0 0.0 ... -1.000000 -1.000000 -1.000000 -1.000000 -1.000000 -1.000000 -1.000000 -1.000000 0-2 1921 384 âge de 50-59 ans 1 0.0 0.0 0.0 0.0 0.0 0.0 ... -1.000000 -1.000000 -1.000000 -1.000000 -1.000000 -1.000000 -1.000000 -1.000000 2-4 1922 384 âge de 50-59 ans 1 0.0 0.0 0.0 0.0 0.0 0.0 ... -1.000000 -1.000000 -1.000000 -1.000000 -1.000000 -1.000000 -1.000000 -1.000000 4-6 1923 384 âge de 50-59 ans 1 0.0 0.0 0.0 0.0 0.0 0.0 ... -1.000000 -1.000000 -1.000000 -1.000000 -1.000000 -1.000000 -1.000000 -1.000000 6-12 1924 384 âge de 50-59 ans 1 0.0 0.0 1.0 0.0 0.0 0.0 ... -0.547619 -0.838384 -0.701863 -0.585967 -0.763868 -0.612903 -0.551337 -0.835052 AU-DESSUS_12 1925 lignes × 231 colonnes df.dtypes IDENTIFIANT_DE_VISITE_DU_PATIENT int64 ÂGE_AU-DESSUS65 int64 ÂGE_POURCENTAGE object GENRE int64 GROUPE DE MALADIES 1 float64 ... DIFFÉRENCE_DE_TAUX_RESPIRATOIRE_REL float64 DIFFÉRENCE_DE_TEMPÉRATURE_REL float64 DIFFÉRENCE_DE SATURATION_D'OXYGÈNE_REL float64 FENÊTRE object USI int64 Longeur: 231, dtype: object Il existe certainement plusieurs options pour définir ce problème et ses approches. La première option qui nous vient à l'esprit est qu'il peut s'agir d'un problème fondamental de "classification binaire". Nous pouvons traiter les 1925 enregistrements comme des enregistrements individuels "apatrides", qu'ils proviennent ou non du même patient. Bien sûr, il pourrait également s'agir d'un problème de "régression" si nous traitions les valeurs de l'unité de soins intensifs et d'autres valeurs comme étant toutes numériques. Il existe certainement d'autres approches possibles. Par exemple, nous pouvons considérer que l'ensemble de données comporte 385 jeux distincts de courtes "séries temporelles", chacun pour un patient. Nous pourrions dissoudre le jeu entier en 385 jeux distincts pour Train/Val/Test, et pourrions-nous essayer des modèles d'apprentissage profond tels que CNN ou LSTM pour capturer la "phase ou le modèle de développement des symptômes" caché dans chaque jeu pour chaque patient individuel ? C'est possible. Ce faisant, nous pourrions également appliquer une augmentation des données pour enrichir les données de test par divers moyens. Il s'agit là d'un sujet qui dépasse le cadre de cet article. Dans cet article, nous nous contenterons de tester rapidement l'approche ML dite "traditionnelle" par rapport à l'approche IntegratedML (une approche AutoML)..     ## Approche ML "traditionnelle" ? Il s'agit d'un jeu de données relativement normalisé par rapport à la plupart des cas réels, à l'exception de quelques valeurs manquantes, de sorte que nous pourrions sauter la partie relative à l'ingénierie des caractéristiques et utiliser directement les colonnes comme caractéristiques. Passons donc directement à la sélection des caractéristiques. ### **Imputation des données manquantes** Il faut d'abord s'assurer que toutes les valeurs manquantes sont remplies au moyen d'une imputation simple : df_cat = df.select_dtypes(include=['object']) df_numeric = df.select_dtypes(exclude=['object']) imp = SimpleImputer(missing_values=np.nan, strategy='mean') idf = pd.DataFrame(imp.fit_transform(df_numeric)) idf.columns = df_numeric.columns idf.index = df_numeric.index idf.isnull().sum() ###   ### **Sélection sur les caractéristiques** Nous pouvons certainement utiliser la fonction de corrélation normale intégrée dans la base de données pour calculer la corrélation entre les valeurs de chaque colonne et les unités de soins intensifs. #### l'ingénierie des caractéristiques - **corrélation** {#featuring-engineering---correlation} idf.drop(["PATIENT_VISIT_IDENTIFIER"],1) idf = pd.concat([idf,df_cat ], axis=1) cor = idf.corr() cor_target = abs(cor["ICU"]) relevant_features = cor_target[cor_target>0.1] # correlation above 0.1 print(cor.shape, cor_target.shape, relevant_features.shape) #relevant_features.index #relevant_features.index.shape Il répertorie 88 caractéristiques présentant une corrélation >0,1 avec la valeur cible de l'unité de soins intensifs. Ces colonnes peuvent être directement utilisées comme entrée de notre modèle J'ai également exécuté quelques autres "méthodes de sélection de caractéristiques" qui sont normalement utilisées dans les tâches traditionnelles de ML : #### Sélection des caractéristiques - **Chi carré** {#feature-selection---Chi-squared} from sklearn.feature_selection import SelectKBest from sklearn.feature_selection import chi2 from sklearn.preprocessing import MinMaxScaler X_norm = MinMaxScaler().fit_transform(X) chi_selector = SelectKBest(chi2, k=88) chi_selector.fit(X_norm, y) chi_support = chi_selector.get_support() chi_feature = X.loc[:,chi_support].columns.tolist() print(str(len(chi_feature)), 'selected features', chi_feature) 88 caractéristiques sélectionnées ['ÂGE_AU-DESSUS65', 'GENRE', 'GROUPE DE MALADIES 1', ... ... 'P02_VENEUS_MIN', 'P02_VENEUS_MAX', ... ... RATURE_MAX', 'DIFFÉRENCE_DE_TENSION_ARTÉRIELLE_DIASTOLIQUE', ... ... 'DIFFÉRENCE_DE_TEMPÉRATURE_REL', 'DIFFÉRENCE_DE SATURATION_D'OXYGÈNE_REL'] Sélection des caractéristiques - **Corrélation de Pearson**  def cor_selector(X, y,num_feats): cor_list = [] feature_name = X.columns.tolist() # calculate the correlation with y for each feature for i in X.columns.tolist(): cor = np.corrcoef(X[i], y)[0, 1] cor_list.append(cor) # replace NaN with 0 cor_list = [0 if np.isnan(i) else i for i in cor_list] # feature name cor_feature = X.iloc[:,np.argsort(np.abs(cor_list))[-num_feats:]].columns.tolist() # Sélection des caractéristiques? 0 for not select, 1 for select cor_support = [Vrai if i in cor_feature else False for i in feature_name] return cor_support, cor_feature cor_support, cor_feature = cor_selector(X, y, 88) print(str(len(cor_feature)), 'selected features: ', cor_feature) 88 caractéristiques sélectionnées: ['TEMPÉRATURE_MOYENNE', 'TENSION_DIASTOLIQUE_MAX', ... ... 'DIFFÉRENCE_DE_TAUX_ RESPIRATOIRE', 'AUX_ RESPIRATOIRE_MAX'] #### Sélection des caractéristiques - **élimination de caractéristiques récursives (RFE)** {#feature-selection---Recursive-Feature-Elimination-(RFE)} from sklearn.feature_selection import RFE from sklearn.linear_model import LogisticRegression rfe_selector = RFE(estimator=LogisticRegression(), n_features_to_select=88, step=100, verbose=5) rfe_selector.fit(X_norm, y) rfe_support = rfe_selector.get_support() rfe_feature = X.loc[:,rfe_support].columns.tolist() print(str(len(rfe_feature)), 'selected features: ', rfe_feature) Estimateur d'ajustement avec 127 caractéristiques. 88 caractéristiques sélectionnées: ['ÂGE_AU-DESSUS65', 'GENRE', ... ... 'DIFFÉRENCE_DE_TAUX_ RESPIRATOIRE_REL', 'DIFFÉRENCE_DE_TEMPÉRATURE_REL'] Sélection des caractéristiques - **Lasso** ffrom sklearn.feature_selection import SelectFromModel from sklearn.linear_model import LogisticRegression from sklearn.preprocessing import MinMaxScaler X_norm = MinMaxScaler().fit_transform(X) embeded_lr_selector = SelectFromModel(LogisticRegression(penalty="l2"), max_features=88) embeded_lr_selector.fit(X_norm, y) embeded_lr_support = embeded_lr_selector.get_support() embeded_lr_feature = X.loc[:,embeded_lr_support].columns.tolist() print(str(len(embeded_lr_feature)), 'selected features', embeded_lr_feature) 65 caractéristiques sélectionnées ['ÂGE_AU-DESSUS65', 'GENRE', ... ... 'DIFFÉRENCE_DE_TAUX_ RESPIRATOIRE_REL', 'DIFFÉRENCE_DE_TEMPÉRATURE_REL'] Sélection des caractéristiques - **RF Tree-based**: SelectFromModel from sklearn.feature_selection import SelectFromModel from sklearn.ensemble import RandomForestClassifier embeded_rf_selector = SelectFromModel(RandomForestClassifier(n_estimators=100), max_features=227) embeded_rf_selector.fit(X, y) embeded_rf_support = embeded_rf_selector.get_support() embeded_rf_feature = X.loc[:,embeded_rf_support].columns.tolist() print(str(len(embeded_rf_feature)), 'selected features', embeded_rf_feature) 48 selected features ['ÂGE_AU-DESSUS65', 'GENRE', ... ... 'DIFFÉRENCE_DE_TEMPÉRATURE_REL', 'DIFFÉRENCE_DE SATURATION_D'OXYGÈNE_REL'] #### Sélection des caractéristiques - **LightGBM** or **XGBoost** {#feature-selection---LightGBM-or-XGBoost} from sklearn.feature_selection import SelectFromModel from lightgbm import LGBMClassifier lgbc=LGBMClassifier(n_estimators=500, learning_rate=0.05, num_leaves=32, colsample_bytree=0.2, reg_alpha=3, reg_lambda=1, min_split_gain=0.01, min_child_weight=40) embeded_lgb_selector = SelectFromModel(lgbc, max_features=128) embeded_lgb_selector.fit(X, y) embeded_lgb_support = embeded_lgb_selector.get_support() embeded_lgb_feature = X.loc[:,embeded_lgb_support].columns.tolist() print(str(len(embeded_lgb_feature)), 'selected features: ', embeded_lgb_feature) embeded_lgb_feature.index 56 selected features: ['ÂGE_AU-DESSUS65', 'GENRE', 'HTN', ... ... 'DIFFÉRENCE_DE_TEMPÉRATURE_REL', 'DIFFÉRENCE_DE SATURATION_D'OXYGÈNE_REL'] #### Sélection des caractéristiques - **Les regrouper tous** {#feature-selection---Ensemble-them-all} feature_name = X.columns.tolist() # regrouper toute la sélection feature_selection_df = pd.DataFrame({'Feature':feature_name, 'Pearson':cor_support, 'Chi-2':chi_support, 'RFE':rfe_support, 'Logistics':embeded_lr_support, 'Random Forest':embeded_rf_support, 'LightGBM':embeded_lgb_support}) # compter les temps sélectionnés pour chaque caractéristique feature_selection_df['Total'] = np.sum(feature_selection_df, axis=1) # afficher les 100 premières num_feats = 227 feature_selection_df = feature_selection_df.sort_values(['Total','Feature'] , ascending=False) feature_selection_df.index = range(1, len(feature_selection_df)+1) feature_selection_df.head(num_feats) df_selected_columns = feature_selection_df.loc[(feature_selection_df['Total'] > 3)] df_selected_columns Nous pouvons dresser la liste des caractéristiques qui ont été sélectionnées dans le cadre d'au moins quatre méthodes : ![](/sites/default/files/inline/images/images/image(810).png) ... ... ![](/sites/default/files/inline/images/images/image(812).png) Nous pouvons certainement choisir ces 58 caractéristiques. Entre-temps, l'expérience nous a appris que la sélection des caractéristiques n'est pas nécessairement toujours un vote démocratique ; le plus souvent, elle peut être spécifique au problème du domaine, aux données spécifiques et parfois au modèle ou à l'approche ML spécifique que nous allons adopter plus tard. Sélection des caractéristiques - **Outils tiers**  Il existe des outils industriels et des outils AutoML largement utilisés, par exemple DataRobot qui peut fournir une bonne sélection automatique des caractéristiques :   ![](/sites/default/files/inline/images/images/capture_feature.png) Le graphe DataRobot ci-dessus montre, sans surprise, que les valeurs de fréquence respiratoire et de tension artérielle sont les caractéristiques les plus pertinentes pour l'admission en soins intensifs.     Sélection des caractéristiques - **Sélection finale** Dans ce cas, j'ai fait quelques expériences rapides et j'ai remarqué que la sélection des caractéristiques par LightGBM donnait un résultat un peu meilleur, c'est pourquoi nous n'utiliserons que cette méthode de sélection.    df_selected_columns = embeded_lgb_feature # mieux que la sélection ensembliste dataS = pd.concat([idf[df_selected_columns],idf['ICU'], df_cat['FENÊTRE']],1) dataS.ICU.value_counts() print(dataS.shape) (1925, 58) Nous pouvons voir que 58 caractéristiques sont sélectionnées, c'est-à-dire ni trop peu, ni trop beaucoup, ce qui semble être la bonne quantité pour ce problème spécifique de classification binaire à cible unique.    ### **Déséquilibre des données** plt.figure(figsize=(10,5)) count = sns.countplot(x = "USI",data=data) count.set_xticklabels(["Non admis", "Admis"]) plt.xlabel("Admission à l'USI") plt.ylabel("Nombre de patients") plt.show() Cela indique que les données sont déséquilibrées, seuls 26 % des enregistrements étant admis en USI. Cela aura un impact sur les résultats et nous pouvons donc envisager des approches normales d'équilibrage des données telles que SMOTE, etc. Nous pouvons essayer toutes sortes d'autres AED pour analyser les différentes distributions de données en conséquence.    ### **Exécuter une formation de base en LR** Le site Kaggle propose de jolis carnets d'entraînement rapide que nous pouvons exécuter rapidement en fonction de notre propre sélection de colonnes de caractéristiques. Commençons par une exécution rapide du classificateur LR pour le pipeline de formation :   data2 = pd.concat([idf[df_selected_columns],idf['USI'], df_cat['FENÊTRE']],1) data2.AGE_ABOVE65 = data2.AGE_ABOVE65.astype(int) data2.ICU = data2.ICU.astype(int) X2 = data2.drop("USI",1) y2 = data2.ICU from sklearn.preprocessing import LabelEncoder label_encoder = LabelEncoder() X2.WINDOW = label_encoder.fit_transform(np.array(X2["FENÊTRE"].astype(str)).reshape((-1,))) confusion_matrix2 = pd.crosstab(y2_test, y2_hat, rownames=['Réel'], colnames=['Prédit']) sns.heatmap(confusion_matrix2, annot=Vrai, fmt = 'g', cmap = 'Reds') print("ORIGINAL") print(classification_report(y_test, y_hat)) print("USI = ",roc_auc_score(y_test, y_hat),'\n\n') print("ENCODAGE D'ÉTIQUETTE") print(classification_report(y2_test, y2_hat)) print("ASC = ",roc_auc_score(y2_test, y2_hat)) y2hat_probs = LR.predict_proba(X2_test) y2hat_probs = y2hat_probs[:, 1] fpr2, tpr2, _ = roc_curve(y2_test, y2hat_probs) plt.figure(figsize=(10,7)) plt.plot([0, 1], [0, 1], 'k--') plt.plot(fpr, tpr, label="Base") plt.plot(fpr2,tpr2,label="Étiquette encodée") plt.xlabel('Taux de faux positifs') plt.ylabel('Taux de vrais positifs') plt.title('Courbe ROC') plt.legend(loc="meilleur") plt.show() ORIGINAL précision rappel score f1 support 0 0.88 0.94 0.91 171 1 0.76 0.57 0.65 54 exactitude 0.85 225 moyenne macro 0.82 0.76 0.78 225 moyenne pondérée 0.85 0.85 0.85 225 ASC= 0.7577972709551657 LABEL ENCODING précision rappel score f1 support 0 0.88 0.93 0.90 171 1 0.73 0.59 0.65 54 accuracy 0.85 225 moyenne macro 0.80 0.76 0.78 225 moyenne pondérée 0.84 0.85 0.84 225 ASC = 0.7612085769980507          Il semble qu'il atteigne une AUC de 76 %, avec une précision de 85 %, mais le rappel pour les patients admis en réanimation n'est que de 59 % - il semble y avoir trop de faux négatifs. Ce n'est certainement pas l'idéal - nous ne voulons pas passer à côté des risques réels de l'USI pour le dossier d'un patient. Toutes les tâches suivantes seront donc axées sur l'objectif sur la manière d'augmenter le taux de rappel, en réduisant le FN, avec une précision globale quelque peu équilibrée, nous l'espérons. Dans les sections précédentes, nous avons mentionné des données déséquilibrées, de sorte que notre premier réflexe serait de stratifier l'ensemble de test et de le MODIFIER pour obtenir un ensemble de données plus équilibré. #stratifier les données de test, afin de s'assurer que les données de train et de test ont le même ratio de 1:0 X3_train,X3_test,y3_train,y3_test = train_test_split(X2,y2,test_size=225/1925,random_state=42, stratify = y2, shuffle = Vrai) <span> </span> # former et prédire LR.fit(X3_train,y3_train) y3_hat = LR.predict(X3_test) #MODIFIER les données pour faire de l'UCI 1:0 une distribution équilibrée from imblearn.over_sampling import SMOTE sm = SMOTE(random_state = 42) X_train_res, y_train_res = sm.fit_sample(X3_train,y3_train.ravel()) LR.fit(X_train_res, y_train_res) y_res_hat = LR.predict(X3_test) #recréer la matrice de confusion, etc. confusion_matrix3 = pd.crosstab(y3_test, y_res_hat, rownames=['Actual'], colnames=['Predicted']) sns.heatmap(confusion_matrix3, annot=Vrai, fmt = 'g', cmap="YlOrBr") print("LABEL ENCODING + STRATIFY") print(classification_report(y3_test, y3_hat)) print("ASC = ",roc_auc_score(y3_test, y3_hat),'\n\n') print("SMOTE") print(classification_report(y3_test, y_res_hat)) print("ASC = ",roc_auc_score(y3_test, y_res_hat)) y_res_hat_probs = LR.predict_proba(X3_test) y_res_hat_probs = y_res_hat_probs[:, 1] fpr_res, tpr_res, _ = roc_curve(y3_test, y_res_hat_probs) plt.figure(figsize=(10,10)) #Et tracez la courbe ROC comme précédemment.   LABEL ENCODING + STRATIFY (CODAGE D'ÉTIQUETTES + STRATIFICATION) précision rappel f1 score support 0 0.87 0.99 0.92 165 1 0.95 0.58 0.72 60 exactitude 0.88 225 moyenne macro 0.91 0.79 0.82 225 moyenne pondérée 0.89 0.88 0.87 225 ASC = 0.7856060606060606 SMOTE précision rappel f1 score support 0 0.91 0.88 0.89 165 1 0.69 0.75 0.72 60 exactitude 0.84 225 moyenne macro 0.80 0.81 0.81 225 moyenne pondérée 0.85 0.84 0.85 225 ASC = 0.8143939393939393              Les traitements des données par STRATIFY (stratification) et SMOT (optimisation) semblent donc améliorer le rappel, qui passe de 0,59 à 0,75, avec une précision globale de 0,84.  Maintenant que le traitement des données est largement effectué comme d'habitude pour le ML traditionnel, nous voulons savoir quel pourrait être le(s) meilleur(s) modèle(s) dans ce cas ; peuvent-ils faire mieux, et pouvons-nous alors essayer une comparaison globale relative ?   ### **Comparaison de l'entraînement à la course de différents modèles**:  Poursuivons l'évaluation de quelques algorithmes de ML couramment utilisés, et générons un tableau de bord de résultats à comparer à l'aide de diagrammes en boîte à moustaches : # comparer les algorithmes from matplotlib import pyplot from sklearn.model_selection import train_test_split from sklearn.model_selection import cross_val_score from sklearn.model_selection import StratifiedKFold from sklearn.linear_model import LogisticRegression from sklearn.tree import DecisionTreeClassifier from sklearn.neighbors import KNeighborsClassifier from sklearn.discriminant_analysis import LinearDiscriminantAnalysis from sklearn.naive_bayes import GaussianNB from sklearn.svm import SVC #Importer un modèle arborescent aléatoire from sklearn.ensemble import RandomForestClassifier from xgboost import XGBClassifier # Répertorier les algorithmes ensemble models = [] models.append(('LR', <strong>LogisticRegression</strong>(solver='liblinear', multi_class='ovr'))) models.append(('LDA', LinearDiscriminantAnalysis())) models.append(('KNN', <strong>KNeighborsClassifier</strong>())) models.append(('CART', <strong>DecisionTreeClassifier</strong>())) models.append(('NB', <strong>GaussianNB</strong>())) models.append(('SVM', <strong>SVC</strong>(gamma='auto'))) models.append(('RF', <strong>RandomForestClassifier</strong>(n_estimators=100))) models.append(('XGB', <strong>XGBClassifier</strong>())) #clf = XGBClassifier() # évaluer chaque modèle à tour de rôle résultats = [] noms = [] pour nom, modèler dans modèles : kfold = StratifiedKFold(n_splits=10, random_state=1) cv_results = cross_val_score(model, X_train_res, y_train_res, cv=kfold, scoring='f1') ## exactitude, précision, rappel results.append(cv_results) names.append(name) print('%s: %f (%f)' % (name, cv_results.mean(), cv_results.std())) # Comparer les performances de tous les modèles. Question - Souhaitez-vous voir un article intégré sur le site ? pyplot.figure(4, figsize=(12, 8)) pyplot.boxplot(résultats, étiquettes=noms) pyplot.title('Comparaison des algorithmes') pyplot.show() LR: 0.805390 (0.021905) LDA: 0.803804 (0.027671) KNN: 0.841824 (0.032945) CART: 0.845596 (0.053828) NB: 0.622540 (0.060390) SVM: 0.793754 (0.023050) RF: 0.896222 (0.033732) XGB: 0.907529 (0.040693) ![](/sites/default/files/inline/images/images/image-20200821155401-1.png) Ce qui précède semble montrer que le classificateur XGB et le classificateur de la forêt aléatoire "Random Forest" obtiendraient un meilleur score F1 que les autres modèles. Comparons leurs résultats réels sur le même ensemble de données de test normalisées : Temps d'importation from pandas import read_csv from sklearn.model_selection import train_test_split from sklearn.metrics import classification_report from sklearn.metrics import confusion_matrix from sklearn.metrics import accuracy_score from sklearn.svm import SVC pour nom, modèler dans modèles : print(name + ':\n\r') start = time.clock() model.fit(X_train_res, y_train_res) print("Temps de formation pour ", model, " ", time.clock() - start) predictions = model.predict(X3_test) #(X_validation) # Evaluate predictions print(accuracy_score(y3_test, predictions)) # Y_validation print(confusion_matrix(y3_test, predictions)) print(classification_report(y3_test, predictions)) LR: Temps de formation pour LogisticRegression(multi_class='ovr', solver='liblinear') 0.02814499999999498 0.8444444444444444 [[145 20] [ 15 45]] précision rappel f1 score support 0 0.91 0.88 0.89 165 1 0.69 0.75 0.72 60 exactitude 0.84 225 moyenne macro 0.80 0.81 0.81 225 moyenne pondérée 0.85 0.84 0.85 225 LDA: Temps de formation pour LinearDiscriminantAnalysis() 0.2280070000000194 0.8488888888888889 [[147 18] [ 16 44]] précision rappel f1 score support 0 0.90 0.89 0.90 165 1 0.71 0.73 0.72 60 exactitude 0.85 225 moyenne macro 0.81 0.81 0.81 225 moyenne pondérée 0.85 0.85 0.85 225 KNN: Temps de formation pour KNeighborsClassifier() 0.13023699999999394 0.8355555555555556 [[145 20] [ 17 43]] précision rappel f1 score support 0 0.90 0.88 0.89 165 1 0.68 0.72 0.70 60 exactitude 0.84 225 moyenne macro 0.79 0.80 0.79 225 moyenne pondérée 0.84 0.84 0.84 225 CART: Temps de formation pour DecisionTreeClassifier() 0.32616000000001577 0.8266666666666667 [[147 18] [ 21 39]] précision rappel f1 score support 0 0.88 0.89 0.88 165 1 0.68 0.65 0.67 60 exactitude 0.83 225 moyenne macro 0.78 0.77 0.77 225 moyenne pondérée 0.82 0.83 0.83 225 NB: Temps de formation pour GaussianNB() 0.0034229999999979555 0.8355555555555556 [[154 11] [ 26 34]] précision rappel f1 score support 0 0.86 0.93 0.89 165 1 0.76 0.57 0.65 60 exactitude 0.84 225 moyenne macro 0.81 0.75 0.77 225 moyenne pondérée 0.83 0.84 0.83 225 SVM: Temps de formation pour SVC(gamma='auto') 0.3596520000000112 0.8977777777777778 [[157 8] [ 15 45]] précision rappel f1 score support 0 0.91 0.95 0.93 165 1 0.85 0.75 0.80 60 exactitude 0.90 225 moyenne macro 0.88 0.85 0.86 225 moyenne pondérée 0.90 0.90 0.90 225 RF: Temps de formation pour RandomForestClassifier() 0.50123099999999 0.9066666666666666 [[158 7] [ 14 46]] précision rappel f1 score support 0 0.92 0.96 0.94 165 1 0.87 0.77 0.81 60 exactitude 0.91 225 moyenne macro 0.89 0.86 0.88 225 moyenne pondérée 0.91 0.91 0.90 225 XGB: Temps de formation pour XGBClassifier(base_score=0.5, booster='gbtree', colsample_bylevel=1, colsample_bynode=1, colsample_bytree=1, gamma=0, gpu_id=-1, importance_type='gain', interaction_constraints='', learning_rate=0.300000012, max_delta_step=0, max_depth=6, min_child_weight=1, missing=nan, monotone_constraints='()', n_estimators=100, n_jobs=0, num_parallel_tree=1, random_state=0, reg_alpha=0, reg_lambda=1, scale_pos_weight=1, subsample=1, tree_method='exact', validate_parameters=1, verbosity=Aucun) 1.649520999999993 0.8844444444444445 [[155 10] [ 16 44]] précision rappel f1 score support 0 0.91 0.94 0.92 165 1 0.81 0.73 0.77 60 exactitude 0.88 225 moyenne macro 0.86 0.84 0.85 225 moyenne pondérée 0.88 0.88 0.88 225 Le résultat semble être que RF est en fait meilleur que XGB. Cela pourrait signifier que XGB est peut-être un peu plus surajouté d'une manière ou d'une autre. Le résultat de RFC est également légèrement meilleur que celui de LR.   ### **Exécuter le modèle sélectionné en poursuivant "Ajustement des paramètres via la recherche par quadrillage"** Supposons maintenant que nous ayons choisi le modèle de classificateur de la forêt aléatoire "Random Forest Classifier". Nous pouvons effectuer une nouvelle recherche sur la grille de ce modèle pour voir s'il est possible d'obtenir des résultats un peu plus performants.  Rappelez-vous que notre objectif est toujours d'optimiser le rappel dans ce cas, en minimisant le nombre de faux négatifs concernant les risques possibles pour l'USI lors de la rencontre avec le patient, nous utiliserons donc 'recall_score' pour réajuster le quadrillage ci-dessous. Une fois de plus, la validation croisée 10 fois sera utilisée comme d'habitude, étant donné que notre ensemble de test ci-dessus a toujours été fixé à environ 12 % de ces 2915 enregistrements. from sklearn.model_selection import GridSearchCV # Créer la grille de paramètres sur la base des résultats de la recherche aléatoire param_grid = {'bootstrap': [Vrai], 'ccp_alpha': [0.0], 'class_weight': [Aucun], 'criterion': ['gini', 'entropy'], 'max_depth': [Aucun], 'max_features': ['auto', 'log2'], 'max_leaf_nodes': [Aucun], 'max_samples': [Aucun], 'min_impurity_decrease': [0.0], 'min_impurity_split': [Aucun], 'min_samples_leaf': [1, 2, 4], 'min_samples_split': [2, 4], 'min_weight_fraction_leaf': [0.0], 'n_estimators': [100, 125], #'n_jobs': [Aucun], 'oob_score': [False], 'random_state': [Aucun], #'verbose': 0, 'warm_start': [False] } #Ajuster par matrice de confusion from sklearn.metrics import roc_curve, précision_recall_curve, auc, make_scorer, recall_score, accuracy_score, précision_score, confusion_matrix scorers = { 'recall_score': make_scorer(recall_score), 'précision_score': make_scorer(précision_score), 'accuracy_score': make_scorer(accuracy_score) } # Créer un modèle de base rfc = RandomForestClassifier() # Instancier le modèle de quadrillage grid_search = GridSearchCV(estimator = rfc, param_grid = param_grid, scoring=scorers, refit='recall_score', cv = 10, n_jobs = -1, verbose = 2) train_features = X_train_res grid_search.fit(train_features, train_labels) rf_best_grid = grid_search.best_estimator_ rf_best_grid.fit(train_features, train_labels) rf_predictions = rf_best_grid.predict(X3_test) print(accuracy_score(y3_test, rf_predictions)) print(confusion_matrix(y3_test, rf_predictions)) print(classification_report(y3_test, rf_predictions)) 0.92 [[ 46 14] [ 4 161]] précision rappel f1 score support 0 0.92 0.77 0.84 60 1 0.92 0.98 0.95 165 exactitude 0.92 225 moyenne macro 0.92 0.87 0.89 225 moyenne pondérée 0.92 0.92 0.92 225 Le résultat a montré qu'un quadrillage a permis d'augmenter légèrement la précision globale, tout en maintenant le FN au même niveau.  Traçons également les comparaisons avec l'ASC : confusion_matrix4 = pd.crosstab(y3_test, rf_predictions, rownames=['Actual'], colnames=['Predicted']) sns.heatmap(confusion_matrix4, annot=Vrai, fmt = 'g', cmap="YlOrBr") print("LABEL ENCODING + STRATIFY") print(classification_report(y3_test, 1-y3_hat)) print("ASC = ",roc_auc_score(y3_test, 1-y3_hat),'\n\n') print("SMOTE") print(classification_report(y3_test, 1-y_res_hat)) print("ASC = ",roc_auc_score(y3_test, 1-y_res_hat), '\n\n') print("SMOTE + LBG Selected Weights + RF Grid Search") print(classification_report(y3_test, rf_predictions)) print("ASC = ",roc_auc_score(y3_test, rf_predictions), '\n\n\n') y_res_hat_probs = LR.predict_proba(X3_test) y_res_hat_probs = y_res_hat_probs[:, 1] predictions_rf_probs = rf_best_grid.predict_proba(X3_test) #(X_validation) predictions_rf_probs = predictions_rf_probs[:, 1] fpr_res, tpr_res, _ = roc_curve(y3_test, 1-y_res_hat_probs) fpr_rf_res, tpr_rf_res, _ = roc_curve(y3_test, predictions_rf_probs) plt.figure(figsize=(10,10)) plt.plot([0, 1], [0, 1], 'k--') plt.plot(fpr, tpr, label="Base") plt.plot(fpr2,tpr2,label="Label Encoded") plt.plot(fpr3,tpr3,label="Stratify") plt.plot(fpr_res,tpr_res,label="SMOTE") plt.plot(fpr_rf_res,tpr_rf_res,label="SMOTE + RF GRID") plt.xlabel('False positive rate') plt.ylabel('Vrai positive rate') plt.title('ROC curve') plt.legend(loc="best") plt.show() CODAGE D'ÉTIQUETTES + STRATIFICATION précision rappel f1 score support 0 0.95 0.58 0.72 60 1 0.87 0.99 0.92 165 exactitude 0.88 225 moyenne macro 0.91 0.79 0.82 225 moyenne pondérée 0.89 0.88 0.87 225 ASC = 0.7856060606060606 MODIFICATION précision rappel f1 score support 0 0.69 0.75 0.72 60 1 0.91 0.88 0.89 165 exactitude 0.84 225 moyenne macro 0.80 0.81 0.81 225 moyenne pondérée 0.85 0.84 0.85 225 ASC = 0.8143939393939394 MODIFICATION + LBG Pondérations sélectionnées + Quadrillage RF précision rappel f1 score support 0 0.92 0.77 0.84 60 1 0.92 0.98 0.95 165 exactitude 0.92 225 moyenne macro 0.92 0.87 0.89 225 moyenne pondérée 0.92 0.92 0.92 225 ASC = 0.8712121212121211       Le résultat a montré qu'après des comparaisons d'algorithmes et un quadrillage suivant, nous avons réussi à faire passer l'ASC de 78 % à 87 %, avec une précision globale de 92 % et un rappel de 77 %.   ### **Récapitulatif de l'approche "ML traditionnelle"** Qu'en est-il réellement de ce résultat ? Il est correct pour un processus manuel de base avec des algorithmes ML traditionnels. Comment ce résultat apparaît-il dans les tableaux de compétition Kaggle ? Eh bien, il ne figurerait pas dans le tableau de classement. J'ai passé le jeu de données brutes par le service AutoML actuel de DataRobot, le meilleur résultat serait un ASC équivalent de ~90+% (à confirmer avec des données similaires) avec le modèle " Classificateur arborescent XGB avec fonctions d'apprentissage non supervisé " (XGB Trees Classifier with Unsupervised Learning Features), sur une comparaison des 43 meilleurs modèles. C'est peut-être le genre de modèle de base que nous devrions utiliser si nous voulons vraiment être compétitifs sur Kaggle. Je joindrai également la liste des meilleurs résultats par rapport aux modèles dans le github. Finalement, pour les cas réels spécifiques aux sites de soins, j'ai le sentiment que nous devons également intégrer un certain degré d'approches d'apprentissage profond personnalisées, comme mentionné dans la section "Données et tâches" de ce billet. Bien sûr, dans les cas réels, l'endroit où collecter des colonnes de données de qualité pourrait également être une question initiale.   ## L'approche IntegratedML? Ce qui précède est un processus de ML dit traditionnel, qui comprend normalement l'EDA des données, l'ingénierie des caractéristiques, la sélection des caractéristiques, la sélection des modèles, et l'optimisation des performances par la quadrillage, etc. C'est l'approche la plus simple à laquelle j'ai pu penser jusqu'à présent pour cette tâche, et nous n'avons même pas encore abordé le déploiement du modèle et les cycles de vie de la gestion des services - nous le ferons dans le prochain article, en examinant comment nous pourrions tirer parti de Flask/FastAPI/IRIS et déployer ce modèle de ML de base dans une pile de services de démonstration de la radiographie de Covid-19. IRIS dispose désormais d'IntegratedML, qui est une enveloppe SQL élégante d'options puissantes d'AutoMLs. Dans la deuxième partie, nous verrons comment accomplir la tâche susmentionnée dans le cadre d'un processus simplifié, de sorte que nous n'aurons plus à nous préoccuper de la sélection des caractéristiques, de la sélection des modèles, de l'optimisation des performances, etc. Jusqu'ici, cet article pourrait être trop long pour une note de 10 minutes visant à intégrer rapidement les mêmes données, c'est pourquoi je le déplace vers [l'article suivant, partie II](https://fr.community.intersystems.com/post/pr%C3%A9dictions-de-covid-19-icu-ml-vs-integratedml-partie-ii).