First steps
Contents
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")
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.