den-dynamiske-rettsstaten/paper/diagnose.py
Kjell Tore Guttormsen 1723d593f8
feat(diagnose): first bottleneck diagnosis on public Norwegian figures
paper/norge_2025.json holds 2025 and October 2026 figures for police and
prosecution, district courts, appeal courts and prison, each with its
source (POD report 1/2026, Domstoladministrasjonen annual report 2025,
Kriminalomsorgen key figures October 2026 and annual report 2025, SSB).
Derived values are marked as estimates.

Result: police and prosecution bind (utilization 1.09 once dismissals
for lack of capacity are counted as untreated), with prison (1.06) and
district courts (1.04) also above full utilization; appeal courts are
below (0.88). README shows the table and its caveats.

diagnose.py accepts an acceptable backlog instead of a target time, and
cases closed without treatment, and lists other links above capacity.
The stylized example still reproduces section 9.5.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-10 13:37:12 +01:00

136 lines
7.2 KiB
Python

"""Flaskehalsdiagnose for straffesakskjeden fra egne tall (første versjon).
Bruk (fra paper/):
../.venv/bin/python diagnose.py eksempel_domstol.json
Inndata er en JSON-fil med ett objekt per ledd i kjeden, i rekkefølge. Alle
strømmer i saker per måned, beholdninger i saker, tider i måneder:
{"ledd": [
{"navn": "Domstol",
"innstromning": 134, # saker inn per måned nå
"kapasitet": 105, # saker ledd kan avgjøre per måned
"restanse": 669, # saker i kø nå
"maal_tid": 3, # mål for saksbehandlingstid (mnd)
"prognose_innstromning": 134, # valgfritt: strømmen når balansen er gjenopprettet
"maks_andel_utenom": 0.2, # valgfritt: andel som kan ledes til forenklede reaksjoner
"restanse_for_12_mnd": 400, # valgfritt: gir observert vekst i restansen
"restanse_maal": 300, # valgfritt: akseptabel restanse (saker) i stedet for maal_tid
"skjult_avgang": 0} # valgfritt: saker per måned som avsluttes uten behandling
# (f.eks. henlagt pga. kapasitet); trekkes fra kapasiteten
]}
Verktøyet følger trinn (c)-(e) i metoden (artikkelen, avsnitt 9.4-9.5):
(c) finn skranken som binder: høyest utnyttelse, deretter raskest voksende restanse;
(d) velg tiltakstype etter Theory of Constraints: utnytt, underordne, utvid;
(e) dimensjoner kapasiteten etter prognosen for strømmen, ikke etter køen, med
den koordinerte regelen fra sim.py (simulate_endo, policy="koord"):
Z_ny = (xi_min * F_prognose + Q_overskudd / T_avvikling) / ups_star.
Forløpet over tid (simulering med egne tall) er ikke med i denne versjonen.
Restansen over målet regnes som restanse - maal_tid * kapasitet (Littles lov).
sim.py trekker i tillegg fra minste behandlingstid m_D; eksempelfila er satt
slik at overskuddet blir det samme som i sim.py i måned 24.
Bruker ingenting fra sim.py og endrer ikke paper/data/.
"""
import json
import sys
UPS_STAR = 0.95 # målutnyttelse ved dimensjonering (ENDO["ups_star"] i sim.py)
UPS_HI = 1.0 # bestill når utnyttelsen er over denne (ENDO["ups_hi"])
T_CLEAR = 24.0 # restansen avvikles over denne tiden, mnd (ENDO["T_clear"])
L1, L2 = 6.0, 24.0 # bestilt kapasitet virker lineært fra L1 til L2 mnd etter vedtak
def tall(x, d=2):
return f"{x:,.{d}f}".replace(",", " ").replace(".", ",")
def analyser(ledd):
F = float(ledd["innstromning"])
Z = float(ledd["kapasitet"]) - float(ledd.get("skjult_avgang", 0.0))
Q = float(ledd["restanse"])
tid_maal = float(ledd["maal_tid"]) if "maal_tid" in ledd else None
Q_ref = float(ledd["restanse_maal"]) if "restanse_maal" in ledd else tid_maal * Z
ups = F / Z
# Littles lov: beholdning / avgangsrate; bare meningsfull når restansen er hele køen
tid = Q / Z if "restanse_maal" not in ledd else None
if "restanse_for_12_mnd" in ledd:
vekst = (Q - float(ledd["restanse_for_12_mnd"])) / 12.0
vekst_kilde = "observert siste 12 mnd"
else:
vekst = F - min(F, Z) # det som ikke avgjøres, legger seg i kø
vekst_kilde = "innstrømning minus kapasitet"
return dict(navn=ledd["navn"], F=F, Z=Z, Q=Q, Q_ref=Q_ref, tid_maal=tid_maal, ups=ups, tid=tid,
vekst=vekst, vekst_kilde=vekst_kilde,
Fp=float(ledd.get("prognose_innstromning", F)),
andel=float(ledd.get("maks_andel_utenom", 0.0)))
def tiltak(a):
"""Trinn (d) og (e) for det bindende leddet."""
ut = []
xi_min = 1.0 - a["andel"]
Q_exc = max(a["Q"] - a["Q_ref"], 0.0) # restanse over målet
ut.append("1. Utnytt leddet: fjern tapt tid i skranken (ikke regnet her).")
if a["andel"] > 0:
ups_u = xi_min * a["Fp"] / a["Z"]
ut.append(f"2. Led saker utenom: med {tall(a['andel'] * 100, 0)} % til forenklede reaksjoner "
f"blir utnyttelsen {tall(ups_u)} (fra {tall(a['Fp'] / a['Z'])} med prognosen).")
if ups_u <= UPS_HI and Q_exc == 0:
ut.append(" Det rekker: kapasiteten trenger ikke utvides.")
return ut
else:
ut.append("2. Led saker utenom: ikke oppgitt at noen andel kan ledes utenom dette leddet.")
behov = (xi_min * a["Fp"] + Q_exc / T_CLEAR) / UPS_STAR
if behov > UPS_HI * a["Z"]:
okning = behov / a["Z"] - 1.0
ut.append(f"3. Utvid: dimensjoner etter prognosen ({tall(a['Fp'], 1)} saker/mnd), ikke etter køen. "
f"Restanse over målet: {tall(Q_exc, 0)} saker, avviklet over {T_CLEAR:.0f} mnd.")
ut.append(f" Kapasitet: ({tall(xi_min)} x {tall(a['Fp'], 1)} + {tall(Q_exc, 0)}/{T_CLEAR:.0f}) / "
f"{tall(UPS_STAR)} = {tall(behov, 1)} saker/mnd, det vil si {tall(okning * 100, 0)} % mer.")
ut.append(f" Bestilt kapasitet virker gradvis fra {L1:.0f} til {L2:.0f} mnd etter vedtak."
+ (" Når restansen er avviklet, går andelen utenom tilbake, og behovet vurderes på nytt."
if a["andel"] > 0 else ""))
else:
ut.append("3. Utvid: ikke nødvendig med disse tallene.")
return ut
def main(fil):
with open(fil, encoding="utf-8") as f:
data = json.load(f)
rader = [analyser(l) for l in data["ledd"]]
print("Trinn (c): utnyttelse og restanse i hvert ledd\n")
print(f"{'Ledd':<22}{'Utnyttelse':>11}{'Saksbeh.tid':>13}{'Mål':>6}{'Restansevekst/mnd':>19}")
for a in rader:
maal = tall(a['tid_maal'], 1) if a['tid_maal'] is not None else "-"
tid = tall(a['tid'], 1) if a['tid'] is not None else "-"
print(f"{a['navn']:<22}{tall(a['ups']):>11}{tid:>13}{maal:>6}"
f"{tall(a['vekst'], 0):>19}")
# skranken: høyest utnyttelse, deretter raskest voksende restanse relativt til kapasiteten
b = max(rader, key=lambda a: (round(a["ups"], 2), a["vekst"] / a["Z"]))
print()
if b["ups"] <= UPS_HI and b["vekst"] <= 0:
print(f"Ingen ledd binder: høyeste utnyttelse er {tall(b['ups'])} ({b['navn']}), og ingen restanse vokser.")
return
print(f"Skranken som binder: {b['navn']} (utnyttelse {tall(b['ups'])}, restanse {tall(b['Q'], 0)} "
f"mot mål {tall(b['Q_ref'], 0)}; vekst: {b['vekst_kilde']}).")
andre = [a["navn"] for a in rader if a is not b and (a["ups"] > UPS_HI or a["vekst"] > 0)]
if andre:
print(f"Også over full utnyttelse eller med voksende restanse: {', '.join(andre)}. "
"Skranken flytter seg dit når den første er løst.")
nedstroms = [a["navn"] for a in rader[rader.index(b) + 1:]]
if nedstroms:
print(f"Merk: tallene for {', '.join(nedstroms)} viser bare det som slipper gjennom skranken. "
"Når den løses, øker innstrømningen dit.")
print("\nTrinn (d)-(e): tiltak etter Theory of Constraints, i denne rekkefølgen\n")
for linje in tiltak(b):
print(linje)
print("\nÅ bremse leddene foran skranken til dens takt er ikke et tiltak: i modellen gir det det dårligste utfallet.")
print("Kontroller så de rettsstatlige skrankene (trinn f): det er sakstyper, ikke prosentandeler, som flyttes.")
if __name__ == "__main__":
if len(sys.argv) != 2:
sys.exit("Bruk: python diagnose.py <inndata.json>")
main(sys.argv[1])