First steps#

This tutorial will show you how to use medkit to annotate a text document, by applying pre-processing, entity matching and context detections operations.

Loading a text document#

For starters, let’s load a text file using the TextDocument class:

# You can download the file available in source code
# !wget https://raw.githubusercontent.com/TeamHeka/medkit/main/docs/data/text/1.txt

from pathlib import Path
from medkit.core.text import TextDocument

doc = TextDocument.from_file(Path("../data/text/1.txt"))

The full raw text can be accessed through the text attribute:

print(doc.text)
SUBJECTIF : Cette femme blanche de 23 ans se plaint d'allergies. Elle avait l'habitude d'avoir des allergies lorsqu'elle vivait à Seattle mais elle pense qu'elles sont pires ici. Dans le passé, elle a essayé le Claritin et le Zyrtec. Les deux ont fonctionné pendant une courte période mais ont ensuite semblé perdre de leur efficacité. Elle a également utilisé Allegra. Elle l'a utilisé l'été dernier et a recommencé à le faire il y a deux semaines. Il ne semble pas fonctionner très bien. Elle a utilisé des vaporisateurs en vente libre, mais pas de vaporisateurs nasaux sur ordonnance. Elle a de l'asthme, mais n'a pas besoin de prendre des médicaments tous les jours pour cela et ne pense pas que son asthme s'aggrave.

MÉDICAMENTS : Son seul médicament est actuellement Ortho Tri-Cyclen et l'Allegra.

ALLERGIES : Elle n'a pas d'allergies médicamenteuses connues.

EXAMEN PHYSIQUE :
Signes vitaux : Poids de 59.3 kilos et pression sanguine de 124/78.
Tete/Yeux/Oreilles/Nez/Gorge : Sa gorge était légèrement érythémateuse sans exsudat. La muqueuse nasale était érythémateuse et enflée. Seul un drainage clair était visible. Les TM étaient claires.
Cou : Souple sans adénopathie.
Poumons : Dégagés.

EVALUATION : Rhinite allergique.

PLAN :
- Elle va réessayer le Zyrtec au lieu de l'Allegra. Une autre option sera d'utiliser la loratadine. Elle ne pense pas être remboursée donc ça pourrait être moins cher.
- Echantillons de Nasonex : deux pulvérisations dans chaque narine pendant trois semaines. Une ordonnance a également été rédigée.

A TextDocument can store TextAnnotation objects but for now, our document is empty.

Splitting a document in sentences#

A common task in natural language processing is to split (or tokenize) text documents in sentences. Medkit provides several segmentation operations, including a rule-based SentenceTokenizer class that relies on a list of punctuation characters. Let’s instantiate it:

from medkit.text.segmentation import SentenceTokenizer

sent_tokenizer = SentenceTokenizer(
    output_label="sentence",
    punct_chars=[".", "?", "!"],
)

As all operations, SentenceTokenizer defines a run() method. This method returns a list of Segment objects (a Segment is a TextAnnotation that represents a portion of a document’s full raw text). As input, it also expects a list of Segment objects. Here, we can pass a special segment containing the whole raw text of the document, that we can retrieve through the raw_segment attribute of TextDocument:

sentences = sent_tokenizer.run([doc.raw_segment])
for sentence in sentences:
    print(f"uid={sentence.uid}")
    print(f"text={sentence.text!r}")
    print(f"spans={sentence.spans}, label={sentence.label}\n")
uid=dfc423a0-8e23-11ee-973f-0242ac110002
text="SUBJECTIF : Cette femme blanche de 23 ans se plaint d'allergies"
spans=[Span(start=0, end=63)], label=sentence

uid=dfc426f2-8e23-11ee-973f-0242ac110002
text="Elle avait l'habitude d'avoir des allergies lorsqu'elle vivait à Seattle mais elle pense qu'elles sont pires ici"
spans=[Span(start=65, end=177)], label=sentence

uid=dfc42bde-8e23-11ee-973f-0242ac110002
text='Dans le passé, elle a essayé le Claritin et le Zyrtec'
spans=[Span(start=179, end=232)], label=sentence

uid=dfc42e86-8e23-11ee-973f-0242ac110002
text='Les deux ont fonctionné pendant une courte période mais ont ensuite semblé perdre de leur efficacité'
spans=[Span(start=234, end=334)], label=sentence

uid=dfc43156-8e23-11ee-973f-0242ac110002
text='Elle a également utilisé Allegra'
spans=[Span(start=336, end=368)], label=sentence

uid=dfc434a8-8e23-11ee-973f-0242ac110002
text="Elle l'a utilisé l'été dernier et a recommencé à le faire il y a deux semaines"
spans=[Span(start=370, end=448)], label=sentence

uid=dfc4370a-8e23-11ee-973f-0242ac110002
text='Il ne semble pas fonctionner très bien'
spans=[Span(start=450, end=488)], label=sentence

uid=dfc43944-8e23-11ee-973f-0242ac110002
text='Elle a utilisé des vaporisateurs en vente libre, mais pas de vaporisateurs nasaux sur ordonnance'
spans=[Span(start=490, end=586)], label=sentence

uid=dfc43b7e-8e23-11ee-973f-0242ac110002
text="Elle a de l'asthme, mais n'a pas besoin de prendre des médicaments tous les jours pour cela et ne pense pas que son asthme s'aggrave"
spans=[Span(start=588, end=720)], label=sentence

uid=dfc43e1c-8e23-11ee-973f-0242ac110002
text="MÉDICAMENTS : Son seul médicament est actuellement Ortho Tri-Cyclen et l'Allegra"
spans=[Span(start=723, end=803)], label=sentence

uid=dfc44092-8e23-11ee-973f-0242ac110002
text="ALLERGIES : Elle n'a pas d'allergies médicamenteuses connues"
spans=[Span(start=806, end=866)], label=sentence

uid=dfc4436c-8e23-11ee-973f-0242ac110002
text='EXAMEN PHYSIQUE :'
spans=[Span(start=869, end=886)], label=sentence

uid=dfc445f6-8e23-11ee-973f-0242ac110002
text='Signes vitaux : Poids de 59'
spans=[Span(start=887, end=914)], label=sentence

uid=dfc44876-8e23-11ee-973f-0242ac110002
text='3 kilos et pression sanguine de 124/78'
spans=[Span(start=915, end=953)], label=sentence

uid=dfc44b0a-8e23-11ee-973f-0242ac110002
text='Tete/Yeux/Oreilles/Nez/Gorge : Sa gorge était légèrement érythémateuse sans exsudat'
spans=[Span(start=955, end=1038)], label=sentence

uid=dfc44d3a-8e23-11ee-973f-0242ac110002
text='La muqueuse nasale était érythémateuse et enflée'
spans=[Span(start=1040, end=1088)], label=sentence

uid=dfc450a0-8e23-11ee-973f-0242ac110002
text='Seul un drainage clair était visible'
spans=[Span(start=1090, end=1126)], label=sentence

uid=dfc4532a-8e23-11ee-973f-0242ac110002
text='Les TM étaient claires'
spans=[Span(start=1128, end=1150)], label=sentence

uid=dfc4562c-8e23-11ee-973f-0242ac110002
text='Cou : Souple sans adénopathie'
spans=[Span(start=1152, end=1181)], label=sentence

uid=dfc459ba-8e23-11ee-973f-0242ac110002
text='Poumons : Dégagés'
spans=[Span(start=1183, end=1200)], label=sentence

uid=dfc45ca8-8e23-11ee-973f-0242ac110002
text='EVALUATION : Rhinite allergique'
spans=[Span(start=1203, end=1234)], label=sentence

uid=dfc45f1e-8e23-11ee-973f-0242ac110002
text='PLAN :'
spans=[Span(start=1237, end=1243)], label=sentence

uid=dfc4619e-8e23-11ee-973f-0242ac110002
text="- Elle va réessayer le Zyrtec au lieu de l'Allegra"
spans=[Span(start=1244, end=1294)], label=sentence

uid=dfc4641e-8e23-11ee-973f-0242ac110002
text="Une autre option sera d'utiliser la loratadine"
spans=[Span(start=1296, end=1342)], label=sentence

uid=dfc46658-8e23-11ee-973f-0242ac110002
text='Elle ne pense pas être remboursée donc ça pourrait être moins cher'
spans=[Span(start=1344, end=1410)], label=sentence

uid=dfc4690a-8e23-11ee-973f-0242ac110002
text='- Echantillons de Nasonex : deux pulvérisations dans chaque narine pendant trois semaines'
spans=[Span(start=1412, end=1501)], label=sentence

uid=dfc46b8a-8e23-11ee-973f-0242ac110002
text='Une ordonnance a également été rédigée'
spans=[Span(start=1503, end=1541)], label=sentence

As you can see, each segment has:

  • an uid attribute, which unique value is automatically generated;

  • a text attribute holding the text that the segment refers to;

  • a spans attribute reflecting the position of this text in the document’s full raw text. Here we only have one span for each segment, but multiple discontinuous spans are supported;

  • and a label, always equal to "sentence" in our case but it could be different for other kinds of segments.

Preprocessing a document#

If you take a look at the 13th and 14th detected sentences, you will notice something strange:

print(repr(sentences[12].text))
print(repr(sentences[13].text))
'Signes vitaux : Poids de 59'
'3 kilos et pression sanguine de 124/78'

This is actually one sentence that was split into two segments, because the sentence tokenizer incorrectly considers the dot in the decimal weight value to mark the end of a sentence. We could be a little smarter when configuring the tokenizer, but instead, for the sake of learning, let’s fix this with a pre-processing step that replaces dots by commas in decimal numbers.

For this, we can use the RegexpReplacer class, a regexp-based “search-and-replace” operation. As many medkit operations, it can be configured with a set of user-determined rules:

from medkit.text.preprocessing import RegexpReplacer

rule = (r"(?<=\d)\.(?=\d)", ",") # => (pattern to replace, new text)
regexp_replacer = RegexpReplacer(output_label="clean_text", rules=[rule])

The run() method of the normalizer takes a list of Segment objects and returns a list of new Segment objects, one for each input Segment. In our case we only want to preprocess the full raw text segment and we will only receive one preprocessed segment, so we can call it with:

clean_segment = regexp_replacer.run([doc.raw_segment])[0]
print(clean_segment.text)
SUBJECTIF : Cette femme blanche de 23 ans se plaint d'allergies. Elle avait l'habitude d'avoir des allergies lorsqu'elle vivait à Seattle mais elle pense qu'elles sont pires ici. Dans le passé, elle a essayé le Claritin et le Zyrtec. Les deux ont fonctionné pendant une courte période mais ont ensuite semblé perdre de leur efficacité. Elle a également utilisé Allegra. Elle l'a utilisé l'été dernier et a recommencé à le faire il y a deux semaines. Il ne semble pas fonctionner très bien. Elle a utilisé des vaporisateurs en vente libre, mais pas de vaporisateurs nasaux sur ordonnance. Elle a de l'asthme, mais n'a pas besoin de prendre des médicaments tous les jours pour cela et ne pense pas que son asthme s'aggrave.

MÉDICAMENTS : Son seul médicament est actuellement Ortho Tri-Cyclen et l'Allegra.

ALLERGIES : Elle n'a pas d'allergies médicamenteuses connues.

EXAMEN PHYSIQUE :
Signes vitaux : Poids de 59,3 kilos et pression sanguine de 124/78.
Tete/Yeux/Oreilles/Nez/Gorge : Sa gorge était légèrement érythémateuse sans exsudat. La muqueuse nasale était érythémateuse et enflée. Seul un drainage clair était visible. Les TM étaient claires.
Cou : Souple sans adénopathie.
Poumons : Dégagés.

EVALUATION : Rhinite allergique.

PLAN :
- Elle va réessayer le Zyrtec au lieu de l'Allegra. Une autre option sera d'utiliser la loratadine. Elle ne pense pas être remboursée donc ça pourrait être moins cher.
- Echantillons de Nasonex : deux pulvérisations dans chaque narine pendant trois semaines. Une ordonnance a également été rédigée.

And then we may use again our previously-defined sentence tokenizer, but this time on the preprocessed text:

sentences = sent_tokenizer.run([clean_segment])
print(sentences[12].text)
Signes vitaux : Poids de 59,3 kilos et pression sanguine de 124/78

Problem fixed!

Finding entities#

the medkit library also comes with operations to perform NER (named entity recognition), for instance RegexpMatcher. Let’s instantiate one with a few simple rules:

from medkit.text.ner import RegexpMatcher, RegexpMatcherRule

regexp_rules = [
    RegexpMatcherRule(regexp=r"\ballergies?\b", label="problem"),
    RegexpMatcherRule(regexp=r"\basthme\b", label="problem"),
    RegexpMatcherRule(regexp=r"\ballegra?\b", label="treatment", case_sensitive=False),
    RegexpMatcherRule(regexp=r"\bvaporisateurs?\b", label="treatment"),
    RegexpMatcherRule(regexp=r"\bloratadine?\b", label="treatment", case_sensitive=False),
    RegexpMatcherRule(regexp=r"\bnasonex?\b", label="treatment", case_sensitive=False),
]
regexp_matcher = RegexpMatcher(rules=regexp_rules)

As you can see, you can also define some rules that ignore case distinctions by setting case-sensitive parameter to False. In this example, we decide to make it for drugs (Allegra, Nasonex and Loratadine).

Note

When RegexpMatcher is instantiated without any rules, it will use a set of default rules that where initially created to be used with documents in french from the APHP EDS. These rules are stored in the regexp_matcher_default_rules.yml file in the medkit.text.ner module.

You may also define your own rules in a .yml file. You can then load them using the RegexpMatcher.load_rules() static method and then pass then to the RegexpMatcher at init.

Since RegexpMatcher is an NER operation, its run() method returns a list of Entity objects representing the entities that were matched (Entity is a subclass of Segment). As input, it expects a list of Segment objects. Let’s give it the sentences returned by the sentence tokenizer:

entities = regexp_matcher.run(sentences)

for entity in entities:
    print(f"uid={entity.uid}")
    print(f"text={entity.text!r}, spans={entity.spans}, label={entity.label}\n")
uid=e18ba76c-8e23-11ee-973f-0242ac110002
text='allergies', spans=[Span(start=54, end=63)], label=problem

uid=e18bac58-8e23-11ee-973f-0242ac110002
text='allergies', spans=[Span(start=99, end=108)], label=problem

uid=e18bb2b6-8e23-11ee-973f-0242ac110002
text='Allegra', spans=[Span(start=361, end=368)], label=treatment

uid=e18bb93c-8e23-11ee-973f-0242ac110002
text='vaporisateurs', spans=[Span(start=509, end=522)], label=treatment

uid=e18bbc48-8e23-11ee-973f-0242ac110002
text='vaporisateurs', spans=[Span(start=551, end=564)], label=treatment

uid=e18bbf68-8e23-11ee-973f-0242ac110002
text='asthme', spans=[Span(start=600, end=606)], label=problem

uid=e18bc1c0-8e23-11ee-973f-0242ac110002
text='asthme', spans=[Span(start=704, end=710)], label=problem

uid=e18bc60c-8e23-11ee-973f-0242ac110002
text='Allegra', spans=[Span(start=796, end=803)], label=treatment

uid=e18bca3a-8e23-11ee-973f-0242ac110002
text='allergies', spans=[Span(start=833, end=842)], label=problem

uid=e18bd340-8e23-11ee-973f-0242ac110002
text='Allegra', spans=[Span(start=1287, end=1294)], label=treatment

uid=e18bd70a-8e23-11ee-973f-0242ac110002
text='loratadine', spans=[Span(start=1332, end=1342)], label=treatment

uid=e18bdb42-8e23-11ee-973f-0242ac110002
text='Nasonex', spans=[Span(start=1430, end=1437)], label=treatment

Just like sentences, each entity has uid, text, spans and label attributes (in this case, determined by the rule that was used to match it).

Detecting negation#

So far we have detected several entities with "problem" or "treatement" labels in our document. We might be tempted to use them directly to build a list of problems that the patient faces and treatments that were given, but if we look at how these entities are used in the document, we will see that some of these entities actually denote the absence of a problem or treatment.

To solve this kind of situations, medkit comes with context detectors, such as NegationDetector. NegationDetector.run() receives a list of Segment objects. It doesn’t return anything but it will append an Attribute object to each segment with a boolean value indicating whether a negation was detected or not (Segment and Entity objects can have a list of Attribute objects, accessible through their AttributeContainer).

Let’s instantiate a NegationDetector with a couple of simplistic handcrafted rules and run it on our sentences:

from medkit.text.context import NegationDetector, NegationDetectorRule

neg_rules = [
    NegationDetectorRule(regexp=r"\bpas\s*d[' e]\b"),
    NegationDetectorRule(regexp=r"\bsans\b", exclusion_regexps=[r"\bsans\s*doute\b"]),
    NegationDetectorRule(regexp=r"\bne\s*semble\s*pas"),
]
neg_detector = NegationDetector(output_label="is_negated", rules=neg_rules)
neg_detector.run(sentences)

Note

Similarly to RegexpMatcher, DetectionDetector also comes with a set of default rules designed for documents from the EDS, stored in negation_detector_default_rules.yml inside medkit.text.context.

And now, let’s look at which sentence have been detected as being negated:

for sentence in sentences:
    neg_attr = sentence.attrs.get(label="is_negated")[0]
    if neg_attr.value:
        print(sentence.text)
Il ne semble pas fonctionner très bien
Elle a utilisé des vaporisateurs en vente libre, mais pas de vaporisateurs nasaux sur ordonnance
ALLERGIES : Elle n'a pas d'allergies médicamenteuses connues
Tete/Yeux/Oreilles/Nez/Gorge : Sa gorge était légèrement érythémateuse sans exsudat
Cou : Souple sans adénopathie

Our simple negation detector doesn’t work so bad, but sometimes some part of the sentence has a negation and the other doesn’t, and in that case the whole sentence gets flagged as being negated.

To mitigate this, we can split each sentence into finer-grained segments called syntagmas. Medkit provide a SyntagmaTokenizer for that purpose. Let’s instantiate one, run it on our sentences and then run again the negation detector but this time on the syntagmas:

Note

SyntagmaTokenizer also has default rules designed for documents from the EDS, stored in default_syntagma_definition.yml inside medkit.text.segmentation.

from medkit.text.segmentation import SyntagmaTokenizer

synt_tokenizer = SyntagmaTokenizer(
    output_label="syntagma",
    separators=[r"\bmais\b", r"\bet\b"],
)
syntagmas = synt_tokenizer.run(sentences)
neg_detector.run(syntagmas)

for syntagma in syntagmas:
    neg_attr = syntagma.attrs.get(label="is_negated")[0]
    if neg_attr.value:
        print(syntagma.text)
Il ne semble pas fonctionner très bien
mais pas de vaporisateurs nasaux sur ordonnance
ALLERGIES : Elle n'a pas d'allergies médicamenteuses connues
Tete/Yeux/Oreilles/Nez/Gorge : Sa gorge était légèrement érythémateuse sans exsudat
Cou : Souple sans adénopathie

That’s a little better. We now have some information about negation attached to syntagmas, but our end goal is really to know, for each entity, whether it should be considered as negated or not. In more practical terms, we now have negation attributes attached to our syntagmas, but what we really want is to have negation attributes attached to entities.

In medkit, the way to do this is to use the attrs_to_copy parameter. This parameter is available on all NER operations. It is used to tell the operation which attributes should be copied from the input segments to the newly matched entities (based on their label). In other words, it provides a way to propagate context attributes (such as negation attributes) for segments to entities.

Let’s again use a RegexpMatcher to find some entities, but this time from syntagmas rather than from sentences, and using attrs_to_copy to copy negation attributes:

regexp_matcher = RegexpMatcher(rules=regexp_rules, attrs_to_copy=["is_negated"])
entities = regexp_matcher.run(syntagmas)

for entity in entities:
    neg_attr = entity.attrs.get(label="is_negated")[0]
    print(f"text='{entity.text}', label={entity.label}, is_negated={neg_attr.value}")
text='allergies', label=problem, is_negated=False
text='allergies', label=problem, is_negated=False
text='Allegra', label=treatment, is_negated=False
text='vaporisateurs', label=treatment, is_negated=False
text='vaporisateurs', label=treatment, is_negated=True
text='asthme', label=problem, is_negated=False
text='asthme', label=problem, is_negated=False
text='Allegra', label=treatment, is_negated=False
text='allergies', label=problem, is_negated=True
text='Allegra', label=treatment, is_negated=False
text='loratadine', label=treatment, is_negated=False
text='Nasonex', label=treatment, is_negated=False

We now have a negation Attribute for each entity!

Augmenting a document#

We now have an interesting set of annotations. We might want to process them directly, for instance to generate table-like data about patient treatment in order to compute some statistics. But we could also want to attach them back to our document in order to save them or export them to some format.

The annotations of a text document can be access with TextDocument.anns, an instance of TextAnnotationContainer) that behaves roughly like a list but also offers additional filtering methods. Annotations can be added by calling its add() method:

for entity in entities:
    doc.anns.add(entity)

The document and its entities can then be exported to supported external formats (cf BratOutputConverter), or serialized in the medkit format. This is not yet supported but will be in a later version. For now, there is an undocumented TextDocument.to_dict() method that will convert a document and its annotations to a json-serializable dict:

doc.to_dict()
{'uid': 'dfbbeadc-8e23-11ee-973f-0242ac110002',
 'text': "SUBJECTIF : Cette femme blanche de 23 ans se plaint d'allergies. Elle avait l'habitude d'avoir des allergies lorsqu'elle vivait à Seattle mais elle pense qu'elles sont pires ici. Dans le passé, elle a essayé le Claritin et le Zyrtec. Les deux ont fonctionné pendant une courte période mais ont ensuite semblé perdre de leur efficacité. Elle a également utilisé Allegra. Elle l'a utilisé l'été dernier et a recommencé à le faire il y a deux semaines. Il ne semble pas fonctionner très bien. Elle a utilisé des vaporisateurs en vente libre, mais pas de vaporisateurs nasaux sur ordonnance. Elle a de l'asthme, mais n'a pas besoin de prendre des médicaments tous les jours pour cela et ne pense pas que son asthme s'aggrave.\n\nMÉDICAMENTS : Son seul médicament est actuellement Ortho Tri-Cyclen et l'Allegra.\n\nALLERGIES : Elle n'a pas d'allergies médicamenteuses connues.\n\nEXAMEN PHYSIQUE :\nSignes vitaux : Poids de 59.3 kilos et pression sanguine de 124/78.\nTete/Yeux/Oreilles/Nez/Gorge : Sa gorge était légèrement érythémateuse sans exsudat. La muqueuse nasale était érythémateuse et enflée. Seul un drainage clair était visible. Les TM étaient claires.\nCou : Souple sans adénopathie.\nPoumons : Dégagés.\n\nEVALUATION : Rhinite allergique.\n\nPLAN :\n- Elle va réessayer le Zyrtec au lieu de l'Allegra. Une autre option sera d'utiliser la loratadine. Elle ne pense pas être remboursée donc ça pourrait être moins cher.\n- Echantillons de Nasonex : deux pulvérisations dans chaque narine pendant trois semaines. Une ordonnance a également été rédigée.\n",
 'metadata': {'path_to_text': '/builds/PffywbzJ/0/heka/medkit/docs/user_guide/../data/text/1.txt'},
 'anns': [{'uid': 'e1940d08-8e23-11ee-973f-0242ac110002',
   'label': 'problem',
   'text': 'allergies',
   'spans': [{'start': 54,
     'end': 63,
     '_class_name': 'medkit.core.text.span.Span'}],
   'attrs': [{'uid': 'e1940fb0-8e23-11ee-973f-0242ac110002',
     'label': 'is_negated',
     'value': False,
     'metadata': {},
     '_class_name': 'medkit.core.attribute.Attribute'}],
   'metadata': {'rule_id': 0, 'version': None},
   '_class_name': 'medkit.core.text.annotation.Entity'},
  {'uid': 'e194153c-8e23-11ee-973f-0242ac110002',
   'label': 'problem',
   'text': 'allergies',
   'spans': [{'start': 99,
     'end': 108,
     '_class_name': 'medkit.core.text.span.Span'}],
   'attrs': [{'uid': 'e1941712-8e23-11ee-973f-0242ac110002',
     'label': 'is_negated',
     'value': False,
     'metadata': {},
     '_class_name': 'medkit.core.attribute.Attribute'}],
   'metadata': {'rule_id': 0, 'version': None},
   '_class_name': 'medkit.core.text.annotation.Entity'},
  {'uid': 'e1941e06-8e23-11ee-973f-0242ac110002',
   'label': 'treatment',
   'text': 'Allegra',
   'spans': [{'start': 361,
     'end': 368,
     '_class_name': 'medkit.core.text.span.Span'}],
   'attrs': [{'uid': 'e1941fa0-8e23-11ee-973f-0242ac110002',
     'label': 'is_negated',
     'value': False,
     'metadata': {},
     '_class_name': 'medkit.core.attribute.Attribute'}],
   'metadata': {'rule_id': 2, 'version': None},
   '_class_name': 'medkit.core.text.annotation.Entity'},
  {'uid': 'e1942644-8e23-11ee-973f-0242ac110002',
   'label': 'treatment',
   'text': 'vaporisateurs',
   'spans': [{'start': 509,
     'end': 522,
     '_class_name': 'medkit.core.text.span.Span'}],
   'attrs': [{'uid': 'e19427d4-8e23-11ee-973f-0242ac110002',
     'label': 'is_negated',
     'value': False,
     'metadata': {},
     '_class_name': 'medkit.core.attribute.Attribute'}],
   'metadata': {'rule_id': 3, 'version': None},
   '_class_name': 'medkit.core.text.annotation.Entity'},
  {'uid': 'e1942b44-8e23-11ee-973f-0242ac110002',
   'label': 'treatment',
   'text': 'vaporisateurs',
   'spans': [{'start': 551,
     'end': 564,
     '_class_name': 'medkit.core.text.span.Span'}],
   'attrs': [{'uid': 'e1942cca-8e23-11ee-973f-0242ac110002',
     'label': 'is_negated',
     'value': True,
     'metadata': {'rule_id': 0},
     '_class_name': 'medkit.core.attribute.Attribute'}],
   'metadata': {'rule_id': 3, 'version': None},
   '_class_name': 'medkit.core.text.annotation.Entity'},
  {'uid': 'e1942ff4-8e23-11ee-973f-0242ac110002',
   'label': 'problem',
   'text': 'asthme',
   'spans': [{'start': 600,
     'end': 606,
     '_class_name': 'medkit.core.text.span.Span'}],
   'attrs': [{'uid': 'e1943184-8e23-11ee-973f-0242ac110002',
     'label': 'is_negated',
     'value': False,
     'metadata': {},
     '_class_name': 'medkit.core.attribute.Attribute'}],
   'metadata': {'rule_id': 1, 'version': None},
   '_class_name': 'medkit.core.text.annotation.Entity'},
  {'uid': 'e19437b0-8e23-11ee-973f-0242ac110002',
   'label': 'problem',
   'text': 'asthme',
   'spans': [{'start': 704,
     'end': 710,
     '_class_name': 'medkit.core.text.span.Span'}],
   'attrs': [{'uid': 'e19439d6-8e23-11ee-973f-0242ac110002',
     'label': 'is_negated',
     'value': False,
     'metadata': {},
     '_class_name': 'medkit.core.attribute.Attribute'}],
   'metadata': {'rule_id': 1, 'version': None},
   '_class_name': 'medkit.core.text.annotation.Entity'},
  {'uid': 'e1943e2c-8e23-11ee-973f-0242ac110002',
   'label': 'treatment',
   'text': 'Allegra',
   'spans': [{'start': 796,
     'end': 803,
     '_class_name': 'medkit.core.text.span.Span'}],
   'attrs': [{'uid': 'e1943fbc-8e23-11ee-973f-0242ac110002',
     'label': 'is_negated',
     'value': False,
     'metadata': {},
     '_class_name': 'medkit.core.attribute.Attribute'}],
   'metadata': {'rule_id': 2, 'version': None},
   '_class_name': 'medkit.core.text.annotation.Entity'},
  {'uid': 'e19442e6-8e23-11ee-973f-0242ac110002',
   'label': 'problem',
   'text': 'allergies',
   'spans': [{'start': 833,
     'end': 842,
     '_class_name': 'medkit.core.text.span.Span'}],
   'attrs': [{'uid': 'e1944476-8e23-11ee-973f-0242ac110002',
     'label': 'is_negated',
     'value': True,
     'metadata': {'rule_id': 0},
     '_class_name': 'medkit.core.attribute.Attribute'}],
   'metadata': {'rule_id': 0, 'version': None},
   '_class_name': 'medkit.core.text.annotation.Entity'},
  {'uid': 'e194513c-8e23-11ee-973f-0242ac110002',
   'label': 'treatment',
   'text': 'Allegra',
   'spans': [{'start': 1287,
     'end': 1294,
     '_class_name': 'medkit.core.text.span.Span'}],
   'attrs': [{'uid': 'e19452cc-8e23-11ee-973f-0242ac110002',
     'label': 'is_negated',
     'value': False,
     'metadata': {},
     '_class_name': 'medkit.core.attribute.Attribute'}],
   'metadata': {'rule_id': 2, 'version': None},
   '_class_name': 'medkit.core.text.annotation.Entity'},
  {'uid': 'e194566e-8e23-11ee-973f-0242ac110002',
   'label': 'treatment',
   'text': 'loratadine',
   'spans': [{'start': 1332,
     'end': 1342,
     '_class_name': 'medkit.core.text.span.Span'}],
   'attrs': [{'uid': 'e19457f4-8e23-11ee-973f-0242ac110002',
     'label': 'is_negated',
     'value': False,
     'metadata': {},
     '_class_name': 'medkit.core.attribute.Attribute'}],
   'metadata': {'rule_id': 4, 'version': None},
   '_class_name': 'medkit.core.text.annotation.Entity'},
  {'uid': 'e1945d94-8e23-11ee-973f-0242ac110002',
   'label': 'treatment',
   'text': 'Nasonex',
   'spans': [{'start': 1430,
     'end': 1437,
     '_class_name': 'medkit.core.text.span.Span'}],
   'attrs': [{'uid': 'e1946028-8e23-11ee-973f-0242ac110002',
     'label': 'is_negated',
     'value': False,
     'metadata': {},
     '_class_name': 'medkit.core.attribute.Attribute'}],
   'metadata': {'rule_id': 5, 'version': None},
   '_class_name': 'medkit.core.text.annotation.Entity'}],
 '_class_name': 'medkit.core.text.document.TextDocument'}

Visualizing entities with displacy#

Rather than printing entities, we can visualize them with displacy, a visualization tool part of the spaCy NLP library. Medkit provides helper functions to facilitate the use of displacy in the displacy_utils module:

from spacy import displacy
from medkit.text.spacy.displacy_utils import medkit_doc_to_displacy

displacy_data = medkit_doc_to_displacy(doc)
displacy.render(displacy_data, manual=True, style="ent")
SUBJECTIF : Cette femme blanche de 23 ans se plaint d' allergies problem . Elle avait l'habitude d'avoir des allergies problem lorsqu'elle vivait à Seattle mais elle pense qu'elles sont pires ici. Dans le passé, elle a essayé le Claritin et le Zyrtec. Les deux ont fonctionné pendant une courte période mais ont ensuite semblé perdre de leur efficacité. Elle a également utilisé Allegra treatment . Elle l'a utilisé l'été dernier et a recommencé à le faire il y a deux semaines. Il ne semble pas fonctionner très bien. Elle a utilisé des vaporisateurs treatment en vente libre, mais pas de vaporisateurs treatment nasaux sur ordonnance. Elle a de l' asthme problem , mais n'a pas besoin de prendre des médicaments tous les jours pour cela et ne pense pas que son asthme problem s'aggrave.

MÉDICAMENTS : Son seul médicament est actuellement Ortho Tri-Cyclen et l' Allegra treatment .

ALLERGIES : Elle n'a pas d' allergies problem médicamenteuses connues.

EXAMEN PHYSIQUE :
Signes vitaux : Poids de 59.3 kilos et pression sanguine de 124/78.
Tete/Yeux/Oreilles/Nez/Gorge : Sa gorge était légèrement érythémateuse sans exsudat. La muqueuse nasale était érythémateuse et enflée. Seul un drainage clair était visible. Les TM étaient claires.
Cou : Souple sans adénopathie.
Poumons : Dégagés.

EVALUATION : Rhinite allergique.

PLAN :
- Elle va réessayer le Zyrtec au lieu de l' Allegra treatment . Une autre option sera d'utiliser la loratadine treatment . Elle ne pense pas être remboursée donc ça pourrait être moins cher.
- Echantillons de Nasonex treatment : deux pulvérisations dans chaque narine pendant trois semaines. Une ordonnance a également été rédigée.

Wrapping it up#

In this tutorial, we have:

  • created a TextDocument from an existing text file;

  • instantiated several pre-processing, segmentation, context detection and entity matching operations;

  • ran these operations sequentially over the document and obtained entities;

  • attached these entities back to the original document.

The operations we have used in this tutorial are rather basic ones, mostly rule-based, but there are many more available in medkit, including model-based NER operations. You can learn about them in the API reference.

That’s a good first overview of what you can do with medkit! To dive in further, you might be interested in how to encapsulate all these operations in a pipeline.