Guida Pratica: Creare una Primary Key (PK) Univoca con pandas.factorize, anche da Colonne Multiple

Cerca:

Generic selectors
Exact matches only
Search in title
Search in content
Post Type Selectors
Creazione di una Primary Key (PK) Surrogata con pandas.factorize

Se lavori con i dati, probabilmente conosci questa situazione: lanci un groupby('citta') e ottieni “New York”, “new york”, e “NY” come tre risultati separati. È un incubo che rende le tue analisi inutilizzabili.

O forse i tuoi dati sono già puliti, ma quel groupby su milioni di righe impiega minuti interi, bloccando il tuo lavoro perché confrontare stringhe è lento.

Entrambi i problemi (dati sporchi e analisi lente) si risolvono smettendo di usare le stringhe. Il primo passo è normalizzare i dati (trasformando “NY” in “new york”). Il secondo è codificarli.

pandas.factorize è lo strumento perfetto per questo secondo passo. È un comando velocissimo che converte le tue colonne di testo pulite in numeri interi leggeri.

In questa guida, vediamo come usarlo per ottimizzare memoria, velocizzare le analisi e creare “chiavi primarie” (PK) uniche, anche quando la chiave è sparsa su più colonne.

1. Cos’è pandas.factorize?

Pensala come un “assegnatore di etichette” incredibilmente veloce.

Il suo compito è prendere una colonna di dati (stringhe, date, oggetti) e “tradurla” in numeri interi. È fondamentale perché la maggior parte delle operazioni sui dati (e quasi tutti gli algoritmi di machine learning) funzionano molto meglio, o esclusivamente, con i numeri.

2. L’analogia: Il Registro di Classe

Il modo migliore per capire factorize è con un’analogia. Immagina di essere il primo giorno di scuola. Hai un elenco di studenti che si iscrivono a un corso, nell’ordine in cui arrivano:

['Anna', 'Bruno', 'Chiara', 'Anna', 'Bruno', 'Davide']

Vuoi creare un registro ufficiale (gli uniques) e assegnare a ogni studente un numero di matricola (i codes) per velocizzare l’appello.

  1. Anna è la prima. Non è nel registro.
    • La aggiungi al registro: ['Anna']
    • Le assegni il primo numero: 0
  2. Bruno è il secondo. Non è nel registro.
    • Lo aggiungi al registro: ['Anna', 'Bruno']
    • Gli assegni il numero successivo: 1
  3. Chiara è la terza. Non è nel registro.
    • La aggiungi al registro: ['Anna', 'Bruno', 'Chiara']
    • Le assegni il numero successivo: 2
  4. Arriva di nuovo Anna. Guardi il registro. C’è già!
    • Non la aggiungi di nuovo.
    • Le assegni il suo numero di matricola esistente: 0
  5. Arriva di nuovo Bruno. C’è già!
    • Gli assegni il suo numero: 1
  6. Davide è l’ultimo. Non è nel registro.
    • Lo aggiungi al registro: ['Anna', 'Bruno', 'Chiara', 'Davide']
    • Gli assegni il numero successivo: 3

Alla fine, pandas.factorize ti restituisce due cose:

  • codes (I numeri di matricola): [0, 1, 2, 0, 1, 3]
  • uniques (Il registro / Legenda): ['Anna', 'Bruno', 'Chiara', 'Davide']

Il “registro” (uniques) è la tua chiave di lettura. Se vedi un 2 nei codes, sai che significa “Chiara”.

3. Esempio Pratico (Singola Colonna)

Vediamo l’analogia in azione con il codice:

import pandas as pd

# I nostri dati originali
studenti = pd.Series(['Anna', 'Bruno', 'Chiara', 'Anna', 'Bruno', 'Davide'])

# Applichiamo factorize
codes, uniques = pd.factorize(studenti)

print(f"Studenti Originali:\n{studenti.values}")
print("---")
print(f"Codes (Numeri di Matricola):\n{codes}")
print("---")
print(f"Uniques (Il Registro / Legenda):\n{uniques}")

Output:

Studenti Originali:
['Anna' 'Bruno' 'Chiara' 'Anna' 'Bruno' 'Davide']
---
Codes (Numeri di Matricola):
[0 1 2 0 1 3]
---
Uniques (Il Registro / Legenda):
['Anna' 'Bruno' 'Chiara' 'Davide']
Pubblicità

4. Sintassi e Parametri

La sintassi completa è pandas.factorize(values, sort=False, na_sentinel=-1, size_hint=None).

I parametri che contano davvero sono due:

  • sort=False (Default): Gli ID (0, 1, 2...) vengono assegnati nell’ordine in cui appaiono nei dati (come nella nostra analogia).
  • sort=True: Prima di assegnare gli ID, factorize ordina alfabeticamente/numericamente i valori unici. (Con sort=True, ‘Anna’ sarebbe sempre 0, ‘Bruno’ 1, ‘Chiara’ 2, ecc.).
  • na_sentinel=-1 (Default): Cosa fare con i valori mancanti (NaN, None)? Di default, gli assegna -1. Questo è utilissimo, come vedremo.
Forse potrebbe interessarti anche:  Guida Pratica a SHAP in Python: Interpretare Modelli Lineari e XGBoost nell'Explainable AI

5.  Il Caso Avanzato: Creare una PK Univoca da Colonne Multiple

Questo è il trucco più potente. Spesso una chiave univoca non è una sola colonna, ma la combinazione di più colonne (es. Nome + Cognome + Data di Nascita).

pandas.factorize non accetta un DataFrame, solo un array. La soluzione? Creare un array di tuple.

Perché le tuple? Perché sono hashable (immutabili, come le stringhe), mentre le liste non lo sono. factorize ha bisogno di oggetti hashable per creare il suo “registro”.

Esempio Pratico Multi-colonna

Immaginiamo di voler creare un ID univoco per ogni coppia (cliente, regione).

import pandas as pd
import numpy as np

df = pd.DataFrame({
    'cliente': ['Mario Rossi', 'Luigi Verdi', 'Mario Rossi', 'Anna Bianchi', 'Mario Rossi'],
    'regione': ['Lombardia', 'Lazio', 'Lombardia', 'Lazio', 'Veneto'],
    'importo': [100, 200, 100, 300, 400]
})

# 1. Definiamo le colonne che compongono la nostra chiave
colonne_pk = ['cliente', 'regione']

# 2. Creiamo l'array di tuple
# Usiamo list(zip(...)), che è molto più veloce di .apply(tuple, axis=1)
valori_combinati = list(zip(*[df[col] for col in colonne_pk]))

# print(valori_combinati)
# [('Mario Rossi', 'Lombardia'),
#  ('Luigi Verdi', 'Lazio'),
#  ('Mario Rossi', 'Lombardia'),
#  ('Anna Bianchi', 'Lazio'),
#  ('Mario Rossi', 'Veneto')]

# 3. Applichiamo factorize ai valori combinati
codes, uniques = pd.factorize(valori_combinati)

# 4. Assegniamo i codici come nuova colonna PK
df['ID_Univoco'] = codes

print("DataFrame con PK Multi-colonna:")
print(df)

print("\nLegenda (Uniques):")
# Nota come gli uniques sono ora tuple!
print(uniques)

Output:

DataFrame con PK Multi-colonna:
        cliente    regione  importo  ID_Univoco
0   Mario Rossi  Lombardia      100           0
1   Luigi Verdi      Lazio      200           1
2   Mario Rossi  Lombardia      100           0
3  Anna Bianchi      Lazio      300           2
4   Mario Rossi     Veneto      400           3

Legenda (Uniques):
[('Mario Rossi', 'Lombardia'), ('Luigi Verdi', 'Lazio'), ('Anna Bianchi', 'Lazio'), ('Mario Rossi', 'Veneto')]

Questo ID_Univoco è ora una chiave surrogata perfetta. Occupa pochissima memoria (un intero) ma rappresenta in modo univoco la combinazione di due colonne stringa.


6. Perché e Quando Usarlo (Il Contesto Professionale)

factorize brilla in tre scenari principali:

1. Risparmio di Memoria (Il Data Engineer)

Invece di memorizzare la stringa “Stati Uniti d’America” un milione di volte (occupando tantissimo spazio), puoi memorizzare l’intero 0 un milione di volte.

Per un Data Engineer, questo è il segreto per ridurre un DataFrame da 10GB a 2GB prima di salvarlo in formato Parquet. pandas.factorize è il motore che alimenta il tipo di dati category in pandas.

2. Prestazioni (Il Data Analyst)

Confrontare, ordinare e raggruppare numeri interi è migliaia di volte più rapido per un computer che farlo con stringhe.

Se devi fare un groupby('Nazione') su un miliardo di righe, trasformare prima ‘Nazione’ in ID_Nazione numerico renderà l’operazione quasi istantanea.

3. Preprocessing (Il Data Scientist)

Un modello di machine learning non capisce “Anna” o “Bruno”. factorize è il modo più rapido per fare un Label Encoding di base.

Il trucco da professionisti: Non usare factorize sul tuo test set.

  1. Esegui factorize solo sul training set e salva l’array uniques (la “legenda”).
  2. Quando hai nuovi dati (test set o dati in produzione), usa quella legenda per tradurli, così:
    # uniques_training = ['Mario', 'Luigi'] (salvati dal training)
    nuovi_dati = pd.Series(['Luigi', 'Anna', 'Mario', 'Luigi'])
    
    # get_indexer mappa i nuovi dati sulla legenda
    # 'Anna' non era nel training, quindi ottiene -1 (non trovato)
    nuovi_codes = pd.Index(uniques_training).get_indexer(nuovi_dati)
    # Risultato: [1, -1, 0, 1]
    

Questo garantisce che ‘Luigi’ sia sempre 1 e ‘Mario’ sempre 0.

7. Analisi Applicativa dei Parametri

Gli esempi di codice sono semplici, ma le loro implicazioni sono profonde.

  • Peculiarità 1: na_sentinel=-1 (Default)
    • Perché è interessante? L’uso di -1 è una scelta di design geniale. Perché non 0? Perché 0 è un codice valido (il primo valore “Anna” nel nostro esempio). Usando -1, i valori NaN sono immediatamente identificabili e filtrabili (df[df['ID_Cliente'] == -1]). È la “chiave nulla” perfetta.
  • Peculiarità 2: na_sentinel=None
    • Perché è interessante (e rischioso)? Questo comando produce NaN (un float) nell’array codes. Questo “inquina” l’array di interi, forzandolo a diventare di tipo float64 per contenere il NaN. Si perde il vantaggio di avere un array di interi pulito e performante. È quasi sempre meglio la soluzione di default (-1).
  • Peculiarità 3: sort=True
    • Perché è interessante? Questo garantisce la riproducibilità. Con sort=True, ‘Anna Bianchi’ (che inizia per ‘A’) riceverà sempre un ID più basso di ‘Mario Rossi’ (che inizia per ‘M’), indipendentemente dall’ordine in cui appaiono nei dati. È fondamentale se, per qualche motivo, devi eseguire factorize separatamente su due file (es. training e test) e vuoi che gli stessi valori abbiano lo stesso ID in entrambi.
Forse potrebbe interessarti anche:  Attribuzione Multi-Touch nel SaaS B2B: Guida Completa a Markov, Shapley e Calcolo del CAC (Parte 2)

 

8. Contesto: Altre Tecniche di Codifica (Alternative)

factorize (o Label Encoding) è solo una delle tante tecniche di codifica. È importante sapere quando le altre sono migliori.

  1. Codifica One-Hot (One-Hot Encoding)
    • È la tecnica standard e più sicura per le variabili Nominali (categorie senza ordine, come “Rosso”, “Verde”, “Blu”).
    • Crea una nuova colonna binaria (0 o 1) per ogni categoria.
    • Vantaggi: Impedisce al modello di pensare che “Blu” (codice 2) sia “più grande” di “Rosso” (codice 0).
    • Svantaggi: Crea moltissime colonne se hai migliaia di categorie.
  2. Codifica Frequenza (Frequency Encoding)
    • Sostituisce la categoria con la frequenza con cui appare (es. “Italia” -> 0.45, perché il 45% delle righe è “Italia”).
    • Vantaggi: Non crea nuove colonne.
    • Svantaggi: Categorie diverse con la stessa frequenza otterranno lo stesso numero, perdendo informazioni.

La Scelta:

  • Variabile Nominale (es. Nomi di città):
    • Per modelli lineari (Regressione, SVM): Usa One-Hot Encoding.
    • Per modelli ad albero (Random Forest, XGBoost): factorize va benissimo ed è più veloce.
  • Variabile Ordinale (es. “Basso”, “Medio”, “Alto”):
    • Usa factorize, ma assicurati che l’ordine sia corretto (es. con sort=True o pre-ordinando i dati).
  • Creazione di PK o Ottimizzazione Memoria:
    • Usa sempre factorize.

Pubblicità

L’esempio nell’introduzione

Come abbiamo visto, pandas.factorize è uno strumento di codifica, non di pulizia.

Se i tuoi dati sono “sporchi” (come “New York”, “new york”, “NY”), factorize li tratterà come tre cose completamente diverse, assegnando loro tre ID differenti (es. 0, 1, 2).

Questo è l’opposto di ciò che vogliamo.

La soluzione è una pipeline in due fasi:

  1. Normalizzazione: Pulire i dati per renderli tutti uguali (es. “new york”).
  2. Codifica: Usare factorize sui dati puliti per ottenere l’ID univoco.

Vediamo come farlo.

Risolviamo il Caso: Normalizzare Dati Sporchi prima di factorize

Prendiamo un DataFrame d’esempio che rappresenta esattamente dall’ “incubo” descritto nell’introduzione:

import pandas as pd

df = pd.DataFrame({
    'citta_ordine': [
        'New York', 
        'Milano', 
        'new york',  # Duplicato 1 (case)
        'Roma', 
        'NY',        # Duplicato 2 (abbreviazione)
        'milano',    # Duplicato 3 (case)
        'New York'   # Duplicato 0 (originale)
    ],
    'valore': [100, 50, 90, 200, 80, 40, 110]
})

print("Dati Originali ('Sporchi'):")
print(df)

Output:

Dati Originali ('Sporchi'):
  citta_ordine  valore
0     New York     100
1       Milano      50
2     new york      90
3         Roma     200
4           NY      80
5       milano      40
6     New York     110

1. Il Problema: factorize su Dati Sporchi

Se applichiamo factorize direttamente, otteniamo un disastro:

codes_errati, uniques_errati = pd.factorize(df['citta_ordine'])

print(f"\nCodes (ERRATI): {codes_errati}")
print(f"Uniques (ERRATI): {uniques_errati.values}")

Output:

Codes (ERRATI): [0 1 2 3 4 5 0]
Uniques (ERRATI): ['New York' 'Milano' 'new york' 'Roma' 'NY' 'milano']

Abbiamo 6 ID univoci… ma in realtà le città sono solo 3 (New York, Milano, Roma). L’analisi fallirebbe.

Forse potrebbe interessarti anche:  Rilevazione Anomalie in Python con Isolation Forest: Guida ed Esercizi

2. La Soluzione: Normalizzazione + factorize

Dobbiamo “insegnare” a Pandas che “NY” e “new york” sono la stessa cosa.

Fase A: Pulizia e Normalizzazione

Per prima cosa, creiamo una colonna normalizzata.

  1. Uniformiamo il “case”: Trasformiamo tutto in minuscolo (.str.lower()).
  2. Mappiamo le eccezioni: Usiamo un dizionario e .replace() per correggere le abbreviazioni.
# 1. Convertiamo tutto in minuscolo
colonna_normalizzata = df['citta_ordine'].str.lower()

# 2. Definiamo un dizionario per le abbreviazioni e i sinonimi
mappa_correzioni = {
    'ny': 'new york'
    # 'mi': 'milano',  # Se avessimo anche 'mi'
    # 'rm': 'roma'      # Se avessimo anche 'rm'
}

# 3. Applichiamo la mappatura (replace è sicuro, sostituisce solo le chiavi)
colonna_normalizzata = colonna_normalizzata.replace(mappa_correzioni)

# Aggiungiamo la colonna pulita al DataFrame per vederla
df['citta_norm'] = colonna_normalizzata

print("\nDataFrame dopo la Normalizzazione:")
print(df)

Output:

DataFrame dopo la Normalizzazione:
  citta_ordine  valore   citta_norm
0     New York     100     new york
1       Milano      50       milano
2     new york      90     new york
3         Roma     200         roma
4           NY      80     new york
5       milano      40       milano
6     New York     110     new york

Ora sì! La colonna citta_norm è pulita e coerente.

Fase B: factorize sui Dati Puliti

Adesso, e solo adesso, applichiamo factorize sulla colonna citta_norm.

codes_corretti, uniques_corretti = pd.factorize(df['citta_norm'])

# Aggiungiamo l'ID finale
df['ID_Citta'] = codes_corretti

print("\nDataFrame Finale con ID Corretti:")
print(df)

print(f"\nCodes (CORRETTI): {codes_corretti}")
print(f"Uniques (CORRETTI): {uniques_corretti.values}")

Output Finale:

DataFrame Finale con ID Corretti:
  citta_ordine  valore  citta_norm  ID_Citta
0     New York     100    new york         0
1       Milano      50      milano         1
2     new york      90    new york         0
3         Roma     200        roma         2
4           NY      80    new york         0
5       milano      40      milano         1
6     New York     110    new york         0

Codes (CORRETTI): [0 1 0 2 0 1 0]
Uniques (CORRETTI): ['new york' 'milano' 'roma']

⇒ Abbiamo risolto l’incubo.

  • Tutte le varianti di “New York” hanno ID = 0.
  • Tutte le varianti di “Milano” hanno ID = 1.
  • “Roma” ha ID = 2.

Questo approccio (Clean -> Normalize -> Factorize) è la vera soluzione al problema che hai citato e risolve anche la seconda parte: un groupby('ID_Citta') ora sarà fulmineo perché confronterà solo interi (0, 1, 2) invece di stringhe sporche.


Conclusione

pandas.factorize è molto più di un semplice strumento di preprocessing. È una leva fondamentale per la performance, l’ottimizzazione della memoria e la creazione di chiavi surrogate robuste. Ora hai non solo l’analogia per ricordarlo, ma anche la tecnica avanzata per applicarlo a chiavi multi-colonna, trasformandolo da un comando utile a uno strumento indispensabile nel tuo arsenale.

Pubblicità