Scoring Sleep Debt from Duty History with Pandas
The exact task this guide solves is turning a crew member’s sequence of rest windows into a running sleep-debt figure that a fatigue model can consume, using pandas to keep the whole history in one vectorised computation. Sleep debt is the accumulated shortfall between the sleep a crew member needed and the sleep opportunity they actually had, and it is the single most useful summary of chronic fatigue risk — a crew can be legal on every individual rest yet be carrying days of accumulated debt. This page derives sleep opportunity from rest windows, applies a nightly need, accumulates the debt with a bounded recovery model, and produces a per-day series. It feeds the homeostatic input of the three-process fatigue model within the fatigue risk scoring models section.
Prerequisites
- Python 3.11+ with
pandas. - A rest-window series — each rest’s UTC start and end, the facility zone, and an interruption flag, from the rest period compliance checks.
- A nightly sleep need — a configurable baseline, commonly eight hours.
- A sleep-efficiency factor — the fraction of a rest window realistically slept, below 1.0.
From rest window to sleep opportunity to debt
A rest window is not sleep. Some of it is spent commuting to accommodation, winding down, and waking, and an interrupted window yields less usable sleep still. The model therefore estimates sleep opportunity as the window duration scaled by an efficiency factor and discounted for interruptions, then compares it against the nightly need. Each day’s shortfall adds to a running debt, and days with surplus sleep pay some of it back — but recovery is bounded, because one long sleep does not erase a week of deprivation.
Step 1 — Estimate sleep opportunity per rest
Working in a pandas frame, the sleep opportunity is the window hours times efficiency, reduced further when the window was interrupted.
import pandas as pd
def sleep_opportunity(rests: pd.DataFrame, efficiency: float = 0.85,
interrupt_penalty: float = 0.75) -> pd.DataFrame:
df = rests.copy()
df["window_hours"] = (
(df["ends_utc"] - df["starts_utc"]).dt.total_seconds() / 3600.0
)
df["sleep_hours"] = df["window_hours"] * efficiency
df.loc[df["interrupted"], "sleep_hours"] *= interrupt_penalty
return df
Verify: a clean 10-hour rest yields 10 * 0.85 = 8.5 sleep hours; the same window flagged interrupted yields 8.5 * 0.75 ≈ 6.4 — the interruption pushes an apparently adequate rest below the nightly need.
Step 2 — Reduce to a daily series and compute the shortfall
Fatigue accumulates by day, so the per-rest sleep hours are aggregated to the acclimatised calendar day, and each day’s shortfall is the nightly need minus the sleep obtained.
def daily_shortfall(rests: pd.DataFrame, nightly_need: float = 8.0) -> pd.DataFrame:
df = sleep_opportunity(rests)
df["day"] = df["ends_utc"].dt.tz_convert("UTC").dt.floor("D")
daily = df.groupby("day", as_index=False)["sleep_hours"].sum()
daily["shortfall"] = nightly_need - daily["sleep_hours"]
return daily
Verify: a day with a single 6.4-hour interrupted sleep shows a shortfall of 1.6 hours against an 8-hour need; a day with 9 hours of sleep shows a negative shortfall of -1.0, i.e. a surplus available for recovery.
Step 3 — Accumulate debt with bounded recovery
The running debt adds each day’s shortfall but caps how much a surplus can repay, so a single long sleep cannot wipe out chronic debt, and the debt never goes negative. A bounded cumulative loop expresses this cleanly.
def accumulate_debt(daily: pd.DataFrame, max_recovery_per_day: float = 2.0) -> pd.DataFrame:
debt = 0.0
out = []
for shortfall in daily["shortfall"]:
if shortfall > 0:
debt += shortfall # deprivation adds fully
else:
debt += max(shortfall, -max_recovery_per_day) # recovery is capped
debt = max(debt, 0.0) # debt cannot be negative
out.append(round(debt, 2))
result = daily.copy()
result["cumulative_debt"] = out
return result
Verify: three consecutive 1.6-hour shortfalls build a debt of 4.8 hours; a following 9-hour night (a −1.0 surplus) reduces it to 3.8, not to zero — the cap keeps one good night from erasing three bad ones.
Failure modes and troubleshooting
- Treating window hours as sleep hours. Counting the full rest window as sleep overstates recovery. Remediation: scale by the efficiency factor and discount interruptions.
- Unbounded recovery. Letting one long sleep repay unlimited debt hides chronic deprivation. Remediation: cap recovery per day and floor the debt at zero.
- Aggregating on station time. Bucketing sleep by departure-station day misassigns a rest that spans midnight in the crew’s acclimatised theatre. Remediation: aggregate on the acclimatised calendar day.
- Ignoring interruptions. A window broken by a callout yields far less usable sleep than its length suggests. Remediation: carry the interruption flag and apply the penalty.
- Negative debt. Allowing debt to go negative banks “credit” that inflates future alertness. Remediation: floor the running debt at zero.
Frequently Asked Questions
Why is sleep debt more useful than a single rest check?
Because a crew member can pass every individual minimum-rest check while accumulating days of shortfall. Sleep debt summarises the chronic picture that per-rest checks miss, which is what distinguishes a sustainable roster from a legal-but-exhausting one.
How is sleep opportunity estimated from a rest window?
By scaling the window duration by an efficiency factor — the realistic fraction slept after commuting and settling — and discounting further when the window was interrupted. The result is an estimate of usable sleep, not clock time in the accommodation.
Why cap recovery from a surplus day?
Because physiology does not repay chronic debt in a single night. Capping the daily recovery models the fact that one long sleep helps but does not erase a week of deprivation, keeping the debt figure honest.
Does this replace the three-process model?
No — it feeds it. The cumulative debt informs the homeostatic starting point of the three-process model, which then predicts alertness across a specific duty. Debt is the chronic input; the three-process curve is the acute prediction.
Related
- Fatigue Risk Scoring Models — the parent section and scoring pipeline.
- Implementing a Three-Process Fatigue Model in Python — the model this debt figure feeds.
- Rest Period Compliance Checks — the rest windows this scoring consumes.
- Validating 168-Hour Weekly Rest with SQL Window Functions — the weekly rest that bounds recovery.
Back to Fatigue Risk Scoring Models.