cassionData Analysis

Lesson 1 of 8

Unit · How the database is shaped

What a value in DHIS2 actually is

Five coordinates, not one number. Data element, category option combination, org unit, period and dataset — and the difference between a data element and an indicator, which is where most confusion about routine figures starts.

PythonR90 minUNICEF indicator definitions

A value has five coordinates

The vaccination extract looks like a spreadsheet: facility, period, antigen, doses. In the system it came from, a single stored value is addressed by five things, and knowing all five is what makes a figure defensible.

Coordinate In the extract What it answers
Data element antigen plus the measure What was counted
Category option combination not present Which breakdown of it
Org unit facility_id Where
Period period When
Dataset not present Which form it was collected on

Two of the five are missing from the export, and that is the normal case. Most of this lesson is about what those two carry and why their absence causes arguments.

Data element: the thing that is counted

A data element is the raw measure a form collects — “Penta3 doses administered”, “BCG doses administered”. It is a count of something, entered by a person, and it is stored exactly as entered.

import pandas as pd

vax = pd.read_csv("vaccination-coverage-2024.v1.csv", parse_dates=["period"])
print(sorted(vax["antigen"].unique()))
print(vax.groupby("antigen")["doses_administered"].sum())
library(dplyr)
library(readr)

vax <- read_csv("vaccination-coverage-2024.v1.csv")

vax |> summarise(doses = sum(doses_administered), .by = antigen)

Six antigens, six data elements. The target_population column is a seventh value of a different kind, and in a real instance it would usually not be a data element at all — it is a denominator, stored either as a data element captured once a year or as an org unit attribute.

Ask which it is. A denominator stored as an annual data element can be revised retrospectively; one stored as an attribute changes for every period at once. That difference is why a coverage figure computed in January and again in June can differ with no new doses.

Category combinations: the breakdown built into the element

A category combination splits a data element into the cells the form actually collects. Penta3 might be broken down by age (under 1 / 1 and over) and by sex, giving four category option combinations per data element.

Data element:  Penta3 doses administered
Category combo: Age (<1, 1+) x Sex (F, M)
Stored values:  4 per org unit per period

This is where the most common misreading of a DHIS2 export lives. If your extract has one row per antigen per facility per month, one of two things is true, and they are not equivalent:

  • The data element has no disaggregation, so there is one value and nothing was lost.
  • The export aggregated across category option combinations, in which case the disaggregation exists in the system and your file cannot recover it.
expected = (vax["facility_id"].nunique() * vax["period"].nunique()
            * vax["antigen"].nunique())
print(f"{expected} expected rows, {len(vax)} in the file")
c(expected = n_distinct(vax$facility_id) * n_distinct(vax$period) *
             n_distinct(vax$antigen),
  actual = nrow(vax))

38 × 12 × 6 = 2,736, and the file has 2,736 rows. The grid is exactly full, which tells you the export is at data element level with no category disaggregation surviving. If a report then asks for coverage by sex, the answer is not in this file, and it may or may not be in the system.

Data element versus indicator

The distinction that causes the most trouble, and it is worth being precise about.

  • A data element is stored. Somebody typed it.
  • An indicator is calculated. It has a numerator, a denominator and a factor, and DHIS2 evaluates it at whatever level you ask for.
Indicator:   Penta3 coverage
Numerator:   #{Penta3 doses administered}
Denominator: #{Surviving infants}
Factor:      100

Three consequences that come up constantly.

An indicator is not stored, so it cannot be wrong in the database. If a coverage figure is wrong, either a data element is wrong or the indicator definition is. Those have different owners and different fixes.

The indicator is evaluated at the level you request, and the arithmetic is sum-then-divide, not divide-then-average. Requesting district-level coverage sums numerators and denominators across facilities; it does not average facility coverages. That is the right behaviour and it is the opposite of what a spreadsheet user usually does.

district = vax[vax["report_submitted"] == True]
correct = (district["doses_administered"].sum()
           / district["target_population"].sum())
wrong = (district["doses_administered"] / district["target_population"]).mean()
print(f"ratio of sums {correct:.3f}, mean of ratios {wrong:.3f}")
vax |>
  filter(report_submitted) |>
  summarise(ratio_of_sums = sum(doses_administered) / sum(target_population),
            mean_of_ratios = mean(doses_administered / target_population))

An indicator can be requested for a period the data does not support. Ask for annual coverage and DHIS2 will sum twelve monthly numerators over a denominator that may be an annual figure or a summed monthly one, depending on how the denominator element is configured. Getting a plausible number out is not evidence that the aggregation was right.

Datasets: the form, and what “expected” means

A dataset is the form a group of data elements is collected on, assigned to a set of org units, with a period type. It is the thing that decides which units are expected to report, which is the foundation of every completeness figure the system produces.

reported = vax["report_submitted"] == True
print(f"{reported.sum()} of {len(vax)} facility-month-antigen values reported")
print(f"reporting rate {reported.mean():.1%}")
mean(vax$report_submitted)

76.5%. That number only means anything because a dataset assignment says all 38 facilities were expected to submit every month. Without the dataset, there is no denominator and no completeness.

Note also what the extract does not carry: reporting here is all-or-nothing per facility-month across all six antigens, which is what a single dataset containing all six looks like. A system with the antigens split across two datasets would show partial months, and that structure would be visible in the data.

partial = (
    vax[reported].groupby(["facility_id", "period"])["antigen"].nunique()
)
print(f"{(partial < 6).sum()} facility-months with only some antigens reported")
vax |>
  filter(report_submitted) |>
  summarise(antigens = n_distinct(antigen), .by = c(facility_id, period)) |>
  filter(antigens < 6) |>
  nrow()

Zero. One dataset, six elements, submitted together.

Ask for the metadata, not just the data

The practical conclusion of this lesson. When you request an extract, request the metadata with it:

  • The data element definitions, including how each is aggregated over periods (summed, averaged, or last value — this matters enormously and nobody asks).
  • The category combination for each element, so you know what was collapsed.
  • The dataset assignment, so you know how many units were expected.
  • The indicator definitions for anything calculated, with numerator and denominator expressions.

That is four short files and they turn an export into something a reference sheet can be written from. The Indicator Design course asked for a source down to the field; this is what that looks like when the source is DHIS2.

What comes next

You know what a value is and where it sits. The next lesson moves it — up an org unit hierarchy that is not fixed, where a facility reassigned between districts in March means a district total that cannot simply be summed.

Teach this lesson

The lesson as a slide deck, with the prose kept in the speaker notes rather than on the slide. Generated from this page, so it cannot fall out of step with it.

Start the slideshowRead the slides

The PDF needs no software and projects from any machine. The PowerPoint file is there to be edited — add your organisation's branding, cut a section for a shorter session, or merge two lessons into a workshop.