2. Python environment setup for crash data analysis
Crash pulse and acceleration analysis sits at the intersection of signal processing, numerical computation, and data engineering. A typical workflow starts with raw accelerometer or crash-sled channels sampled at 10–100 kHz, passes through filtering, integration, and peak detection, and ends with plots and tabulated injury metrics. Before any of that can happen, you need a Python environment that is reproducible, isolated, and version-controlled. This section walks through installing the core scientific stack, isolating it inside a virtual environment, and putting the whole setup under version control so that a colleague (or your future self) can rebuild it exactly.
The Core Stack and What Each Library Does
Five packages cover the vast majority of crash data work:
- NumPy — the array engine. A crash pulse is naturally a 1-D array of acceleration samples; NumPy gives you vectorized arithmetic, slicing, and the
ndarraytype that every other library builds on. - SciPy — the signal-processing toolbox.
scipy.signal.butter,sosfiltfilt,find_peaks, andcumtrapz(orcumulative_trapezoidin newer versions) are the workhorses for filtering, peak detection, and velocity/displacement integration. - pandas — tabular handling. Channel metadata, test matrices, and computed metrics (HIC, peak g, time-to-peak) live comfortably in DataFrames, and pandas handles CSV/Parquet I/O and time-index alignment.
- Matplotlib — plotting. Acceleration-time traces, filtered overlays, and multi-channel comparison figures are all produced here.
- Jupyter — interactive exploration. Notebooks let you inspect a pulse, tweak a filter cutoff, and immediately see the effect, which is invaluable during method development.
These five are not independent choices; they form a dependency chain. pandas depends on NumPy, SciPy depends on NumPy, and Matplotlib integrates with all of them. Installing them together in one environment avoids the classic "works on my machine" mismatch where a filter behaves differently because SciPy resolved to a different version.
Why Virtual Environments Are Non-Negotiable
Crash analysis projects often coexist with unrelated Python work — a data-logger script, a report generator, a machine-learning experiment. If everything installs into the system interpreter, upgrading SciPy for one project can silently break another. A virtual environment gives each project its own isolated site-packages directory and its own interpreter symlink, so dependency changes are contained.
The standard tool is venv, shipped with Python 3.3+. Create and activate one from your project root:
# Create an isolated environment named .venv
python -m venv .venv
# Activate it (Windows PowerShell)
.venv\Scripts\Activate.ps1
# Activate it (macOS / Linux)
source .venv/bin/activate
# Confirm you are inside the environment
python -c "import sys; print(sys.prefix)"
Once activated, pip install writes only into .venv. Install the stack in one command so pip resolves a consistent set of versions:
python -m pip install --upgrade pip
python -m pip install numpy scipy pandas matplotlib jupyterlab
After installation, verify the versions actually resolved. This matters because SciPy's filter API has changed across releases, and a notebook written against 1.10 may behave differently on 1.13:
python -c "import numpy, scipy, pandas, matplotlib; \
print('numpy', numpy.__version__); \
print('scipy', scipy.__version__); \
print('pandas', pandas.__version__); \
print('matplotlib', matplotlib.__version__)"
A healthy output looks like numpy 1.26.4, scipy 1.13.0, pandas 2.2.2, matplotlib 3.8.4. Exact numbers will differ; what matters is that all four import without error and report versions you can record.
conda or uv as an alternative environment manager. uv in particular resolves and installs the scientific stack much faster, but the isolation principle is identical: one environment per project, never the system interpreter.
Pinning Dependencies for Reproducibility
An environment that works today may not work after a dependency releases a breaking change. Freeze the exact versions into a requirements file and commit it:
python -m pip freeze > requirements.txt
The resulting file lists every package and its exact version, including transitive dependencies. A teammate can then rebuild the environment with:
python -m venv .venv
source .venv/bin/activate
python -m pip install -r requirements.txt
For a crash analysis project, a minimal requirements.txt might look like this:
numpy==1.26.4
scipy==1.13.0
pandas==2.2.2
matplotlib==3.8.4
jupyterlab==4.1.6
Pin exact versions (==) rather than ranges (>=) when the analysis must be reproducible for a report or a regulatory submission. Ranges are acceptable during early exploration but should be frozen before results are published.
Version Control: What to Commit and What to Ignore
Git is the standard for tracking analysis code, notebooks, and configuration. The critical rule is: commit the recipe, not the kitchen. The virtual environment directory is large, machine-specific, and fully reconstructible from requirements.txt, so it must be excluded.
A .gitignore tailored to this stack:
.venv/
__pycache__/
*.pyc
.ipynb_checkpoints/
*.egg-info/
data/raw/
*.h5
*.parquet
Note the last three entries. Raw crash data files are often hundreds of megabytes and may be subject to confidentiality agreements; they should live outside the repository or in a separate data store. Commit the code that reads them, not the files themselves. If you must version small reference datasets, use Git LFS or a dedicated data directory with a clear README.
Notebooks deserve special attention. Jupyter stores execution counts and output cells in the .ipynb JSON, which produces noisy diffs. Two practical options: clear outputs before committing (jupyter nbconvert --clear-output --inplace *.ipynb), or use nbstripout as a Git filter. Either way, the goal is a diff that shows code changes, not a wall of base64-encoded PNGs.
Putting It Together: A Reproducible Project Layout
A clean layout separates code, configuration, and data:
crash-pulse-analysis/
├── .venv/ # ignored
├── .gitignore
├── requirements.txt
├── README.md
├── notebooks/
│ └── 01_pulse_exploration.ipynb
├── src/
│ ├── __init__.py
│ ├── io.py # channel loading
│ └── filters.py # Butterworth, CFC filtering
└── data/
└── raw/ # ignored, holds .h5 / .csv
With this structure, a new team member clones the repository, creates the virtual environment, installs from requirements.txt, and runs the notebooks. The environment is identical, the code is versioned, and the data stays where it belongs.
Verifying the Setup with a Minimal Smoke Test
Before writing any analysis code, confirm the stack actually works end-to-end. The following script generates a synthetic half-sine crash pulse, applies a low-pass Butterworth filter, and plots the result. It exercises NumPy, SciPy, and Matplotlib in one pass:
import numpy as np
from scipy.signal import butter, sosfiltfilt
import matplotlib.pyplot as plt
# Synthetic 50 g half-sine pulse, 10 ms duration, 10 kHz sample rate
fs = 10_000
t = np.arange(0, 0.010, 1/fs)
pulse = 50 * np.sin(np.pi * t / t[-1])
# 2nd-order low-pass at 1650 Hz (SAE J211 CFC 1000 class)
sos = butter(2, 1650, btype='low', fs=fs, output='sos')
filtered = sosfiltfilt(sos, pulse)
print(f"peak raw = {pulse.max():.2f} g")
print(f"peak filtered = {filtered.max():.2f} g")
plt.plot(t*1000, pulse, label='raw')
plt.plot(t*1000, filtered, '--', label='filtered')
plt.xlabel('Time (ms)'); plt.ylabel('Acceleration (g)')
plt.legend(); plt.grid(True); plt.show()
Expected output: the raw peak prints near 50.00 g, and the filtered peak is slightly lower (typically 48–49 g) because the filter attenuates the highest-frequency content. If the script raises ModuleNotFoundError, the environment is not activated. If butter raises a ValueError about the cutoff frequency, check that fs is passed correctly — omitting fs makes SciPy interpret 1650 as a normalized frequency, which is a common and confusing failure.
pip install without an activated virtual environment. If sys.prefix points to your system Python rather than .venv, you are installing globally and will eventually hit a version conflict that is painful to unwind.
Summary
A reliable crash analysis environment rests on three practices: isolate dependencies in a per-project virtual environment, pin exact versions in requirements.txt, and version-control the recipe while excluding the environment and raw data. The five-package stack — NumPy, SciPy, pandas, Matplotlib, and Jupyter — covers loading, filtering, integrating, detecting peaks, and reporting. Once the smoke test runs cleanly and the repository is committed, you have a foundation that any collaborator can rebuild byte-for-byte, which is exactly what crash pulse analysis demands when results feed into safety reports.