Prioritizing Restoration with SAIDI/SAIFI Scoring
When a storm opens more work than the available crews can clear at once, the order in which outages are restored is the single largest lever a utility holds over its published reliability numbers — and over which customers sit in the dark longest. Restoring a feeder that serves four hundred customers before a lateral that serves six is not a judgment call to be made ad hoc on a dispatch board; it is a deterministic ranking that should fall directly out of the outage impact set. The scoring model on this page turns customer counts, critical-facility flags, and marginal reliability impact into one reproducible priority number, so that the crew dispatch and route optimization pipeline routes crews to the highest-value work first and can defend every ordering decision after the fact.
The stakes are regulatory as much as operational. SAIDI, SAIFI, and CAIDI are the reliability indices utilities report to their regulators, and they are computed from exactly the customer-count and duration data this score consumes. A priority function that is opaque or non-deterministic cannot be audited, and an ordering that ignores marginal reliability impact leaves customer-minutes on the table that the utility will answer for in its next rate case. This score sits under the outage routing and impact automation discipline and feeds the queue that the routing solver optimizes.
Environment Prerequisites
Lock these before wiring the score into a live dispatch feed. The reliability-metric definitions and the served-customer base are the inputs most often gotten wrong.
- Python runtime: Python 3.11 in an isolated environment, so scoring weights and metric constants stay reproducible across dispatch nodes.
- Standard library only: the reference implementation uses
dataclasses,datetime,typing, andlogging— no third-party dependency — so it can run inside any dispatch service without a heavy install. - A validated impact set per outage: affected-customer count and critical-facility flags produced by the affected-customer tracing workflow, not re-counted from raw geometry at scoring time.
- The total served-customer base (N): the denominator for SAIDI and SAIFI, taken from the billing or customer-information system for the reporting boundary, held as a configuration constant that changes only on a connected-customer audit.
- A single event-time reference: every outage start and SLA deadline expressed against one clock, so the duration proxy and urgency term are directly comparable across outages.
- Versioned scoring weights: the per-customer weight, critical multiplier, and SLA-urgency weight held under version control and deployed through CI/CD, because a change to the weighting changes the restoration order and is itself an auditable event.
Understanding SAIDI, SAIFI, and CAIDI — Run the Definitions Before You Score
Getting the score right depends entirely on using the reliability metrics correctly; the most common scoring bug is a formula that conflates duration and frequency. Work these definitions first. All three are computed over a defined reporting period and a total served-customer base N, per the framework in IEEE Std 1366, which regulators adopt as the reference for distribution reliability indices.
| Metric | Full name | Formula | Units | Reads as |
|---|---|---|---|---|
| SAIFI | System Average Interruption Frequency Index | Σ(customers interrupted) / N | interruptions per customer | how often the average customer loses power |
| SAIDI | System Average Interruption Duration Index | Σ(customer-minutes interrupted) / N | minutes per customer | how long the average customer is out, in total |
| CAIDI | Customer Average Interruption Duration Index | SAIDI / SAIFI | minutes per interruption | average length of one interruption |
The relationship that matters for prioritization is that SAIDI accumulates every minute an outage stays unrestored, weighted by the customers it affects. The marginal SAIDI cost of leaving an outage open for one more hour is therefore customers_out × 60 / N — and that quantity, customer-minutes per served customer, is the reliability heart of the priority score. SAIFI is fixed the moment the interruption occurs (the customers are already counted as interrupted), so it drives which outages count toward the frequency index but not the ordering among already-open outages. CAIDI is a derived reporting figure, useful for benchmarking crew performance after the fact, not an input to the live queue.
The practical consequence is that a restoration-priority score should rank open outages by the SAIDI they are still accruing — customers-out scaled by how long they have already been out — not merely by raw customer count. A three-hundred-customer outage that started five hours ago is accruing more reportable customer-minutes right now than a four-hundred-customer outage that just occurred, and a defensible score reflects that.
Minimal Reproducible Implementation
The following computes a deterministic restoration-priority score for each open outage. It derives the marginal SAIDI contribution from customers-out and elapsed duration, applies a critical-facility multiplier and an SLA-urgency term that rises as the notification deadline nears, and returns a ranked, structured result. It uses only the standard library and is runnable as written.
from dataclasses import dataclass, field
from datetime import datetime, timedelta
from typing import Optional
import logging
logging.basicConfig(level=logging.INFO, format="%(levelname)s: %(message)s")
@dataclass(frozen=True)
class ScoreConfig:
"""Deterministic, version-controlled weights for the priority score."""
served_base: int # N: total customers served (denominator)
critical_multiplier: float = 3.0 # weight for critical-facility outages
sla_urgency_weight: float = 5000.0 # added as the SLA deadline approaches
saidi_weight: float = 1.0 # scales marginal customer-minutes/customer
@dataclass(frozen=True)
class Outage:
outage_id: str
customers_out: int
started_at: datetime
critical: bool = False
sla_deadline: Optional[datetime] = None # statutory notify/restore deadline
@dataclass
class ScoredOutage:
outage_id: str
score: float
marginal_saidi_per_hour: float
breakdown: dict[str, float] = field(default_factory=dict)
def marginal_saidi_per_hour(customers_out: int, served_base: int) -> float:
"""SAIDI (customer-minutes per served customer) accrued per additional hour.
This is the rate at which leaving the outage open worsens the system SAIDI.
"""
if served_base <= 0:
raise ValueError("served_base (N) must be positive")
return (customers_out * 60.0) / served_base
def sla_fraction_used(started_at: datetime, deadline: Optional[datetime],
now: datetime) -> float:
"""Share of the SLA window already elapsed (0.0 fresh, >=1.0 past due)."""
if deadline is None:
return 0.0
window = (deadline - started_at).total_seconds()
if window <= 0:
return 1.0
return max(0.0, (now - started_at).total_seconds() / window)
def score_outage(outage: Outage, cfg: ScoreConfig, now: datetime) -> ScoredOutage:
"""Compute a deterministic restoration-priority score for one outage."""
if outage.customers_out < 0:
raise ValueError(f"{outage.outage_id}: negative customers_out")
hours_out = max(0.0, (now - outage.started_at).total_seconds() / 3600.0)
saidi_rate = marginal_saidi_per_hour(outage.customers_out, cfg.served_base)
# Reliability term: accrued customer-minutes so far, scaled by served base.
accrued_saidi = saidi_rate * hours_out * cfg.saidi_weight
reliability_term = accrued_saidi * 1_000_000 # lift into a readable integer range
urgency = sla_fraction_used(outage.started_at, outage.sla_deadline, now)
urgency_term = urgency * cfg.sla_urgency_weight
subtotal = reliability_term + urgency_term
multiplier = cfg.critical_multiplier if outage.critical else 1.0
total = subtotal * multiplier
return ScoredOutage(
outage_id=outage.outage_id,
score=round(total, 3),
marginal_saidi_per_hour=round(saidi_rate, 6),
breakdown={
"reliability_term": round(reliability_term, 3),
"urgency_term": round(urgency_term, 3),
"critical_multiplier": multiplier,
},
)
def rank_outages(outages: list[Outage], cfg: ScoreConfig,
now: Optional[datetime] = None) -> list[ScoredOutage]:
"""Return outages ranked by descending priority score (highest first)."""
now = now or datetime.utcnow()
scored = [score_outage(o, cfg, now) for o in outages]
scored.sort(key=lambda s: s.score, reverse=True)
logging.info("ranked %d outages; top=%s", len(scored),
scored[0].outage_id if scored else "none")
return scored
if __name__ == "__main__":
reference = datetime(2026, 7, 13, 18, 0, 0)
config = ScoreConfig(served_base=50_000)
events = [
Outage("O-100", 400, reference - timedelta(minutes=20)),
Outage("O-101", 300, reference - timedelta(hours=5)),
Outage("O-102", 45, reference - timedelta(hours=1), critical=True,
sla_deadline=reference + timedelta(minutes=30)),
]
for row in rank_outages(events, config, now=reference):
print(f"{row.outage_id}: score={row.score} "
f"saidi/h={row.marginal_saidi_per_hour} {row.breakdown}")
Three design choices make the score defensible. The reliability term is the accrued SAIDI contribution — customers-out multiplied by hours already out — so a small outage that has dragged on can outrank a larger, fresher one, which is exactly the customer-minutes-saved logic a regulator expects. The critical-facility multiplier is applied last, as a multiplier rather than an additive bonus, so a hospital or water-treatment lift station cannot be buried under a large residential outage no matter how the raw counts fall. And the SLA-urgency term rises smoothly toward the statutory deadline, so a critical customer with a hard notification window climbs the queue as the clock runs down rather than jumping only at the moment it expires.
Production Deployment Pattern
Promoting the score from a function to a dispatch control means enforcing determinism, provenance, and auditability around it.
- Score on every impact-set change. The dispatch loop calls
rank_outageswhenever a new outage is confirmed or an existing one is partially restored, passing a singlenowso the whole batch is scored against one clock and the ranking is reproducible. - Keep N and the weights in version control. The served-customer base and the scoring weights are configuration, not code constants to edit live. Deploy them through CI/CD so a change to the critical multiplier or the SLA weight is a reviewed, timestamped event, exactly the discipline any data ingestion pipeline for utility assets applies to its reference data.
- Persist the breakdown, not just the score. Log the
breakdowndict for every scored outage so an auditor can see how much of a ranking came from reliability, urgency, and criticality. The score alone is not evidence; the decomposition is. - Reconcile the live score against the reported index. After each event, recompute the realized SAIDI and SAIFI from the restoration timestamps and compare against what the live score predicted, so drift between the priority model and the reported metric is caught before a rate-case review, not during one.
- Hand the ranking to the router unchanged. Pass the ordered
ScoredOutagelist straight into crew dispatch and route optimization as the objective weights, so the score that explains the priority is the same number that drives the routing solver — no second, divergent ranking on the dispatch board.
Conclusion
A restoration-priority score built on marginal SAIDI impact turns the storm dispatch queue from a judgment call into a deterministic, auditable ranking. By accruing customer-minutes as an outage stays open, applying a critical-facility multiplier as a true multiplier, and letting SLA urgency rise toward statutory deadlines, the score surfaces the work that saves the most reportable reliability first while never burying a critical customer. Because it uses only the standard library and one shared clock, it is cheap enough to re-run on every impact-set change and simple enough to defend line by line in a reliability review. The next refinement is to calibrate the reliability and urgency weights against a season of realized SAIDI outcomes so the model’s ranking and the reported index converge.
Related
- Up to the parent topic: Crew Dispatch & Route Optimization
- Up to the section: Outage Routing & Impact Automation
- Solving Crew Routing with OR-Tools — the solver that consumes these priority weights.
- Impact Analysis & Affected-Customer Tracing — the customer counts and critical-facility flags this score reads.
- Automating Critical-Facility Outage Notifications — the statutory windows behind the SLA-urgency term.
For authoritative reference, consult the IEEE reliability-indices standards program and the Python datetime reference.