Article
· Juin 13 9m de lecture

Utilisation de la recherche vectorielle pour le détection des patient en double

J'ai dû récemment rafraîchir mes connaissances sur le module EMPI de HealthShare et, comme je travaillais depuis quelque temps sur les fonctionnalités de stockage et de recherche vectorielle d'IRIS, j'ai tout naturellement fait le rapprochement.

Pour ceux d'entre vous qui ne connaissent pas bien les fonctionnalités EMPI, voici une brève introduction:

Index patient principal "Enterprise Master Patient Index"

En général, tous les EMPI fonctionnent de manière très similaire : ils ingèrent les informations, les normalisent et les comparent avec les données déjà présentes dans leur système. Dans le cas de l'EMPI de HealthShare, ce processus est connu sous le nom de NICE:

  • Normalisation: tous les textes ingérés à partir de la production interopérable sont normalisés en supprimant les caractères spéciaux.
  • Indexation: des index sont générés à partir d'une sélection de données démographiques afin d'accélérer la recherche de correspondances.
  • Comparaison: les correspondances trouvées dans les index sont comparées entre les données démographiques, puis des pondérations sont attribuées en fonction de critères correspondant au niveau de coïncidence.
  • Évaluation: la possibilité de relier les patients est évaluée à partir de la somme des pondérations obtenues.

Si vous voulez en savoir plus sur HealthShare Patient Index, vous pouvez consulter une série d'articles que j'ai écrits il y a quelque temps ici.

Quel est le défi?

Bien que le processus de configuration nécessaire pour obtenir les liens possibles ne soit pas extrêmement compliqué, je me suis demandé... Serait-il possible d'obtenir des résultats similaires en utilisant les fonctionnalités de stockage et de recherche vectorielle et en supprimant les étapes d'ajustement de la pondération? Au travail!

De quoi ai-je besoin pour mettre en œuvre mon idée?

J'aurai besoin des ingrédients suivants:

  • IRIS for Health pour mettre en œuvre la fonctionnalité et utiliser le moteur d'interopérabilité pour l'ingestion de messages HL7.
  • Bibliothèque Python pour générer les vecteurs à partir des données démographiques, dans ce cas, il s'agira de sentence-transformers.
  • Modèle pour générer les embeddings, après une recherche rapide sur Hugging Faces, j'ai choisi all-MiniLM-L6-v2.

Production d'interopérabilité

La première étape consistera à configurer la production chargée d'ingérer les messages HL7, de les transformer en messages contenant les données démographiques les plus pertinentes du patient, de générer les intégrations à partir de ces données démographiques et enfin de générer le message de réponse avec les correspondances possibles.

Jetons un coup d'œil à notre production:

Comme vous pouvez le voir, rien de plus simple. J'ai un Service métier HL7FileService qui récupère les messages HL7 à partir d'un répertoire (/shared/in), puis envoie le message au Processus métier (BP) EMPI.BP.FromHL7ToPatientRequestBPL où je vais créer le message avec les données démographiques du patient, puis nous l'envoyons à un autre BP appelé EMPI.BP.VectorizationBP où les informations démographiques seront vectorisées et où la recherche vectorielle sera effectuée, ce qui renverra un message avec tous les patients en double possibles.

Comme vous pouvez le voir, le BP FromHL7ToPatientRequesBPL est très simple:

Nous convertissons le message HL7 en un message que nous avons créé afin de stocker les données démographiques que nous avons jugées les plus pertinentes.

Messages entre les composants

Nous avons créé deux types de messages spécifiques: 

EMPI.Message.PatientRequest

Class EMPI.Message.PatientRequest Extends (Ens.Request, %XML.Adaptor)
{

Property Patient As EMPI.Object.Patient;
}

EMPI.Message.PatientResponse

Class EMPI.Message.PatientResponse Extends (Ens.Response, %XML.Adaptor)
{

Property Patients As list Of EMPI.Object.Patient;
}

Ce type de message contiendra une liste des patients susceptibles d'être en double.

Voyons la définition de la classe EMPI.Object.Patient :

Class EMPI.Object.Patient Extends (%SerialObject, %XML.Adaptor)
{

Property Name As %String(MAXLEN = 1000);
Property Address As %String(MAXLEN = 1000);
Property Contact As %String(MAXLEN = 1000);
Property BirthDateAndSex As %String(MAXLEN = 100);
Property SimilarityName As %Double;
Property SimilarityAddress As %Double;
Property SimilarityContact As %Double;
Property SimilarityBirthDateAndSex As %Double;
}

Nom, Address, Contact et BirthDateAndSex sont les propriétés dans lesquelles nous allons enregistrer les données démographiques les plus pertinentes sur les patients.

Voyons maintenant la magie: la génération d'intégration et la recherche vectorielle dans la production.

EMPI.BP.VectorizationBP

Intégration et recherche vectorielle

Une fois la requête PatientRequest reçue, nous allons générer les intégrations à l'aide d'une méthode Python:

Method VectorizePatient(name As %String, address As %String, contact As %String, birthDateAndSex As %String) As %String [ Language = python ]
{
    import iris
    import os
    import sentence_transformers

    try :
        if not os.path.isdir("/iris-shared/model/"):
            model = sentence_transformers.SentenceTransformer("sentence-transformers/all-MiniLM-L6-v2")            
            model.save('/iris-shared/model/')
        model = sentence_transformers.SentenceTransformer("/iris-shared/model/")
        embeddingName = model.encode(name, normalize_embeddings=True).tolist()
        embeddingAddress = model.encode(address, normalize_embeddings=True).tolist()
        embeddingContact = model.encode(contact, normalize_embeddings=True).tolist()
        embeddingBirthDateAndSex = model.encode(birthDateAndSex, normalize_embeddings=True).tolist()

        stmt = iris.sql.prepare("INSERT INTO EMPI_Object.PatientInfo (Name, Address, Contact, BirthDateAndSex, VectorizedName, VectorizedAddress, VectorizedContact, VectorizedBirthDateAndSex) VALUES (?,?,?,?, TO_VECTOR(?,DECIMAL), TO_VECTOR(?,DECIMAL), TO_VECTOR(?,DECIMAL), TO_VECTOR(?,DECIMAL))")
        rs = stmt.execute(name, address, contact, birthDateAndSex, str(embeddingName), str(embeddingAddress), str(embeddingContact), str(embeddingBirthDateAndSex))
        return "1"
    except Exception as err:
        iris.cls("Ens.Util.Log").LogInfo("EMPI.BP.VectorizationBP", "VectorizePatient", repr(err))
        return "0"
}

Analysons le code:

  • Avec la bibliothèque sentence-transformer, nous obtenons le modèle all-MiniLM-L6-v2 et l'enregistrons sur la machine locale (pour éviter toute connexion supplémentaire à l'Internet).
  • Une fois le modèle importé, nous pouvons générer les intégrations pour les champs démographiques à l'aide de la méthode encode.
  • À l'aide de la bibliothèque IRIS, nous exécutons la requête d'insertion pour conserver les intégrations pour le patient.

Recherche de patients en double

Nous avons maintenant la liste des patients enregistrés avec les intégrations générées à partir des données démographiques, interrogeons-la!

Method OnRequest(pInput As EMPI.Message.PatientRequest, Output pOutput As EMPI.Message.PatientResponse) As %Status
{
    try{
        set result = ..VectorizePatient(pInput.Patient.Name, pInput.Patient.Address, pInput.Patient.Contact, pInput.Patient.BirthDateAndSex)
        set pOutput = ##class(EMPI.Message.PatientResponse).%New()
        if (result = 1)
        {
            set sql = "SELECT * FROM (SELECT p1.Name, p1.Address, p1.Contact, p1.BirthDateAndSex, VECTOR_DOT_PRODUCT(p1.VectorizedName, p2.VectorizedName) as SimilarityName, VECTOR_DOT_PRODUCT(p1.VectorizedAddress, p2.VectorizedAddress) as SimilarityAddress, "_
                    "VECTOR_DOT_PRODUCT(p1.VectorizedContact, p2.VectorizedContact) as SimilarityContact, VECTOR_DOT_PRODUCT(p1.VectorizedBirthDateAndSex, p2.VectorizedBirthDateAndSex) as SimilarityBirthDateAndSex "_
                    "FROM EMPI_Object.PatientInfo p1, EMPI_Object.PatientInfo p2 WHERE p2.Name = ? AND p2.Address = ?  AND p2.Contact = ? AND p2.BirthDateAndSex = ?) "_
                    "WHERE SimilarityName > 0.8 AND SimilarityAddress > 0.8 AND SimilarityContact > 0.8 AND SimilarityBirthDateAndSex > 0.8"
            set statement = ##class(%SQL.Statement).%New(), statement.%ObjectSelectMode = 1
            set status = statement.%Prepare(sql)
            if ($$$ISOK(status)) {
                set resultSet = statement.%Execute(pInput.Patient.Name, pInput.Patient.Address, pInput.Patient.Contact, pInput.Patient.BirthDateAndSex)
                if (resultSet.%SQLCODE = 0) {
                    while (resultSet.%Next() '= 0) {
                        set patient = ##class(EMPI.Object.Patient).%New()
                        set patient.Name = resultSet.%GetData(1)
                        set patient.Address = resultSet.%GetData(2)
                        set patient.Contact = resultSet.%GetData(3)
                        set patient.BirthDateAndSex = resultSet.%GetData(4)
                        set patient.SimilarityName = resultSet.%GetData(5)
                        set patient.SimilarityAddress = resultSet.%GetData(6)
                        set patient.SimilarityContact = resultSet.%GetData(7)
                        set patient.SimilarityBirthDateAndSex = resultSet.%GetData(8)
                        do pOutput.Patients.Insert(patient)
                    }
                }
            }
        }
    }
    catch ex {
        do ex.Log()
    }
    return $$$OK
}

Voici la requête. Pour notre exemple, nous avons inclus une restriction afin d'obtenir les patients présentant une similitude supérieure à 0,8 pour toutes les données démographiques, mais nous pourrions la configurer pour affiner la requête.

Voyons l'exemple présentant le fichier messagesa28.hl7 avec des messages HL7 tels que ceux-ci:

MSH|^~\&|HIS|HULP|EMPI||20241120103314||ADT^A28|269304|P|2.5.1
EVN|A28|20241120103314|20241120103314|1
PID|||1220395631^^^SERMAS^SN~402413^^^HULP^PI||FERNÁNDEZ LÓPEZ^JOSÉ MARÍA^^^||19700611|M|||PASEO JUAN FERNÁNDEZ^183 2 A^LEGANÉS^CÁDIZ^28566^SPAIN||555749170^PRN^^JOSE-MARIA.FERNANDEZ@GMAIL.COM|||||||||||||||||N|
PV1||N

MSH|^~\&|HIS|HULP|EMPI||20241120103314||ADT^A28|570814|P|2.5.1
EVN|A28|20241120103314|20241120103314|1
PID|||1122730333^^^SERMAS^SN~018565^^^HULP^PI||GONZÁLEZ GARCÍA^MARÍA^^^||19660812|F|||CALLE JOSÉ MARÍA FERNÁNDEZ^281 8 IZQUIERDA^MADRID^BARCELONA^28057^SPAIN||555386663^PRN^^MARIA.GONZALEZ@GMAIL.COM|||||||||||||||||N|
PV1||N
DG1|1||T001^TRAUMATISMOS SUPERF AFECTAN TORAX CON ABDOMEN, REG LUMBOSACRA Y PELVIS^||20241120|||||||||||^CONTRERAS ÁLVAREZ^ENRIQUETA^^^Dr|

MSH|^~\&|HIS|HULP|EMPI||20241120103314||ADT^A28|40613|P|2.5.1
EVN|A28|20241120103314|20241120103314|1
PID|||1007179467^^^SERMAS^SN~122688^^^HULP^PI||OLIVA OLIVA^JIMENA^^^||19620222|F|||CALLE ANTONIO ÁLVAREZ^51 3 D^MÉRIDA^MADRID^28253^SPAIN||555638305^PRN^^JIMENA.OLIVA@VODAFONE.COM|||||||||||||||||N|
PV1||N
DG1|1||Q059^ESPINA BIFIDA, NO ESPECIFICADA^||20241120|||||||||||^SANZ LÓPEZ^MARIO^^^Dr|

MSH|^~\&|HIS|HULP|EMPI||20241120103314||ADT^A28|61768|P|2.5.1
EVN|A28|20241120103314|20241120103314|1
PID|||1498973060^^^SERMAS^SN~719939^^^HULP^PI||PÉREZ CABEZUELA^DIANA^^^||19820309|F|||AVENIDA JULIA ÁLVAREZ^253 1 A^PERELLONET^BADAJOZ^28872^SPAIN||555705148^PRN^^DIANA.PEREZ@YAHOO.COM|||||||||||||||||N|
PV1||N
AL1|1|MA|^Polen de gramineas^|SV^^||20340919051523
MSH|^~\&|HIS|HULP|EMPI||20241120103314||ADT^A28|128316|P|2.5.1
EVN|A28|20241120103314|20241120103314|1
PID|||1632386689^^^SERMAS^SN~601379^^^HULP^PI||GARCÍA GARCÍA^MARIO^^^||19550603|M|||PASEO JOSÉ MARÍA TREVIÑO^153 3 D^JEREZ DE LA FRONTERA^MADRID^28533^SPAIN||555231628^PRN^^MARIO.GARCIA@GMAIL.COM|||||||||||||||||N|
PV1||N

Dans ce fichier, tous les patients sont différents, donc le résultat de l'opération sera du type suivant:

La seule correspondance est le patient lui-même. Introisons les messages HL7 provenant du fichier messagesa28Duplicated.hl7 avec des patients en double:

Comme vous pouvez le voir, le code a détecté le patient en double avec des différences mineures dans le nom (Maruchi est un diminutif affectueux de María et Mª est l'abréviation), bien sûr, ce cas est simplifié à l'extrême, mais vous pouvez vous faire une idée des capacités de la recherche vectorielle pour trouver des données en double, non seulement pour les patients, mais aussi pour tout autre type d'informations.

Prochaines étapes...

Pour cet exemple, j'ai utilisé un modèle commun pour générer les intégrations, mais le comportement du code pourrait être amélioré à l'aide d'un réglage fin utilisant des diminutifs, des surnoms, etc.

Merci de votre attention!

Discussion (0)2
Connectez-vous ou inscrivez-vous pour continuer