Preparazione avanzata dei dati utilizzando trasformatori personalizzati in Scikit-Learn
Advanced data preparation with custom transformers in Scikit-Learn.
Vai oltre la “modalità principiante” e sfrutta appieno le capacità più avanzate di scikit-learn
Scikit-Learn fornisce molti strumenti utili per la preparazione dei dati, ma talvolta le opzioni predefinite non sono sufficienti.
In questo articolo ti mostrerò come creare flussi di lavoro avanzati per la preparazione dei dati utilizzando i Transformer personalizzati. Se usi scikit-learn da un po’ di tempo e vuoi migliorare le tue competenze, imparare i Transformer è un ottimo modo per avanzare oltre la “modalità principiante” e apprendere alcune delle capacità più avanzate richieste dalle moderne squadre di Data Science.
Se l’argomento ti sembra un po’ avanzato, non preoccuparti – questo articolo è pieno di esempi che ti aiuteranno a sentirti sicuro sia con il codice che con i concetti.
Inizierò con una breve panoramica sulla classe Transformer di scikit-learn e poi ti guiderò attraverso due modi per costruire Transformer personalizzati:
- Data Scraping Demystified Come Estrarre Informazioni Preziose dal Web
- Integra SageMaker Autopilot nei tuoi processi di MLOps utilizzando un progetto personalizzato di SageMaker.
- Teoria delle Risorse Dove la Matematica incontra l’Industria.
- Utilizzando un
FunctionTransformer
- Scrivere un Transformer personalizzato da zero
Transformer: Il modo migliore per preprocessare i dati in scikit-learn
Il Transformer è uno dei blocchi di costruzione centrali di scikit-learn. È così fondamentale, infatti, che è probabile tu ne abbia già utilizzato uno senza nemmeno accorgertene.
In scikit-learn, un Transformer è qualsiasi oggetto con i metodi fit()
e transform()
. In parole semplici, ciò significa che un Transformer è una classe (cioè un pezzo di codice riutilizzabile) che prende il tuo dataset grezzo come input e restituisce una versione trasformata di quel dataset.

È importante sottolineare che i Transformer di scikit-learn NON sono gli stessi “transformer” utilizzati nei modelli di Large Language Models (LLM) come BERT e GPT-4, o nei modelli disponibili attraverso la libreria HuggingFace transformers. Nel contesto dei LLM, un “transformer” (con la t minuscola) è un modello di deep learning; un Transformer di scikit-learn (con la T maiuscola) è un’entità completamente diversa (e molto più semplice). Puoi pensarlo semplicemente come uno strumento per preprocessare i dati in un tipico workflow di ML.
Quando importi scikit-learn, ottieni automaticamente l’accesso a un insieme di Transformer predefiniti progettati per le comuni attività di preprocessing dei dati di ML, come il completamento dei valori mancanti, la ridimensionamento delle caratteristiche e la codifica one-hot. Alcuni dei Transformer più popolari includono:
sklearn.impute.SimpleImputer
– un Transformer che sostituirà i valori mancanti nel tuo datasetsklearn.preprocessing.MinMaxScaler
– un Transformer che può ridimensionare le caratteristiche numeriche nel tuo datasetsklearn.preprocessing.OneHotEncoder
– un Transformer per la codifica one-hot delle caratteristiche categoriche
Utilizzando una Pipeline di scikit-learn sklearn.pipeline.Pipeline
, puoi addirittura concatenare più Transformer per costruire flussi di lavoro di preparazione dei dati a più passaggi, in preparazione per la successiva modellizzazione di ML:

Se non sei familiare con le Pipeline o i ColumnTransformer, sono un ottimo modo per semplificare il tuo codice di ML, e puoi leggere di più su di essi nel mio articolo precedente:
Semplifica la preparazione dei dati con queste 4 Classi di Scikit-Learn meno conosciute
Dimentica train_test_split: Pipeline, ColumnTransformer, FeatureUnion e FunctionTransformer sono indispensabili anche se…
towardsdatascience.com
Cosa c’è di sbagliato nei pre-built Transformer di scikit-learn?
Niente assolutamente!
Se stai lavorando con dataset semplici e stai eseguendo passaggi standard di preparazione dei dati, è probabile che i transformer pre-built di scikit-learn siano perfettamente adeguati. Non c’è bisogno di reinventare la ruota scrivendo quelli personalizzati da zero.
Ma, e siamo onesti, quando i dataset sono mai davvero semplici nella vita reale?
(Spoiler: mai.)
Se stai lavorando con dati del mondo reale o devi implementare un metodo di preprocessing succoso, è probabile che i Transformer integrati di scikit-learn non siano sempre adeguati. Prima o poi, avrai bisogno di implementare trasformazioni personalizzate sui dati.
Fortunatamente, scikit-learn fornisce alcuni modi per estendere le sue funzionalità di base del Transformer e costruire Transformer più personalizzati. Per mostrare come funzionano, userò il dataset canonico di Titanic Survival Prediction. Anche su questo dataset apparentemente “semplice”, troverai che ci sono molte opportunità per essere creativi nella preparazione dei dati. E, come mostrerò, i Transformer personalizzati sono lo strumento ideale per il compito.
Dati
Prima di tutto, carichiamo il dataset e suddividiamolo in sottinsiemi di addestramento e di test:
import pandas as pdfrom sklearn.datasets import fetch_openmlfrom sklearn.model_selection import train_test_split# Load data and split into training and testing setsX, y = fetch_openml("titanic", version=1, as_frame=True, return_X_y=True)X.drop(['boat', 'body', 'home.dest'], axis=1, inplace=True)X_train, X_test, y_train, y_test = train_test_split(X, y, stratify=y, test_size=0.2)X_train.head()

Perché questo articolo si concentra su come costruire Transformer personalizzati, non entrerò nei dettagli sui passaggi standard di preprocessing che possono essere facilmente applicati a questo dataset utilizzando i Transformer integrati di scikit-learn (ad esempio, la codifica one-hot delle variabili categoriche come sex
utilizzando un OneHotEncoder
, o la sostituzione dei valori mancanti utilizzando un SimpleImputer
).
Invece, mi concentrerò su come incorporare passaggi di preprocessing più complessi che non possono essere implementati utilizzando i Transformer “pronti all’uso”.
Uno di questi passaggi prevede l’estrazione del titolo di ogni passeggero (ad esempio, Mr, Mrs, Master) dal campo name
. Perché dovremmo farlo? Beh, se sappiamo che il titolo di ogni passeggero contiene un’indicazione della loro classe/età/sesso, e assumiamo che questi fattori abbiano influenzato la capacità dei passeggeri di salire sui battelli di salvataggio, è ragionevole ipotizzare che i titoli possano essere informativi sulle possibilità di sopravvivenza. Ad esempio, un passeggero con il titolo “Master” (indicando che è un bambino) potrebbe avere maggiori probabilità di sopravvivere rispetto a un passeggero con il titolo “Mr” (indicando che è un adulto).
Il problema, naturalmente, è che non c’è una classe scikit-learn integrata che possa fare qualcosa di specifico come l’estrazione del titolo dal campo name
. Per estrarre i titoli, è necessario costruire un Transformer personalizzato.
1. FunctionTransformer
Il modo più rapido per costruire un Transformer personalizzato è utilizzare la classe FunctionTransformer
, che consente di creare Transformer direttamente dalle normali funzioni Python.
Per utilizzare un FunctionTransformer
, si inizia definendo una funzione che prende in input un dataset X
, esegue la trasformazione desiderata e restituisce una versione trasformata di X
. Quindi, avvolgi la tua funzione in un FunctionTransformer
, e scikit-learn creerà un Transformer personalizzato che implementa la tua funzione.
Ad esempio, ecco una funzione che può estrarre il Titolo di un passeggero dal campo name
del nostro dataset Titanic:
from sklearn.preprocessing import FunctionTransformerdef extract_title(X): """Estrae il titolo dal campo `name` di ogni passeggero.""" X['title'] = X['name'].str.split(', ', expand=True)[1].str.split('.', expand=True)[0] return Xextract_title_transformer = FunctionTransformer(extract_title)print(type(extract_title_transformer))# <class 'sklearn.preprocessing._function_transformer.FunctionTransformer'>
Come puoi vedere, avvolgere la funzione in un FunctionTransformer
la ha trasformata in un Transformer di scikit-learn, conferendole i metodi .fit()
e .transform()
.
Poi possiamo incorporare questo Transformer nella nostra pipeline di preparazione dei dati insieme ad ulteriori passaggi/preparatori di dati che vogliamo includere:
from sklearn.pipeline import Pipelinepreprocessor = Pipeline(steps=[ ('extract_title', extract_title_transformer), # ... altri eventuali transformer da includere, ad esempio SimpleImputer o MinMaxScaler])X_train_transformed = preprocessor.fit_transform(X_train)X_train_transformed

Se vuoi definire una funzione/Transformer più complessa che richiede argomenti aggiuntivi, puoi passarli alla funzione incorporandoli nell’argomento kw_args
di FunctionTransformer
. Ad esempio, definiamo un’altra funzione che identifica se ogni passeggero proviene da un background di classe alta/professionale, basandosi sul loro titolo:
def extract_title(X): """Estrae il titolo dal campo `name` di ogni passeggero.""" X['title'] = X['name'].str.split(', ', expand=True)[1].str.split('.', expand=True)[0]def is_upper_class(X, upper_class_titles): """Se il titolo del passeggero è nell'elenco di `upper_class_titles`, restituisce 1, altrimenti 0.""" X['upper_class'] = X['title'].apply(lambda x: 1 if x in upper_class_titles else 0) return Xpreprocessor = Pipeline(steps=[ ('extract_title', FunctionTransformer(extract_title)), ('is_upper_class', FunctionTransformer(is_upper_class, kw_args={'upper_class_titles':['Dr', 'Col', 'Major', 'Lady', 'Rev', 'Sir', 'Capt']})), # ... altri eventuali transformer da includere, ad esempio SimpleImputer o MinMaxScaler])X_train_transformed = preprocessor.fit_transform(X_train)X_train_transformed

Come puoi vedere, usare FunctionTransformer
è un modo molto semplice per incorporare questi complessi passaggi di preelaborazione in una Pipeline senza modificare fondamentalmente la struttura del nostro codice.
1.1 Limitazioni di FunctionTransformer: trasformazioni con stato
FunctionTransformer
è una soluzione potente ed elegante, ma è adatta solo quando si desidera applicare trasformazioni senza stato (cioè trasformazioni basate su regole che non dipendono da valori precedenti calcolati dai dati di addestramento). Se si desidera definire un Transformer personalizzato che possa trasformare i set di dati di testing basandosi sui valori osservati nel set di dati di addestramento, non è possibile utilizzare un FunctionTransformer
, e si dovrà adottare un approccio diverso.
Se ciò suona un po’ confuso, prenditi un minuto per ripensare alla funzione che abbiamo appena scritto per estrarre i titoli dei passeggeri:
def extract_title(X): """Estrae il titolo dal campo `name` di ogni passeggero.""" X['title'] = X['name'].str.split(', ', expand=True)[1].str.split('.', expand=True)[0]
La funzione è senza stato perché non ha memoria del passato; non utilizza valori pre-calcolati durante l’operazione. Ogni volta che chiamiamo questa funzione, verrà applicata da zero come se fosse stata eseguita per la prima volta.
Una funzione stateful, al contrario, trattiene le informazioni delle operazioni precedenti e le utilizza per implementare l’operazione corrente. Per illustrare questa distinzione, ecco due funzioni che sostituiscono i valori mancanti nel nostro dataset con un valore medio:
# Stateless - nessuna informazione precedente viene utilizzata nella trasformazionedef impute_mean_stateless(X): X['column1'] = X['column1'].fillna(X['column1'].mean())# Stateful - viene utilizzata informazione precedente sul set di addestramento nella trasformazione.column1_mean_train = np.mean(X_train['column1'])def impute_mean(X): X['column1'] = X['column1'].fillna(column1_mean_train) return X
La prima funzione è una funzione stateless perché nessuna informazione precedente viene utilizzata nella trasformazione; la media viene calcolata solo utilizzando il dataset X
che viene passato alla funzione.
La seconda è una funzione stateful che utilizza column1_mean_train
(cioè la media di column1
dal set di addestramento X_train
) per sostituire i valori mancanti in X
.
La distinzione tra trasformazione stateless e stateful potrebbe sembrare un po’ astrusa, ma è un concetto incredibilmente importante nelle attività di ML in cui abbiamo set di dati di addestramento e di test separati. Ogni volta che vogliamo sostituire i valori mancanti, scalare le caratteristiche o eseguire la codifica one-hot sui nostri set di dati di test, vogliamo che queste trasformazioni siano basate sui valori osservati nel set di dati di addestramento. In altre parole, vogliamo che il nostro Transformer sia fit al set di addestramento. Utilizzando l’esempio dell’imputazione dei valori mancanti con la media, vorremmo che il valore “medio” sia il valore medio del set di addestramento.
Il problema nell’utilizzo di FunctionTransformer
è che non può essere utilizzato per implementare trasformazioni stateful. Anche se un Transformer
creato con FunctionTransformer
tecnicamente ha il metodo .fit()
, chiamarlo non farà nulla e quindi non possiamo realmente “fit” questo Transformer ai dati di addestramento. Perché? Perché le trasformazioni in un Transformer creato da FunctionTransformer
dipendono sempre dal valore di input della funzione X
. Il nostro Transformer ricalcolerà sempre i valori utilizzando l’insieme di dati che gli viene passato; non ha modo di imputare/trasformare con un valore pre-calcolato.
1.2 Un esempio per illustrare le limitazioni di FunctionTransformer
Per illustrare questo, ecco un esempio in cui cerco di “fit” un Transformer basato su FunctionTransformer a un set di addestramento e quindi trasformo il set di dati di test utilizzando questo Transformer suppostamente “fitted”. Come puoi vedere, i valori mancanti nel set di dati di test non vengono sostituiti con il valore medio del set di dati di addestramento; vengono ricalcolati in base al set di dati di test. In altre parole, il Transformer non è stato in grado di applicare una trasformazione stateful.
# Mostra il set di dati di test, pre-trasformazioneX_test.head(3)

print("Media di X_train: ", X_train['age'].mean())# Media di X_train: 29.857414148681055print("Media di X_test: ", X_test['age'].mean())# Media di X_test: 29.97444952830189
def impute_mean(X): X['age'] = X['age'].fillna(X['age'].mean()) return Ximpute_mean_FT = FunctionTransformer(impute_mean) # Converte la funzione in Transformerprepro = impute_mean_FT.fit(X_train) # Il Transformer è "fitted" al set di dati di addestramentoprepro.transform(X_test) # Il Transformer "fitted" viene utilizzato per trasformare il set di dati di test

Se tutto ciò sembra un po’ confuso, non preoccupatevi. Il messaggio chiave è: se si vuole definire un Transformer personalizzato che può preelaborare i set di dati di test in base ai valori osservati nel set di dati di formazione, non si può utilizzare un FunctionTransformer
, ma è necessario adottare un approccio diverso.
2. Creare un Transformer personalizzato da zero
Un’alternativa consiste nel definire una nuova classe Transformer che eredita da una classe trovata nel modulo sklearn.base
: TransformerMixin
. Questa nuova classe funzionerà poi come Transformer ed è adatta per applicare trasformazioni stateless e stateful.
Ecco come prendere il nostro frammento di codice extract_title
e trasformarlo in un Transformer utilizzando questo approccio:
from sklearn.base import TransformerMixinclass ExtractTitle(TransformerMixin): def fit(self, X, y=None): return self def transform(self, X, y=None): X['title'] = X['name'].str.split(', ', expand=True)[1].str.split('.', expand=True)[0] return Xpreprocessor = Pipeline(steps=[ ('extract_title', ExtractTitle()),])X_train_transformed = preprocessor.fit_transform(X_train)X_train_transformed.head()

Come si può vedere, otteniamo la stessa trasformazione esatta che abbiamo ottenuto quando abbiamo costruito il nostro Transformer utilizzando FunctionTransformer.
2.1 Passare argomenti a un Transformer personalizzato
Se è necessario passare dati al proprio Transformer personalizzato, definire semplicemente un metodo __init__()
prima di definire i metodi fit()
e transform()
:
class IsUpperClass(TransformerMixin): def __init__(self, upper_class_titles): self.upper_class_titles = upper_class_titles def fit(self, X, y=None): return self def transform(self, X, y=None): X['upper_class'] = X['title'].apply(lambda x: 1 if x in self.upper_class_titles else 0) return Xpreprocessor = Pipeline(steps=[ ('IsUpperClass', IsUpperClass(upper_class_titles=['Dr', 'Col', 'Major', 'Lady', 'Rev', 'Sir', 'Capt'])),])X_train_transformed = preprocessor.fit_transform(X_train)X_train_transformed.head()

Ecco due metodi per costruire Transformer personalizzati in scikit-learn. La capacità di utilizzare questi metodi è estremamente preziosa e qualcosa che uso regolarmente nel mio lavoro quotidiano come Data Scientist. Spero che vi sia stato utile.
Se avete apprezzato questo articolo e desiderate ulteriori consigli e informazioni sul lavoro in Data Science, considerate di seguirmi qui su Nisoo o LinkedIn. Se desiderate avere accesso illimitato a tutte le mie storie (e al resto di Nisoo.com), potete registravi tramite il mio link di riferimento per $5 al mese. Non aggiunge alcun costo extra rispetto alla registrazione tramite la pagina di registrazione generale e aiuta a supportare la mia scrittura in quanto ricevo una piccola commissione.
Grazie per la lettura!