Mensile · dati aperti Rivista di ottimizzazione applicata RSS ↗
Caso N. 002·Organizzazione

Chi apre la biblioteca?

Quattro addetti sempre in servizio, dalle nove alle diciannove. Ma il pubblico non arriva mai tutto insieme — e i turni possono seguirlo.

La redazione 12 giugno 2026 3 min di lettura
Metodo
Programmazione lineare intera (ILP)
Strumenti
Python, PuLP
Fonte dati
Open data comunali — ingressi biblioteca · ISTAT — Aspetti della vita quotidiana
Risultato
−15% — ore-persona a parità di copertura

Le code al banco di una biblioteca civica non fanno notizia, ma sono un problema di ottimizzazione da manuale: la domanda varia durante il giorno, l’offerta no. La biblioteca di questo numero apre dalle 9 alle 19 con quattro addetti fissi, dall’apertura alla chiusura. Il contapersone all’ingresso — i cui dati il comune pubblica come open data — racconta una giornata molto meno uniforme.

01Il problema e i dati

Gli ingressi medi per fascia oraria disegnano la classica doppia gobba, con il pomeriggio che vale il doppio della mattina:

La giornata della biblioteca

Ingressi medi per fascia oraria (media dei giorni feriali)

0255075100349–116111–134513–157815–179617–19
Vedi i dati in tabella
Ingressi
9–1134
11–1361
13–1545
15–1778
17–1996

Open data comunali, contapersone all'ingresso (dati esemplificativi)

Vogliamo turni che garantiscano una copertura minima proporzionale agli ingressi — un addetto ogni venti presenze stimate, mai meno di due — al costo minimo di ore-persona, usando turni contrattuali ammissibili (6 oppure 8 ore, con ingresso a ogni ora piena).

02Il modello

È un problema di copertura di turni (set covering pesato): per ogni turno ammissibile t, la variabile intera yt conta quante persone lo fanno.

min   Σt ore(t) · yt
  t.c.   Σt ∋ f yt ≥ copertura(f)    per ogni fascia f
         yt ∈ ℕ

import pulp

m = pulp.LpProblem("turni_biblioteca", pulp.LpMinimize)
y = {t: pulp.LpVariable(f"turno_{t}", lowBound=0, cat="Integer")
     for t in turni_ammissibili}

m += pulp.lpSum(ore[t] * y[t] for t in turni_ammissibili)

for f in fasce:
    m += pulp.lpSum(y[t] for t in turni_ammissibili if copre(t, f)) >= copertura[f]

m.solve()

03I risultati

La soluzione ottima usa cinque persone su turni sfalsati invece di quattro su turno pieno — ma meno ore totali, perché nessuno presidia a vuoto le fasce tranquille:

Addetti in servizio per fascia

Prima (presidio fisso) e dopo (turni sfalsati sull'affluenza)

Turno fissoTurni ottimizzati
02469–11211–13313–15315–17417–195
Vedi i dati in tabella
Turno fissoTurni ottimizzati
9–1142
11–1343
13–1543
15–1744
17–1945

Copertura minima: un addetto ogni 20 ingressi stimati, mai meno di due.

Soluzione del modello ILP (caso esemplificativo)

Ore-persona a settimana240 → 204−36 ore
Risparmio−15%≈ 36.000 €/anno a costo pieno
Ora di punta (17–19)+1 addetto5 invece di 4

Il punto non è “tagliare”: è che il vecchio orario sotto-serviva l’ora di punta e sovra-serviva la mattina. Il modello sposta il lavoro dove c’è il pubblico; il risparmio è un sottoprodotto.

Prima di chiedere più risorse, vale la pena chiedersi se quelle attuali sono nel posto giusto. Spesso la risposta costa venti righe di codice.

04I limiti

I turni veri hanno vincoli che qui abbiamo semplificato: pause pranzo, part-time, preferenze del personale, picchi stagionali (sessioni d’esame!). Tutti si aggiungono al modello come righe in più, ed è esattamente ciò che rende l’approccio interessante: il negoziato sindacale può discutere di vincoli espliciti invece che di abitudini implicite.