Matrici in Python: Dal Parcheggio alla Computer Vision con NumPy

Cerca:

Generic selectors
Exact matches only
Search in title
Search in content
Post Type Selectors
Guida pratica alle matrici in Python

Tutto è una tabella, anche se non sembra.

Quando guardi la tua libreria di Spotify, vedi copertine e titoli. Il computer vede solo righe e colonne. Quando scatti una foto al tramonto, tu vedi sfumature arancioni, il processore vede una griglia di numeri che indicano l’intensità del rosso, del verde e del blu.

La verità è che finché tratti i dati come singole variabili o liste semplici, resti in superficie. Ti limiti a grattare la crosta della programmazione. Il vero salto di qualità – quello che distingue chi scrive script per hobby da chi costruisce motori di raccomandazione o sistemi di visione artificiale – avviene quando il tuo cervello smette di vedere “oggetti” e inizia a ragionare per matrici.

Non è solo matematica. È il modo in cui le macchine interpretano la realtà. Se impari a manipolare queste griglie, smetti di dare ordini al computer riga per riga e inizi a fargli processare interi blocchi di mondo in un colpo solo.

Oggi facciamo esattamente questo: prendiamo due problemi classici e li smontiamo, prima con la logica artigianale e poi con gli strumenti pesanti usati nell’industria.

Pubblicità

Esercizio 1: Gestione di un Parcheggio Multilivello (Livello Intermedio)

Testo: Un parcheggio ha 3 livelli, ognuno con 20 posti auto. I posti possono essere liberi (0), occupati da auto (1), o occupati da furgoni (2). Simula la situazione con una matrice 3×20. Scrivi funzioni per:

  1. Inizializzare il parcheggio con posti casuali

  2. Trovare il livello con più posti liberi

  3. Contare quanti furgoni ci sono in totale

  4. Data una riga di posti (livello), dire se ci sono 3 posti liberi consecutivi (per parcheggiare un camper)

Soluzione

import random

# Costanti per leggibilità
LIBERO = 0
AUTO = 1
FURGONE = 2
# 1. Inizializzazione parcheggio
def inizializza_parcheggio(livelli=3, posti=20):
    """
    Crea una matrice che rappresenta il parcheggio.
    0 = libero, 1 = auto, 2 = furgone
    """
    parcheggio = []
    for livello in range(livelli):
        posti_livello = []
        for posto in range(posti):
            # Probabilità: 40% libero, 50% auto, 10% furgone
            rand = random.random()
            if rand < 0.4:
                posti_livello.append(LIBERO)
            elif rand < 0.9:
                posti_livello.append(AUTO)
            else:
                posti_livello.append(FURGONE)
        parcheggio.append(posti_livello)
    return parcheggio
# 2. Livello con più posti liberi
def livello_piu_libero(parcheggio):
    """
    Trova il livello con il maggior numero di posti liberi.
    """
    max_liberi = -1
    livello_idx = -1
    
    for i, livello in enumerate(parcheggio):
        liberi = livello.count(LIBERO)
        if liberi > max_liberi:
            max_liberi = liberi
            livello_idx = i
    
    return livello_idx, max_liberi
# 3. Conta furgoni totali
def conta_furgoni(parcheggio):
    """
    Conta il numero totale di furgoni nel parcheggio.
    """
    totale = 0
    for livello in parcheggio:
        totale += livello.count(FURGONE)
    return totale
# 4. Verifica 3 posti liberi consecutivi
def trova_posti_consecutivi(livello, consecutivi=3):
    """
    Cerca 'consecutivi' posti liberi consecutivi in un livello.
    Restituisce l'indice del primo posto della sequenza, o -1 se non trovato.
    """
    for i in range(len(livello) - consecutivi + 1):
        # Controlla se i prossimi 'consecutivi' posti sono tutti liberi
        tutti_liberi = True
        for j in range(consecutivi):
            if livello[i + j] != LIBERO:
                tutti_liberi = False
                break
        
        if tutti_liberi:
            return i  # Trovato! Restituisce l'indice di inizio
    
    return -1  # Non trovato
# Funzione di visualizzazione
def visualizza_parcheggio(parcheggio):
    """
    Visualizza il parcheggio in modo leggibile.
    """
    simboli = {LIBERO: "🟩", AUTO: "🚗", FURGONE: "🚚"}
    
    print("PARCHEGGIO MULTILIVELLO")
    print("🟩 = libero, 🚗 = auto, 🚚 = furgone")
    print("-" * 60)
    
    for i, livello in enumerate(parcheggio):
        print(f"Livello {i}: ", end="")
        for posto in livello:
            print(simboli[posto], end=" ")
        print()

# ESECUZIONE PRINCIPALE
print("=== GESTIONE PARCHEGGIO MULTILIVELLO ===\n")

# Inizializza
parcheggio = inizializza_parcheggio()
visualizza_parcheggio(parcheggio)

# Analisi
livello, liberi = livello_piu_libero(parcheggio)
print(f"\n1. Livello con più posti liberi: livello {livello} con {liberi} posti liberi")

furgoni_totali = conta_furgoni(parcheggio)
print(f"2. Furgoni totali nel parcheggio: {furgoni_totali}")

print("\n3. Ricerca 3 posti liberi consecutivi per camper:")
for i, livello in enumerate(parcheggio):
    inizio = trova_posti_consecutivi(livello, 3)
    if inizio != -1:
        print(f"  Livello {i}: TROVATI ai posti {inizio}, {inizio+1}, {inizio+2}")
    else:
        print(f"  Livello {i}: NON trovati 3 posti liberi consecutivi")
# Statistiche aggiuntive
print("\n4. Statistiche dettagliate:")
for i, livello in enumerate(parcheggio):
    liberi = livello.count(LIBERO)
    auto = livello.count(AUTO)
    furgoni = livello.count(FURGONE)
    print(f"  Livello {i}: {liberi} liberi, {auto} auto, {furgoni} furgoni")

Statistiche Risultanti

Al termine dell’esecuzione, l’algoritmo produce un report dettagliato per ogni livello:

  • Livello con massima disponibilità: calcolato tramite la funzione [math]livello\_piu\_libero[/math].
  • Densità veicoli pesanti: totale dei furgoni presenti nell’intera struttura.
  • Idoneità Camper: verifica della presenza di una sottosequenza di zeri [math](0, 0, 0)[/math] all’interno della matrice.
Forse potrebbe interessarti anche:  Coefficiente Binomiale: Definizione, Formula, Applicazioni Pratiche ed Esercizi Risolti

📌 Nota: Questo script è un ottimo esercizio per comprendere la manipolazione delle liste annidate in Python e l’applicazione di algoritmi di ricerca su griglie.

=== GESTIONE PARCHEGGIO MULTILIVELLO ===

PARCHEGGIO MULTILIVELLO
🟩 = libero, 🚗 = auto, 🚚 = furgone
------------------------------------------------------------
Livello 0: 🟩 🟩 🚗 🚗 🚗 🟩 🚚 🟩 🚚 🟩 🚚 🟩 🟩 🟩 🚗 🚗 🟩 🚗 🚚 🚗 
Livello 1: 🚗 🟩 🟩 🟩 🟩 🚗 🚗 🟩 🚗 🚚 🟩 🚗 🚗 🟩 🚗 🚗 🟩 🚚 🚗 🟩 
Livello 2: 🚗 🟩 🟩 🚗 🚗 🚚 🚗 🟩 🚗 🚗 🟩 🟩 🟩 🚗 🟩 🚚 🟩 🚗 🚗 🚚 

1. Livello con più posti liberi: livello 0 con 9 posti liberi
2. Furgoni totali nel parcheggio: 9

3. Ricerca 3 posti liberi consecutivi per camper:
  Livello 0: TROVATI ai posti 11, 12, 13
  Livello 1: TROVATI ai posti 1, 2, 3
  Livello 2: TROVATI ai posti 10, 11, 12

4. Statistiche dettagliate:
  Livello 0: 9 liberi, 7 auto, 4 furgoni
  Livello 1: 9 liberi, 9 auto, 2 furgoni
  Livello 2: 8 liberi, 9 auto, 3 furgoni

Spiegazione:

  1. Rappresentazione stati diversi: Usiamo costanti numeriche per rappresentare stati diversi (0, 1, 2) per maggiore leggibilità.

  2. Conteggio elementi: Il metodo .count(valore) di Python conta le occorrenze di un valore in una lista.

  3. Ricerca pattern: Cercare posti consecutivi liberi significa cercare una sequenza di zeri di lunghezza specificata.

  4. Scansione sequenziale: Scorriamo il livello e per ogni posizione controlliamo le n posizioni successive.

💡 Osservazione:

L’algoritmo di ricerca di n posti consecutivi liberi ha complessità O(m×n) dove m è la lunghezza della lista. Potremmo ottimizzarlo usando una finestra scorrevole che mantiene il conteggio dei posti liberi nella finestra corrente.

Il codice che abbiamo scritto funziona per un parcheggio di 60 posti. Ma se i posti fossero 600.000? Qui entra in gioco la necessità di librerie come NumPy. In produzione, non useremmo liste di liste, ma array multidimensionali ottimizzati per la velocità della CPU.

Domanda di riflessione: Come potresti modificare trova_posti_consecutivi per trovare il PRIMO insieme di posti liberi consecutivi, piuttosto che fermarti al primo trovato?

Esercizio 2: Gioco del Campo Minato (Livello Intermedio-Avanzato)

Testo: Implementa una versione semplificata del campo minato. Data una matrice 6×6 che rappresenta un campo, dove 0 = sicuro e 1 = mina:

  1. Genera un campo con il 20% di mine

  2. Crea una seconda matrice che per ogni cella calcoli quante mine ci sono nelle 8 celle adiacenti

  3. Implementa una funzione per rivelare una cella: se è una mina, game over; altrimenti mostra il numero di mine adiacenti

Soluzione

import random

# 1. Generazione campo minato
def genera_campo(righe=6, colonne=6, probabilita_mina=0.2):
    """
    Crea una matrice campo minato con probabilità data di mine.
    """
    campo = []
    for i in range(righe):
        riga = []
        for j in range(colonne):
            if random.random() < probabilita_mina:
                riga.append(1)  # Mina
            else:
                riga.append(0)  # Sicuro
        campo.append(riga)
    return campo

# 2. Calcolo mine adiacenti
def calcola_adiacenti(campo):
    """
    Crea una matrice con il conteggio delle mine nelle 8 celle adiacenti.
    """
    righe = len(campo)
    colonne = len(campo[0])
    adiacenti = []
    
    for i in range(righe):
        riga_adiacenti = []
        for j in range(colonne):
            # Se la cella è una mina, metti -1 (per indicare mina)
            if campo[i][j] == 1:
                riga_adiacenti.append(-1)
            else:
                # Conta mine nelle 8 celle adiacenti
                conteggio = 0
                
                # Itera sulle 8 direzioni: (-1,-1), (-1,0), (-1,1), ...
                for di in [-1, 0, 1]:
                    for dj in [-1, 0, 1]:
                        # Salta la cella stessa
                        if di == 0 and dj == 0:
                            continue
                        
                        ni, nj = i + di, j + dj
                        
                        # Controlla se la cella adiacente è dentro i bordi
                        if 0 <= ni < righe and 0 <= nj < colonne:
                            if campo[ni][nj] == 1:
                                conteggio += 1
                
                riga_adiacenti.append(conteggio)
        adiacenti.append(riga_adiacenti)
    
    return adiacenti

# 3. Funzione per rivelare una cella
def rivelacella(campo, adiacenti, i, j):
    """
    Rivela una cella del campo minato.
    Restituisce: True se è una mina (game over), False altrimenti.
    """
    if campo[i][j] == 1:
        return True  # Mina! Game over
    else:
        return False  # Sicuro

# 4. Visualizzazione
def visualizza_gioco(campo, adiacenti, rivelate):
    """
    Visualizza lo stato corrente del gioco.
    """
    righe = len(campo)
    colonne = len(campo[0])
    
    print("   " + " ".join(str(j) for j in range(colonne)))
    print("  +" + "-" * (2*colonne))
    
    for i in range(righe):
        print(f"{i} |", end=" ")
        for j in range(colonne):
            if (i, j) in rivelate:
                if campo[i][j] == 1:
                    print("💣", end=" ")
                else:
                    num = adiacenti[i][j]
                    if num == 0:
                        print("·", end=" ")
                    else:
                        print(num, end=" ")
            else:
                print("■", end=" ")
        print()

# ESECUZIONE PRINCIPALE
print("=== CAMPO MINATO ===\n")

# Genera campo
campo = genera_campo(6, 6, 0.2)
adiacenti = calcola_adiacenti(campo)
rivelate = set()  # Insieme di celle già rivelate
game_over = False

print("Campo iniziale (■ = nascosto):")
visualizza_gioco(campo, adiacenti, rivelate)

# Simulazione di gioco
print("\n--- SIMULAZIONE DI GIOCO ---")

# Mosse di esempio
mosse = [(0, 0), (2, 2), (1, 1), (3, 3)]

for mossa_idx, (i, j) in enumerate(mosse):
    if game_over:
        break
    
    print(f"\nMossa {mossa_idx+1}: rivela cella ({i}, {j})")
    
    if rivelacella(campo, adiacenti, i, j):
        print("BOOM! Hai trovato una mina! Game Over!")
        game_over = True
    else:
        print(f"Sicuro! Ci sono {adiacenti[i][j]} mine adiacenti.")
        rivelate.add((i, j))
    
    visualizza_gioco(campo, adiacenti, rivelate)

# Mostra il campo completo alla fine
if game_over or mossa_idx == len(mosse) - 1:
    print("\n--- CAMPO COMPLETO (tutte le mine rivelate) ---")
    tutte_rivelate = [(i, j) for i in range(6) for j in range(6)]
    visualizza_gioco(campo, adiacenti, tutte_rivelate)

# Statistiche
print("\n--- STATISTICHE ---")
mine_totali = sum(sum(riga) for riga in campo)
print(f"Mine totali nel campo: {mine_totali}")
print(f"Celle rivelate: {len(rivelate)}")
print(f"Celle sicure rivelate: {sum(1 for (i,j) in rivelate if campo[i][j] == 0)}")
=== CAMPO MINATO ===

Campo iniziale (■ = nascosto):
   0 1 2 3 4 5
  +------------
0 | ■ ■ ■ ■ ■ ■ 
1 | ■ ■ ■ ■ ■ ■ 
2 | ■ ■ ■ ■ ■ ■ 
3 | ■ ■ ■ ■ ■ ■ 
4 | ■ ■ ■ ■ ■ ■ 
5 | ■ ■ ■ ■ ■ ■ 

--- SIMULAZIONE DI GIOCO ---

Mossa 1: rivela cella (0, 0)
BOOM! Hai trovato una mina! Game Over!
   0 1 2 3 4 5
  +------------
0 | ■ ■ ■ ■ ■ ■ 
1 | ■ ■ ■ ■ ■ ■ 
2 | ■ ■ ■ ■ ■ ■ 
3 | ■ ■ ■ ■ ■ ■ 
4 | ■ ■ ■ ■ ■ ■ 
5 | ■ ■ ■ ■ ■ ■ 

--- CAMPO COMPLETO (tutte le mine rivelate) ---
   0 1 2 3 4 5
  +------------
0 | 💣 1 · 1 💣 1 
1 | 2 2 1 2 2 2 
2 | 1 💣 2 3 💣 2 
3 | 2 3 💣 4 💣 3 
4 | 💣 3 2 4 💣 2 
5 | 1 2 💣 2 1 1 

--- STATISTICHE ---
Mine totali nel campo: 9
Celle rivelate: 0
Celle sicure rivelate: 0

Spiegazione:

  1. Generazione casuale: Usiamo random.random() per decidere se una cella contiene una mina.

  2. Contorno adiacenze: Per ogni cella, controlliamo le 8 direzioni possibili (orizzontale, verticale, diagonale).

  3. Gestione bordi: È cruciale controllare che le coordinate adiacenti siano dentro i limiti della matrice.

  4. Insieme di celle rivelate: Usiamo un set per memorizzare quali celle sono state rivelate (efficiente per controllo appartenenza).

Forse potrebbe interessarti anche:  Statistica Bayesiana e Decisioni Pubbliche: 3 Esercizi Svolti di Analisi Economica

💡 Osservazione: Nel gioco reale del campo minato, quando si rivela una cella con 0 mine adiacenti, si rivelano automaticamente tutte le celle adiacenti (ricorsivamente). Questo è un ottimo esercizio aggiuntivo!

Domanda di riflessione: Come modificheresti calcola_adiacenti se le mine fossero considerate “toroidali” (cioè il campo si ripete ciclicamente sui bordi)?

Pubblicità

Risposte alle Domande di Riflessione

Esercizio 1

Domanda: Come potresti modificare trova_posti_consecutivi per trovare il PRIMO insieme di posti liberi consecutivi, piuttosto che fermarti al primo trovato?

Risposta: La funzione già trova il primo insieme, perché scorre la lista dall’inizio e si ferma appena trova una sequenza valida. Se volessimo trovare TUTTI gli insiemi di posti liberi consecutivi, dovremmo restituire una lista di posizioni invece che una sola:

def trova_tutti_posti_consecutivi(livello, consecutivi=3):
    posizioni = []
    for i in range(len(livello) - consecutivi + 1):
        tutti_liberi = True
        for j in range(consecutivi):
            if livello[i + j] != LIBERO:
                tutti_liberi = False
                break
        if tutti_liberi:
            posizioni.append(i)
    return posizioni

Esercizio 2

Domanda: Come modificheresti calcola_adiacenti se le mine fossero considerate “toroidali” (cioè il campo si ripete ciclicamente sui bordi)?

Risposta: Dovrei usare l’aritmetica modulare per gestire i bordi come se fossero connessi:

def calcola_adiacenti_toroidali(campo):
    righe = len(campo)
    colonne = len(campo[0])
    adiacenti = []
    
    for i in range(righe):
        riga_adiacenti = []
        for j in range(colonne):
            if campo[i][j] == 1:
                riga_adiacenti.append(-1)
            else:
                conteggio = 0
                for di in [-1, 0, 1]:
                    for dj in [-1, 0, 1]:
                        if di == 0 and dj == 0:
                            continue
                        # Usa modulo per gestire bordi toroidali
                        ni = (i + di) % righe
                        nj = (j + dj) % colonne
                        if campo[ni][nj] == 1:
                            conteggio += 1
                riga_adiacenti.append(conteggio)
        adiacenti.append(riga_adiacenti)
    
    return adiacenti

 

Perché questi esercizi sono fondamentali?

1. Il Parcheggio: L’embrione della Smart City Questo esercizio non serve solo a contare auto. Rappresenta la sfida della gestione delle risorse scarse.

  • L’aspetto interessante: La ricerca di posti consecutivi per il camper è un’introduzione semplificata alla Memory Allocation dei sistemi operativi. Proprio come un camper ha bisogno di spazio contiguo, un file sul disco o una variabile in RAM richiedono blocchi di memoria consecutivi. Chi scrive questo codice sta, di fatto, imparando le basi della gestione di basso livello.

  • Professioni: Fondamentale per chi lavora in Logistica 4.0 e IoT.

Forse potrebbe interessarti anche:  👻Episodio 2 – Correlazione tossica: quando i dati si tengono per mano ma non si amano

2. Campo Minato: La base della Computer Vision Il calcolo delle mine adiacenti è il “nonno” dei filtri di convoluzione usati nelle Intelligenze Artificiali.

  • L’aspetto interessante: Quando un’IA analizza un’immagine per trovare i bordi di un oggetto, applica una logica molto simile: guarda un pixel e i suoi 8 vicini per capire cosa sta succedendo. Gestire i bordi della matrice (evitando l’errore IndexOutOfBounds) è il rito di passaggio che trasforma un principiante in un programmatore solido.

  • Professioni: Essenziale per aspiranti Game Developers e AI Engineer.

Perché il Python puro “fallisce” su larga scala

In Python, una lista di liste è una collezione di oggetti sparsi nella memoria. Per contare i furgoni in un parcheggio gigante, il processore deve saltare da un punto all’altro della RAM, rallentando tutto.

NumPy invece memorizza i dati in blocchi contigui. Immagina la differenza tra cercare un libro in una biblioteca dove i libri sono sparsi a caso (Python list) e cercarlo in uno scaffale dove sono tutti ordinati per numero (NumPy array).

Esercizio 1: Il Parcheggio “Turbo” con NumPy

Tutto questo codice funziona benissimo per 60 posti. Ma se dovessimo gestire i dati di traffico di tutta Milano in tempo reale, Python puro si fermerebbe a respirare. Ecco perché esiste NumPy.

Vediamo come si riduce drasticamente il codice e come aumenta la velocità.

import numpy as np

# Inizializzazione istantanea (anche per milioni di posti)
# 0: libero, 1: auto, 2: furgone
def inizializza_numpy(livelli=3, posti=20):
    return np.random.choice([0, 1, 2], size=(livelli, posti), p=[0.4, 0.5, 0.1])

parcheggio_np = inizializza_numpy()

# 1. Livello con più posti liberi (operazione vettorizzata)
# Contiamo gli zeri per ogni riga (axis=1)
liberi_per_livello = np.sum(parcheggio_np == 0, axis=1)
livello_max = np.argmax(liberi_per_livello)

# 2. Conta furgoni totali
totale_furgoni = np.count_nonzero(parcheggio_np == 2)

La peculiarità: Noti che sono spariti i cicli for? NumPy sposta il calcolo dal “lento” interprete Python al “veloce” linguaggio C sottostante. Se avessi un parcheggio da 10.000 livelli, questa versione sarebbe circa 50-100 volte più veloce.

Esercizio 2: Campo Minato e la “Magia” della Convoluzione

Qui entriamo nel territorio della Computer Vision. Invece di controllare manualmente gli 8 vicini con due cicli, usiamo un’operazione matematica chiamata Convoluzione.

Immaginiamo di far scorrere una “maschera” (kernel) [math]3 \times 3[/math] sopra ogni cella del campo minato:

[math]\displaystyle \begin{aligned}
\text{Kernel} = \begin{bmatrix} 1 & 1 & 1 \\ 1 & 0 & 1 \\ 1 & 1 & 1 \end{bmatrix}
\end{aligned}[/math]

Ogni volta che il kernel incontra una mina (valore 1), la somma aumenta. In un colpo solo, calcoliamo l’intero campo.

from scipy.signal import convolve2d

def calcola_adiacenti_pro(campo):
    # Definiamo la maschera per guardare i vicini
    kernel = np.array([[1, 1, 1],
                       [1, 0, 1],
                       [1, 1, 1]])
    
    # Eseguiamo la convoluzione in un'unica riga di codice
    # 'boundary=fill' gestisce i bordi automaticamente
    adiacenti = convolve2d(campo, kernel, mode='same', boundary='fill', fillvalue=0)
    
    return adiacenti

Perché studiare questo approccio?

Se punti a diventare un Data Scientist o un Ingegnere di Machine Learning, non scriverai mai cicli for per analizzare dati.

  • Analisi Immagini: Ogni filtro di Instagram o algoritmo di riconoscimento facciale usa la convoluzione che abbiamo appena visto nel Campo Minato.
  • Big Data: Quando lavori con milioni di sensori (Smart Cities), la velocità di NumPy è l’unica cosa che impedisce al server di crashare.

Il programmatore moderno non deve solo “far funzionare” il codice, deve saper scegliere lo strumento giusto. Python puro va bene per la logica di business; NumPy è obbligatorio per la potenza di calcolo.

Pubblicità