edc-lab-results¶
Simple blood result data collection format for django models
- In this design
a specimen requisition for a panel is completed first (SubjectRequisition)
result is received and entered into a result form
if a result is admnormal or gradable, an ActionItem is created.
Building the Model¶
Below we create a model class with BloodResultsModelMixin. On the class we specify the lab_panel and limit the FK the requisitions of this panel using limit_choices_to.
# models.py
from edc_lab.model_mixins import CrfWithRequisitionModelMixin, requisition_fk_options
from edc_lab_panels.panels import chemistry_panel
class BloodResultsFbc(
CrfWithRequisitionModelMixin,
BloodResultsModelMixin,
BaseUuidModel,
):
lab_panel = fbc_panel
requisition = models.ForeignKey(
limit_choices_to={"panel__name": fbc_panel.name}, **requisition_fk_options
)
class Meta(CrfWithActionModelMixin.Meta, BaseUuidModel.Meta):
verbose_name = "Blood Result: FBC"
verbose_name_plural = "Blood Results: FBC"
The above example has no fields for results, so let’s add some model mixins, one for each result item.
# models.py
class BloodResultsFbc(
CrfWithRequisitionModelMixin,
HaemoglobinModelMixin,
HctModelMixin,
RbcModelMixin,
WbcModelMixin,
PlateletsModelMixin,
MchModelMixin,
MchcModelMixin,
McvModelMixin,
BloodResultsModelMixin,
CrfStatusModelMixin,
BaseUuidModel,
):
lab_panel = fbc_panel
requisition = models.ForeignKey(
limit_choices_to={"panel__name": fbc_panel.name}, **requisition_fk_options
)
class Meta(CrfWithActionModelMixin.Meta, BaseUuidModel.Meta):
verbose_name = "Blood Result: FBC"
verbose_name_plural = "Blood Results: FBC"
If an ActionItem is to be created because of an abnormal or reportable result item, add the ActionItem.
# models.py
class BloodResultsFbc(
CrfWithActionModelMixin,
CrfWithRequisitionModelMixin,
HaemoglobinModelMixin,
HctModelMixin,
RbcModelMixin,
WbcModelMixin,
PlateletsModelMixin,
MchModelMixin,
MchcModelMixin,
McvModelMixin,
BloodResultsModelMixin,
CrfStatusModelMixin,
BaseUuidModel,
):
action_name = BLOOD_RESULTS_FBC_ACTION
lab_panel = fbc_panel
requisition = models.ForeignKey(
limit_choices_to={"panel__name": fbc_panel.name}, **requisition_fk_options
)
class Meta(CrfWithActionModelMixin.Meta, BaseUuidModel.Meta):
verbose_name = "Blood Result: FBC"
verbose_name_plural = "Blood Results: FBC"
Building the ModeForm class¶
The ModelForm class just needs the Model class and the panel. In this case BloodResultsFbc and fbc_panel.
# forms.py
class BloodResultsFbcFormValidator(BloodResultsFormValidatorMixin, CrfFormValidator):
panel = fbc_panel
class BloodResultsFbcForm(ActionItemCrfFormMixin, CrfModelFormMixin, forms.ModelForm):
form_validator_cls = BloodResultsFbcFormValidator
class Meta(ActionItemCrfFormMixin.Meta):
model = BloodResultsFbc
fields = "__all__"
Building the ModelAdmin class¶
The ModelAdmin class needs the Model class, ModelForm class and the panel.
# admin.py
@admin.register(BloodResultsFbc, site=intecomm_subject_admin)
class BloodResultsFbcAdmin(BloodResultsModelAdminMixin, CrfModelAdmin):
form = BloodResultsFbcForm
fieldsets = BloodResultFieldset(
BloodResultsFbc.lab_panel,
model_cls=BloodResultsFbc,
extra_fieldsets=[(-1, action_fieldset_tuple)],
).fieldsets
The SubjectRequistion ModelAdmin class¶
When using autocomplete for the subject requsition FK on the result form ModelAdmin class, the subject requsition model admin class needs to filter the search results passed to the autocomplete control.
If all result models are prefixed with “bloodresult”, you can filter on the path name like this:
# admin.py
@admin.register(SubjectRequisition, site=intecomm_subject_admin)
class SubjectRequisitionAdmin(RequisitionAdminMixin, CrfModelAdmin):
form = SubjectRequisitionForm
# ...
def get_search_results(self, request, queryset, search_term):
queryset, use_distinct = super().get_search_results(request, queryset, search_term)
path = urlsplit(request.META.get("HTTP_REFERER")).path
query = urlsplit(request.META.get("HTTP_REFERER")).query
if "bloodresult" in str(path):
attrs = parse_qs(str(query))
try:
subject_visit = attrs.get("subject_visit")[0]
except (TypeError, IndexError):
pass
else:
queryset = queryset.filter(subject_visit=subject_visit, is_drawn=YES)
return queryset, use_distinct
Importing External Lab Results¶
edc_lab_results provides management commands and models to import lab results
from external sources (e.g. PDF reports from a hospital laboratory) into the EDC.
Models¶
ResultStores raw parsed lab data. Each row represents one investigation result from a PDF. Fields include patient identifiers, specimen details, timestamps, result values, units, flags, and reference ranges. A
utest_idfield links the result to the EDC’s internal test identifier. Asubject_identifierfield is resolved from thename_idon the PDF viaRegisteredSubject.InvestigationMappingPersists the mapping between a laboratory’s investigation name (as printed on the PDF) and the EDC
utest_id. Scoped bylaboratoryso the same investigation name can map differently at different labs. Anin_reportableboolean records whether theutest_idexists inedc_reportable.NormalData.
Settings¶
Two settings control the import behavior:
EDC_LAB_RESULTS_PARSERSA dict mapping laboratory names to dotted paths of parser callables. Each parser must accept
(folder, *, tz=None)and return apandas.DataFrame.# settings.py EDC_LAB_RESULTS_PARSERS = { "MNH": "parse_trial_labs.parse_folder", }
EDC_LAB_RESULTS_DEFAULT_MAPPINGSA dict of dicts providing default investigation-to-utest_id mappings per laboratory. Used as best guesses during the interactive prompt when no saved mapping exists.
# settings.py EDC_LAB_RESULTS_DEFAULT_MAPPINGS = { "MNH": { "WBC": "wbc", "RBC": "rbc", "HGB": "haemoglobin", "CREATININE": "creatinine", "CHOLESTEROL": "chol", # ... }, }
Writing a Custom Parser¶
A parser is any callable with the signature:
def parse_folder(
folder: str | Path,
*,
tz: ZoneInfo | None = None,
) -> pd.DataFrame:
...
The returned DataFrame must include columns matching the Result model fields.
At minimum: source_file, name_id, investigation, result, units,
flag, reference_range_lower, reference_range_upper, and the various
datetime and specimen metadata columns.
Register the parser in EDC_LAB_RESULTS_PARSERS:
EDC_LAB_RESULTS_PARSERS = {
"MNH": "parse_trial_labs.parsers.parse_mnh",
"KCMC": "my_project.parsers.kcmc_parse_folder",
}
Management Commands¶
import_labsParses PDF files, resolves investigation mappings interactively, and saves results to the database.
manage.py import_labs /path/to/pdf_folder --laboratory "MNH" manage.py import_labs /path/to/pdf_folder --laboratory "MNH" --dry-run manage.py import_labs /path/to/pdf_folder --laboratory "MNH" --output results.csv
The
--laboratoryflag is required. It selects the parser fromEDC_LAB_RESULTS_PARSERSand scopes the investigation mappings inInvestigationMapping.On first run, the command prompts for each unknown investigation:
Unknown investigation: CHOLESTEROL Best guess: chol Enter utest_id for 'CHOLESTEROL' [chol] or 'u' for unknown:
Accepted mappings are saved to
InvestigationMappingand reused on subsequent runs. The command also checksedc_reportable.NormalDataand warns about mappedutest_idvalues that have no normal range data.update_mappingUpdates an existing mapping and backfills the
utest_idon all matchingResultrows.manage.py update_mapping --laboratory "MNH" \ --investigation "ABS NEUTROPHIL" --utest-id "neutrophil"
Checks for conflicts (another investigation already mapped to the same
utest_id) and refreshes thein_reportableflag.