Predictive Safety Intelligence
Phase 1: Foundation
Predictive Safety Intelligence provides explainable risk scoring for entities (zones, drivers, workers, equipment) based on historical data patterns. This is NOT machine learning - it uses deterministic heuristics for transparent, explainable safety analytics.
Overview
The Predictive Safety Intelligence system computes risk scores (0-100) and confidence coefficients (0-1) for entities based on:
- Incident frequency (weighted)
- Near-miss frequency (weighted)
- Shift hours (weighted)
- Workload/volume (weighted)
All scores include full explainability - you can see exactly which inputs contributed to each score.
Database Schema
predictive_models
Stores predictive model configuration per tenant.
CREATE TABLE public.predictive_models (
id uuid PRIMARY KEY,
tenant_id uuid NOT NULL REFERENCES public.tenants(id),
weight_incident numeric DEFAULT 2.0,
weight_near_miss numeric DEFAULT 1.5,
weight_shift_hours numeric DEFAULT 0.3,
weight_load numeric DEFAULT 0.2,
enabled boolean DEFAULT true,
updated_at timestamp with time zone DEFAULT now(),
created_at timestamp with time zone DEFAULT now(),
UNIQUE(tenant_id)
);Default Weights:
- Incident: 2.0 (highest weight - incidents are most critical)
- Near Miss: 1.5 (important but less critical than incidents)
- Shift Hours: 0.3 (fatigue factor)
- Workload: 0.2 (volume/stress factor)
predictive_scores
Stores computed predictive scores for entities.
CREATE TABLE public.predictive_scores (
id uuid PRIMARY KEY,
tenant_id uuid NOT NULL REFERENCES public.tenants(id),
entity_type text CHECK (entity_type IN ('zone', 'driver', 'worker', 'equipment')),
entity_id uuid NOT NULL,
score numeric CHECK (score >= 0 AND score <= 100),
confidence numeric CHECK (confidence >= 0 AND confidence <= 1),
computed_at timestamp with time zone DEFAULT now(),
inputs jsonb DEFAULT '{}'::jsonb,
created_at timestamp with time zone DEFAULT now()
);Score Ranges:
- 0-30: Low risk (green)
- 30-50: Medium risk (yellow)
- 50-70: High risk (orange)
- 70-100: Critical risk (red)
Confidence:
- Based on data availability (0 = no data, 1 = all data points available)
- Higher confidence = more reliable score
Inputs JSONB:
{
"incident_count": 5,
"near_miss_count": 12,
"shift_hours": 180.5,
"workload": 45,
"period_days": 30
}Edge Function API
Calculate Predictive Safety Scores
Endpoint: POST /functions/v1/calculate-predictive-safety-scores
Request Body:
{
"entity_type": "zone", // Optional: 'zone', 'driver', 'worker', 'equipment'
"entity_id": "uuid", // Optional: specific entity ID
"period_days": 30 // Optional: lookback period (default: 30)
}Query Parameters (GET):
entity_type(optional)entity_id(optional)period_days(optional, default: 30)
Response:
{
"success": true,
"tenant_id": "uuid",
"scores_computed": 15,
"scores": [
{
"entity_type": "zone",
"entity_id": "uuid",
"score": 65.5,
"confidence": 0.75,
"inputs": {
"incident_count": 3,
"near_miss_count": 8,
"shift_hours": 0,
"workload": 12,
"period_days": 30
}
}
],
"model": {
"weight_incident": 2.0,
"weight_near_miss": 1.5,
"weight_shift_hours": 0.3,
"weight_load": 0.2
}
}Usage:
import { calculatePredictiveSafetyScores } from '@/lib/api-edge';
// Calculate all scores
await calculatePredictiveSafetyScores();
// Calculate for specific entity
await calculatePredictiveSafetyScores('zone', zoneId, 30);Score Calculation Algorithm
Step 1: Gather Inputs
For each entity, collect data for the specified period:
- Incidents: Count of incidents in period
- Near Misses: Count of near-miss reports in period
- Shift Hours: Total shift hours (for drivers/workers)
- Workload: Shipments (drivers), pick lists (workers), active shipments (zones)
Step 2: Normalize Inputs
Normalize to 0-1 scale using maximum thresholds:
- Max Incidents: 10 (10+ = 1.0)
- Max Near Misses: 20 (20+ = 1.0)
- Max Shift Hours: 200 (200+ = 1.0)
- Max Workload: 100 (100+ = 1.0)
Step 3: Weighted Sum
weightedSum =
(normalizedIncidents * weight_incident) +
(normalizedNearMisses * weight_near_miss) +
(normalizedShiftHours * weight_shift_hours) +
(normalizedWorkload * weight_load);
totalWeight = weight_incident + weight_near_miss + weight_shift_hours + weight_load;
score = (weightedSum / totalWeight) * 100; // 0-100Step 4: Confidence Calculation
dataPoints = incident_count + near_miss_count + (shift_hours > 0 ? 1 : 0) + (workload > 0 ? 1 : 0);
confidence = min(1.0, dataPoints / 4.0); // 0-1UI Components
Predictive Dashboard
Route: Admin → Safety → Predictive Dashboard
Features:
- Score table with latest scores per entity
- Trend line chart (last 7 days)
- Filter by entity type (zones, drivers, workers, equipment)
- Period selector (7, 30, 60, 90 days)
- Model settings (Admin only)
- Calculate scores button
Access:
- Admin: Full access, can modify model weights
- Safety Officer: Read-only access, cannot modify model
Score Display
- Color-coded badges: Green (low), Yellow (medium), Orange (high), Red (critical)
- Confidence indicator: Progress bar showing confidence level
- Input details: Expandable view showing raw input data
- Trend arrows: Visual indicators for score trends
Permissions & RLS
Row-Level Security
Predictive Models:
- Admins: Full access
- Safety Officers: Full access (within tenant)
- Tenant Admins: Full access (within tenant)
Predictive Scores:
- Admins: Full access
- Safety Officers: Read access (within tenant)
- Tenant Admins: Read access (within tenant)
- Workers/Drivers: Read own scores only
UI Permissions
- Admin: Can view all scores, modify model weights, calculate scores
- Safety Officer: Can view all scores, calculate scores, cannot modify model
- Workers/Drivers: Cannot access Predictive Dashboard
Usage Examples
Calculate Scores for All Entities
import { useCalculatePredictiveScores } from '@/lib/queries';
const { mutateAsync: calculateScores } = useCalculatePredictiveScores();
await calculateScores({
periodDays: 30
});Fetch Latest Scores
import { usePredictiveScores } from '@/lib/queries';
// All scores
const { data: allScores } = usePredictiveScores();
// Filter by type
const { data: zoneScores } = usePredictiveScores('zone');Update Model Weights
import { useUpdatePredictiveModel } from '@/lib/queries';
const { mutateAsync: updateModel } = useUpdatePredictiveModel();
await updateModel({
weight_incident: 2.5,
weight_near_miss: 1.8,
weight_shift_hours: 0.4,
weight_load: 0.3,
});Performance Considerations
- Edge Function: Aggregates multiple queries, reduces REST calls
- Caching: Scores are computed on-demand, not real-time
- Batch Calculation: Calculate all entities at once for efficiency
- Historical Tracking: Multiple scores per entity over time for trend analysis
Phase 2: Risk Clusters
Status: ✅ Implemented
Risk Clusters group entities with similar risk patterns into clusters for easier management and visualization.
Database Schema
risk_clusters
Stores risk cluster definitions.
CREATE TABLE public.risk_clusters (
id uuid PRIMARY KEY,
tenant_id uuid NOT NULL REFERENCES public.tenants(id),
cluster_name text NOT NULL,
severity numeric CHECK (severity >= 0 AND severity <= 100),
description text,
created_at timestamp with time zone DEFAULT now(),
updated_at timestamp with time zone DEFAULT now()
);Severity Ranges:
- Critical Risk (70-100): Very high risk entities
- High Risk (50-70): High risk entities
- Medium Risk (30-50): Moderate risk entities
- Low Risk (0-30): Low risk entities
risk_cluster_entities
Junction table linking entities to clusters.
CREATE TABLE public.risk_cluster_entities (
id uuid PRIMARY KEY,
cluster_id uuid NOT NULL REFERENCES public.risk_clusters(id),
entity_type text CHECK (entity_type IN ('zone', 'driver', 'worker', 'equipment')),
entity_id uuid NOT NULL,
assigned_at timestamp with time zone DEFAULT now(),
UNIQUE(cluster_id, entity_type, entity_id)
);Edge Function API
Assign Risk Clusters
Endpoint: POST /functions/v1/assign-risk-clusters
Request Body:
{
"entity_type": "zone", // Optional: filter by entity type
"min_score": 0, // Optional: minimum score threshold
"max_score": 100 // Optional: maximum score threshold
}Response:
{
"success": true,
"tenant_id": "uuid",
"clusters_created": 4,
"entities_assigned": 25,
"clusters": [
{
"id": "uuid",
"cluster_name": "Critical Risk",
"severity": 85.5,
"description": "Entities with very high risk scores (70-100)",
"entity_count": 8
}
]
}Usage:
import { assignRiskClusters } from '@/lib/api-edge';
// Assign all entities to clusters
await assignRiskClusters();
// Assign only zones
await assignRiskClusters('zone');Clustering Algorithm
- Fetch Latest Scores: Gets the most recent predictive score for each entity
- Group by Severity: Groups entities into 4 severity ranges
- Calculate Average: Computes average severity for each cluster
- Create/Update Clusters: Creates new clusters or updates existing ones
- Assign Entities: Links entities to their respective clusters
UI Components
Cluster Map
Visual representation of all risk clusters with:
- Cluster name and description
- Average severity score
- Entity count
- Click to view cluster entities
Top 5 Highest Risk
Displays the 5 clusters with highest severity scores, sorted by risk level.
Cluster Filter
Filter entities by cluster membership in the scores table.
React Query Hooks
import {
useRiskClusters,
useRiskClustersWithCounts,
useClusterEntities,
useAssignRiskClusters,
} from '@/lib/queries';
// Fetch all clusters
const { data: clusters } = useRiskClusters();
// Fetch clusters with entity counts
const { data: clustersWithCounts } = useRiskClustersWithCounts();
// Fetch entities in a specific cluster
const { data: entities } = useClusterEntities(clusterId);
// Assign clusters mutation
const { mutateAsync: assignClusters } = useAssignRiskClusters();
await assignClusters({ entityType: 'zone' });Permissions & RLS
Risk Clusters:
- Admins: Full access
- Safety Officers: Full access (within tenant)
- Tenant Admins: Full access (within tenant)
Risk Cluster Entities:
- Admins: Full access
- Safety Officers: Read access (within tenant)
- Tenant Admins: Read access (within tenant)
Future Enhancements (Phases 3-6)
- Phase 3: Predictive alerts
- Phase 4: Intervention recommendations
- Phase 5: Machine learning integration (optional)
- Phase 6: Real-time streaming scores