Engineering / Decision Systems · 2026

Vigil: Deciding When a Car Is Being Stolen

An engine that reads a live vehicle-tracker feed and escalates on what it believes, tested by staging thefts in real driving history

Context & Problem

Vehicles taken each year

CarjackingsVehicles stolen, nobody inside

010k20k30k40k2021/222022/232023/242024/2517.5k29.7k2025/26

Recorded by the police, April to March, from the SAPS quarterly crime statistics for 2025/26. Vehicle theft includes motorcycles. Tap or hover over a year for its counts.

In September 2023 a man in Mbalenhle, Mpumalanga, parked his Isuzu bakkie at home. When he woke up on the morning of Friday 1 September it was gone. A tip from a resident and his tracking company led police to a yard in Tsakane, across the provincial border in Gauteng, where they found the bakkie tampered with, IOL reported. He was lucky.

Two years later thieves taking bakkies from homes in Springs and Brakpan were striking between 01:00 and 04:00 and poisoning the dogs so they would not wake the owners, and a security company there said the bakkies' trackers "can be removed effortlessly", the Brakpan Herald reported. By the time an owner wakes up, the car may be far away and its tracker gone.

In the year to March 2026 the police recorded 17,464 carjackings in South Africa, about 48 a day, and 29,671 vehicles stolen with nobody in them, about 81 a day. Both have fallen in the past few years: carjackings by 23% from 22,808 in 2023/24, and vehicle thefts by 21% from 37,426 in 2022/23. That still leaves about 130 vehicles taken every day, and the counts hold only the crimes reported to the police.

A tracker is meant to help get a stolen vehicle back. It reports where the vehicle is and what its unit senses, and the tracking company raises an alert when the unit is jammed, loses power or crosses a boundary the owner has drawn. Those alerts are blunt. Most odd trips are someone running late or a unit misbehaving, so an alert on each one is soon ignored. A theft that looks like an ordinary late drive raises none, and once a thief has found and removed the unit, as in Springs and Brakpan, there is nothing left to report.

I wanted something that watches the feed the way someone who knows the vehicle would: something that can tell an owner running late from a vehicle being driven away, and that does not lose interest when the tracker goes quiet.

What I built

What I ended up with is Vigil, an engine that sits on a tracker's live feed and decides, every minute, whether anything should be done about each vehicle. It is a personal project, and it comes with a fleet dashboard and a mobile app.

Vigil keeps a running belief about what each vehicle is doing, and what it does follows that belief. Most of the time that means nothing at all. When the belief turns, it escalates, and the demo below shows what that looks like when a bakkie's tracker falls silent on the highway.

Tipo Couriers

Owner · fleet account

Vehicles62 trucks
Vigil's watchWatchingAsking about unexpected trips; device faults are reported
Moving now2of 6 reporting

Live

VehicleStatusHealthSpeedWhereLast report
Van 01DrivingGood36 km/hKempton ParkJust now
Van 02ParkedGood0 km/hKempton Park5 min ago
Bakkie 03DrivingGood183 km/hGermistonJust now
Truck 04ParkedGood0 km/hGermiston13 min ago
Truck 05ParkedGood0 km/hBoksburg3 h ago
Van 06ParkedGood0 km/hEdenvale8 min ago
02:14
Tipo Couriers
MapList
6 vehicles · 2 moving
VehiclesAccount
Prototype demo

Request a demoSlides

How I built it

Vigil is written in Python. A timer runs it in the cloud once a minute, and each run collects the latest data from the tracking units of every connected organisation through their telemetry provider, keeps what has changed since the last read and passes each vehicle's new reports to the engine. The engine works from three things: what is normal for the vehicle, learned from its own history; how likely vehicle crime is where and when the vehicle is, from a model of police statistics; and the health of its tracking unit. The fleet dashboard is a web page, the mobile app is written in SwiftUI, and 538 automated tests cover the engine, the accounts and the connection to the telemetry provider.

Each part was tested before I relied on it. The crime model was fitted on public data and tested on precincts and years it had not seen. The engine was tested by replaying a test fleet's real history minute by minute, with thefts staged in it. Both tests are described below.

The data

Vigil reads two kinds of data. The first is the data from the tracking unit fitted in each car. The unit sends a report whenever something changes and, while the car moves, about every 30 seconds. The telemetry provider passes the reports on, and Vigil collects them once a minute for every vehicle. There is no way to ask only for what is new, so each collection looks back over the last half hour and Vigil keeps each report once.

What a tracker sends
1
221:11:5821:12:04•••0On1014.14.1Ignition on
321:12:2821:12:33•••38NEOn1113.94.1Routine
421:12:5821:13:04•••64NEOn1113.94.1Routine
521:13:2821:13:35•••71NEOn1113.94.1Routine
621:13:5821:14:41•••68EOn413.94.1Routine
721:14:2821:14:41•••42EOn913.84.1Routine
821:14:5821:15:03•••0On1113.84.1Routine
921:15:2021:15:26•••0Off1112.64.1Ignition off

B · Received: When the report reached the telemetry provider. This is the earliest moment anyone could act on it, so in the tests Vigil only sees a report from this time on.

A few minutes of reports from one unit, with made-up values, as they would look exported to a spreadsheet. Choose a column to read what it holds.

The two times on a report matter more than they look. A unit that loses signal, in a basement or out of network, keeps its readings and sends them in a batch when it reconnects, sometimes hours later. A report is of use only from the moment it is received, and the tests below hold Vigil to that.

When an organisation connects its tracker, Vigil imports the last 90 days of its history, about ten days' worth a minute, and learns from it before it starts watching. The test fleet's full history holds 2,273 trips, and 346 of them, about 15%, start at night. That is why a trip at night cannot be enough to raise an alarm on its own.

The second kind is public: the police's crime statistics, the census and OpenStreetMap, which go into the crime model in the next section. Nothing from the tracker goes into the crime model. The charts on this page use public data or made-up values, and the test results are counts from the test fleet, with no positions or places.

Where cars are taken

Vigil starts from where vehicle crime happens. The police publish monthly counts for each of the country's 1,167 police precincts, and Vigil uses four of their crimes from January 2020 to March 2026: carjacking, vehicle theft, theft from vehicles and truck hijacking. The raw counts are a poor guide on their own. A busy precinct with many cars records many thefts without being riskier for any one car, and 444 precincts recorded no carjacking at all in 2024/25, which does not mean a carjacking there is impossible.

The monthly counts up to September 2025 come from crime-stats, an open project that gathers the police's releases, and the six months after that from the police's quarterly workbooks. Twelve stations opened during the period. Until each one opened, its area's crimes were recorded at an older station next door, so in those months both stations are left for the model to fill, and the combined count the older station recorded is kept back to check the fill. What is known about each precinct comes from two places. OpenStreetMap gives its roads, fuel stations, shops, land use and distance to a border post. Census 2022 gives the share of households with a car and the kinds of dwelling they live in, and Stats SA tabulates the census by police district, so the figures are exported per precinct from its SuperWEB tool with no map overlay needed. I also tried the number of swimming pools as a sign of wealth and dropped it: OpenStreetMap shows 33,110 pools in the Western Cape against 3,405 in Gauteng and 29 in Sandton, which says more about who maps than about who is wealthy.

The model is a Bayesian count model, fitted separately for each crime with R-INLA. A precinct's monthly count depends on what is known about the precinct, a spatial effect that lets neighbouring precincts share strength, its own trend, national and provincial trends, the calendar month, and a precinct-by-year effect that lets part of a bad year carry into the next. The model turns the counts into a risk per car, and the map compares that risk with the national rate.

The risk Vigil starts from, police precinct by police precinct
Crime
Area

Under half0.5 to 0.8About national1.25 to 22 to 4Over 4 times

Bramley, Gauteng: 9.7 times the national rate of carjacking, which is 3.0 a year for every 1,000 households with a car. Here it is about 29.4 (24.4 to 36.0).

Each precinct's expected count for April 2025 to March 2026 from the model, divided by its households with a car (Census 2022) and set against the national rate. Precincts where few households own a car are pulled towards the national rate. Vigil caps the figure at 0.25 and 10 times. Precinct boundaries from SAPS, simplified.

Time matters as much as place. Carjacking peaks in the early evening: 30.7% happen between 18:00 and 21:00, more than twice an even share. Fridays and Saturdays carry about 18% each. The police last published time-of-day tables for 2016/17 to 2019/20, and most give only the peak hours or days for one province, so I combined 83 such shares for the hours and 149 for the days with a Gibbs sampler that fills in what each report left out.

When each crime happens
Crime

Three-hour band, from midnight

0%10%20%30%40%0003060912151821

Day of the week

0%10%20%30%40%MonTueWedThuFriSatSun

18:00 to 21:00: 30.7% of carjackings (29% to 32%), against 12.5% if they were spread evenly. Saturday: 18.0%, against 14.3%.

National shares of incidents, combined from the partial tables in SAPS annual crime reports for 2016/17 to 2019/20, with 95% ranges. The dashed line is an even spread.

To test the map, 117 precincts were hidden from the model, which then had to fill them in from their neighbours and their census and map data alone. Its estimates were closer to the recorded counts than the national rate for every crime, and closer than the neighbours' rate for all but carjacking, where the two are too close to separate: carjacking hotspots cluster so tightly that copying the rate next door works well.

Filling in precincts the model never saw

The modelNeighbours' rateNational rate

Carjacking

The model: 9.4
Neighbours' rate: 8.0
National rate: 16.8

Vehicle theft

The model: 13.7
Neighbours' rate: 23.1
National rate: 28.6

Theft from vehicles

The model: 23.0
Neighbours' rate: 53.4
National rate: 66.8

Truck hijacking

The model: 0.87
Neighbours' rate: 1.33
National rate: 1.68

117 precincts, a random tenth, were hidden for all 75 months and the model refitted without them. Error is the average gap between the filled-in and the recorded count for April 2025 to March 2026, so a shorter bar is better. The two rules scale the national or the neighbouring precincts' rate per resident by the hidden precinct's population.

What Vigil needs from the model is a forecast: a precinct's risk in the months ahead from its record so far. So I hid the last year of every precinct, refitted the model on the months before and scored its forecast, then did the same for each of the two years before that. The model beat the rule of repeating last year's count in 9 of 12 crime-years and drew level in the other three, all in 2023/24. The trend and the year effect stayed in only because they won these tests. Without them the model's error was higher in 11 of the 12 crime-years, and above last year's count for theft from vehicles in all three.

Forecasting a year the model had not seen
Crime
Precincts

The modelLast year's count

2023/24

The model: 4.77
Last year's count: 5.21

2024/25

The model: 4.60
Last year's count: 5.00

2025/26

The model: 4.25
Last year's count: 5.03

For 2025/26 the model missed each precinct's carjackings by 4.25 on average, against 5.03 for last year's count: a difference of −0.78, with a 95% interval of −1.12 to −0.47.

Each year was hidden and the model refitted on the months before it. Error is the average gap per precinct between the forecast and the recorded count for the year, so a shorter bar is better. For the precincts that recorded none the year before, the score is how well each method ranked which of them would go on to record one: 0.5 is a coin toss, 1 is perfect, so a longer bar is better.

The precincts that record nothing are the reason the model exists. Of the 444 precincts with no carjacking in 2024/25, 68 recorded at least one the next year, 103 carjackings in all, against 121 the model expected. Among those zero precincts, the model's riskiest quarter went on to record one 40% of the time, against 2% for its safest quarter. A few zeros look more like a change in recording than a change in crime: Harrismith, Hout Bay and Madadeni went from 4 to 11 carjackings a year to none in 2025, and the model takes that as a real fall.

All of this is crime recorded by the police, and much goes unrecorded. Stats SA's victim survey for 2025/26 estimates about 87,000 vehicle thefts and 122,000 hijackings of households in a year, against about 30,000 and 17,500 recorded, though the survey counts households and uses a wider meaning of hijacking. The map shows where crime is relatively likely more than how much there is.

The crime model in full

How it decides

A theft rarely looks strange in itself. A car leaving at 02:00 is odd for one owner and ordinary for a nurse on night shift, so Vigil judges every vehicle against its own history and keeps up as habits change. It learns the tracking unit as well, and keeps an eye on its health, because a worn unit can look like a theft and a theft can hide behind a worn unit.

From all of this Vigil keeps a running belief about what each vehicle is doing, and most of the time that belief is that nothing is wrong. What it does follows the belief, weighed against what it costs to be wrong either way: a false alarm wears out the people who answer it, and a missed theft can cost a vehicle. As the belief that something is wrong grows, Vigil escalates.

From belief to action
InputsThe engineOutputBelief 1Belief 2Belief 3⋮Belief nVigilweighs them against thecost of being wrongActionMost minutes: noneInputsThe engineOutputBelief 1Belief 2Belief 3Belief nVigilweighs them against thecost of being wrongActionMost minutes: none

Vigil keeps several beliefs about each vehicle, one of them that nothing is wrong, and updates them every minute. What it does follows from all of them together.

How it runs

Vigil's customer is an organisation: a company with a fleet, or an owner with one car, who is an organisation of one. A person can belong to several organisations and sees only their side of each. Owners and admins see their whole organisation, fleet managers the vehicle groups they are given, and drivers their own trips and the questions about the vehicle they drive.

An organisation joins by connecting its account with its telemetry provider. Vigil checks the account's details with the provider before saving them, stores them encrypted, and never shows or logs them again. It then imports the history and keeps each vehicle in learning mode until it has seen about 30 of its trips, and the dashboard shows which vehicles are still learning and how many trips each has been learned from. Owners and managers can also tell Vigil what it cannot learn on its own: depots, shift hours, which drivers may drive which vehicle, and the boundaries they want watched. Disconnecting the provider deletes everything Vigil kept for that organisation.

At the end of each day Vigil grades every vehicle's tracking unit from that day's readings: whether it reported as it should, kept its GPS fix, and held its batteries and ignition reading. When a part looks faulty the owner is told once which part it is, with a reminder each week while it stays faulty. A failing unit is not an emergency, but it is worth fixing, because a worn unit makes a theft harder to see.

Each organisation costs one run a minute, 43,200 a month, whatever the size of its fleet. On the cloud provider's published prices, the free monthly allowance covers about a dozen organisations, and each one after that costs about 53 US cents a month, with storage at about a cent per vehicle. So a fleet of 100 vehicles costs about 1.5 cents a vehicle, while an owner with one car costs over 50 cents, and the price will have to follow that.

What it costs to run
Running the engine: $46 a month
Storing what it keeps: $5.00 a month

100 organisations of 5 vehicles would cost about $51 a month, or 10 cents per vehicle.

An estimate, not a bill: one run per organisation per minute at the cloud provider's published prices in US dollars, after its free monthly allowance, which covers about 12 organisations. Each run is assumed to take 1.5 seconds whatever the fleet's size, which a large fleet would not. Storage is assumed at 1 cent per vehicle a month.

How it was tested

Vigil was tested on a test fleet's own history, and the rules of the test were set before it ran. Vigil learned from the trips before 1 September 2026 and was then run over the 511 trips after it, minute by minute as if live. It saw each report only from the moment it had been received, not the moment it was taken, because a unit that goes quiet and later sends its readings in a batch looks very different in the moment than in hindsight.

Three targets were set first: at most about one question per vehicle a month on ordinary trips, at most about one false escalation per 100 vehicles a month, and clear incidents caught within 5 minutes. All 511 trips were ordinary, so any question or alert would have been a false alarm. Vigil raised none. That is a good result on a small test. The 511 trips are about 3.4 vehicle-months of driving, and no question in 3.4 vehicle-months is consistent with as many as 0.9 a vehicle-month at 95% confidence, so the test only just reaches the first target and is far too small to check the second.

The history holds no thefts, so they were staged: made-up reports written into the real history, 30 of each kind. A jammed tracker was caught in all 30 cases, a median 3 minutes in, inside the 5-minute target. A stripped unit was caught in 29 of 30, a median 6 minutes in, a minute outside it, and a towed car in 29 of 30.

30 staged incidents of each kind

Caught within 15 minutesCaught within the hourNot caught within the hour

Theft, unit stripped: The car is driven off at night and its unit is torn out. Caught in 28 of 30 within 15 minutes and 29 of 30 within the hour.

Staged reports written into the test fleet's real history after 1 September 2026, with the engine trained only on the trips before it. Choose a row to read what was staged.

These are the results of a second run, made after a daily grade for each tracking unit was added. Run without the grading, Vigil sent 2 fault notes on the ordinary trips, judging healthy units faulty. With it, Vigil sent none and caught 29 stripped units within the hour instead of 27. The towed cars paid for it: they were still caught within the hour, but only 17 within 15 minutes against 26 before, because one car's unit had a doubtful reading on 265 of 312 days and a tow shows partly in that reading. The grading stayed in, because a fault note on a healthy unit is the kind of alarm people learn to ignore.

How strongly a theft shows in the reports is an assumption, because the history has no real thefts to measure it from. Halving or doubling those assumptions moves the stripped-unit catches from 13 to 30 of 30, so they matter, and the owners' answers to Vigil's questions are what could pin them down.

One idea was tested and left out. The trackers' own harsh-driving fields were empty, so I had Vigil work out harsh braking, acceleration, turning, speeding and hard launches itself, and compare each trip with the car's usual style. It caught no more thefts. Against a style for the whole car it fired mostly on the harder of the two people who drive it, and its apparent power to tell drivers apart, an AUC of 0.81 to 0.91, came mostly from the roads each of them used: trained on other roads, it recognised one driver in only 2 of 11 trips on the other's roads. A signal that mostly learns who drives where would raise false alarms without catching more thefts, so it stayed out.

Limitations

The incident side is assumed. The staged thefts follow the engine's own idea of what a theft looks like, so the tests check the engine against that idea as much as against thieves. The ordinary side is real but small: no false alarm in 511 trips is consistent with as many as about 1 in 170 trips drawing one, at 95% confidence.

Not every theft can be told apart from ordinary driving from the tracker alone. The crime risk is per police precinct, so a whole precinct shares one rate, and it rests on crimes reported to the police, which leave out many.

Only one telemetry provider has been tested, and questions reach the mobile app without push notifications for now, so an answer can be slow. The running cost is an estimate from an assumed run time and has not yet been checked against a bill.

Methods: Bayesian belief updating, Decision analysis, Statistical crime modelling, Replay and staged-incident validation. Tools: Python, Azure Functions, Azure Storage, Application Insights, JavaScript, SwiftUI.