Detecting Harsh Braking Events from Speed Profiles
This page extends the speed profiling cluster with the differentiation step most fleet safety programs need next: turning a clean speed_kmh_smoothed series into discrete, debounced harsh-braking and harsh-acceleration events for driver scoring. The distinction from ordinary velocity analysis matters because acceleration is the second derivative of position — every noise-amplification problem that affects instantaneous speed is squared again when computing its derivative, which makes smoothing order and sampling-rate awareness far less optional here than upstream in the pipeline.
Compatibility & Configuration Requirements
| Requirement | Minimum version / value | Notes |
|---|---|---|
| Python | 3.10 | — |
| numpy | 1.24 | vectorized differencing and rolling statistics |
| pandas | 2.0 | .diff() / .rolling() semantics used throughout |
| Input columns | timestamp (UTC, tz-aware), speed_ms |
must be the smoothed output of the speed profiling pipeline, not raw haversine-derived speed |
| Sampling rate | ≥ 1 Hz recommended | below 1 Hz, short braking events are averaged out — see pitfalls |
| Units | metres, seconds, m/s² | convert speed_kmh_smoothed to m/s before use: speed_ms = speed_kmh / 3.6 |
Production-Ready Implementation
The class below computes acceleration from an already-smoothed speed series, flags samples past a deceleration threshold, and debounces consecutive flags into discrete events with a severity grade.
import numpy as np
import pandas as pd
from dataclasses import dataclass
from typing import List
@dataclass
class HarshEvent:
event_type: str # 'harsh_braking' or 'harsh_acceleration'
start_time: pd.Timestamp
end_time: pd.Timestamp
peak_accel_ms2: float # most negative (braking) or most positive value
severity: str # 'moderate' or 'severe'
n_samples: int
class HarshEventDetector:
"""
Detect discrete harsh braking / harsh acceleration events from a
smoothed GPS speed profile.
Parameters
----------
decel_threshold_ms2 : float
Deceleration magnitude (negative, m/s^2) at or below which a
sample is a harsh-braking candidate. -3.0 m/s^2 (~0.3g) is a
common fleet-safety starting point.
accel_threshold_ms2 : float
Acceleration magnitude (positive, m/s^2) at or above which a
sample is a harsh-acceleration candidate. 2.5 m/s^2 is a
reasonable default for loaded commercial vehicles.
severe_decel_ms2 : float
Deceleration magnitude beyond which an event is graded 'severe'
rather than 'moderate'. -5.0 m/s^2 (~0.5g) approaches emergency
braking for a laden vehicle.
smoothing_window : int
Rolling-mean window (samples) applied to speed_ms before
differentiation. 3 is the minimum that meaningfully suppresses
GPS jitter without eroding genuine short braking events.
min_event_gap_s : float
Minimum gap between flagged samples before they are treated as
two separate events rather than one continuous braking action.
2.0 s absorbs the natural multi-sample tail of a single hard
stop without merging genuinely distinct events.
"""
def __init__(
self,
decel_threshold_ms2: float = -3.0,
accel_threshold_ms2: float = 2.5,
severe_decel_ms2: float = -5.0,
smoothing_window: int = 3,
min_event_gap_s: float = 2.0,
):
self.decel_threshold_ms2 = decel_threshold_ms2
self.accel_threshold_ms2 = accel_threshold_ms2
self.severe_decel_ms2 = severe_decel_ms2
self.smoothing_window = smoothing_window
self.min_event_gap_s = min_event_gap_s
def _smooth_speed(self, df: pd.DataFrame) -> pd.Series:
"""Rolling-mean smoothing applied before differentiation, never after."""
return (
df["speed_ms"]
.rolling(window=self.smoothing_window, center=True, min_periods=1)
.mean()
)
def compute_acceleration(self, df: pd.DataFrame) -> pd.DataFrame:
"""
Return a copy of df with 'speed_smoothed_ms' and 'accel_ms2' added.
Expected output shape: same row count; first row has NaN
acceleration (no predecessor).
"""
df = df.sort_values("timestamp").copy()
df["speed_smoothed_ms"] = self._smooth_speed(df)
dt_s = df["timestamp"].diff().dt.total_seconds()
dv_ms = df["speed_smoothed_ms"].diff()
df["accel_ms2"] = dv_ms / dt_s.replace(0, np.nan)
return df
def detect_events(self, df: pd.DataFrame) -> List[HarshEvent]:
"""
Full pipeline: smooth, differentiate, threshold, debounce, grade.
Parameters
----------
df : DataFrame with 'timestamp' (UTC) and 'speed_ms' for a single
device / single trip.
Returns
-------
List of HarshEvent, one per debounced braking or acceleration
incident, ordered by start_time.
"""
df = self.compute_acceleration(df)
braking_flag = df["accel_ms2"] <= self.decel_threshold_ms2
accel_flag = df["accel_ms2"] >= self.accel_threshold_ms2
events: List[HarshEvent] = []
events.extend(self._debounce(df, braking_flag, "harsh_braking"))
events.extend(self._debounce(df, accel_flag, "harsh_acceleration"))
events.sort(key=lambda e: e.start_time)
return events
def _debounce(
self, df: pd.DataFrame, flag: pd.Series, event_type: str
) -> List[HarshEvent]:
"""
Merge consecutive flagged rows separated by less than
min_event_gap_s into a single event.
"""
flagged = df[flag].copy()
if flagged.empty:
return []
gap = flagged["timestamp"].diff().dt.total_seconds()
new_event = (gap.isna()) | (gap > self.min_event_gap_s)
flagged["_event_id"] = new_event.cumsum()
results: List[HarshEvent] = []
for _, group in flagged.groupby("_event_id"):
peak = (
group["accel_ms2"].min()
if event_type == "harsh_braking"
else group["accel_ms2"].max()
)
severity = (
"severe"
if event_type == "harsh_braking" and peak <= self.severe_decel_ms2
else "moderate"
)
results.append(
HarshEvent(
event_type=event_type,
start_time=group["timestamp"].iloc[0],
end_time=group["timestamp"].iloc[-1],
peak_accel_ms2=float(peak),
severity=severity,
n_samples=len(group),
)
)
return results
Execution & Tuning Guidelines
detector = HarshEventDetector(
decel_threshold_ms2=-3.0,
accel_threshold_ms2=2.5,
smoothing_window=3,
min_event_gap_s=2.0,
)
events = detector.detect_events(trip_df)
for e in events:
print(e.event_type, e.severity, e.start_time, round(e.peak_accel_ms2, 2))
| Parameter | Default | Effect of raising magnitude | Effect of lowering magnitude |
|---|---|---|---|
decel_threshold_ms2 |
-3.0 m/s² | Fewer, harsher events flagged; misses moderate braking | More events flagged, including ordinary traffic deceleration |
smoothing_window |
3 samples | Flatter acceleration curve; short, genuine hard-stops can be averaged below threshold | Preserves sharp transients; more susceptible to GPS-noise false positives |
min_event_gap_s |
2.0 s | Merges more nearby flagged samples into one event; may combine two distinct stops | Reports more discrete events for a single continuous braking action |
severe_decel_ms2 |
-5.0 m/s² | Fewer events graded severe | More events graded severe, including moderate braking |
Feed events output into a driver-scoring aggregation keyed by vehicle_id and shift, and cross-reference peak timestamps against heading synchronization output — a harsh-braking event that coincides with a sharp heading change is far more likely to indicate a genuine evasive manoeuvre than one on a straight road segment.
Common Pitfalls
GPS noise produces false-positive events
Differentiating an unsmoothed or under-smoothed speed series turns ordinary positional jitter into apparent deceleration spikes well past -3 m/s². The fix is strict pipeline ordering: smooth first, differentiate second, threshold third. compute_acceleration above enforces this order internally, but if you are feeding in your own speed_ms column, verify it has already passed through the speed profiling Savitzky-Golay stage — raw haversine-derived speed at 1 Hz commonly produces implied decelerations of 5–10 m/s² from GPS jitter alone at rest.
Detection sensitivity depends on sampling rate
Numerical differentiation averages a physical event across the time step it is computed over. A genuinely hard 0.5-second brake application registers a much larger peak deceleration at 5 Hz sampling than the same event does at 1 Hz, where the same energy is smeared across a full second. Fleets with mixed device sampling rates need per-device threshold calibration, or a minimum sampling-rate floor (1 Hz recommended) enforced upstream before events are compared across the fleet.
Downhill coasting is indistinguishable from braking
Speed alone cannot separate gravity-assisted deceleration on a downgrade from active brake application — both produce a negative accel_ms2 value that can exceed threshold. This detector will flag both identically. Where road grade or elevation data is available, filter flagged events against a grade threshold before scoring; where an OBD-II brake-pedal signal is available, use it as ground truth and treat this detector’s output as a GPS-only fallback for vehicles without CAN bus access, not a hard signal in isolation.
Up: Speed Profiling from Raw GPS Coordinates | Trajectory Analysis & Map Matching Techniques
Related
- Speed Profiling from Raw GPS Coordinates — the parent pipeline supplying the smoothed speed series this detector differentiates
- Calculating Instantaneous vs Average Speed from GPS Traces — background on why instantaneous (not average) speed feeds harsh-event detection
- Kalman Filtering for GPS Noise Reduction — an alternative smoothing stage when rolling-mean is insufficient at higher sampling rates
- Directionality & Heading Synchronization — corroborating heading-change data improves confidence that a flagged event is a genuine evasive manoeuvre
- Multi-Modal Route Matching for Mixed Fleets — harsh-event thresholds should be recalibrated per vehicle mode rather than applied uniformly across a mixed fleet