Un pipeline contiene múltiples transformadores (¡o incluso modelos!) y realiza operaciones en datos EN SECUENCIA. Comparen esto en ColumnTransformers que realiza operaciones en los datos EN PARALELO.
Cuando un pipeline se ajusta a los datos, se ajustan todos los transformadores dentro de ella. Cuando los datos se transforman usando un pipeline, los datos son transformados por el primer transformador primero, el segundo transformador segundo, etc. Un pipeline pueden contener cualquier número de transformadores siempre y cuando tengan los métodos .fit()
y .transform()
. Esto se llaman steps
(pasos).
Si lo necesitan, un solo estimador o modelo se puede colocar al final de un pipeline. Aprenderán más sobre esto después.
azones para utilizar pipelines:
Los pipelines usan menos códigos que hacer cada transformador individualmente. Debido a que cada transformador se ajusta en una sola llamada
.fit()
, y los datos son transformados por todos los transformadores en el pipeline en una sola llamada.transform()
, los pipelines usan muchos menos códigos.Los pipelines hacen que el procesamiento del flujo de trabajo sea más fáciles de entender. Al reducir el código y mostrar el diagrama del pipeline, les pueden mostrar a sus lectores claramente cómo sus datos se transforman antes de modelarlos.
Los pipelines son fáciles de usar en la producción de modelos. Cuando estén listos para despleguar el modelo para usar los nuevos datos, un pipeline de preprocesamiento puede garantizar que los nuevos datos puedan ser rápidas y fácilmente preprocesados para el modelado.
Los pipelines pueden evitar una fuga de datos. Los pipelines están diseñados para ajustarse únicamente a los datos de entrenamiento. Después aprenderán una técnica llamada “cross-validation”, y los pipelines simplificarán la realización de los mismos sin que se filtren los datos.
Veamos un ejemplo con Python:
# Imports
import pandas as pd
import numpy as np
from sklearn.pipeline import make_pipeline
from sklearn.impute import SimpleImputer
from sklearn.preprocessing import StandardScaler
from sklearn.model_selection import train_test_split
from sklearn import set_config
set_config(display='diagram')
# leer datos
#load the data
df = pd.read_csv("data/life_expectancy.csv", index_col='CountryYear')
df.head()
Status | Life expectancy | Adult Mortality | infant deaths | Alcohol | percentage expenditure | Hepatitis B | Measles | BMI | under-five deaths | Polio | Total expenditure | Diphtheria | HIV/AIDS | GDP | Population | thinness 1-19 years | thinness 5-9 years | Income composition of resources | Schooling | |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
CountryYear | ||||||||||||||||||||
Afghanistan2015 | 0 | 65.0 | 263 | 62 | 0.01 | 71.279624 | 65.0 | 1154 | 19.1 | 83 | 6.0 | 8.16 | 65.0 | 0.1 | 584.259210 | 33736494.0 | 17.2 | 17.3 | 0.479 | 10.1 |
Afghanistan2014 | 0 | 59.9 | 271 | 64 | 0.01 | 73.523582 | 62.0 | 492 | 18.6 | 86 | 58.0 | 8.18 | 62.0 | 0.1 | 612.696514 | 327582.0 | 17.5 | 17.5 | 0.476 | 10.0 |
Afghanistan2013 | 0 | 59.9 | 268 | 66 | 0.01 | 73.219243 | 64.0 | 430 | 18.1 | 89 | 62.0 | 8.13 | 64.0 | 0.1 | 631.744976 | 31731688.0 | 17.7 | 17.7 | 0.470 | 9.9 |
Afghanistan2012 | 0 | 59.5 | 272 | 69 | 0.01 | 78.184215 | 67.0 | 2787 | 17.6 | 93 | 67.0 | 8.52 | 67.0 | 0.1 | 669.959000 | 3696958.0 | 17.9 | 18.0 | 0.463 | 9.8 |
Afghanistan2011 | 0 | 59.2 | 275 | 71 | 0.01 | 7.097109 | 68.0 | 3013 | 17.2 | 97 | 68.0 | 7.87 | 68.0 | 0.1 | 63.537231 | 2978599.0 | 18.2 | 18.2 | 0.454 | 9.5 |
Podemos ver que los valores en las columnas están en diferentes escalas. Muchos tipos de modelo asumen que los datos se escalan antes de ajustar el modelo, por lo que en este caso querremos escalar los datos antes de modelar.
#inspect the data
print(df.info(), '\n')
<class 'pandas.core.frame.DataFrame'> Index: 2928 entries, Afghanistan2015 to Zimbabwe2000 Data columns (total 20 columns): # Column Non-Null Count Dtype --- ------ -------------- ----- 0 Status 2928 non-null int64 1 Life expectancy 2928 non-null float64 2 Adult Mortality 2928 non-null int64 3 infant deaths 2928 non-null int64 4 Alcohol 2735 non-null float64 5 percentage expenditure 2928 non-null float64 6 Hepatitis B 2375 non-null float64 7 Measles 2928 non-null int64 8 BMI 2896 non-null float64 9 under-five deaths 2928 non-null int64 10 Polio 2909 non-null float64 11 Total expenditure 2702 non-null float64 12 Diphtheria 2909 non-null float64 13 HIV/AIDS 2928 non-null float64 14 GDP 2485 non-null float64 15 Population 2284 non-null float64 16 thinness 1-19 years 2896 non-null float64 17 thinness 5-9 years 2896 non-null float64 18 Income composition of resources 2768 non-null float64 19 Schooling 2768 non-null float64 dtypes: float64(15), int64(5) memory usage: 480.4+ KB None
print(df.isna().sum())
Status 0 Life expectancy 0 Adult Mortality 0 infant deaths 0 Alcohol 193 percentage expenditure 0 Hepatitis B 553 Measles 0 BMI 32 under-five deaths 0 Polio 19 Total expenditure 226 Diphtheria 19 HIV/AIDS 0 GDP 443 Population 644 thinness 1-19 years 32 thinness 5-9 years 32 Income composition of resources 160 Schooling 160 dtype: int64
Podemos ver que diversas columnas le faltan datos. Se quiere imputar los datos faltantes antes que escalemos los datos, por lo que el pipeline se ordenará como:
- Paso 1. Imputar
- Paso 2. Escalar
Todos nuestros datos son numéricos, así que no necesitamos realizar una codificación one-hot a los datos. También podemos usar una imputación de mediana o de media en todas las columnas.
Si quisiéramos, PODRÍAMOS usar ColumnTransformer
para dividir las columnas por números enteros y flotantes y aplicar la imputación de la media a los flotantes, y la imputación de la mediana a los enteros, y luego escalarlos a todos.
Vamos a predecir la "'Life expectancy" (esperanza de vida), por lo que la fijaremos como objetivo.
# dividan la característica y el objetivo y realicen un train/test split.
X = df.drop(columns=['Life expectancy'])
y = df['Life expectancy']
X_train, X_test, y_train, y_test = train_test_split(X, y, random_state = 42)
# instancien un imputer y un scaler
median_imputer = SimpleImputer(strategy='median')
scaler = StandardScaler()
# combinen el imputer y scale en un pipeline
preprocessing_pipeline = make_pipeline(median_imputer, scaler)
preprocessing_pipeline
Pipeline(steps=[('simpleimputer', SimpleImputer(strategy='median')), ('standardscaler', StandardScaler())])In a Jupyter environment, please rerun this cell to show the HTML representation or trust the notebook.
On GitHub, the HTML representation is unable to render, please try loading this page with nbviewer.org.
Pipeline(steps=[('simpleimputer', SimpleImputer(strategy='median')), ('standardscaler', StandardScaler())])
SimpleImputer(strategy='median')
StandardScaler()
Podemos ver en el diagrama anterior que el primer paso en la tubería es el imputer y el segundo paso es el scaler.
# ajustar el pipeline en los datos de entrenamiento
preprocessing_pipeline.fit(X_train)
Pipeline(steps=[('simpleimputer', SimpleImputer(strategy='median')), ('standardscaler', StandardScaler())])In a Jupyter environment, please rerun this cell to show the HTML representation or trust the notebook.
On GitHub, the HTML representation is unable to render, please try loading this page with nbviewer.org.
Pipeline(steps=[('simpleimputer', SimpleImputer(strategy='median')), ('standardscaler', StandardScaler())])
SimpleImputer(strategy='median')
StandardScaler()
# transformen los conjuntos de entrenamiento y de prueba
X_train_processed = preprocessing_pipeline.transform(X_train)
X_test_processed = preprocessing_pipeline.transform(X_test)
Los transformadores scikit-learn y pipelines siempre devuelven arrays de NumPy, no en DataFrames de Pandas. Podemos usar np.isnan(array).sum().sum()
(no el método .isna()
) para contar los valores faltantes en el array resultante. Podemos ver que no hay valores faltantes y que todos los valores parecen estar escalados.
# inspeccionen el resultado de la transformación
print(np.isnan(X_train_processed).sum().sum(), 'missing values \n')
0 missing values
X_train_processed
array([[ 0. , -0.81229166, -0.26366021, ..., -0.87868801, 1.19451878, 1.92222335], [ 0. , 1.43809769, 0.15576412, ..., 0.58477555, 0.22791761, 0.08271906], [ 0. , 2.02690924, -0.18501814, ..., 0.87303352, -0.68443553, -0.80637468], ..., [ 0. , -1.10266448, -0.11511409, ..., -0.10260885, -0.88170108, -1.17427554], [ 0. , -0.73163255, -0.24618419, ..., -0.96738278, 0.97259504, 0.87983758], [ 0. , 1.43003177, -0.20249416, ..., 1.07259673, -3.11080174, -2.24731971]])
Pipelines y ColumnTransformers juntos¶
Los pipelines pueden ir dentro de ColumnTransformer para realizar una transformación secuencial después de dividir las columnas. Y los objetos ColumnTransformer pueden colocarse dentro de los pipelines. Pueden lograr las transformaciones descritas anteriormente ya sea con un conjunto de ColumnTransformer en un pipeline O dos pipelines dentro de un ColumnTransformer. Hasta podrían poner un ColumnTransformer en un pipeline dentro de un ColumnTransformer dentro de un pipeline.
Como pueden observar, esto se puede volver un poco complicado, así que puede ser útil diagramar las transformaciones que quieren en los datos. ¿Quieren imputar la mediana de los datos numéricos, imputar la media de los datos flotantes, escalar ambos tipos, imputar datos de objetos con los valores más frecuentes y luego realizar una codificación one-hot?
El diagrama anterior usa un ColumnTransformer con dos pipelines dentro. Uno de esos pipelines también tiene un ColumnTransformer dentro. De hecho, cuando estemos listos para usar esto para modelar, pondremos todo este objeto de preprocesamiento dentro de OTRO pipeline con el modelo, al final de él.
Veamos un ejemplo en python!
# imports
import pandas as pd
import numpy as np
from sklearn.preprocessing import StandardScaler, OneHotEncoder
from sklearn.impute import SimpleImputer
from sklearn.compose import make_column_transformer, make_column_selector
from sklearn.pipeline import make_pipeline
from sklearn.model_selection import train_test_split
from sklearn import set_config
set_config(display='diagram')
# Import the data
df = pd.read_csv("data/medical_data.csv")
df.head()
State | Lat | Lng | Area | Children | Age | Income | Marital | Gender | ReAdmis | ... | Hyperlipidemia | BackPain | Anxiety | Allergic_rhinitis | Reflux_esophagitis | Asthma | Services | Initial_days | TotalCharge | Additional_charges | |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
0 | AL | 34.34960 | -86.72508 | Suburban | 1.0 | 53 | 86575.93 | Divorced | Male | 0 | ... | 0.0 | 1.0 | 1.0 | 1.0 | 0 | 1 | Blood Work | 10.585770 | 3726.702860 | 17939.403420 |
1 | FL | 30.84513 | -85.22907 | Urban | 3.0 | 51 | 46805.99 | Married | Female | 0 | ... | 0.0 | 0.0 | 0.0 | 0.0 | 1 | 0 | Intravenous | 15.129562 | 4193.190458 | 17612.998120 |
2 | SD | 43.54321 | -96.63772 | Suburban | 3.0 | 53 | 14370.14 | Widowed | Female | 0 | ... | 0.0 | 0.0 | 0.0 | 0.0 | 0 | 0 | Blood Work | 4.772177 | 2434.234222 | 17505.192460 |
3 | MN | 43.89744 | -93.51479 | Suburban | 0.0 | 78 | 39741.49 | Married | Male | 0 | ... | 0.0 | 0.0 | 0.0 | 0.0 | 1 | 1 | Blood Work | 1.714879 | 2127.830423 | 12993.437350 |
4 | VA | 37.59894 | -76.88958 | Rural | 1.0 | 22 | 1209.56 | Widowed | Female | 0 | ... | 1.0 | 0.0 | 0.0 | 1.0 | 0 | 0 | CT Scan | 1.254807 | 2113.073274 | 3716.525786 |
5 rows × 32 columns
Al revisar la cabecera de los datos vemos que "Complication_risk" es un valor categórico ordinal (Volveremos a hablar de ello después de seguir explorando los datos.).
Comprobaremos los tipos de datos con df.info()
df.info()
<class 'pandas.core.frame.DataFrame'> RangeIndex: 1000 entries, 0 to 999 Data columns (total 32 columns): # Column Non-Null Count Dtype --- ------ -------------- ----- 0 State 995 non-null object 1 Lat 1000 non-null float64 2 Lng 1000 non-null float64 3 Area 995 non-null object 4 Children 993 non-null float64 5 Age 1000 non-null int64 6 Income 1000 non-null float64 7 Marital 995 non-null object 8 Gender 995 non-null object 9 ReAdmis 1000 non-null int64 10 VitD_levels 1000 non-null float64 11 Doc_visits 1000 non-null int64 12 Full_meals_eaten 1000 non-null int64 13 vitD_supp 1000 non-null int64 14 Soft_drink 1000 non-null int64 15 Initial_admin 995 non-null object 16 HighBlood 1000 non-null int64 17 Stroke 1000 non-null int64 18 Complication_risk 995 non-null object 19 Overweight 1000 non-null int64 20 Arthritis 994 non-null float64 21 Diabetes 994 non-null float64 22 Hyperlipidemia 998 non-null float64 23 BackPain 992 non-null float64 24 Anxiety 998 non-null float64 25 Allergic_rhinitis 994 non-null float64 26 Reflux_esophagitis 1000 non-null int64 27 Asthma 1000 non-null int64 28 Services 995 non-null object 29 Initial_days 1000 non-null float64 30 TotalCharge 1000 non-null float64 31 Additional_charges 1000 non-null float64 dtypes: float64(14), int64(11), object(7) memory usage: 250.1+ KB
Aquí veremos una mezcla de tipos de datos con datos faltantes en columnas flotantes y columnas de objetos. No faltan datos enteros.
Podemos codificar datos de forma ordinal sin demasiado riesgo de fuga de datos. Generalmente son un número pequeño de variables ordinales y es probable que estén en datos de entrenamiento y de prueba. Si ese no es el caso, el transformador sklearn llamado OrdinalEncoder se puede agregar a un pipeline de preprocesamiento.
df['Complication_risk'].value_counts()
Medium 459 High 311 Low 221 Med 4 Name: Complication_risk, dtype: int64
Podemos ver que hay algunos valores incoherentes (Medium y Med). Podemos corregirlos en el mismo paso que codificamos de forma ordinal esta columna.
# Codificación ordinal "Complication_risk"
replacement_dictionary = {'High':2, 'Medium':1, 'Med':1, 'Low':0}
df['Complication_risk'].replace(replacement_dictionary, inplace=True)
df['Complication_risk']
0 1.0 1 2.0 2 1.0 3 1.0 4 0.0 ... 995 2.0 996 2.0 997 1.0 998 1.0 999 0.0 Name: Complication_risk, Length: 1000, dtype: float64
“Complication_risk” es ahora un tipo de dato flotante y con codificación ordinal.
# Dividan
X = df.drop('Additional_charges', axis=1)
y = df['Additional_charges']
X_train, X_test, y_train, y_test = train_test_split(X, y, random_state=42)
Crearemos nuestros selectores de columnas para usarlos con nuestro transformador de columna más tarde. En su lugar, podemos utilizar listas de columnas, pero un selector de columna lo hace más algorítmico. En este caso, el código seguirá funcionando, incluso si las columnas en un DataFrame cambian después que el pipeline se haya puesto en producción.
# Selectors
cat_selector = make_column_selector(dtype_include='object')
num_selector = make_column_selector(dtype_include='number')
Usaremos tres diferentes transformadores: SimpleImputer, StandardScaler y OneHotEncoder. Habrá dos diferentes SimpleImputers con diferentes estrategias de imputación: “most_frequent” y “mean”
# Imputers
freq_imputer = SimpleImputer(strategy='most_frequent')
mean_imputer = SimpleImputer(strategy='mean')
# Scaler
scaler = StandardScaler()
# One-hot encoder
ohe = OneHotEncoder(handle_unknown='ignore', sparse_output=False)
Usaremos DOS diferentes pipelines. Uno para los datos numéricos y otros para los datos nominales categóricos.
# Numeric pipeline
numeric_pipe = make_pipeline(mean_imputer, scaler)
numeric_pipe
Pipeline(steps=[('simpleimputer', SimpleImputer()), ('standardscaler', StandardScaler())])In a Jupyter environment, please rerun this cell to show the HTML representation or trust the notebook.
On GitHub, the HTML representation is unable to render, please try loading this page with nbviewer.org.
Pipeline(steps=[('simpleimputer', SimpleImputer()), ('standardscaler', StandardScaler())])
SimpleImputer()
StandardScaler()
# Categorical pipeline
categorical_pipe = make_pipeline(freq_imputer, ohe)
categorical_pipe
Pipeline(steps=[('simpleimputer', SimpleImputer(strategy='most_frequent')), ('onehotencoder', OneHotEncoder(handle_unknown='ignore', sparse_output=False))])In a Jupyter environment, please rerun this cell to show the HTML representation or trust the notebook.
On GitHub, the HTML representation is unable to render, please try loading this page with nbviewer.org.
Pipeline(steps=[('simpleimputer', SimpleImputer(strategy='most_frequent')), ('onehotencoder', OneHotEncoder(handle_unknown='ignore', sparse_output=False))])
SimpleImputer(strategy='most_frequent')
OneHotEncoder(handle_unknown='ignore', sparse_output=False)
make_column_transformer
utiliza tuplas para hacer coincidir los transformadores con los tipos de datos sobre los que deben actuar. Podemos usar pipelines como esos transformadores, que es lo que haremos a continuación.
# Tuples para Column Transformer
number_tuple = (numeric_pipe, num_selector)
category_tuple = (categorical_pipe, cat_selector)
# ColumnTransformer
preprocessor = make_column_transformer(number_tuple, category_tuple)
preprocessor
ColumnTransformer(transformers=[('pipeline-1', Pipeline(steps=[('simpleimputer', SimpleImputer()), ('standardscaler', StandardScaler())]), <sklearn.compose._column_transformer.make_column_selector object at 0x000001AB20385130>), ('pipeline-2', Pipeline(steps=[('simpleimputer', SimpleImputer(strategy='most_frequent')), ('onehotencoder', OneHotEncoder(handle_unknown='ignore', sparse_output=False))]), <sklearn.compose._column_transformer.make_column_selector object at 0x000001AB20385220>)])In a Jupyter environment, please rerun this cell to show the HTML representation or trust the notebook.
On GitHub, the HTML representation is unable to render, please try loading this page with nbviewer.org.
ColumnTransformer(transformers=[('pipeline-1', Pipeline(steps=[('simpleimputer', SimpleImputer()), ('standardscaler', StandardScaler())]), <sklearn.compose._column_transformer.make_column_selector object at 0x000001AB20385130>), ('pipeline-2', Pipeline(steps=[('simpleimputer', SimpleImputer(strategy='most_frequent')), ('onehotencoder', OneHotEncoder(handle_unknown='ignore', sparse_output=False))]), <sklearn.compose._column_transformer.make_column_selector object at 0x000001AB20385220>)])
<sklearn.compose._column_transformer.make_column_selector object at 0x000001AB20385130>
SimpleImputer()
StandardScaler()
<sklearn.compose._column_transformer.make_column_selector object at 0x000001AB20385220>
SimpleImputer(strategy='most_frequent')
OneHotEncoder(handle_unknown='ignore', sparse_output=False)
Ajustaremos el ColumnTransformer, el cual se llamará “preprocessor” en los datos de entrenamiento. (Nunca en los datos de prueba)
# fit on train
preprocessor.fit(X_train)
ColumnTransformer(transformers=[('pipeline-1', Pipeline(steps=[('simpleimputer', SimpleImputer()), ('standardscaler', StandardScaler())]), <sklearn.compose._column_transformer.make_column_selector object at 0x000001AB20385130>), ('pipeline-2', Pipeline(steps=[('simpleimputer', SimpleImputer(strategy='most_frequent')), ('onehotencoder', OneHotEncoder(handle_unknown='ignore', sparse_output=False))]), <sklearn.compose._column_transformer.make_column_selector object at 0x000001AB20385220>)])In a Jupyter environment, please rerun this cell to show the HTML representation or trust the notebook.
On GitHub, the HTML representation is unable to render, please try loading this page with nbviewer.org.
ColumnTransformer(transformers=[('pipeline-1', Pipeline(steps=[('simpleimputer', SimpleImputer()), ('standardscaler', StandardScaler())]), <sklearn.compose._column_transformer.make_column_selector object at 0x000001AB20385130>), ('pipeline-2', Pipeline(steps=[('simpleimputer', SimpleImputer(strategy='most_frequent')), ('onehotencoder', OneHotEncoder(handle_unknown='ignore', sparse_output=False))]), <sklearn.compose._column_transformer.make_column_selector object at 0x000001AB20385220>)])
<sklearn.compose._column_transformer.make_column_selector object at 0x000001AB20385130>
SimpleImputer()
StandardScaler()
<sklearn.compose._column_transformer.make_column_selector object at 0x000001AB20385220>
SimpleImputer(strategy='most_frequent')
OneHotEncoder(handle_unknown='ignore', sparse_output=False)
El método fit funcionó para ajustar todos los 4 transformadores dentro de ColumnTransformer. Usaremos este ColumnTransformer ajustado para transformar nuestros conjuntos de datos de entrenamiento y de prueba.
# transform train and test
X_train_processed = preprocessor.transform(X_train)
X_test_processed = preprocessor.transform(X_test)
Todos los transformadores Scikit-Learn devuelven arrays de NumPy, NO DataFrames de Pandas. Debido a esto, necesitamos usar funciones de Numpy, como np.isnan(), para inspeccionar nuestros datos. En algunos casos podemos transformar fácilmente nuestros datos devuelta a un DataFrame de Pandas, pero no siempre es fácil obtener la columna de nombres devuelta. El OneHotEncoder creó columnas extras y es complicado recuperar los nombres de columna correctos para todas las columnas.
Nos aseguraremos de que sustituyan los datos faltantes. que los datos categóricos realicen una codificación one-hot y que los datos numéricos se escalen.
# Comprueben los valores faltantes y que los datos se escalen y tengan una codificación one-hot
print(np.isnan(X_train_processed).sum().sum(), 'missing values in training data')
print(np.isnan(X_test_processed).sum().sum(), 'missing values in testing data')
0 missing values in training data 0 missing values in testing data
print('All data in X_train_processed are', X_train_processed.dtype)
print('All data in X_test_processed are', X_test_processed.dtype)
All data in X_train_processed are float64 All data in X_test_processed are float64
print('shape of data is', X_train_processed.shape)
shape of data is (750, 97)
X_train_processed
array([[-0.50820472, 0.28193545, -0.06527826, ..., 0. , 1. , 0. ], [-0.72064168, 0.25283631, 1.23912135, ..., 0. , 0. , 0. ], [-0.49340318, 0.48282262, -0.50007813, ..., 0. , 1. , 0. ], ..., [ 0.27295848, 0.63816773, -0.93487801, ..., 0. , 0. , 0. ], [-0.89653885, -1.73729615, -0.93487801, ..., 0. , 0. , 0. ], [ 0.30727477, 1.1082109 , -0.93487801, ..., 0. , 0. , 0. ]])
Si bien podemos ver todas las columnas aquí, observamos que no faltan datos, todos los datos están en tipo float64 y que hay 97 columnas ahora, en lugar del original, 32. Es justo asumir que las columnas categóricas han realizado una codificación one-hot.