3. Engineer Time-Domain Features from Accelerometer Signals for Deployment Classification

Why Time-Domain Features Matter for Deployment Classification

Airbag deployment decisions are driven by the shape of the crash pulse, not just its magnitude. A 40 g pulse that rises in 8 ms and decays quickly is a fundamentally different event from a 40 g pulse that ramps up over 60 ms. Time-domain features capture exactly this: how fast the acceleration builds, how long it stays high, how abruptly it changes, and how much energy it delivers. These are the quantities a crash-sensing algorithm (and a downstream classifier) can act on within the 10–30 ms decision window available before deployment.

In this case we work with two accelerometer channels recorded at 10 kHz: a vehicle channel (B-pillar or tunnel-mounted, measuring the crash pulse) and an occupant channel (seat- or chest-mounted, measuring occupant response). We extract five time-domain features per channel — peak, rise time, impulse, jerk, and energy — and then evaluate how well each feature separates deploying from non-deploying events.

Feature Definitions and Sign Conventions

All signals are expressed in g with the crash direction defined as positive (a frontal impact produces a positive acceleration pulse). This convention is fixed for the entire case; every formula below assumes it. Sampling interval is dt = 1/10000 = 1e-4 s.

FeatureDefinitionUnitsWhat it captures
Peakmax(a[n])gSeverity of the pulse
Rise timetime from 10% to 90% of peak, on the leading edgemsHow fast the crash develops
ImpulseΣ a[n]·dt over the pulse windowg·sIntegrated momentum change (≈ delta-v)
Jerkmax(|a[n] − a[n−1]|)/dtg/sAbruptness / high-frequency content
EnergyΣ a[n]²·dtg²·sSignal power delivered to the structure

Rise time is computed on the first crossing of the 10% and 90% thresholds relative to the peak, searching forward from the pulse onset. If the signal never reaches 90% of peak before the peak sample, rise time is set to the time from onset to peak. This edge case matters for clipped or saturated channels.

Implementation

The extractor below is self-contained: it takes a dictionary of channels (each a 1-D NumPy array in g) and returns a feature row. It is written to run as-is with only NumPy and pandas.

import numpy as np
import pandas as pd

FS = 10_000.0
DT = 1.0 / FS

def time_domain_features(a, fs=FS):
    a = np.asarray(a, dtype=float)
    dt = 1.0 / fs
    peak = float(np.max(a))
    # onset: first sample exceeding 5% of peak
    onset_idx = int(np.argmax(a >= 0.05 * peak))
    peak_idx = int(np.argmax(a))
    # rise time: 10% -> 90% of peak on leading edge
    lo = 0.10 * peak
    hi = 0.90 * peak
    seg = a[onset_idx:peak_idx + 1]
    t10 = np.argmax(seg >= lo)
    t90 = np.argmax(seg >= hi)
    if seg[t90] < hi:            # never reached 90% before peak
        rise_ms = (peak_idx - onset_idx) * dt * 1000.0
    else:
        rise_ms = (t90 - t10) * dt * 1000.0
    impulse = float(np.sum(a) * dt)
    jerk = float(np.max(np.abs(np.diff(a))) / dt)
    energy = float(np.sum(a ** 2) * dt)
    return {
        "peak": peak,
        "rise_ms": rise_ms,
        "impulse": impulse,
        "jerk": jerk,
        "energy": energy,
    }

def build_feature_table(channels, labels):
    rows = []
    for i, (veh, occ) in enumerate(zip(channels["vehicle"], channels["occupant"])):
        fv = time_domain_features(veh)
        fo = time_domain_features(occ)
        row = {"event_id": i, "label": labels[i]}
        row.update({f"veh_{k}": v for k, v in fv.items()})
        row.update({f"occ_{k}": v for k, v in fo.items()})
        rows.append(row)
    return pd.DataFrame(rows)

Two implementation details are worth calling out. First, the onset threshold (5% of peak) prevents pre-trigger noise from inflating impulse and energy. Second, jerk is computed on the raw signal; if your channel has sensor noise above ~2 kHz, low-pass filter at 1.5 kHz before computing jerk, otherwise the derivative amplifies noise and the feature becomes dominated by measurement artifacts rather than crash dynamics.

Discriminative Power: How to Judge Each Feature

With a labeled set of events (deploy = 1, no-deploy = 0), the practical question is which features actually separate the classes. Two cheap, robust checks are used here:

  • Class separation ratio — |mean_deploy − mean_nodeploy| / pooled_std. Values above ~1.0 indicate a feature worth keeping; below ~0.3 it is likely noise for this task.
  • Univariate AUC — rank-based area under the ROC curve for a single feature. AUC near 0.5 means no discrimination; above 0.85 is strong for a single time-domain feature.

Both are computed without any model training, so they give a fast read on feature quality before committing to a classifier. The snippet below assumes the feature table from above:

from sklearn.metrics import roc_auc_score

def separation_ratio(x, y):
    x = np.asarray(x); y = np.asarray(y)
    m1, m0 = x[y == 1].mean(), x[y == 0].mean()
    s1, s0 = x[y == 1].std(ddof=1), x[y == 0].std(ddof=1)
    pooled = np.sqrt((s1**2 + s0**2) / 2.0)
    return abs(m1 - m0) / pooled if pooled > 0 else 0.0

feat_cols = [c for c in df.columns if c not in ("event_id", "label")]
report = []
for c in feat_cols:
    report.append({
        "feature": c,
        "sep_ratio": separation_ratio(df[c], df["label"]),
        "auc": roc_auc_score(df["label"], df[c]),
    })
report = pd.DataFrame(report).sort_values("auc", ascending=False)
print(report.to_string(index=False))

Reading the Results

On a typical frontal-crash dataset, the expected pattern is that veh_peak, veh_impulse, and veh_energy carry the strongest separation, because deployment is fundamentally a severity decision. veh_rise_ms adds value for borderline events: a slow-ramp 30 g pulse may not deploy while a sharp 30 g pulse does. Occupant-channel features tend to be weaker and noisier — the occupant channel is downstream of the restraint system, so its signal is partly a consequence of deployment rather than a cause. Treat occupant features as secondary and check for leakage: if the occupant channel was recorded after deployment, it cannot be used as a pre-deployment predictor.

Do not compute features over the full recording window. Truncate each channel to the decision window (e.g., 0–30 ms after trigger) before extraction. Features computed over the full event will leak post-deployment information and inflate AUC unrealistically.
Time-Domain Feature Extraction and Discriminative Power Vehicle channel (g) deploy no-deploy Feature Extractor peak · rise_ms impulse · jerk · energy window: 0–30 ms dt = 1e-4 s Univariate AUC (example) veh_peak 0.95 veh_impulse 0.90 veh_energy 0.86 veh_rise_ms 0.70 veh_jerk 0.60 occ_peak 0.50 Feature Selection Guidance Strong (AUC > 0.85) veh_peak, veh_impulse, veh_energy → primary predictors → keep for classifier Moderate (0.6–0.85) veh_rise_ms, veh_jerk → useful for borderline → check noise / filtering Weak (AUC < 0.6) occ_peak (example) → check leakage → likely drop

Verification Checklist

Before moving on, confirm the following on your own feature table:

  1. Every event has exactly 10 feature columns (5 per channel) plus event_id and label.
  2. No feature is computed over samples beyond the 30 ms decision window.
  3. rise_ms is positive and less than the window length for all events; a zero or negative value indicates an onset-detection bug.
  4. impulse and energy are non-negative for a positive-polarity pulse; negative values mean the sign convention was violated.
  5. The AUC report is sorted and reproducible — rerunning the script on the same table yields identical values.

If veh_jerk shows an AUC near 0.5 while peak and impulse are strong, the most common cause is unfiltered sensor noise dominating the derivative. Apply a 1.5 kHz low-pass filter and recompute; jerk should then track the sharpness of the leading edge rather than measurement noise.