Functional Weave
Code in TypeScript

education.attendance-rate

Pupil attendance and absence rates in basis points, with the DfE persistent and severe absence flags.

1.0.1 · published 2026-10-03 by charlie · Anterra

Pinned by 19 tests, run in TypeScript, Python and Rust.

What it does

A pupil's attendance and absence rates in basis points (9000 is 90.00%), and whether they are persistently or severely absent under the Department for Education's definitions for schools in England.

## The definitions, and why they are data

For example

  • attendanceRate(380, 38, 2025-10-01) → attendance basis points 90%, absence basis points 10%, persistently absent true, severely absent false 38 of 380 sessions missed is exactly 10%: persistently absent
  • attendanceRate(380, 37, 2025-10-01) → attendance basis points 90.26%, absence basis points 9.74%, persistently absent false, severely absent false 37 of 380 is 9.74%: not persistently absent
  • attendanceRate(2,501, 250, 2025-10-01) → attendance basis points 90%, absence basis points 10%, persistently absent false, severely absent false 250 of 2,501 rounds to 10.00% but is under 10%: not persistently absent

The function

The same function in TypeScript, Python and Rust, pinned by the same tests. Pick your language; the choice follows you around the registry.

export function attendanceRate(possibleSessions: number, missedSessions: number, onDate: string): AttendanceRate
possibleSessionsintsessions the pupil could have attended: two a day, morning and afternoon
missedSessionsintsessions missed for any reason, authorised or not
onDatedatea date in the period measured, which picks the definitions in force
returnsAttendanceRaterates and flags; the flags use the exact fraction, not the rounded rate

The type it declares, generated into your project

/** Attendance, absence and the DfE absence flags for one pupil. */
export interface AttendanceRate {
  /** 10000 minus absenceBasisPoints */
  readonly attendanceBasisPoints: number;
  /** missed / possible, rounded half-up to a basis point */
  readonly absenceBasisPoints: number;
  /** missed 10% or more of possible sessions */
  readonly persistentlyAbsent: boolean;
  /** missed 50% or more; null on a date before that measure was published */
  readonly severelyAbsent: boolean | null;
}

Your code names it in one line, in the file that uses it

import { attendanceRate } from "#fune/education.attendance-rate@^1";
impl/typescript.ts · 44 lines · open · raw

Imports name this capability’s declared dependencies, which fune builds next to it in your project; each one links to its page.

import { parseIsoDate } from "./dates_add_days.ts";  ← from dates.add-days ^1.0.0 · built alongside by fune
import { roundDiv } from "./math_round_div.ts";  ← from math.round-div ^1.0.0 · built alongside by fune
import { ABSENCE_THRESHOLDS, ABSENCE_THRESHOLDS_HISTORY, ABSENCE_THRESHOLDS_HORIZON } from "./education_attendance_rate_data.ts";  ← this capability’s own data, compiled from data/absence-thresholds.json into the same file by fune build
import { type AttendanceRate } from "./education_attendance_rate_types.ts";

function thresholdOn(measure: string, onDate: string): number | null {
  for (const r of ABSENCE_THRESHOLDS) {
    if (r.measure === measure && onDate >= r.validFrom && (r.validTo === null || onDate <= r.validTo)) return r.basisPoints;
  }
  return null;
}

/**
 * Attendance and absence in basis points, and the DfE absence flags.
 *
 * The flags compare missed x 10000 with possible x threshold, exactly: a rate
 * of 9.996% rounds to 10.00% but is not persistent absence.
 */
export function attendanceRate(possibleSessions: number, missedSessions: number, onDate: string): AttendanceRate {
  if (!Number.isInteger(possibleSessions) || possibleSessions < 1) {
    throw new RangeError(`possibleSessions must be a whole number of 1 or more, received ${possibleSessions}`);
  }
  if (!Number.isInteger(missedSessions) || missedSessions < 0 || missedSessions > possibleSessions) {
    throw new RangeError(`missedSessions must be a whole number from 0 to possibleSessions (${possibleSessions}), received ${missedSessions}`);
  }
  parseIsoDate(onDate);
  const persistent = thresholdOn("persistent", onDate);
  if (persistent === null) {
    if (ABSENCE_THRESHOLDS_HISTORY !== "full" && ABSENCE_THRESHOLDS_HORIZON !== null && onDate < ABSENCE_THRESHOLDS_HORIZON) {
      throw new RangeError(
        `no persistent absence definition on ${onDate}: this build was installed with history=${ABSENCE_THRESHOLDS_HISTORY}, so it only carries rules from ${ABSENCE_THRESHOLDS_HORIZON}. Reinstall with history=full for earlier years.`,
      );
    }
    throw new RangeError(`no persistent absence definition on ${onDate}`);
  }
  const severe = thresholdOn("severe", onDate);
  const absence = roundDiv(missedSessions * 10000, possibleSessions, "half-up");
  return {
    attendanceBasisPoints: 10000 - absence,
    absenceBasisPoints: absence,
    persistentlyAbsent: missedSessions * 10000 >= possibleSessions * persistent,
    severelyAbsent: severe === null ? null : missedSessions * 10000 >= possibleSessions * severe,
  };
}

Install

fune build

With that line in your source, in a TypeScript project (language typescript in fune.project), fune build resolves it and its 2 dependencies, pins them in fune.lock, downloads only the TypeScript package of each, and builds the code above into your project’s .fune/build, one readable file per capability with a header linking back here. Or pin a range in fune.project and build in one step:

fune add education.attendance-rate
Download for TypeScript education.attendance-rate-1.0.1-typescript.fune · 14,013 bytes sha256 ed6ad358deac7248ce4a4446e34f1b08227993fd5cbddfd03d8a4466b7d913e8

The manifest, vectors and README with only the TypeScript implementation. Install it without the registry with fune add ./education.attendance-rate-1.0.1-typescript.fune, or fetch it from a terminal with fune pull education.attendance-rate@1.0.1:typescript.

The whole function, every language, is one file too: education.attendance-rate-1.0.1.fune, 20,274 bytes, sha256 628cb864d801b56920bd73fdf860b8bf0b0145e5019b166523a736ed57a86b22. It installs into a project of any language.

Customise it in your app

The seams this capability offers. Put a marker directly above a function of your own and fune build wires it into the built code; the package on the registry is not changed, the built file’s header lists it under CUSTOMISED, and fune hooks lists every hook in the project. How hooks work.

before — your function gets the arguments and returns them, changed or not, or throws to refuse the call.

// fune: before education.attendance-rate

after — your function gets the result and the arguments, and returns the final result.

// fune: after education.attendance-rate

replace — inside this capability’s code only, calls to a dependency go to your function, with the same signature. Other capabilities that use it are unaffected; write in * to replace it everywhere.

// fune: replace dates.add-days in education.attendance-rate
// fune: replace math.round-div in education.attendance-rate

step — your function runs at a numbered point inside the function’s body, receives the in-scope values it names as parameters, and may return replacements. List the points with fune show education.attendance-rate --steps.

// fune: step education.attendance-rate after <n|label>

Tests

A version published now needs at least 8 tests for every function, and one that expects the error for each function that throws; the registry refuses it otherwise. fune verify --all runs each case in TypeScript, Python and Rust, and a project runs them again with fune verify. This page lists the cases; it does not run them. The exact JSON is vectors.json.

CaseArgumentsExpected
38 of 380 sessions missed is exactly 10%: persistently absent 380, 38, 2025-10-01 → attendance basis points 90%, absence basis points 10%, persistently absent true, severely absent false
37 of 380 is 9.74%: not persistently absent 380, 37, 2025-10-01 → attendance basis points 90.26%, absence basis points 9.74%, persistently absent false, severely absent false
250 of 2,501 rounds to 10.00% but is under 10%: not persistently absent 2,501, 250, 2025-10-01 → attendance basis points 90%, absence basis points 10%, persistently absent false, severely absent false
190 of 380 is exactly half: severely absent 380, 190, 2025-10-01 → attendance basis points 50%, absence basis points 50%, persistently absent true, severely absent true
189 of 380 is just under half 380, 189, 2025-10-01 → attendance basis points 50.26%, absence basis points 49.74%, persistently absent true, severely absent false
no sessions missed 380, 0, 2025-10-01 → attendance basis points 100%, absence basis points 0%, persistently absent false, severely absent false
every session missed 380, 380, 2025-10-01 → attendance basis points 0%, absence basis points 100%, persistently absent true, severely absent true
a half basis point rounds absence up and attendance is what is left, so they sum to 100% 20,000, 1, 2025-10-01 → attendance basis points 99.99%, absence basis points 0.01%, persistently absent false, severely absent false
a single possible session, attended 1, 0, 2025-10-01 → attendance basis points 100%, absence basis points 0%, persistently absent false, severely absent false
2016/17, before severe absence was published: severe is null 380, 190, 2016-10-01 → attendance basis points 50%, absence basis points 50%, persistently absent true, severely absent —
Show the other 9 tests
CaseArgumentsExpected
the last day before severe absence data 380, 190, 2018-08-31 → attendance basis points 50%, absence basis points 50%, persistently absent true, severely absent —
the first day of 2018/19 380, 190, 2018-09-01 → attendance basis points 50%, absence basis points 50%, persistently absent true, severely absent true
the first day of the 10% definition 380, 38, 2015-09-01 → attendance basis points 90%, absence basis points 10%, persistently absent true, severely absent —
before 2015/16 there is no per-pupil 10% definition 380, 38, 2015-08-31 → error: no persistent absence definition on 2015-08-31
more sessions missed than possible is an error 380, 381, 2025-10-01 → error: missedSessions must be a whole number from 0 to possibleSessions (380), received 381
negative missed sessions is an error 380, -1, 2025-10-01 → error: missedSessions must be a whole number from 0 to possibleSessions (380), received -1
half a session is an error 380, 1.5, 2025-10-01 → error: missedSessions must be a whole number from 0 to possibleSessions (380), received 1.5
no possible sessions is an error 0, 0, 2025-10-01 → error: possibleSessions must be a whole number of 1 or more, received 0
an impossible date is an error 380, 38, 2025-02-30 → error: "2025-02-30" is not a real calendar date

More from the author

- **Persistent absence**: missing 10% or more of the pupil's own possible sessions. This is the definition from the 2015/16 academic year. It was introduced in 2005/06 at 20%, and from 2010/11 was about 15% measured against a standard number of sessions rather than the pupil's own, which cannot be expressed as a percentage of one pupil's sessions; a date before 1 September 2015 is therefore refused rather than answered with today's rule. - **Severe absence**: missing 50% or more. The data carries it from the 2018/19 academic year, the first year for which the DfE statistics release reports it; before 1 September 2018 `severelyAbsent` is null.

Both are dated rows in `data/absence-thresholds.json`, and `onDate` picks the row in force.

## Decisions

- **The flags use the exact fraction.** A pupil who missed 250 of 2,501 sessions has an absence rate of 9.996%, which rounds to 1000 basis points, but is not persistently absent: 250 is less than 10% of 2,501. Comparing the rounded rate is the naive mistake; a vector pins it. - **Absence is rounded, attendance is derived.** Absence is rounded half-up to a basis point and attendance is 10000 minus that, so the two always add up to 100%. (Rounding both half-up gives 10001 when each sits on a half.) - Sessions are whatever the school counts: two per day (morning and afternoon) in England. Missed sessions include authorised and unauthorised absence, as the DfE measures do; pass only the sessions you want counted. - No possible sessions is an error: there is no rate to report.

## Sources

- Department for Education, "Pupil absence statistics: methodology" (Explore education statistics): definitions of persistent absence (10% or more of a pupil's own possible sessions, from 2015/16) and severe absence (50% or more), and the history of the persistent absence threshold: https://explore-education-statistics.service.gov.uk/methodology/pupil-absence-statistics-methodology - Department for Education, "Pupil absence in schools in England: 2018/19", the first release reporting severe absence: https://explore-education-statistics.service.gov.uk/find-statistics/pupil-absence-in-schools-in-england/2018-19

## Notices

Contains public sector information licensed under the Open Government Licence v3.0 (https://www.nationalarchives.gov.uk/doc/open-government-licence/version/3/).

1.0.1 adds its attribution notices (NOTICE). The code and the tests are unchanged.

Files

PathBytes
NOTICE188
README.md2,744
data/absence-thresholds.json477
impl/python.py2,516
impl/rust.rs3,537
impl/typescript.ts2,332
vectors.json4,449