Ten answers about last night, and an estimate of how much of your time in bed you actually spent asleep.
The Random Forest from Comparative Analysis of Machine Learning Methods for Enhancing Sleep Efficiency and Prediction, wrapped in a Flask app you can run locally. The paper compared four regressors on 452 nights of sleep-study data. Random Forest won, at R² 0.8569 and MSE 0.0027, and this repository ships it.
Hammad Ahmad, M. Umar Khan and Maleeha Azam. First-author paper, ICSMAI 2024. In "Advances in Smart Medical, IoT and Artificial Intelligence", Information Systems Engineering and Management series, Springer Nature Switzerland, 2024, pages 3 to 15. doi.org/10.1007/978-3-031-66854-8_1
Full case study: hammadahmad.co.uk/projects/sleep-efficiency
A 60 year old smoker, sedentary, four drinks, five awakenings, and a night that was 60% light sleep against 25% deep, comes back at 0.58. That sits left of both cuts on the band ruler, and the advice that follows is to get assessed rather than to tweak a bedtime routine.
Two pages, one stylesheet, no build step and no framework.
- The three percentages are checked as you type. Deep, light and REM have to total 100, which is the model's own constraint. The panel beside those fields fills as you go and says how far off the total is, rather than rejecting the night after you have already submitted it.
- Errors are visible. Field and radio errors were rendered nowhere before, so leaving a radio unset just redisplayed the form with no explanation.
- The score lands on a scale. The result page puts it on the 0.50 to 1.00 axis the data actually spans, with the 0.64 and 0.80 cuts marked, instead of printing a bare number.
- The inputs are grouped by what they are: the person, the night, the day before. Every numeric field carries the range the model was trained on, since nothing stops you entering a value from outside it.
- Colour carries meaning. The same three hues are deep, light and REM wherever they appear, and amber is only ever an action.
- Below 900px the layout collapses to one column and the readout sticks to the top of the screen, so it stays in view while the percentages are being typed.
Type is IBM Plex Mono for anything numeric or structural, Instrument Sans for prose. Both come from Google Fonts and fall back to system faces offline.
Time asleep divided by time in bed. Eight hours in bed and six and a half asleep is about 0.81. The app splits the range three ways:
| Band | Range | What it means |
|---|---|---|
| High | 0.80 and above | Typical of healthy adults |
| Normal | 0.64 to 0.79 | Room to improve, mostly behavioural |
| Low | below 0.64 | Worth a clinical opinion |
Sleep architecture is almost the whole model. Deep sleep percentage, light sleep percentage and awakenings account for 87.7% of the forest's split importance between them. Everything a person controls directly on a given evening, caffeine at 0.7%, exercise at 1.2%, alcohol at 1.5%, is close to noise by comparison.
That is worth stating plainly, because it sets what the tool is for. It predicts efficiency well from measurements you need a sleep tracker to obtain. It is not a lifestyle calculator, and the inputs a person can act on are the ones it weighs least.
The raw correlations agree. Light sleep runs against efficiency at -0.82 and deep sleep with it at +0.79, close to two views of one underlying quantity. Awakenings at -0.56 and alcohol at -0.39 come next. Total sleep duration is essentially unrelated at -0.03: how long you were in bed says almost nothing about how much of it you slept.
The numbers behind both charts
Feature importances, read from sleep_efficiency_model.pkl:
| Feature | Importance |
|---|---|
| Deep sleep percentage | 0.3745 |
| Light sleep percentage | 0.3605 |
| Awakenings | 0.1420 |
| Age | 0.0405 |
| Smoking status | 0.0299 |
| REM sleep percentage | 0.0156 |
| Alcohol consumption | 0.0146 |
| Exercise frequency | 0.0124 |
| Caffeine consumption | 0.0071 |
| Gender | 0.0030 |
Regenerate both charts with python tools/make_readme_charts.py.
Sleep_Efficiency.csv, 452 nights across 15 columns. Efficiency runs from 0.50
to 0.99 with a median of 0.82, so the cohort skews towards good sleepers: 257
nights land in the high band, 119 normal, 76 low.
Four columns carry missing values, 65 cells in total, and dropping incomplete rows leaves 388 usable nights. REM, deep and light percentages sum to exactly 100 on every row, which is why the form rejects any combination that does not.
flowchart LR
FORM[Flask form<br/>10 fields] --> V[validate<br/>REM + deep + light = 100]
V --> VEC[feature vector<br/>ordered to match the model]
VEC --> RF[(RandomForestRegressor<br/>100 trees)]
RF --> BAND[band at 0.80 / 0.64]
BAND --> OUT[score, colour, advice]
Two routes. / renders the form and handles the POST, /results/<prediction>
renders the score. The model loads once at import.
The feature vector has to be built in the order the model was fitted in, which puts smoking status before exercise frequency. Passing a bare NumPy array means scikit-learn cannot check names, so getting that order wrong fails silently rather than raising.
The ten inputs
| Field | Type | Range in the data |
|---|---|---|
| Age | integer | 9 to 69 |
| Gender | Female / Male | |
| REM sleep percentage | number | 15 to 30 |
| Deep sleep percentage | number | 18 to 75 |
| Light sleep percentage | number | 7 to 63 |
| Awakenings | integer | 0 to 4 |
| Caffeine consumption | mg | 0 to 200 |
| Alcohol consumption | oz | 0 to 5 |
| Exercise frequency | Sedentary to Heavy | |
| Smoking status | No / Yes |
The three percentages must total 100. The form tracks that as you type and the server checks it again on submit.
Requires Python 3.11.
git clone https://github.com/1oNN/sleep-efficiency-app.git
cd sleep-efficiency-app
python -m venv .venv
source .venv/bin/activate # Windows: .venv\Scripts\activate
pip install -r requirements.txt
python app.py # http://localhost:5000The scikit-learn pin matters. The model was serialised under scikit-learn 1.3, and 1.4 changed the tree internals, so a newer release imports the pickle without complaint and then raises on the first prediction. If you want a current scikit-learn, retrain rather than force the load. Set
PORTif 5000 is already taken.
Docker works as well:
docker compose up -d- 452 nights is a small dataset, from one sleep study, skewed towards good sleepers. Treat this as a demonstration of the paper's method rather than a general instrument.
- The strongest inputs need a sleep tracker. Deep and light sleep percentages are not values you can estimate honestly from memory, and they dominate the prediction.
- The form offers four exercise levels, the data has six. Training used frequency 0 to 5; the radio buttons stop at 3, so the top two levels cannot be entered.
- The form accepts values the model never saw. Inputs cap at sane bounds rather than at the training range, and the hint under each field says what that range was. Anything outside it still produces a number, and it will not mean much.
- A research artifact, not a medical device. It cannot diagnose a sleep disorder.
@incollection{ahmad2024sleep,
author = {Ahmad, Hammad and Khan, M. Umar and Azam, Maleeha},
title = {Comparative Analysis of Machine Learning Methods for Enhancing
Sleep Efficiency and Prediction},
booktitle = {Advances in Smart Medical, IoT and Artificial Intelligence},
series = {Information Systems Engineering and Management},
publisher = {Springer Nature Switzerland},
year = {2024},
pages = {3--15},
doi = {10.1007/978-3-031-66854-8_1}
}Machine-readable version in CITATION.cff.
MIT. See LICENSE.


