Quante volte hai unito due file CSV o Excel con pd.concat e ti sei ritrovato con un disastro di ID duplicati? O forse hai eliminato una riga e ti sei chiesto: “E ora? Devo ricalcolare tutti gli ID a mano?”
Se usi Pandas per pulire e preparare i dati, prima o poi ti scontri con questo problema: Pandas non è un database. Non ha vincoli, non gestisce automaticamente l’univocità di una chiave, e non sa cosa sia l’integrità referenziale. È il “Far West” dei dati.
E se potessimo insegnare a Pandas a comportarsi con la disciplina di un database SQL?
In questo articolo non ci limitiamo a “usare Pandas”.
Costruiamo un mini-sistema robusto per gestire i nostri dati. Vedremo come definire e mantenere una chiave primaria (PK) autoincrementante, come aggiungere ed eliminare record in modo sicuro e, nel finale, come simulare persino le relazioni tra più tabelle (FK).
Niente magia, solo buona ingegneria dei dati applicata a Pandas.
Vediamola passo per passo con esempi pratici.
Dataset di partenza
import pandas as pd
df = pd.DataFrame({
'nome': ['Anna', 'Luca', 'Giulia'],
'corso': ['Matematica', 'Fisica', 'Informatica']
})
# Aggiungi PK autoincrementante (base 1)
df['id'] = range(1, len(df) + 1)
df = df.set_index('id')
print(df)
Risultato:
nome corso
id
1 Anna Matematica
2 Luca Fisica
3 Giulia Informatica
✅ 1. Aggiungere record senza rompere la PK
Se aggiungiamo nuove righe, dobbiamo assegnare nuovi ID univoci.
➕ Metodo sicuro
nuovi = pd.DataFrame({
'nome': ['Marco', 'Sara'],
'corso': ['Chimica', 'Storia']
})
# Calcola il prossimo ID disponibile
next_id = df.index.max() + 1
# Assegna PK autoincrementanti ai nuovi record
nuovi.index = range(next_id, next_id + len(nuovi))
# Unisci i due dataset
df = pd.concat([df, nuovi])
print(df)
Output:
nome corso
id
1 Anna Matematica
2 Luca Fisica
3 Giulia Informatica
4 Marco Chimica
5 Sara Storia
👉 Nessuna collisione: gli ID restano univoci e consecutivi.
✅ 2. Eliminare record mantenendo la PK coerente
Puoi rimuovere una riga senza toccare gli altri ID (come in un database reale).
# Elimina l’elemento con PK = 2 (Luca)
df = df.drop(index=2)
print(df)
Output:
nome corso
id
1 Anna Matematica
3 Giulia Informatica
4 Marco Chimica
5 Sara Storia
🔸 Importante: non devi “ricompattare” gli ID dopo una cancellazione.
Come in SQL, gli ID rimossi restano vuoti, perché servono solo a garantire un’identità univoca, non una sequenza continua.
✅ 3. Controllare l’integrità della PK
Dopo modifiche o concatenazioni, conviene sempre assicurarsi che la PK:
-
sia univoca,
-
non contenga valori nulli.
if df.index.is_unique and df.index.notna().all():
print("✅ Integrità della PK mantenuta")
else:
print("⚠️ PK non valida (duplicati o NaN)")
✅ 4. Reindicizzare solo se necessario
Se per motivi tecnici (es. visualizzazione) vuoi “ripulire” gli ID, puoi rigenerarli consapevolmente, ma è meglio farlo solo in copie o snapshot:
df_reset = df.reset_index(drop=True)
df_reset.index = range(1, len(df_reset) + 1)
df_reset.index.name = 'id'
✅ 5. Funzioni riutilizzabili
Puoi anche incapsulare questa logica in funzioni riutilizzabili:
def add_records(df, new_rows):
next_id = df.index.max() + 1 if len(df) > 0 else 1
new_rows.index = range(next_id, next_id + len(new_rows))
return pd.concat([df, new_rows])
def delete_record(df, pk):
if pk in df.index:
return df.drop(index=pk)
else:
print("⚠️ ID non trovato")
return df
Uso:
df = add_records(df, pd.DataFrame({'nome': ['Paolo'], 'corso': ['Biologia']}))
df = delete_record(df, 3)
✅ 6. (Avanzato) Aggiornare record esistenti
Se vuoi modificare un record senza alterarne la PK:
df.loc[4, 'corso'] = 'Biochimica'
→ Mantieni la PK, aggiorni solo i dati.
Riepilogo pratico
| Operazione | Metodo | Integrità PK |
|---|---|---|
| Aggiungere record | pd.concat + next_id = df.index.max() + 1 |
✅ |
| Eliminare record | df.drop(index=pk) |
✅ |
| Controllo integrità | df.index.is_unique |
✅ |
| Reindicizzare forzatamente | reset_index() |
⚠️ Solo se necessario |
| Aggiornare record | df.loc[pk, 'colonna'] = nuovo_valore |
✅ |
Esempio Completo
Setup iniziale
import pandas as pd
# Creiamo una "tabella" di esempio
def create_table():
df = pd.DataFrame({
'nome': ['Anna', 'Luca', 'Giulia'],
'corso': ['Matematica', 'Fisica', 'Informatica']
})
# Aggiungiamo PK autoincrementante e impostiamo come indice
df['id'] = range(1, len(df) + 1)
df = df.set_index('id')
return df
studenti = create_table()
print(studenti)
Output iniziale
nome corso
id
1 Anna Matematica
2 Luca Fisica
3 Giulia Informatica
2. Funzione insert()
Aggiunge nuovi record con PK autoincrementante, garantendo unicità.
def insert(df, new_rows):
"""Inserisce nuovi record con PK autoincrementante."""
next_id = df.index.max() + 1 if len(df) > 0 else 1
new_rows.index = range(next_id, next_id + len(new_rows))
df = pd.concat([df, new_rows])
df.index.name = 'id'
return df
Esempio
nuovi_studenti = pd.DataFrame({
'nome': ['Sara', 'Paolo'],
'corso': ['Storia', 'Biologia']
})
studenti = insert(studenti, nuovi_studenti)
print(studenti)
Risultato
nome corso
id
1 Anna Matematica
2 Luca Fisica
3 Giulia Informatica
4 Sara Storia
5 Paolo Biologia
3. Funzione delete()
Elimina un record in base alla chiave primaria (PK).
def delete(df, pk):
"""Elimina un record per chiave primaria."""
if pk in df.index:
return df.drop(index=pk)
else:
print(f"⚠️ Nessun record con id={pk}")
return df
Esempio
studenti = delete(studenti, 2) # Elimina Luca
print(studenti)
Output
nome corso
id
1 Anna Matematica
3 Giulia Informatica
4 Sara Storia
5 Paolo Biologia
4. Funzione update()
Aggiorna un record esistente mantenendo la PK.
def update(df, pk, **kwargs):
"""Aggiorna un record esistente per PK."""
if pk not in df.index:
print(f"⚠️ Nessun record con id={pk}")
return df
for col, val in kwargs.items():
if col in df.columns:
df.loc[pk, col] = val
else:
print(f"⚠️ Colonna '{col}' non trovata")
return df
Esempio
studenti = update(studenti, 3, corso='Data Science')
print(studenti)
Output
nome corso
id
1 Anna Matematica
3 Giulia Data Science
4 Sara Storia
5 Paolo Biologia
5. Funzione check_integrity()
Controlla che la PK sia univoca e priva di valori nulli.
def check_integrity(df):
"""Verifica integrità della chiave primaria."""
if df.index.is_unique and df.index.notna().all():
print("✅ Integrità PK mantenuta")
else:
print("❌ Errore: PK duplicata o mancante")
Esempio
check_integrity(studenti)
6. Extra: “relazioni” tra tabelle (FK simulata)
Simuliamo una tabella iscrizioni con una foreign key che punta a studenti.
iscrizioni = pd.DataFrame({
'id_studente': [1, 3, 5],
'corso': ['Matematica', 'Data Science', 'Biologia']
})
# Controllo integrità referenziale
valid_fk = iscrizioni['id_studente'].isin(studenti.index)
if valid_fk.all():
print("✅ Tutte le FK sono valide")
else:
print("❌ Alcune FK non corrispondono a studenti esistenti")
7. Riepilogo finale
| Funzione | Scopo | Mantiene integrità PK? |
|---|---|---|
insert() |
Aggiunge record con ID coerente | ✅ |
delete() |
Rimuove record per PK | ✅ |
update() |
Aggiorna record mantenendo PK | ✅ |
check_integrity() |
Verifica univocità e coerenza | ✅ |
merge() / isin() |
Simula FK | ✅ (manuale) |
8. Esempio completo in azione
studenti = create_table()
studenti = insert(studenti, pd.DataFrame({'nome': ['Sara'], 'corso': ['Storia']}))
studenti = update(studenti, 3, corso='AI Applicata')
studenti = delete(studenti, 2)
check_integrity(studenti)
print(studenti)
Esempio 2: versione “multi-tabella” completa
Costruiamo un mini–database relazionale interamente in pandas, dove tre tabelle — studenti, corsi e iscrizioni — sono collegate da chiavi primarie (PK) e chiavi esterne (FK).
L’obiettivo è simulare le relazioni di un database SQL dentro un ambiente Python/pandas, mantenendo integrità referenziale, inserimenti coerenti e join sicuri, senza dover usare SQLite o PostgreSQL.
Obiettivo concettuale
In un database relazionale:
-
Ogni tabella ha una chiave primaria (PK) univoca.
-
Le relazioni tra tabelle si basano su chiavi esterne (FK).
-
Quando aggiungi o cancelli dati, devi mantenere l’integrità referenziale,
cioè: le FK devono sempre puntare a PK esistenti.
In pandas non ci sono vincoli automatici,
ma possiamo simularli perfettamente usando merge(), controlli isin(), e gestione esplicita delle PK.
1. Creazione delle tabelle di base
Tabella studenti
import pandas as pd
def create_studenti():
df = pd.DataFrame({
'nome': ['Anna', 'Luca', 'Giulia'],
'eta': [20, 22, 21]
})
df['id_studente'] = range(1, len(df) + 1)
return df.set_index('id_studente')
Tabella corsi
def create_corsi():
df = pd.DataFrame({
'corso': ['Matematica', 'Fisica', 'Informatica'],
'crediti': [6, 8, 9]
})
df['id_corso'] = range(1, len(df) + 1)
return df.set_index('id_corso')
Tabella iscrizioni (relazione molti-a-molti)
def create_iscrizioni():
df = pd.DataFrame({
'id_studente': [1, 2, 3],
'id_corso': [1, 2, 3],
'anno': [2025, 2025, 2025]
})
return df
2. Inserimento e aggiornamento con PK coerenti
Funzioni generiche per mantenere l’integrità della PK.
def insert(df, new_rows):
next_id = df.index.max() + 1 if len(df) > 0 else 1
new_rows.index = range(next_id, next_id + len(new_rows))
df = pd.concat([df, new_rows])
df.index.name = df.index.name or 'id'
return df
def delete(df, pk):
if pk in df.index:
return df.drop(index=pk)
else:
print(f"⚠️ Nessun record con id={pk}")
return df
3. Controllo di integrità delle relazioni PK–FK
Verifica che ogni FK in iscrizioni esista in studenti e corsi.
def check_integrity(studenti, corsi, iscrizioni):
fk_studenti_valide = iscrizioni['id_studente'].isin(studenti.index)
fk_corsi_valide = iscrizioni['id_corso'].isin(corsi.index)
if fk_studenti_valide.all() and fk_corsi_valide.all():
print("✅ Integrità referenziale mantenuta")
else:
print("❌ Errore: FK non valide")
if not fk_studenti_valide.all():
print(" - Alcuni id_studente non esistono in studenti")
if not fk_corsi_valide.all():
print(" - Alcuni id_corso non esistono in corsi")
4. Join relazionale (simile a una JOIN SQL)
Componiamo una vista completa che unisce le tre tabelle.
def vista_iscrizioni(studenti, corsi, iscrizioni):
join1 = iscrizioni.merge(studenti, on='id_studente', how='left')
join2 = join1.merge(corsi, on='id_corso', how='left')
return join2[['nome', 'eta', 'corso', 'crediti', 'anno']]
5. Esecuzione
studenti = create_studenti()
corsi = create_corsi()
iscrizioni = create_iscrizioni()
check_integrity(studenti, corsi, iscrizioni)
print(vista_iscrizioni(studenti, corsi, iscrizioni))
Output
✅ Integrità referenziale mantenuta
nome eta corso crediti anno
0 Anna 20 Matematica 6 2025
1 Luca 22 Fisica 8 2025
2 Giulia 21 Informatica 9 2025
6. Simuliamo operazioni tipiche di un DB
➕ Aggiungere un nuovo corso e iscriverci uno studente
# Nuovo corso
corsi = insert(corsi, pd.DataFrame({'corso': ['Data Science'], 'crediti': [10]}))
# Nuovo studente
studenti = insert(studenti, pd.DataFrame({'nome': ['Sara'], 'eta': [23]}))
# Iscrizione coerente
iscrizioni.loc[len(iscrizioni)] = {'id_studente': 4, 'id_corso': 4, 'anno': 2025}
Controlliamo di nuovo:
check_integrity(studenti, corsi, iscrizioni)
print(vista_iscrizioni(studenti, corsi, iscrizioni))
7. Cancellazione controllata (tipo “ON DELETE RESTRICT”)
Impediamo la cancellazione di record ancora referenziati.
def safe_delete_student(studenti, iscrizioni, pk):
if pk in iscrizioni['id_studente'].values:
print(f"❌ Impossibile eliminare studente {pk}: ha iscrizioni attive.")
return studenti
return delete(studenti, pk)
Esempio:
studenti = safe_delete_student(studenti, iscrizioni, 1) # Anna iscritta
Output:
❌ Impossibile eliminare studente 1: ha iscrizioni attive.
8. Conclusione
In questo mini–sistema hai riprodotto le regole di base dei database relazionali:
| Concetto SQL | Equivalente in pandas |
|---|---|
| Primary Key (PK) | indice univoco |
| Foreign Key (FK) | colonna con controllo isin() |
| INSERT | insert() |
| DELETE | delete() o safe_delete_*() |
| UPDATE | df.loc[pk, 'colonna'] = valore |
| JOIN | merge() multipli |
| ON DELETE RESTRICT | controllo manuale sulle FK |
| CHECK INTEGRITY | check_integrity() |
In sintesi
Hai ora un micro ORM (Object Relational Mapper) realizzato in pandas,
utile per simulazioni didattiche, test di logiche relazionali o pre-elaborazione dati prima di caricarli in SQL.
Commento Applicativo
Il Problema Reale:
La maggior parte dei dati nel mondo reale non proviene da database puliti, ma da CSV, Excel o API “sporchi”. Questi esercizi affrontano il problema numero uno del Data Cleaning: come imporre una struttura e regole (come una PK) a dati che non ne hanno.
L’Indice come PK:
Molti utenti Pandas trattano l’indice come un fastidio (usano reset_index(drop=True) ovunque). Questo articolo riabilita l’indice e mostra il suo vero scopo: essere un identificatore univoco della riga, proprio come una Primary Key. È il modo più efficiente in Pandas per selezionare, aggiornare e unire dati.
Prototipazione ETL:
L’esercizio multi-tabella non è solo un “gioco”. È una simulazione di un micro-ETL. Si stanno prendendo dati da fonti diverse (simulate dalle funzioni create_*), li si sta validando (il check_integrity) e li si sta unendo in una vista unificata (vista_iscrizioni). Questo è un prototipo perfetto per una pipeline di dati che un giorno potrebbe finire su un Data Warehouse.
Dal “Selvaggio” allo “Strutturato”:
L’obiettivo è quello di acquisire una mentalità. Sposta il programmatore Pandas da un approccio “usa e getta” (carico un csv, faccio un plot, lo butto) a un approccio da ingegnere (creo un modello dati affidabile, manutenibile e robusto, anche se solo in memoria).
SQL vs Pandas: PK/FK e integrità referenziale
Ecco una tabella comparativa testuale tra SQL e Pandas, focalizzata su chiavi primarie (PK), chiavi esterne (FK) e integrità referenziale.
| Concetto | SQL (Database relazionale) | Pandas (DataFrame in Python) |
|---|---|---|
| Chiave primaria (PK) | Definita esplicitamente con PRIMARY KEY al momento della creazione della tabella. Garantisce unicità e non nullità. |
Non esiste vincolo nativo. Si gestisce verificando l’unicità di una colonna (df['id'].is_unique) e rimuovendo duplicati con drop_duplicates(). |
| Chiave esterna (FK) | Definita con FOREIGN KEY e collegata a una PK di un’altra tabella. Il DBMS applica automaticamente i vincoli. |
Non esiste vincolo nativo. Si simula con pd.merge() tra DataFrame e si verifica la corrispondenza con isin(). |
| Integrità referenziale | Gestita dal DBMS: impedisce inserimenti o aggiornamenti che violano i vincoli FK. | Deve essere controllata manualmente: ad esempio verificando che tutti i valori di FK siano presenti nella PK di riferimento (df_orders['customer_id'].isin(df_customers['id']).all()). |
| Gestione errori | Il DBMS blocca operazioni non valide (es. inserimento di FK inesistente). | L’errore non viene segnalato automaticamente: occorre scrivere codice per intercettare e correggere incoerenze. |
| Approccio generale | Strutturato, con vincoli rigidi e garantiti dal sistema. | Flessibile, ma richiede disciplina e controlli manuali per simulare la logica dei database. |
Sintesi
- SQL → garantisce automaticamente integrità e vincoli grazie al motore del database.
- Pandas → offre grande flessibilità, ma la responsabilità di mantenere coerenza e integrità è affidata al programmatore.
Articoli di approfondimento
- 🔴 Progettare database relazionali: guida pratica dall’ERD all’integrità dei dati
- 🔴 Normalizzazione database: guida pratica per sviluppatori con esempio reale
- 🔴 Python: unire file CSV presenti in una directory
- 🔴 5 esercizi di Pandas in Python spiegati passo passo: guida pratica da base ad esperto
- 🔴 Pandas Python Tutorial completo: analisi e manipolazione dei dati





