Dopo questa guida saprai costruire da zero un modello ad agenti in Python partendo da NetLogo: dalla struttura del codice alla gestione degli agenti, dal movimento su griglia al contagio, fino alla raccolta dei dati, alla curva SIR e alla GIF animata del mondo.
Un modello ad agenti (ABM, Agent-Based Model) è una simulazione in cui il comportamento globale emerge dalle interazioni locali di tante entità autonome dette agenti. Ogni agente ha uno stato interno, percepisce l’ambiente circostante e agisce secondo regole semplici. Da queste interazioni locali emergono pattern complessi a livello di sistema — pattern che non si potrebbero prevedere guardando solo le regole dei singoli agenti.
Ogni ABM è composto da quattro elementi fondamentali:
Agenti
Le entità attive della simulazione. Ogni agente ha uno stato (es. sano, infetto, guarito) e un insieme di comportamenti che esegue ad ogni passo.
Ambiente
Lo spazio in cui gli agenti esistono e si muovono. Può essere una griglia discreta (celle intere), uno spazio continuo 2D, una rete di relazioni, o anche solo un insieme senza geometria.
Regole
Cosa fa ogni agente ad ogni tick: come si muove, quando interagisce con altri, come cambia stato. Le regole sono locali — ogni agente vede solo ciò che è vicino a lui.
Emergenza
Il pattern globale che nasce dalle interazioni locali. Non è scritto da nessuna parte: emerge dalla simulazione. È ciò che si vuole studiare o misurare.
Cosa si modella prima: gli agenti o l’ambiente?
La risposta è che si parte sempre dal fenomeno che si vuole studiare. Il processo di design segue questo ordine:
Cosa vuoi osservare emergere?
Chi agisce? Che stato ha?
In che spazio si muovono?
Movimento, interazione, aggiornamento
NetLogo è l’ambiente più usato per costruire modelli ad agenti in ambito didattico e di ricerca. Ha un linguaggio semplice e un’interfaccia grafica integrata. Python è il linguaggio di riferimento per la ricerca computazionale moderna: è molto più versatile, si integra con librerie di analisi dati e machine learning, ed è quello che si trova nella maggior parte dei progetti di ricerca.
Questa guida mostra come costruire un modello ad agenti in Python partendo da NetLogo usando come esempio il modello SIR discreto. Ogni concetto NetLogo viene affiancato al suo equivalente Python, spiegando non solo come si scrive ma perché.
Il modello SIR è il modello epidemiologico più classico. Ogni agente è una persona che si trova in uno di tre stati: Suscettibile (può ammalarsi), Infetto (può contagiare), Rimosso (guarito e immune). Gli agenti si muovono su una griglia discreta; quando un infetto è nella stessa cella o in una cella adiacente a un suscettibile, lo contagia con probabilità β. Ad ogni tick, ogni infetto guarisce con probabilità γ.
Applicando il processo di design visto sopra:
| Domanda | Risposta nel modello SIR discreto | |
|---|---|---|
| Fenomeno | Come si diffonde un’epidemia? Emerge una curva a campana per i casi attivi? | |
| Agenti | Persone con stato S/I/R e posizione intera (cella) sulla griglia | |
| Ambiente | Griglia discreta di celle intere, mondo da −16 a +16 su entrambi gli assi, bordi duri (no wrap) | |
| Regole | Movimento di 1 cella in direzione casuale (N/S/E/W); I contagia S a distanza di Chebyshev ≤ 1 con probabilità β; ogni tick I guarisce con probabilità γ | |
| Emergenza | Curva epidemica: S scende, I sale poi scende, R sale. L’epidemia si esaurisce da sola. | |
Regole di transizione degli stati:
| Transizione | Condizione | Nota |
|---|---|---|
| S → I | un agente I è a distanza di Chebyshev ≤ 1 e random() < β | Probabilistica — non sempre avviene |
| I → R | random() < γ ad ogni tick | Probabilistica — ogni tick c’è una chance di guarire |
| R → R | — | I rimossi sono immuni, non cambiano più stato |
In NetLogo tutto il codice sta in un unico file .nlogo. In Python costruiamo il modello in un unico file SIR_model_discreto.py. La struttura è identica: setup, ciclo principale, procedure. Il SIR discreto prevede tre procedure per tick: muovi-agenti, contagio e guarigione.
to setup
clear-all
reset-ticks
create-turtles population [...]
ask n-of initial-infected turtles [
set state "I" ]
end
to go
if days-passed >= days [ stop ]
move
infect
recover
set days-passed days-passed + 1
tick
end
to move ... end
to infect ... end
to recover ... enddef crea_agenti(cfg):
# equivale a crea-agenti
...
def muovi_agenti(agents, cfg): ...
def contagio(agents, cfg): ...
def guarigione(agents, cfg): ...
def run_simulation(cfg):
# equivale a go
agents = crea_agenti(cfg)
for day in range(cfg.days):
muovi_agenti(agents, cfg)
contagio(agents, cfg)
guarigione(agents, cfg)
return history, snapshots
if __name__ == "__main__":
cfg = config()
history, snapshots = run_simulation(cfg)I parametri del modello — da slider a config:
In NetLogo i parametri si impostano tramite slider nell’interfaccia grafica. In Python li raccogliamo in una @dataclass chiamata config. Questo blocco è il primo che scrivi: definisce tutto ciò che è configurabile nel modello.
population 100 ; slider (1–100) ; mondo: -16 a +16 (world settings) initial-infected 50 ; inputbox — N agenti I beta 1.0 ; slider — probabilità contagio gamma 0.19 ; slider — probabilità guarigione days 100 ; inputbox — durata simulazione
@dataclass
class config:
population: int = 100
grid_size: int = 16
initial_infected: int = 10 # resto parte S
beta: float = 0.9
gamma: float = 0.05
days: int = 100
def __post_init__(self):
if self.initial_infected > self.population:
raise ValueError("infected > population")turtles-own a @dataclassIn NetLogo ogni agente è una turtle con attributi propri dichiarati in turtles-own. In Python costruiamo un agente come una dataclass. L’agente SIR discreto ha tre attributi: lo stato epidemico e la posizione sulla griglia (pos_x, pos_y), che sono interi perché rappresentano celle, non punti continui. Non c’è velocità (il passo è sempre 1 cella) né timer (la guarigione è probabilistica).
; attributi automatici di ogni turtle: ; xcor, ycor (interi su patch) ; + quelli che dichiariamo: turtles-own [ state ; "S", "I" o "R" ] ; xcor e ycor sono già interi ; (coordinate di patch)
@dataclass
class agent:
state: str = 'S' # S, I o R
pos_x: int = 0 # ← xcor (cella, non float)
pos_y: int = 0 # ← ycor (cella, non float)
# pos_x e pos_y partono da 0
# come default neutro — vengono
# assegnati davvero in crea_agentiCreare tutti gli agenti:
to crea-agenti
create-turtles population [
setxy random-pxcor random-pycor
set state "S"
set color blue
]
ask n-of initial-infected turtles [
set state "I"
set color red
]
enddef crea_agenti(cfg):
agents = []
for i in range(cfg.population):
a = agent() # agente vuoto con default
a.pos_x = np.random.randint(
-cfg.grid_size, cfg.grid_size + 1)
a.pos_y = np.random.randint(
-cfg.grid_size, cfg.grid_size + 1)
if i < cfg.initial_infected:
a.state = 'I'
else:
a.state = 'S' # tutti gli altri partono S
agents.append(a)
return agents@dataclass genera automaticamente il metodo __init__, così non devi scrivere self.state = state ecc. per ogni attributo. I default (state = 'S', pos_x = 0) servono come placeholder — i valori reali vengono assegnati subito dopo in crea_agenti.
Il modello usa una griglia discreta: ogni agente occupa una cella intera identificata da due coordinate intere (pos_x, pos_y). Il mondo va da −16 a +16 su entrambi gli assi, con bordi duri: un agente che cerca di uscire dalla griglia rimane fermo al bordo (non c’è wrap-around).
Il movimento è di 1 cella per tick in una delle quattro direzioni cardinali (N, S, E, W). Non serve trigonometria: le direzioni sono predefinite come coppie (dx, dy).
| NetLogo | Python | Significato |
|---|---|---|
| xcor, ycor (int su patch) | pos_x: int, pos_y: int | Coordinate intere della cella |
| random-pxcor | np.random.randint(-16, 17) | Posizione casuale intera |
| rt one-of [0 90 180 270] | DIRECTIONS[np.random.randint(0, 4)] | Direzione casuale tra 4 |
| fd 1 | pos_x += dx; pos_y += dy | Spostamento di 1 cella |
| if patch-ahead 1 != nobody | np.clip(valore, -16, 16) | Bordi duri — nessun wrap |
La costante DIRECTIONS:
Le quattro direzioni cardinali sono rappresentate come una lista di tuple (dx, dy). È in MAIUSCOLO perché è una costante: il suo valore non cambia mai durante la simulazione.
to move
ask turtles [
let new-x xcor
let new-y ycor
let dir random 4
if dir = 0 [ set new-y ycor + 1 ] ; Nord
if dir = 1 [ set new-y ycor - 1 ] ; Sud
if dir = 2 [ set new-x xcor + 1 ] ; Est
if dir = 3 [ set new-x xcor - 1 ] ; Ovest
; clamp ai bordi del mondo
if new-x > max-pxcor [ set new-x max-pxcor ]
if new-x < min-pxcor [ set new-x min-pxcor ]
if new-y > max-pycor [ set new-y max-pycor ]
if new-y < min-pycor [ set new-y min-pycor ]
setxy new-x new-y
]
end# N, S, E, W — costante, non cambia mai DIRECTIONS = [(0, 1), (0, -1), (1, 0), (-1, 0)] def muovi_agenti(agents, cfg): for a in agents: # pesca una tupla casuale e spacchetta dx, dy = DIRECTIONS[np.random.randint(0, 4)] # sposta e blocca al bordo con np.clip a.pos_x = np.clip( a.pos_x + dx, -cfg.grid_size, cfg.grid_size) a.pos_y = np.clip( a.pos_y + dy, -cfg.grid_size, cfg.grid_size)
Distanza di Chebyshev — naturale per le griglie
d = max(|dx|, |dy|) — il massimo tra la differenza assoluta sulle x e quella sulle y.
continueQuesta è la regola di interazione del SIR. Per ogni coppia di agenti vicini (uno I e uno S), si applica la probabilità β di contagio. La prossimità si misura con la distanza di Chebyshev: d = max(|dx|, |dy|). Se d ≤ 1, i due agenti sono nella stessa cella o in celle adiacenti.
to infect
ask turtles with [state = "I"] [
let x1 xcor
let y1 ycor
; distanza di Chebyshev esplicita
ask other turtles with [state = "S"] [
let d max list
(abs (xcor - x1))
(abs (ycor - y1))
if d <= 1 [
if random-float 1 < beta [
set state "I"
set color red
]
]
]
]
enddef contagio(agents, cfg):
for i in range(len(agents)):
for j in range(i + 1, len(agents)):
a, b = agents[i], agents[j]
d = max(abs(a.pos_x - b.pos_x),
abs(a.pos_y - b.pos_y)) # Chebyshev
if d > 1:
continue # troppo lontani, prossima coppia
if a.state == 'I' and b.state == 'S':
if np.random.rand() < cfg.beta:
b.state = 'I'
elif a.state == 'S' and b.state == 'I':
if np.random.rand() < cfg.beta:
a.state = 'I'| NetLogo | Python | Nota |
|---|---|---|
| ask other turtles with [state="S"] + Chebyshev esplicito | for i, for j con Chebyshev ≤ 1 | Vicinato di Moore (8 celle + sé) |
| with [state = "S"] | a.state == 'I' and b.state == 'S' | Filtra per stato |
| random-float 1 < beta | np.random.rand() < cfg.beta | Evento probabilistico in [0,1) |
| set state "I" | b.state = 'I' | Modifica diretta sul campo |
Questa procedura gestisce il cambiamento I → R. È completamente indipendente dalla posizione: ogni tick, ogni agente infetto ha una probabilità γ di guarire. Non serve un timer — la durata dell’infezione emerge statisticamente da γ: un γ basso significa guarigioni rare e infezione lunga, un γ alto guarigioni rapide.
to recover
ask turtles with [state = "I"] [
; probabilistico: random-float 1.0
if random-float 1.0 < gamma [
set state "R"
set color green
]
]
enddef guarigione(agents, cfg):
for a in agents:
if a.state == 'I': # solo gli infetti
if np.random.rand() < cfg.gamma: # tira il dado
a.state = 'R' # guariscetimer nell’agente.
to go al loop PythonIn NetLogo to go è la procedura chiamata ad ogni tick. In Python è un for loop che chiama le tre procedure nell’ordine corretto. Il ciclo raccoglie anche due tipi di dati: i conteggi S/I/R per il grafico e gli snapshot di posizioni e stati per la GIF animata.
to go if days-passed >= days [ stop ] move ; 1. movimento infect ; 2. contagio recover ; 3. guarigione set days-passed days-passed + 1 tick ; il plot si aggiorna automaticamente end
def run_simulation(cfg):
agents = crea_agenti(cfg)
history = {'S': [], 'I': [], 'R': []}
snapshots = [] # lista di liste di tuple
for day in range(cfg.days):
muovi_agenti(agents, cfg) # 1. movimento
contagio(agents, cfg) # 2. contagio
guarigione(agents, cfg) # 3. guarigione
s, i, r = conta_stati(agents) # conta S/I/R
history['S'].append(s)
history['I'].append(i)
history['R'].append(r)
snap = [(a.pos_x, a.pos_y, a.state)
for a in agents] # snapshot del giorno
snapshots.append(snap)
return history, snapshots| NetLogo | Python | Nota |
|---|---|---|
| move | muovi_agenti(agents, cfg) | Sposta tutti gli agenti |
| infect | contagio(agents, cfg) | Controlla tutte le coppie |
| recover | guarigione(agents, cfg) | Aggiorna stati I→R |
| plot automatico | history['S'].append(s) | Raccolta manuale dei dati |
| vista automatica | snapshots.append(snap) | Raccolta posizioni per la GIF |
| tick | indice del for loop (day) | Contatore automatico |
return history, snapshots e non solo history
La funzione restituisce due valori insieme come tupla. Chi chiama la funzione li riceve separati: history, snapshots = run_simulation(cfg). Senza return entrambi andrebbero persi — esistono solo dentro la funzione.
In NetLogo il plot delle popolazioni S/I/R si configura nell’interfaccia grafica e si aggiorna automaticamente. In Python raccogliamo i dati nel dizionario history durante la simulazione e poi li plottiamo con Matplotlib. Il risultato atteso è la classica curva epidemica: S decresce, I sale poi scende a campana, R cresce.
; configurato nell'interfaccia: ; plot count turtles with [state="S"] ; plot count turtles with [state="I"] ; plot count turtles with [state="R"] ; → si aggiorna ad ogni tick
def plot_risultati(history):
giorni = range(len(history['S']))
plt.figure(figsize=(9, 5))
plt.plot(giorni, history['S'],
label='Susceptible', color='steelblue')
plt.plot(giorni, history['I'],
label='Infected', color='crimson')
plt.plot(giorni, history['R'],
label='Recovered', color='seagreen')
plt.xlabel('Giorni')
plt.ylabel('Numero di agenti')
plt.title('Modello SIR — spazio discreto')
plt.legend()
plt.tight_layout()
plt.show()beta allarga e anticipa il picco della curva I. Diminuire gamma allunga il periodo infettivo e amplifica l’epidemia. Ridurre grid_size aumenta la densità degli agenti e accelera il contagio perché gli agenti si incontrano più spesso. Questi sono esattamente i parametri che gli epidemiologi variano negli studi di intervento.
In NetLogo la vista grafica si aggiorna automaticamente ad ogni tick: le turtle si muovono e cambiano colore in tempo reale. In Python questa visualizzazione non esiste di default — dobbiamo costruirla con matplotlib.animation.FuncAnimation, che crea un’animazione frame per frame e la salva come GIF.
; NetLogo aggiorna la vista ; automaticamente ad ogni tick. ; Per colorare gli agenti per stato: ask turtles [ if state = "S" [ set color blue ] if state = "I" [ set color red ] if state = "R" [ set color green ] ] ; La griglia (patch) è già visibile. ; Nessun codice aggiuntivo.
STATE_COLORS = {
'S': 'steelblue', # ← color blue
'I': 'crimson', # ← color red
'R': 'seagreen' # ← color green
}
def crea_animazione(snapshots, cfg,
filename='SIR_animazione.gif'):
fig, ax = plt.subplots(figsize=(6, 6))
# griglia: tick a ±0.5 → agenti al centro della cella
ticks = [x - 0.5 for x in
range(-cfg.grid_size, cfg.grid_size + 2)]
ax.set_xticks(ticks); ax.set_yticks(ticks)
ax.tick_params(labelbottom=False, labelleft=False)
ax.grid(True, color='lightgray', linewidth=0.5)
ax.set_aspect('equal')
scatter = ax.scatter([], [], s=50)
day_text = ax.text(0.02, 0.95, '',
transform=ax.transAxes)
legend_patches = [Patch(color=c, label=s)
for s, c in STATE_COLORS.items()]
ax.legend(handles=legend_patches, loc='upper right')
def update(frame):
snap = snapshots[frame]
xs = [p[0] for p in snap]
ys = [p[1] for p in snap]
colors = [STATE_COLORS[p[2]] for p in snap]
scatter.set_offsets(list(zip(xs, ys)))
scatter.set_color(colors)
day_text.set_text(f'Giorno {frame}')
return scatter, day_text
anim = animation.FuncAnimation(
fig, update, frames=len(snapshots),
interval=100, blit=True)
anim.save(filename, writer='pillow', fps=10)
plt.close()La struttura di snapshots:
snapshots è una lista di liste di tuple: per ogni giorno, una lista con una tupla per agente. Ogni tupla contiene tre valori: (pos_x, pos_y, state).
Tutto insieme in un unico file SIR_model_discreto.py. Copia, esegui con python SIR_model_discreto.py e ottieni la curva epidemica e la GIF animata.
pip install numpy matplotlib pillow, poi esegui con python SIR_model_discreto.py. Ottieni la curva S/I/R e la GIF SIR_animazione.gif nella stessa cartella.
Hai costruito un modello ad agenti completo in Python partendo da zero. Il passo successivo è sperimentare: cambia i parametri, aggiungi regole, osserva cosa cambia nella curva e nella GIF.
Esperimenti suggeriti
Varia beta (0.1 → 0.9) e osserva come cambia il picco. Riduci grid_size a 8: più densità, più contagio. Aumenta gamma e nota come la curva I si abbassa. Questi sono gli stessi esperimenti che si fanno su NetLogo con gli slider.
Estensioni possibili
Aggiungi uno stato E (esposto, SEIR). Introduci la vaccinazione: una frazione di S inizia come R. Aggiungi mortalità: alcuni I escono dalla simulazione. Ogni estensione aggiunge un campo alla dataclass e una funzione al ciclo.
Continuo vs discreto
Il passo successivo naturale è la versione continua: posizioni float, movimento trigonometrico (cos/sin), distanza euclidea, bordi toroidali. La struttura del codice è identica — cambiano solo la dataclass agente (aggiunge velocity) e le funzioni di movimento e contagio.
Passo successivo: Mesa
Mesa è il framework Python standard per ABM. Ora che conosci la struttura sottostante (agenti, ciclo, procedure), passare a Mesa significa guadagnare visualizzazione integrata e strumenti di batch-run, senza cambiare il modo di pensare.
Ora che hai visto il SIR pezzo per pezzo, hai tutti gli strumenti per costruire qualsiasi modello ad agenti in Python. Questa sezione distilla il processo in una ricetta riutilizzabile: come si decide cosa va in ogni blocco del codice, in che ordine scrivere le cose, e come capire quali librerie e parametri servono.
Il template che si ripete sempre
Ogni ABM in Python ha la stessa struttura di file, indipendentemente dal fenomeno che si modella:
importlibrerie
configparametri
agentdataclass
regole
run()ciclo
visualizza()grafici + GIF
__main__punto di ingresso
agent e config, run() usa le funzioni, __main__ usa tutto. Scrivere in ordine significa che il codice è sempre eseguibile fino al punto in cui sei arrivato.
Dove mettere cosa — blocco per blocco
Come decidere gli attributi dell’agente
La @dataclass agent contiene tutto ciò che l’agente deve ricordare tra un tick e l’altro. Per decidere quali campi mettere, fai queste domande:
| Domanda | Se sì → aggiungi | Esempio SIR discreto |
|---|---|---|
| L’agente ha uno stato discreto che cambia nel tempo? | un campo state: str |
state = 'S' / 'I' / 'R' |
| L’agente si muove su una griglia discreta? | pos_x: int, pos_y: int |
Celle intere, no float, no angolo |
| L’agente si muove in uno spazio continuo? | pos_x: float, pos_y: float, velocity: float |
Solo nel modello continuo |
| Una transizione avviene dopo N tick (deterministica)? | un timer: int = 0 |
Non presente nel SIR discreto (guarigione probabilistica) |
| L’agente ha una quantità che varia (energia, età)? | un campo numerico, es. energy: float = 100.0 |
Non presente nel SIR base |
config. Non mettere la lista degli altri agenti — viene passata come argomento alle funzioni. La dataclass agent contiene solo lo stato individuale dell’agente.
Come capire quali librerie importare
| Libreria | Quando importarla | Funzioni tipicamente usate |
|---|---|---|
numpy |
Se usi numeri casuali o operazioni su array | np.random.randint(), np.random.rand(), np.clip() |
matplotlib.pyplot |
Per la curva S/I/R nel tempo | plt.plot(), plt.legend(), plt.show() |
matplotlib.animation |
Per la GIF animata del mondo | animation.FuncAnimation(), anim.save() |
matplotlib.patches |
Per la legenda colorata nell’animazione | Patch(color=..., label=...) |
dataclasses |
Sempre — serve per @dataclass |
from dataclasses import dataclass |
math |
Solo nel modello continuo (trigonometria) | math.sin, math.cos, math.pi, math.hypot |
Lo scheletro di partenza — copia e adatta
Questo è il template minimo da cui partire per qualsiasi nuovo modello discreto. Le parti in grigio sono sempre identiche; le parti in verde sono quelle che tu devi riempire:
config). Le funzioni muovi_agenti e crea_animazione sono quasi sempre identiche tra modelli — puoi riusarle direttamente. Se una funzione non riesci a scriverla subito, metti pass come corpo e completala dopo.