Skip to content

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.

sql
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.

sql
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:

json
{
  "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:

json
{
  "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:

json
{
  "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:

typescript
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

typescript
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-100

Step 4: Confidence Calculation

typescript
dataPoints = incident_count + near_miss_count + (shift_hours > 0 ? 1 : 0) + (workload > 0 ? 1 : 0);
confidence = min(1.0, dataPoints / 4.0); // 0-1

UI 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

typescript
import { useCalculatePredictiveScores } from '@/lib/queries';

const { mutateAsync: calculateScores } = useCalculatePredictiveScores();

await calculateScores({
  periodDays: 30
});

Fetch Latest Scores

typescript
import { usePredictiveScores } from '@/lib/queries';

// All scores
const { data: allScores } = usePredictiveScores();

// Filter by type
const { data: zoneScores } = usePredictiveScores('zone');

Update Model Weights

typescript
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.

sql
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.

sql
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:

json
{
  "entity_type": "zone",  // Optional: filter by entity type
  "min_score": 0,         // Optional: minimum score threshold
  "max_score": 100        // Optional: maximum score threshold
}

Response:

json
{
  "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:

typescript
import { assignRiskClusters } from '@/lib/api-edge';

// Assign all entities to clusters
await assignRiskClusters();

// Assign only zones
await assignRiskClusters('zone');

Clustering Algorithm

  1. Fetch Latest Scores: Gets the most recent predictive score for each entity
  2. Group by Severity: Groups entities into 4 severity ranges
  3. Calculate Average: Computes average severity for each cluster
  4. Create/Update Clusters: Creates new clusters or updates existing ones
  5. 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

typescript
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

Released under Commercial License