Section Tokenizer#

This tutorial will show an example of how to apply section tokenizer medkit operation on a text document.

Loading a text document#

For beginners, 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.

Defining section definition rules#

To split the text document into medkit segments corresponding to each section, we have to define a set of rules. These rules allow the operation to detect keywords triggering a new section.

from medkit.text.segmentation.section_tokenizer import SectionTokenizer

section_dict = {
    "patient": ["SUBJECTIF"],
    "traitement": ["MÉDICAMENTS", "PLAN"],
    "allergies": ["ALLERGIES"],
    "examen clinique": ["EXAMEN PHYSIQUE"],
    "diagnostique": ["EVALUATION"],
}

tokenizer = SectionTokenizer(section_dict=section_dict)

The sections definition is a dictionary of key-values where key will be the section name and value is a list of keywords to detect as the start of the section.

For example, if we detect the keyword EVALUATION in text, a new section named diagnostique will begin with this keyword and will end with the next detected section or otherwise, the end of the text.

As all operations, SectionTokenizer 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:

sections = tokenizer.run([doc.raw_segment])

print(f"Number of detected sections: {len(sections)}\n")

for section in sections:
    print(f"metadata = {section.metadata}")
    print(f"label = {section.label}")
    print(f"spans = {section.spans}")
    print(f"text = {section.text!r}\n")
Number of detected sections: 6

metadata = {'name': 'patient'}
label = section
spans = [Span(start=0, end=721)]
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."

metadata = {'name': 'traitement'}
label = section
spans = [Span(start=723, end=804)]
text = "MÉDICAMENTS : Son seul médicament est actuellement Ortho Tri-Cyclen et l'Allegra."

metadata = {'name': 'allergies'}
label = section
spans = [Span(start=806, end=867)]
text = "ALLERGIES : Elle n'a pas d'allergies médicamenteuses connues."

metadata = {'name': 'examen clinique'}
label = section
spans = [Span(start=869, end=1201)]
text = 'EXAMEN 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.'

metadata = {'name': 'diagnostique'}
label = section
spans = [Span(start=1203, end=1235)]
text = 'EVALUATION : Rhinite allergique.'

metadata = {'name': 'traitement'}
label = section
spans = [Span(start=1237, end=1542)]
text = "PLAN :\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."

As you can see, we have detected 6 different sections. Each section is a segment which 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 "SECTION" in our case but it could be different for other kinds of segments or if you initialize the operation with your own output label.

  • a metadata attribute, which contains a dictionary with section name value.

Defining section rules with renaming#

SectionTokenizer also allows to define rules (i.e., SectionModificationRule) for renaming detected sections based on the context of the section in the text.

Let’s take the same example.

from medkit.text.segmentation.section_tokenizer import SectionTokenizer, SectionModificationRule

section_dict = {
    "patient": ["SUBJECTIF"],
    "traitement": ["MÉDICAMENTS", "PLAN"],
    "allergies": ["ALLERGIES"],
    "examen clinique": ["EXAMEN PHYSIQUE"],
    "diagnostique": ["EVALUATION"],
}

Now, let’s add some rules for managing these cases:

  • if traitement section is detected before diagnostique section, then we rename it into traitement_entree

  • if traitement section is detected after diagnostique section, then we rename it into traitement_sortie

treatment_rules = [
    SectionModificationRule(
        section_name="traitement",
        new_section_name="traitement_entree",
        other_sections=["diagnostique"],
        order="BEFORE"),
    SectionModificationRule(
        section_name="traitement",
        new_section_name="traitement_sortie",
        other_sections=["diagnostique"],
        order="AFTER")
]

tokenizer = SectionTokenizer(section_dict=section_dict, section_rules=treatment_rules)

Let’s run this new operation on document raw text.

sections = tokenizer.run([doc.raw_segment])

print(f"Number of detected sections: {len(sections)}\n")

for section in sections:
    print(f"metadata = {section.metadata}")
    print(f"label = {section.label}")
    print(f"spans = {section.spans}")
    print(f"text = {section.text!r}\n")
Number of detected sections: 6

metadata = {'name': 'patient'}
label = section
spans = [Span(start=0, end=721)]
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."

metadata = {'name': 'traitement_entree'}
label = section
spans = [Span(start=723, end=804)]
text = "MÉDICAMENTS : Son seul médicament est actuellement Ortho Tri-Cyclen et l'Allegra."

metadata = {'name': 'allergies'}
label = section
spans = [Span(start=806, end=867)]
text = "ALLERGIES : Elle n'a pas d'allergies médicamenteuses connues."

metadata = {'name': 'examen clinique'}
label = section
spans = [Span(start=869, end=1201)]
text = 'EXAMEN 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.'

metadata = {'name': 'diagnostique'}
label = section
spans = [Span(start=1203, end=1235)]
text = 'EVALUATION : Rhinite allergique.'

metadata = {'name': 'traitement_sortie'}
label = section
spans = [Span(start=1237, end=1542)]
text = "PLAN :\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."

As you can see, we still detect 6 sections but 2 have been renamed into traitement_entree and traitement_sortie.

Using a yaml definition file#

We have seen how to write rules programmatically.

However, it is also possible to load a yaml file containing all your rules.

First, let’s create the yaml file based on previous steps.

import pathlib

filepath = pathlib.Path("section.yml")

SectionTokenizer.save_section_definition(
    section_dict=section_dict, 
    section_rules=treatment_rules,
    filepath=filepath,
    encoding='utf-8')

with open(filepath, 'r') as f:
    print(f.read())
rules:
- new_section_name: traitement_entree
  order: BEFORE
  other_sections:
  - diagnostique
  section_name: traitement
- new_section_name: traitement_sortie
  order: AFTER
  other_sections:
  - diagnostique
  section_name: traitement
sections:
  allergies:
  - ALLERGIES
  diagnostique:
  - EVALUATION
  examen clinique:
  - EXAMEN PHYSIQUE
  patient:
  - SUBJECTIF
  traitement:
  - MÉDICAMENTS
  - PLAN

Now, we will see how to initialize the SectionTokenizer operation for using this yaml file.

# Use tokenizer initialized using a yaml file
from medkit.text.segmentation.section_tokenizer import SectionTokenizer

section_dict, section_rules = SectionTokenizer.load_section_definition(filepath)

print(f"section_dict = {section_dict!r}\n")
print(f"section_rules = {section_rules!r}")

tokenizer = SectionTokenizer(section_dict=section_dict, section_rules=section_rules)
section_dict = {'allergies': ['ALLERGIES'], 'diagnostique': ['EVALUATION'], 'examen clinique': ['EXAMEN PHYSIQUE'], 'patient': ['SUBJECTIF'], 'traitement': ['MÉDICAMENTS', 'PLAN']}

section_rules = (SectionModificationRule(section_name='traitement', new_section_name='traitement_entree', other_sections=['diagnostique'], order='BEFORE'), SectionModificationRule(section_name='traitement', new_section_name='traitement_sortie', other_sections=['diagnostique'], order='AFTER'))

Now, let’s run the operation. We can observe that the results are the same.

sections = tokenizer.run([doc.raw_segment])

print(f"Number of detected sections: {len(sections)}\n")

for section in sections:
    print(f"metadata = {section.metadata}")
    print(f"label = {section.label}")
    print(f"spans = {section.spans}")
    print(f"text = {section.text!r}\n")
Number of detected sections: 6

metadata = {'name': 'patient'}
label = section
spans = [Span(start=0, end=721)]
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."

metadata = {'name': 'traitement_entree'}
label = section
spans = [Span(start=723, end=804)]
text = "MÉDICAMENTS : Son seul médicament est actuellement Ortho Tri-Cyclen et l'Allegra."

metadata = {'name': 'allergies'}
label = section
spans = [Span(start=806, end=867)]
text = "ALLERGIES : Elle n'a pas d'allergies médicamenteuses connues."

metadata = {'name': 'examen clinique'}
label = section
spans = [Span(start=869, end=1201)]
text = 'EXAMEN 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.'

metadata = {'name': 'diagnostique'}
label = section
spans = [Span(start=1203, end=1235)]
text = 'EVALUATION : Rhinite allergique.'

metadata = {'name': 'traitement_sortie'}
label = section
spans = [Span(start=1237, end=1542)]
text = "PLAN :\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."