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, and logging — 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.

Composition of the deterministic restoration-priority score Three inputs feed the score. Customers-out and elapsed hours combine into a marginal SAIDI reliability term. An SLA deadline produces an urgency term that grows toward the deadline. These two terms are summed, then multiplied by a critical-facility multiplier to produce the final priority score that ranks the queue. The reliability term is drawn as the primary teal contributor, urgency as a copper contributor, and the critical multiplier as a moss gate. Customers out from impact set Hours already out duration proxy SLA deadline statutory window RELIABILITY TERM marginal SAIDI URGENCY TERM rises to deadline + sum × CRITICAL multiplier PRIORITY score

Production Deployment Pattern

Promoting the score from a function to a dispatch control means enforcing determinism, provenance, and auditability around it.

  1. Score on every impact-set change. The dispatch loop calls rank_outages whenever a new outage is confirmed or an existing one is partially restored, passing a single now so the whole batch is scored against one clock and the ranking is reproducible.
  2. 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.
  3. Persist the breakdown, not just the score. Log the breakdown dict 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.
  4. 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.
  5. Hand the ranking to the router unchanged. Pass the ordered ScoredOutage list 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.

For authoritative reference, consult the IEEE reliability-indices standards program and the Python datetime reference.