Pandas come un Database: Guida Pratica alla Gestione di Chiavi Primarie (PK) e Integrità Referenziale (FK)

Cerca:

Generic selectors
Exact matches only
Search in title
Search in content
Post Type Selectors
come gestire id univoci in pandas

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.

Pubblicità


✅ 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 ✅
Forse potrebbe interessarti anche:  Come calcolare il break-even del re-commerce con Python e misurare la cannibalizzazione delle vendite

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)

Pubblicità

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.

Forse potrebbe interessarti anche:  Esercizi Avanzati di Bioinformatica con BioPython

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.

Forse potrebbe interessarti anche:  Statistica e sostenibilità: come i dati aiutano a ridurre l'impatto ambientale

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.
Pubblicità