My Fall-Detection Model Scored 94%, and It Was Lying to Me


scored 94.3% accuracy. I went back over the terminal output and the confusion matrix more than once. They agreed every time. The number went into my README and onto my CV.

It was also wrong. Not by a rounding error, either. The honest figure was 69%.

No one caught this for me. It only came to light because I decided to build tools that could verify my own results. Fixing it taught me more about production machine learning than building the original system did.

Plenty of articles explain data leakage in the abstract. This one is about what it looks like from the inside, with the numbers from before and after. If you evaluate models on sequential data of any kind (video, audio, wearable sensors, time series), there’s a decent chance the same bug is sitting in your pipeline right now.

The system

The project is a real-time fall detector for assistive monitoring. A webcam runs through MediaPipe Pose to get skeletal landmarks. From the landmarks I compute a few simple biomechanical features: torso angle, normalised hip and shoulder heights, and the geometry of the bounding box. A Random Forest classifies every frame, and a smoothing window over the last few predictions decides whether to raise the alarm. It all runs on CPU in real time. No video ever leaves the device. The only thing that leaves is the skeleton maths.

I trained it on the Le2i fall detection dataset (Charfi et al., 2013), which contains staged falls and daily activities recorded in home-like rooms. I extracted features per frame, labelled per frame, ran a standard train_test_split, and got 94.3% accuracy. As far as I was concerned, the project was done.

The leak

Here’s the line that lied to me:

X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, stratify=y)

The problem is that this split is random over individual frames, and frames from the same video are not independent. Two frames captured 80 milliseconds apart look almost the same: same person, same pose, same lighting, almost the same feature vector. When you shuffle all the frames together and hold out 20% for testing, nearly every test frame has a close twin in the training set.

So the test doesn’t measure whether the model generalises to people and rooms it hasn’t seen. It measures whether the model can remember, and a Random Forest can remember almost everything. This is data leakage in its most ordinary form: the samples aren’t independent, so the random split hands the test set the answers in advance.

The fix is simple to state: split by the thing that makes your samples dependent. For video, that means splitting by video. I re-ran the same features and the same model with GroupKFold, grouping by video, so no recording could appear on both sides of a fold.

Read Also:  Building AI Automations with Google Opal
The identical model and features under two evaluation protocols. The only difference is what the split respects. (Image by author)

The grouped score came back at 69.1%. One fold, the same code split honestly, landed at 31%.

What stung wasn’t the 25-point drop. It was that 94.3% had felt completely earned: a good number sitting next to a reasonable-looking pipeline, and nobody thinks to look twice. I didn’t.

The labels

Once I had audited the split, I started auditing everything else. The labels turned out to be worse.

My training labels came from folder names. If a video sat in the falls/ folder, every frame of it was labelled fall. But a staged fall only lasts one to two seconds, and the rest of the clip is ordinary activity: walking around, sitting down, standing up. The dataset actually provides frame-level annotations that mark the exact start and end of every fall, so I cross-referenced my labels against them. The result was embarrassing.

In 76% of the frames carrying a fall label, the person was just walking around normally. Only 6% captured an actual fall, and the remaining 18% showed someone already lying on the floor afterwards. One 78-second video contributed 44% of the entire positive class, and 1.5 seconds of it was a fall.

fig2 label audit
A label audit of the original training set: almost nothing labelled “fall” was one. (Image by author)

My labels had changed the task without me realising. The model solved the easier problem they described, not the one I meant to set. That’s exactly the kind of failure leakage hides. A leaky evaluation can’t tell you that your labels are wrong, because it only measures memorisation, and a model can memorise wrong labels just as easily as right ones. If your labels come from folder names, file paths or any similar shortcut, they deserve the same suspicion as your split.

So I rebuilt the training set, starting from the ground truth this time.

A frame now counts as positive if it sits inside the annotated fall window or in the two seconds of lying that follow it. Everything before the fall is negative. The ambiguous frames late after a fall, where the person might still be lying down or might be getting up and there is no ground truth either way, I excluded completely instead of guessing.

I also used the whole dataset this time: 186 of its 190 videos, instead of my original 28. That meant doing work the first pipeline had skipped. Two of the scenes come with no annotation files at all, so I watched all 59 of those videos frame by frame and hand-annotated 29 fall windows myself. Around my estimated boundaries I excluded a safety buffer, so that my own imprecision could not contaminate the negative class. One annotation file was malformed, with its fall window buried in the middle of the file between bounding-box rows. The parser now handles that case, and the fix is documented in the repository.

Read Also:  Generating Data Dictionary for Excel Files Using OpenPyxl and AI Agents

A fall is movement, so the features needed movement in them too. I added velocity features such as hip descent rate and torso angular rate, computed over seconds rather than frames. That way the same model works on the dataset’s mixed frame rates and on a webcam with a variable frame rate, with no special cases.

Measure what the user experiences

After the rebuild, the honestly evaluated frame-level numbers recovered, with grouped out-of-fold accuracy back above 0.97. By then I no longer trusted frame accuracy as a headline, and not only because correct labels leave the classes lopsided enough to flatter it. A caregiver never sees a frame. They either get an alert or they don’t. For an alarm system, the questions that matter are simpler: when someone falls, does the alarm fire? How quickly? And how often does it fire when nobody falls?

So the evaluation now simulates the exact production alarm logic, a voting window over per-frame probabilities, on held-out videos, and reports event-level results:

  • 121 of 126 falls detected (96.0%)
  • 0.58 s median alarm latency from the start of the fall
  • a false alarm on 3 to 7 of the 60 no-fall videos, depending on the model

Every miss came down to the same thing: the pose estimator lost the person mid-fall, whether from occlusion behind furniture, motion blur or footage too low-res to track. If the pose estimator can’t see the fall, the classifier never gets its turn. You only find that out by evaluating whole events, and it’s what’s pushing my next round of work toward perception rather than more classifier tuning.

The trade-off a single metric can’t express

Honest evaluation also surfaced a decision I had never noticed I was making. My most sensitive model treats lying on the floor after a fall as positive. That maximises detections, but it also means the alarm goes off when someone lies down on purpose. So I retrained on the fall motion alone. That model stays silent on a held-out video of a deliberate lie-down, with a peak probability of 0.17, but it detects fewer falls, 89.7%, because the pose estimator has to actually see the fall happen.

Model Falls detected False-alarm videos Deliberate lie-down
“person down” labels 96.0% 7/60 alarms
“fall motion” labels 89.7% 3/60 silent

Neither model is wrong. In an elderly-care setting, you probably do want an alarm whenever someone is on the floor. If the goal is to show the system can tell a fall from a lie-down, the motion model wins. The behaviour is controlled by the label policy, not the architecture, and a single accuracy score would have hidden the whole trade-off. I ship both models and document the difference.

The system around it

I wanted the model to be something strangers could poke at, so I built the rest of the system around it. Detection runs on the edge device. When a fall is detected, the device sends roughly 100 bytes of JSON, and nothing else, to a FastAPI and PostgreSQL backend on AWS through an authenticated write API. The video itself never leaves the device, so privacy is built into the architecture instead of promised in a policy. On top of the backend there is a live monitoring dashboard, an acknowledgement workflow for caregivers, and a React Native mobile app that pushes the alert to a caregiver’s phone within seconds of the fall. All three repositories run their tests in CI on every push.

Read Also:  Why You Should Not Replace Blanks with 0 in Power BI
system in action
The moment the platform catches a live fall: edge inference on the left, the cloud dashboard flipping to red on the right. (Image by author)

Then I fell over. Deliberately, onto the carpet, in front of the webcam. Every fall was planned and done carefully, and it still took three takes to get one worth keeping.

Each take ran from start to finish with the whole pipeline live. The one that survived is fifty-five seconds, unedited: the edge model detects the fall, the event crosses the internet, the cloud dashboard turns red on its own five seconds later, and the alert is acknowledged back to all clear. No cuts anywhere. If the video needed editing, it would prove nothing.

I only recorded that video because I finally trusted the system behind it. I would never have dared to film the 94.3% version.

The checklist

If you take one thing from my 25 wasted percentage points, take the first item below. The rest follow from it.

  1. Group your splits by whatever makes samples dependent: video, patient, subject, session, device. If switching to a random split makes your score jump, it’s the higher number that’s lying.
  2. Compare your labels with the real annotations instead of trusting how the files are organised. Then look inside your positive class and see what it really contains.
  3. Measure whole events, the way a user would: how many incidents it caught, how quickly it raised the alert, and how often it went off when nothing happened. Per-sample accuracy on its own hides too much.
  4. Keep a held-out behavioural test for the failure mode that scares you most. Mine is a deliberate lie-down that must not raise an alarm.
  5. Publish the correction. A corrected system people can actually believe is worth more than an impressive one they can’t.

Everything above is public: the detection code and evaluation harness, the platform, the mobile app, and the 55-second live demo.


References

Dataset. The Le2i (IMViA) Fall Detection Dataset, Université de Bourgogne (Charfi et al., 2013), is distributed openly with a request to cite the authors’ paper and carries no explicit licence. This article reproduces no images or video from the dataset. It reports only aggregate metrics computed from it, with attribution.

I. Charfi, J. Mitéran, J. Dubois, M. Atri, R. Tourki, “Optimised spatio-temporal descriptors for real-time fall detection: comparison of SVM and Adaboost based classification,” Journal of Electronic Imaging (JEI), Vol. 22, Issue 4, pp.17, October 2013.

Leave a Comment

Your email address will not be published. Required fields are marked *

Scroll to Top