Severity Scoring Algorithms for DVIR Defects
A binary pass/fail flag cannot tell a dispatcher whether a reported brake condition is a scheduled-maintenance item or an out-of-service (OOS) event, yet 49 CFR § 396.11(a)(3) obligates the carrier to correct — and certify corrected — every Driver Vehicle Inspection Report (DVIR) defect that affects safe operation before the vehicle is dispatched again. Deterministic severity scoring is the layer that turns a normalized defect record into a reproducible 0–100 number, so that the same defect always resolves to the same compliance action and never depends on which dispatcher is on shift. This page specifies that scoring engine inside the Defect Classification & Repair Order Routing pipeline: it consumes a validated defect, computes a weighted, auditable score, and maps that score to the same severity bands that Critical vs Non-Critical Routing Logic uses to trigger an OOS hold or defer to a scheduled window.
Prerequisites and Environment Setup
Anchor link to "Prerequisites and Environment Setup"The scoring service is a pure, side-effect-free consumer that sits between ingestion and routing. It does not fetch the record and it does not perform the routing itself — it receives a validated defect and returns a score plus its supporting factor vector. Target Python 3.10+ (the code below uses match statements and the union type operator) with:
- Pydantic 2.x — immutable input models and strict type coercion at the boundary.
- NumPy 1.26+ / Pandas 2.x — vectorized factor arithmetic for fleet-scale batches.
decimal(stdlib) — fixed-point rounding to eliminate floating-point drift before threshold evaluation.- PyYAML — version-controlled weight matrices loaded from configuration rather than hard-coded.
Every inbound defect must already conform to the canonical contract defined in the Standardized DVIR JSON Schema Design; a payload that fails that contract must be rejected before it reaches this layer, never defaulted to a low score. The component taxonomy the safety factor keys off is the same one produced by Defect Code Standardization Across Fleets, so a component_code that maps to a steering or brake group carries its OOS weight consistently across the pipeline.
Data Schema and Normalization
Anchor link to "Data Schema and Normalization"Before scoring can run, heterogeneous DVIR payloads from telematics gateways and mobile inspection apps must be normalized into one defect record with strict type enforcement. The field names and the 0–100 scale are held identical to the routing and threshold pages so a single defect can be reconstructed across the pipeline during a DOT audit.
| Field | Type | Enumeration / Range | Compliance annotation |
|---|---|---|---|
dvir_id |
string (UUID) |
— | Immutable identifier for audit-trail reconstruction |
vehicle_vin |
string (17-char) |
— | Asset-level tracking and OOS/recall cross-referencing |
component_code |
string |
SAE J1939 SPN / OEM fault tree | Selects the safety criticality weight |
defect_description |
string |
— | Sanitized driver input; never an input to a compliance decision |
driver_severity_flag |
enum |
minor, major, critical |
§ 396.11(a)(3) driver flag; treated as a floor, never a ceiling |
timestamp_utc |
ISO8601 |
— | Inspection timestamp for SLA windows and audit keying |
odometer_reading |
integer |
≥ 0 |
Usage context for wear-based scoring adjustments |
compliance_tags |
array[string] |
e.g. FMCSA_396_11, CVSA_OOS |
Regulatory markers evaluated by the regulatory-risk factor |
severity_score |
integer |
0–100 |
Output emitted to the routing layer |
factor_vector |
object |
four floats 0.0–1.0 |
Per-factor breakdown persisted for audit reconstruction |
The normalization step strips unstructured free-text, maps driver-reported symptoms to standardized component_code identifiers, and tags the § 396.11 markers at ingestion so downstream auditability is guaranteed. Records that arrive with malformed VINs or unmappable component codes are rejected — a defect that cannot be typed cannot be scored, and an unscorable safety defect must escalate, not silently default.
Core Algorithm: Weighted Factor Scoring
Anchor link to "Core Algorithm: Weighted Factor Scoring"A deterministic severity score S is a linear combination of four normalized factors, each in the range 0.0–1.0, multiplied by configurable policy weights that sum to 1.0:
- Safety Impact Factor () — derived from component criticality tables (brakes, steering, suspension, tires, lighting). A steering or service-brake defect keyed to a CVSA OOS component maps to 1.0.
- Regulatory Risk Factor () — mapped to FMCSA OOS thresholds and any state-specific inspection mandates present in
compliance_tags. - Operational Downtime Factor () — estimated repair labor hours plus parts-procurement lead time, normalized against a fleet ceiling.
- Historical Recurrence Factor () — defect frequency for the same
component_codeon the asset or fleet segment over a rolling 90-day window.
The raw output is clamped and normalized to a 0–100 integer. The steps below are ordered so the computation is reproducible and side-effect-free.
1. Model the input and the weight matrix. Coerce and freeze the record at the boundary so nothing downstream can mutate it.
from decimal import Decimal, ROUND_HALF_UP
from enum import StrEnum
from pydantic import BaseModel, Field, field_validator
class DriverFlag(StrEnum):
MINOR = "minor"
MAJOR = "major"
CRITICAL = "critical"
class DefectRecord(BaseModel, frozen=True):
dvir_id: str
vehicle_vin: str = Field(min_length=17, max_length=17)
component_code: str
driver_severity_flag: DriverFlag
odometer_reading: int = Field(ge=0)
compliance_tags: list[str] = Field(default_factory=list)
@field_validator("component_code")
@classmethod
def _known_code(cls, v: str) -> str:
if v not in COMPONENT_CRITICALITY:
raise ValueError(f"unmapped component_code: {v!r}")
return v
class PolicyWeights(BaseModel, frozen=True):
safety: float = 0.45
regulatory: float = 0.30
downtime: float = 0.15
recurrence: float = 0.10
@field_validator("safety", "regulatory", "downtime", "recurrence")
@classmethod
def _unit_interval(cls, v: float) -> float:
if not 0.0 <= v <= 1.0:
raise ValueError("each weight must be in [0.0, 1.0]")
return v
def validate_sum(self) -> "PolicyWeights":
total = self.safety + self.regulatory + self.downtime + self.recurrence
if abs(total - 1.0) > 1e-9:
raise ValueError(f"weights must sum to 1.0, got {total}")
return self
2. Derive the four factors. Each factor is a pure function of the record and reference tables. The driver’s own flag is applied as a floor so the engine never routes below the severity the driver already asserted under § 396.11(a)(3).
# component_code -> safety criticality in [0.0, 1.0]
COMPONENT_CRITICALITY: dict[str, float] = {
"SPN_513_STEERING": 1.00, # steering — CVSA OOS component
"SPN_597_SVC_BRAKE": 1.00, # service brakes — CVSA OOS component
"SPN_929_SUSPENSION": 0.75,
"SPN_1213_LIGHTING": 0.40,
"SPN_2000_WIPER": 0.20,
}
OOS_TAGS = {"CVSA_OOS", "FMCSA_396_11_OOS"}
DRIVER_FLOOR = {DriverFlag.MINOR: 0.0, DriverFlag.MAJOR: 0.55, DriverFlag.CRITICAL: 0.85}
def safety_factor(rec: DefectRecord) -> float:
base = COMPONENT_CRITICALITY[rec.component_code]
return max(base, DRIVER_FLOOR[rec.driver_severity_flag])
def regulatory_factor(rec: DefectRecord) -> float:
return 1.0 if OOS_TAGS & set(rec.compliance_tags) else 0.35
def downtime_factor(labor_hours: float, parts_lead_days: float) -> float:
# normalize against a 24-labor-hour / 5-day fleet ceiling
return min(1.0, (labor_hours / 24.0) * 0.6 + (parts_lead_days / 5.0) * 0.4)
def recurrence_factor(occurrences_90d: int) -> float:
return min(1.0, occurrences_90d / 4.0) # 4+ recurrences saturates the factor
3. Combine, clamp, and round deterministically. Fixed-point rounding before the final cast removes floating-point drift, so the same input always crosses the same threshold.
def severity_score(
rec: DefectRecord,
weights: PolicyWeights,
*,
labor_hours: float,
parts_lead_days: float,
occurrences_90d: int,
) -> tuple[int, dict[str, float]]:
weights.validate_sum()
factors = {
"safety": safety_factor(rec),
"regulatory": regulatory_factor(rec),
"downtime": downtime_factor(labor_hours, parts_lead_days),
"recurrence": recurrence_factor(occurrences_90d),
}
raw = (
weights.safety * factors["safety"]
+ weights.regulatory * factors["regulatory"]
+ weights.downtime * factors["downtime"]
+ weights.recurrence * factors["recurrence"]
)
scaled = Decimal(raw * 100).quantize(Decimal("1"), rounding=ROUND_HALF_UP)
score = max(0, min(100, int(scaled)))
return score, factors
For fleet-scale batches the same arithmetic runs vectorized over a Pandas frame — the full NumPy/Pandas implementation, including config hot-reload and per-vehicle aggregation, is developed in Building a Weighted Defect Scoring Model in Python.
Compliance Thresholding and Routing
Anchor link to "Compliance Thresholding and Routing"The 0–100 score maps to exactly one severity band, and each band binds an imperative compliance action. The band boundaries are identical to every other page in this section so a defect keeps the same tier from scoring through routing to certification.
| Score range | Tier | Compliance action | Routing behavior |
|---|---|---|---|
0–34 |
Minor | Log for the next scheduled preventive-maintenance window | Queue for standard bay assignment |
35–69 |
Major | Record a documented 24-hour pre-trip clearance obligation | Route to the expedited lane in Critical vs Non-Critical Routing Logic |
70–100 |
Critical / OOS | Trigger an immediate OOS hold and freeze dispatch | Immobilize the asset and fan out an emergency alert |
The band boundaries themselves are not universal — a refrigerated-box fleet raises the downtime weight so a reefer failure crosses the critical floor sooner, while a passenger carrier raises the safety weight for suspension and tire defects. Those fleet-aware cut points are supplied by Dynamic Threshold Tuning for Fleet Types, which the scoring engine reads at classification time rather than hard-coding. Whatever the profile, the mapping is a hard gate: a critical score cannot be overridden into a scheduled queue, because a critical band that reaches the router sets compliance_hold=true and the transition table has no legal edge from there to a deferred lane.
def band(score: int) -> DriverFlag:
match score:
case s if s >= 70:
return DriverFlag.CRITICAL
case s if s >= 35:
return DriverFlag.MAJOR
case _:
return DriverFlag.MINOR
Production Integration and Platform Synchronization
Anchor link to "Production Integration and Platform Synchronization"A severity score is only useful once it is propagated across the maintenance stack without ambiguity. The scoring engine emits a structured event — the severity_score, the factor_vector, the applied weight version, and a SHA-256 hash of the source record — to the CMMS, telematics, or ELD platform over a webhook or message queue (Kafka, RabbitMQ, AWS SQS). Two invariants keep the integration audit-safe:
- Idempotent upserts. The event key is a deterministic SHA-256 of
vehicle_vin+component_code+timestamp_utc, so a retry or replay resolves to the same record rather than creating a duplicate repair order. The prioritized ticket then flows to Mechanic Assignment & Workload Balancing, which matches it against technician certification and bay capacity. - Cryptographic chaining. Each emitted score hash-links to the previous score for the same asset, so a deleted or reordered scoring decision is detectable during a DOT audit.
When mapping to a commercial platform’s native priority field, verify the band-to-priority mapping during onboarding and store the original score alongside it — never overwrite the computed score with the platform’s coarser priority label.
Engineering Standards Checklist
Anchor link to "Engineering Standards Checklist"- Schema validation — coerce and freeze every record with Pydantic at the boundary; reject malformed VINs and unmapped
component_codevalues before they enter the scoring queue. - Deterministic execution — no
random, no wall-clock reads inside the score function; round withDecimal/ROUND_HALF_UPbefore the integer cast so the same input never straddles a threshold. - Audit logging — persist the raw factor vector, the weight version, and the final score to an append-only ledger so a scoring decision can be reconstructed years later.
- Configuration management — keep weight matrices in version-controlled YAML/JSON and hot-reload them without a service restart; every score records the config hash that produced it.
Frequently Asked Questions
Anchor link to "Frequently Asked Questions"Can a driver’s own severity flag lower the computed score?
No. The driver_severity_flag from § 396.11(a)(3) is applied as a floor through DRIVER_FLOOR, never a ceiling. A driver who flags a defect critical guarantees a minimum safety factor of 0.85; the engine can raise the score above the driver’s flag when the component and regulatory context justify it, but it will never route below what the driver already asserted.
Why round with Decimal instead of Python’s built-in round?
Anchor link to "Why round with Decimal instead of Python’s built-in round?" Binary floating-point cannot represent many decimal fractions exactly, so a raw score that is mathematically 69.5 can land at 69.4999… and fall into the major band instead of critical. Quantizing with Decimal and ROUND_HALF_UP before the integer cast makes the threshold crossing reproducible, which is what an audit requires.
What happens to a defect whose component_code is unmapped?
The Pydantic validator raises before scoring, so the payload is rejected and escalated rather than scored against a default. Silently assigning a low score to a defect the taxonomy does not recognize is exactly the failure mode that puts an unsafe vehicle on the road.
How are the weights allowed to differ between fleets?
The four policy weights are loaded per fleet profile from version-controlled configuration and must still sum to 1.0. A refrigerated fleet raises the downtime weight; a passenger carrier raises the safety weight. The band boundaries stay fixed at 0–34 / 35–69 / 70–100, and the specific cut points come from Dynamic Threshold Tuning for Fleet Types so scores remain comparable across the fleet.
Related
Anchor link to "Related"- Critical vs Non-Critical Routing Logic — consumes the 0–100 score this engine emits and binds it to an OOS hold or a deferred window.
- Building a Weighted Defect Scoring Model in Python — the full NumPy/Pandas implementation with config hot-reload and per-vehicle aggregation.
- Dynamic Threshold Tuning for Fleet Types — supplies the fleet-aware cut points the bands are evaluated against.
- Mechanic Assignment & Workload Balancing — receives the prioritized ticket the scored defect produces.
- Standardized DVIR JSON Schema Design — the canonical contract every inbound defect must satisfy before it can be scored.