1. Build a Baseline Occupant Kinematics Dataset for Airbag Deployment Prediction
Why a Baseline Dataset Comes First
Every prediction model in this course — deployment classifiers, injury-risk regressors, sequence models — consumes the same underlying object: a table of occupant kinematics signals aligned to a crash pulse, with a deployment outcome attached. If that table is inconsistent (mixed units, unaligned time bases, ambiguous labels), no amount of model tuning will recover the lost signal. This case builds that table once, correctly, and freezes its schema so later cases can extend it without renegotiating conventions.
The dataset has three logical layers: crash pulse (vehicle-level deceleration), occupant kinematics (derived acceleration, velocity, displacement of the occupant reference point), and deployment label (the outcome we will later predict). The critical engineering decision is that velocity and displacement are integrated from acceleration, not measured independently — so the integration convention (initial conditions, filtering, drift handling) is part of the dataset definition, not an afterthought.
Signal Chain and Integration Convention
The figure below shows the processing chain from raw accelerometer channel to the three stored kinematics signals, and where the deployment label attaches. Note that the label is attached at the record level, not per-sample: one crash event yields one label, while the signals are time series.
Two conventions must be fixed before any data is written. First, sign: we store acceleration in the vehicle-forward direction as positive, so a frontal crash produces a negative-going pulse; velocity and displacement inherit that sign. Second, time origin: t = 0 is the pulse onset (first sample exceeding 0.5 g after bias removal), and every record is resampled onto a uniform 0.1 ms grid covering 0–200 ms. Fixing both means a record from one test rig is directly comparable to another.
Building the Dataset in Code
The following script is self-contained: it generates a small synthetic set of crash pulses (so the example runs without proprietary test data), conditions them, integrates to velocity and displacement, attaches deployment labels, and writes a single Parquet file plus a JSON schema. The synthetic pulses are clearly labeled as such — they are for pipeline validation, not for training a production model.
import numpy as np
import pandas as pd
import json
FS = 10_000 # Hz, raw sample rate
DT = 1.0 / FS
T_END = 0.200 # s, stored window
N = int(T_END / DT) + 1
T = np.arange(N) * DT
def synth_pulse(delta_v, peak_g, duration, seed):
"""Half-sine crash pulse, negative-going (frontal convention)."""
rng = np.random.default_rng(seed)
t = T.copy()
pulse = np.zeros_like(t)
mask = t <= duration
pulse[mask] = -peak_g * np.sin(np.pi * t[mask] / duration)
noise = rng.normal(0, 0.02, size=t.size)
return pulse + noise
def condition(a_raw):
"""Remove bias, apply simple 2nd-order low-pass (CFC-60 style)."""
a = a_raw - np.mean(a_raw[:50]) # bias from pre-trigger window
# 2nd-order Butterworth, fc = 60 Hz, applied via biquad coefficients
fc = 60.0
w0 = 2 * np.pi * fc
alpha = np.sin(w0 * DT / 2) / np.sqrt(2)
b0 = (1 - np.cos(w0 * DT)) / 2
b1 = 1 - np.cos(w0 * DT)
b2 = b0
a0 = 1 + alpha
a1 = -2 * np.cos(w0 * DT)
a2 = 1 - alpha
y = np.zeros_like(a)
x1 = x2 = y1 = y2 = 0.0
for i, x in enumerate(a):
y[i] = (b0 * x + b1 * x1 + b2 * x2 - a1 * y1 - a2 * y2) / a0
x2, x1 = x1, x
y2, y1 = y1, y[i]
return y
def integrate(a):
"""Trapezoidal integration with zero initial conditions."""
v = np.zeros_like(a)
d = np.zeros_like(a)
for i in range(1, a.size):
v[i] = v[i-1] + 0.5 * (a[i] + a[i-1]) * DT
d[i] = d[i-1] + 0.5 * (v[i] + v[i-1]) * DT
return v, d
def find_onset(a, thresh_g=0.5):
idx = np.argmax(np.abs(a) > thresh_g)
return idx if np.abs(a).max() > thresh_g else 0
records = []
specs = [
# delta_v m/s, peak_g, duration s, deployed
(8.0, 22, 0.045, 1),
(5.5, 15, 0.060, 0),
(12.0, 35, 0.040, 1),
(4.0, 10, 0.070, 0),
(9.5, 28, 0.050, 1),
]
for i, (dv, pk, dur, dep) in enumerate(specs):
a_raw = synth_pulse(dv, pk, dur, seed=i)
a = condition(a_raw)
onset = find_onset(a)
a = a[onset:onset + N]
if a.size < N:
a = np.pad(a, (0, N - a.size))
v, d = integrate(a)
records.append({
"event_id": f"SYN-{i:03d}",
"delta_v_mps": dv,
"peak_g": pk,
"duration_s": dur,
"deployed": dep,
"a_g": a.tolist(),
"v_mps": v.tolist(),
"d_m": d.tolist(),
})
df = pd.DataFrame(records)
df.to_parquet("occupant_kinematics_baseline.parquet", index=False)
schema = {
"fs_hz": FS, "dt_s": DT, "n_samples": N,
"t_start_s": 0.0, "t_end_s": T_END,
"sign_convention": "vehicle-forward positive; frontal pulse negative",
"units": {"a_g": "g", "v_mps": "m/s", "d_m": "m"},
"label_field": "deployed", "label_values": [0, 1],
}
with open("occupant_kinematics_schema.json", "w") as f:
json.dump(schema, f, indent=2)
print(df[["event_id", "delta_v_mps", "peak_g", "deployed"]])
print("rows:", len(df), "samples/record:", N)
Running this produces five records, each with 2001 samples per signal, and a schema file that pins the conventions. The printed table is the human-readable index; the Parquet file is what later cases load. Note that delta_v_mps is stored as a scalar summary alongside the full arrays — it is convenient for quick filtering but is derived from the pulse, so it must never be treated as an independent measurement.
Verifying the Dataset Before You Trust It
A baseline dataset is only useful if its internal consistency holds. Run these checks immediately after writing the file; each maps to a specific failure mode.
| Check | Expected result | Failure symptom | Likely cause |
|---|---|---|---|
| Array length | All records = 2001 samples | Ragged lengths | Onset trimming not padded |
| Initial velocity | v[0] ≈ 0 | Non-zero start | Bias not removed before integration |
| Final displacement | Monotonic sign, no reversal | Drift back toward zero | Residual bias integrated twice |
| Label balance | Both classes present | All zeros or all ones | Label join key mismatch |
| Peak magnitude | Matches peak_g within 5% | Large mismatch | Filter over-attenuation |
What This Dataset Enables
The output is deliberately minimal: one row per crash event, three aligned signal arrays, a scalar summary, and a binary label. That is enough to train a sequence classifier directly, and it is also the substrate from which time-domain features will later be extracted. Because the schema file records the sign convention, units, and time base, any downstream case can assert compatibility rather than assume it. Keep the schema alongside the Parquet file — a dataset without its conventions is not reproducible.
event_id naming and the 0.1 ms grid. Adding records is safe; changing the grid or sign convention invalidates every model trained on the earlier version.