Minería de Datos: Preprocesamiento y clasificación
Máster en Ciencias de Datos e Ingeniería de Computadores
Antes de nada vamos a recordar cómo seleccionar atributos e instancias.
Cargamos pandas.
Leemos datos
Podemos consultar los atributos con:
Sobre este conjunto de datos haremos las siguientes operaciones de selección (en todas ellas el resultado es un nuevo conjunto de datos):
Es fácil filtrar un valor numérico o por valor exacto:
Se combina con & y | (no dobles) usando paréntesis:
Es más difícil si queremos filtrar según uno o varios valores:
Por varios valores
| name | height | mass | hair_color | skin_color | eye_color | homeworld | species | |
|---|---|---|---|---|---|---|---|---|
| 28 | Wicket Systri Warrick | 88.0 | 20 | brown | brown | brown | Endor | Ewok |
| 50 | Eeth Koth | 171.0 | NaN | black | brown | brown | Iridonia | Zabrak |
| 67 | Dexter Jettster | 198.0 | 102 | none | brown | yellow | Ojom | Besalisk |
| 77 | Tarfful | 234.0 | 136 | brown | brown | blue | Kashyyyk | Wookiee |
Expresiones regulares
| name | height | mass | hair_color | skin_color | eye_color | homeworld | species | |
|---|---|---|---|---|---|---|---|---|
| 2 | R2-D2 | 96.0 | 32 | NaN | white, blue | red | Naboo | Droid |
| 37 | Watto | 137.0 | NaN | black | blue, grey | yellow | Toydaria | Toydarian |
| 43 | Ayla Secura | 178.0 | 55 | none | blue | hazel | Ryloth | Twi'lek |
| 44 | Dud Bolt | 94.0 | 45 | none | blue, grey | yellow | Vulpter | Vulptereen |
| 45 | Gasgano | 122.0 | NaN | none | white, blue | black | Troiken | Xexto |
| 55 | Mas Amedda | 196.0 | NaN | none | blue | blue | Champala | Chagrian |
| 71 | Ratts Tyerell | 79.0 | 15 | none | grey, blue | NaN | Aleen Minor | Aleena |
| 75 | Shaak Ti | 178.0 | 57 | none | red, blue, white | black | Shili | Togruta |
Se puede hacer directamente editando columns:
Index(['V1', 'V2', 'V3', 'V4', 'V5', 'V6', 'V7', 'V8'], dtype='object')
Pero lo suyo es renombrar usando un diccionario:
info nos devuelve los tipos (object son string)
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 87 entries, 0 to 86
Data columns (total 10 columns):
# Column Non-Null Count Dtype
--- ------ -------------- -----
0 name 87 non-null object
1 height 81 non-null float64
2 mass 59 non-null object
3 hair_color 81 non-null object
4 skin_color 85 non-null object
5 eye_color 84 non-null object
6 birth_year 43 non-null object
7 gender 84 non-null object
8 homeworld 77 non-null object
9 species 82 non-null object
dtypes: float64(1), object(9)
memory usage: 6.9+ KB
Para conocer información sobre los valores numéricos se puede hacer:
Y más en detalle se puede usar describe con un atributo:
Para ver las frecuencias se puede usar values_counts().
Human 35
Droid 5
Gungan 3
Mirialan 2
Wookiee 2
Name: species, dtype: int64
Se puede normalizar (y no ordenar si se quiere):
value_counts() también permite medir frecuencia de combinaciones:
species hair_color
Human brown 0.181818
black 0.103896
none 0.038961
blond 0.038961
Gungan none 0.038961
Twi'lek none 0.025974
Mirialan black 0.025974
Human white 0.025974
dtype: float64
Un problema habitual suele consistir en la presencia de datos datos.
Es importante tener claro cómo leer los datos indicando la posible ausencia de valor, usando na_values:
Hay múltiples técnicas para tratar los datos perdidos. Es importante valorar si la técnica de aprendizaje es capaz de trabajar con datos perdidos o no.
Para conocer los nulos (en porcentaje):
Se pueden eliminar o bien los atributos que tienen demasiados nulos, o eliminar tuplas.
Eliminar atributos que superen un umbral:
Eliminar todas las filas con algún nulo
nombre edad color_pelo ciudad
3 Virginia 41.0 negro Paris
Dado que el aprendizaje en scikit-learn no es compatible con valores perdidos, vamos a probar distintas opciones que la propia librería nos permite.
Para probar los métodos añadidos nulos al dataset:
#Prepare the dataset to test sk-learn imputation values tools
np.random.seed(42)
rows = np.random.randint(0, np.shape(X_iris)[0], 50)
# No modifico la última característica
cols = np.random.randint(0, np.shape(X_iris)[1]-1, 50)
X_iris_missing = X_iris.to_numpy()
#Add missing values in random entries from the iris dataset
X_iris_missing[rows, cols] = np.NaN
X_iris_missing = pd.DataFrame(X_iris_missing, columns=X_iris.columns)Tenemos ahora nulos
El paquete yellowbricks presenta muchas opciones visuales.
Los objetos de tipo Impute permite reemplazar los valores nulos. Para ello pueden usar un valor constante o una estadística (media, mediana o más frecuente) para cada columna con nulos.
from sklearn.impute import SimpleImputer
# strategy puede ser "mean", "median", "most_frequent", "constant".
imp = SimpleImputer(missing_values=np.nan, strategy='mean')
imputed_X = pd.DataFrame(imp.fit_transform(X_iris_missing), columns=X_iris.columns)
print(imputed_X.iloc[:10,:]) sepal length (cm) sepal width (cm) petal length (cm) petal width (cm)
0 5.100000 3.500000 1.400000 0.2
1 4.900000 3.060606 1.400000 0.2
2 4.700000 3.200000 1.300000 0.2
3 4.600000 3.100000 1.500000 0.2
4 5.000000 3.600000 1.400000 0.2
5 5.400000 3.900000 1.700000 0.4
6 4.600000 3.400000 1.400000 0.3
7 5.832836 3.400000 1.500000 0.2
8 4.400000 2.900000 3.759124 0.2
9 4.900000 3.100000 1.500000 0.1
En este caso cara característica con valores perdidos se modela en función de otras usadas para estimar la imputación.
from sklearn.experimental import enable_iterative_imputer
#Build an iterative imutation object and fit it to the data
from sklearn.impute import IterativeImputer
imp = IterativeImputer(max_iter=10, random_state=0).fit(X_iris_missing)
imputed_X = pd.DataFrame(imp.transform(X_iris_missing), columns=X_iris.columns)
print(imputed_X.iloc[:10,:]) sepal length (cm) sepal width (cm) petal length (cm) petal width (cm)
0 5.100000 3.500000 1.40000 0.2
1 4.900000 3.319683 1.40000 0.2
2 4.700000 3.200000 1.30000 0.2
3 4.600000 3.100000 1.50000 0.2
4 5.000000 3.600000 1.40000 0.2
5 5.400000 3.900000 1.70000 0.4
6 4.600000 3.400000 1.40000 0.3
7 5.009993 3.400000 1.50000 0.2
8 4.400000 2.900000 1.35012 0.2
9 4.900000 3.100000 1.50000 0.1
Se pueden imputar usando el algoritmo de K vecinos (KNN). Para cada atributo perdido se calcula a partir de los K vecinos más cercanos que no sea nulo. Los vecinos pueden ser diferentes para cada atributo.
Si no encuentra vecinos sin nulos, el atributo es borrado.
from sklearn.impute import KNNImputer
Knn_imp = KNNImputer(n_neighbors=4).fit(X_iris_missing)
imputed_X = pd.DataFrame(Knn_imp.transform(X_iris_missing), columns=X_iris.columns)
print(imputed_X.iloc[:10,:]) sepal length (cm) sepal width (cm) petal length (cm) petal width (cm)
0 5.10 3.50 1.400 0.2
1 4.90 3.45 1.400 0.2
2 4.70 3.20 1.300 0.2
3 4.60 3.10 1.500 0.2
4 5.00 3.60 1.400 0.2
5 5.40 3.90 1.700 0.4
6 4.60 3.40 1.400 0.3
7 5.15 3.40 1.500 0.2
8 4.40 2.90 1.375 0.2
9 4.90 3.10 1.500 0.1
También se puede ver la distribución de nulos en las instancias (por ver si hay instancias con muchos concentrados).
Sklearn permite mostrar visualmente los valores nulos con MissingIndicator.
from sklearn.impute import MissingIndicator
indicator = MissingIndicator(missing_values=np.nan).fit(X_iris_missing)
print(indicator.transform(X_iris_missing)[:8,:])
print(indicator.features_)[[False False False]
[False True False]
[False False False]
[False False False]
[False False False]
[False False False]
[False False False]
[ True False False]]
[0 1 2]
Visualmente se puede mostrar usando yellobrick
La otra opción es cuando se aplique el modelo, añadir el add_indicator (falso por defecto) para que lo muestre.
#Build an Knn imutation object and fit it to the data, setting add_indicator=True
Knn_imp = KNNImputer(n_neighbors=4, add_indicator=True).fit(X_iris_missing)
print(X_iris_missing.head(5))
imputed_X = pd.DataFrame(Knn_imp.transform(X_iris_missing))
print(imputed_X.head(5)) sepal length (cm) sepal width (cm) petal length (cm) petal width (cm)
0 5.1 3.5 1.4 0.2
1 4.9 NaN 1.4 0.2
2 4.7 3.2 1.3 0.2
3 4.6 3.1 1.5 0.2
4 5.0 3.6 1.4 0.2
0 1 2 3 4 5 6
0 5.1 3.50 1.4 0.2 0.0 0.0 0.0
1 4.9 3.45 1.4 0.2 0.0 1.0 0.0
2 4.7 3.20 1.3 0.2 0.0 0.0 0.0
3 4.6 3.10 1.5 0.2 0.0 0.0 0.0
4 5.0 3.60 1.4 0.2 0.0 0.0 0.0
Estandarización es un requisito de muchos modelos de ML, como los basados en distancias.
scikit-learn permite hacer estandarización, hay múltiples opciones
#Build a preprocessing object
from sklearn.preprocessing import StandardScaler
iris_dataset = datasets.load_iris(as_frame=True)
X_iris = iris_dataset.data.copy()
scaler = StandardScaler().fit(X_iris)
#Check the mean and the std of the training set
print(scaler.mean_)
print(scaler.scale_)[5.84333333 3.05733333 3.758 1.19933333]
[0.82530129 0.43441097 1.75940407 0.75969263]
Una vez entrenado se puede aplicar:
X_iris_scaled = scaler.transform(X_iris)
print(X_iris.iloc[:5,:])
print("StandardScaler: ")
print(X_iris_scaled[:5,:]) sepal length (cm) sepal width (cm) petal length (cm) petal width (cm)
0 5.1 3.5 1.4 0.2
1 4.9 3.0 1.4 0.2
2 4.7 3.2 1.3 0.2
3 4.6 3.1 1.5 0.2
4 5.0 3.6 1.4 0.2
StandardScaler:
[[-0.90068117 1.01900435 -1.34022653 -1.3154443 ]
[-1.14301691 -0.13197948 -1.34022653 -1.3154443 ]
[-1.38535265 0.32841405 -1.39706395 -1.3154443 ]
[-1.50652052 0.09821729 -1.2833891 -1.3154443 ]
[-1.02184904 1.24920112 -1.34022653 -1.3154443 ]]
Confirmemos:
#Transform the dataset using the preprocessin object and check results
X_scaled = pd.DataFrame(scaler.fit_transform(X_iris), columns=X_iris.columns)
print(X_scaled.mean(axis=0))
print(X_scaled.std(axis=0))sepal length (cm) -4.736952e-16
sepal width (cm) -7.815970e-16
petal length (cm) -4.263256e-16
petal width (cm) -4.736952e-16
dtype: float64
sepal length (cm) 1.00335
sepal width (cm) 1.00335
petal length (cm) 1.00335
petal width (cm) 1.00335
dtype: float64
Visualmente
Otro muy común es MinMaxScaler:
from sklearn.preprocessing import MinMaxScaler
X_iris_scaled2 = MinMaxScaler().fit_transform(X_iris)
print(X_iris.iloc[:5,:])
print("MinMaxScaler: ")
print(X_iris_scaled2[:5,:]) sepal length (cm) sepal width (cm) petal length (cm) petal width (cm)
0 5.1 3.5 1.4 0.2
1 4.9 3.0 1.4 0.2
2 4.7 3.2 1.3 0.2
3 4.6 3.1 1.5 0.2
4 5.0 3.6 1.4 0.2
MinMaxScaler:
[[0.22222222 0.625 0.06779661 0.04166667]
[0.16666667 0.41666667 0.06779661 0.04166667]
[0.11111111 0.5 0.05084746 0.04166667]
[0.08333333 0.45833333 0.08474576 0.04166667]
[0.19444444 0.66666667 0.06779661 0.04166667]]
La normalización es escalar las muestras individuales para que tenga una normal unidad.
Es esencial para espresiones cuadráticas, o que usen un kernel que mida similaridad de pares de instancias.
from sklearn.preprocessing import normalize
print(X_iris.iloc[:4,:])
X_normalized = pd.DataFrame(normalize(X_iris), columns=X_iris.columns)
print(X_normalized.iloc[:4,:]) sepal length (cm) sepal width (cm) petal length (cm) petal width (cm)
0 5.1 3.5 1.4 0.2
1 4.9 3.0 1.4 0.2
2 4.7 3.2 1.3 0.2
3 4.6 3.1 1.5 0.2
sepal length (cm) sepal width (cm) petal length (cm) petal width (cm)
0 0.803773 0.551609 0.220644 0.031521
1 0.828133 0.507020 0.236609 0.033801
2 0.805333 0.548312 0.222752 0.034269
3 0.800030 0.539151 0.260879 0.034784
Es común atributos con valores categóricos. Scikit-learn no es capaz de procesarlos, por lo que es necesario transformarlo a valores numéricos.
LabelEncoder y OrdinalEncoder: Asigna un valor numérico por cada categoría.
OneHotEncoder: Codifica cada categoría usando una nueva columna.
Los Label/OrdinalEncoder asigna un orden entre las categorías que suele ser ‘falso’ si ese concepto no existe.
Ambos asignan un valor numérico distinto a cada categoría.
Diferencia:
OrdinalEncoder puede procesar varias columnas, se usa para características.
LabelEncoder solo procesa un elemento, se usa para el atributo objetivo (target).
Advertencia: Evitar hacer esto:
from sklearn.preprocessing import LabelEncoder
targets_train = ["rubio", "moreno", "pelirrojo", "azul"]
targets_test = ["moreno", "pelirrojo"]
targets_train_num = LabelEncoder().fit_transform(targets_train)
targets_test_num = LabelEncoder().fit_transform(targets_test)
print(targets_train)
print(targets_train_num)
print(targets_test)
print(targets_test_num)['rubio', 'moreno', 'pelirrojo', 'azul']
[3 1 2 0]
['moreno', 'pelirrojo']
[0 1]
Las etiquetas no coinciden.
Para evitarlo hay que hacer fit solo con el de entrenamiento.
labeler_target = LabelEncoder()
targets_train_num = labeler_target.fit_transform(targets_train)
targets_test_num = labeler_target.transform(targets_test)
print(targets_train)
print(targets_train_num)
print(targets_test)
print(targets_test_num)['rubio', 'moreno', 'pelirrojo', 'azul']
[3 1 2 0]
['moreno', 'pelirrojo']
[1 2]
Guardar siempre los labeler (diccionario por nombre, …).
Ejemplo:
data_train_df = pd.DataFrame({'age': [30, 41, 42, 21],
'pelo': targets_train,
'ojos': ['azules', 'verdes', 'marrones', 'marrones']})
data_test_df = pd.DataFrame({'age': [25, 23],
'pelo': targets_test,
'ojos': ['verdes', 'azules']})
print(data_train_df) age pelo ojos
0 30 rubio azules
1 41 moreno verdes
2 42 pelirrojo marrones
3 21 azul marrones
Vamos a aplicar el etiquetado.
labelers = {}
cols = {}
atribs = ["pelo", "ojos"]
data_train_num = data_train_df.copy()
data_test_num = data_test_df.copy()
for i in atribs:
cols[i] = LabelEncoder()
data_train_num[i] = cols[i].fit_transform(data_train_num[i])
data_test_num[i] = cols[i].transform(data_test_num[i])
print(data_train_num)
print(data_test_num) age pelo ojos
0 30 3 0
1 41 1 2
2 42 2 1
3 21 0 1
age pelo ojos
0 25 1 2
1 23 2 0
OrdinalEncoderfrom sklearn.preprocessing import OrdinalEncoder
atribs = ["pelo", "ojos"]
labelers = OrdinalEncoder(dtype=np.int32) # Por defecto usa float
data_train_num = data_train_df.copy()
data_test_num = data_test_df.copy()
data_train_num[atribs] = labelers.fit_transform(data_train_df[atribs])
data_test_num[atribs] = labelers.transform(data_test_df[atribs])
print(data_train_num)
print(data_test_num) age pelo ojos
0 30 3 0
1 41 1 2
2 42 2 1
3 21 0 1
age pelo ojos
0 25 1 2
1 23 2 0
También se puede invertir el etiquetado:
Esta codificación está considerando un orden entre categorías.
En algunos casos como [‘pequeño’, ‘mediano’, ‘grande’] puede tener sentido pero la mayoría de las veces no.
Cuando no (como ‘pelo’ o ‘color’ del ejemplo anterior) es necesario aplicar OneHotEncoder.
OneHotEncoder crea una columna por categoría (ej: ‘azul’) indicando si se cumple o no.
Aumenta el número de columnas.
Evita suponer un orden.
Se puede convertir a dataframe:
new_columns = encoder.get_feature_names_out()
print(new_columns)
data_train_hot = pd.DataFrame(data_train_hot, columns=new_columns)
# Copio el resto de atributos
data_train_hot['age'] = data_train_df['age']
print(data_train_hot)['pelo_azul' 'pelo_moreno' 'pelo_pelirrojo' 'pelo_rubio' 'ojos_azules'
'ojos_marrones' 'ojos_verdes']
pelo_azul pelo_moreno pelo_pelirrojo pelo_rubio ojos_azules \
0 0 0 0 1 1
1 0 1 0 0 0
2 0 0 1 0 0
3 1 0 0 0 0
ojos_marrones ojos_verdes age
0 0 0 30
1 0 1 41
2 1 0 42
3 1 0 21
Pandas ya soporta el hotencoding, pero presenta problemas.
| pelo_azul | pelo_moreno | pelo_pelirrojo | pelo_rubio | ojos_azules | ojos_marrones | ojos_verdes | |
|---|---|---|---|---|---|---|---|
| 0 | 0 | 0 | 0 | 1 | 1 | 0 | 0 |
| 1 | 0 | 1 | 0 | 0 | 0 | 0 | 1 |
| 2 | 0 | 0 | 1 | 0 | 0 | 1 | 0 |
| 3 | 1 | 0 | 0 | 0 | 0 | 1 | 0 |
Recomiendo usar OneHotEncoder por tener más opciones.
Si el valor numérico es binario, ej: vivo/muerto no es necesario aplicar el hotencoding.
columns = ["Employed", "Place", 'Browser']
X = [['employed', 'from US', 'uses Safari'], ['unemployed', 'from Europe', 'uses Firefox'], ['unemployed', 'from Asia', 'uses Chrome']]
enc = OneHotEncoder(drop='if_binary')
trans_X = enc.fit_transform(X)
transformed_X = pd.DataFrame(trans_X.toarray(), columns=enc.get_feature_names_out())
print(transformed_X) x0_unemployed x1_from Asia x1_from Europe x1_from US x2_uses Chrome \
0 0.0 0.0 0.0 1.0 0.0
1 1.0 0.0 1.0 0.0 0.0
2 1.0 1.0 0.0 0.0 1.0
x2_uses Firefox x2_uses Safari
0 0.0 1.0
1 1.0 0.0
2 0.0 0.0
OneHotEncoder es para convertir atributos, LabelBinarizer es para convertir el target (por ejemplo: para redes neuronales).
La diferencia principal es:
LabelBinarizer devuelve una matriz numpy, OneHotEncoder devuelve por defecto matriz sparse.
LabelBinarizer devuelve de tipo entero, OneHotEncoder devuelve por defecto de tipo float.
OneHotEncoder puede convertir distintos atributos a la vez, LabelBinarizer no.
from sklearn.preprocessing import LabelBinarizer
lb = LabelBinarizer()
lb.fit_transform(data_train_df['ojos'])
# Da error
# lb.fit_transform(data_train_df['ojos','pelo'])array([[1, 0, 0],
[0, 0, 1],
[0, 1, 0],
[0, 1, 0]])
¿Cuándo usar LabelBinarizer?
Para etiquetar el objetivo.
¿Qué pasa cuando un atributo es una combinación de valores?
Ejemplo: género de una película.
La clase Multilabel permite identificar:
Aviso: Trabaja con vector de conjunto, no con vector:
mlb = MultiLabelBinarizer()
print(mlb.fit_transform(['sci-fi', 'thriller', 'comedy']))
list(mlb.classes_)[[1 1 0 0 1 0 1 0 0 0 0 1 0 0]
[0 0 0 1 0 1 1 1 0 0 1 0 1 0]
[0 1 1 1 0 0 0 0 1 1 0 0 0 1]]
['-', 'c', 'd', 'e', 'f', 'h', 'i', 'l', 'm', 'o', 'r', 's', 't', 'y']
La otra opción es un vector de vectores:
OneHotEncoderScikit-learn permite combinar transformaciones con ColumnTransformer.
from sklearn.compose import make_column_transformer
transformer = make_column_transformer(
(OneHotEncoder(), ['pelo', 'ojos']),
remainder='passthrough') # Para ignorar el resto y no dar error
transformed = transformer.fit_transform(data_train_df)
data_train_num = pd.DataFrame(transformed, columns=transformer.get_feature_names_out())
print(data_train_num) onehotencoder__pelo_azul onehotencoder__pelo_moreno \
0 0.0 0.0
1 0.0 1.0
2 0.0 0.0
3 1.0 0.0
onehotencoder__pelo_pelirrojo onehotencoder__pelo_rubio \
0 0.0 1.0
1 0.0 0.0
2 1.0 0.0
3 0.0 0.0
onehotencoder__ojos_azules onehotencoder__ojos_marrones \
0 1.0 0.0
1 0.0 0.0
2 0.0 1.0
3 0.0 1.0
onehotencoder__ojos_verdes remainder__age
0 0.0 30.0
1 1.0 41.0
2 0.0 42.0
3 0.0 21.0
ColumnTransformer permite procesar distintos datasets.
En conjunción con make_column_selector (que permite filtrar atributos por su tipo) es muy potente y cómodo.
from sklearn.compose import make_column_transformer
from sklearn.compose import make_column_selector
X = pd.DataFrame({'city': ['London', 'London', 'Paris', 'Sallisaw'],
'rating': [5, 3, 4, 5]})
ct = make_column_transformer(
(StandardScaler(),
make_column_selector(dtype_include=np.number)), # rating
(OneHotEncoder(),
make_column_selector(dtype_include=object))) # city
ct.fit_transform(X)array([[ 0.90453403, 1. , 0. , 0. ],
[-1.50755672, 1. , 0. , 0. ],
[-0.30151134, 0. , 1. , 0. ],
[ 0.90453403, 0. , 0. , 1. ]])
Facilitan aplicar distintos preprocesamientos.
Un pipeline se compone de una serie de transformaciones que van sufriendo el dataset (se puede incluir el modelo a aprender).
Un pipeline se usa igual que un modelo.
from sklearn.svm import SVC
from sklearn.preprocessing import StandardScaler
from sklearn.pipeline import Pipeline
iris_targets = iris_dataset.target
from sklearn.model_selection import cross_val_score, train_test_split
X_train, X_test, y_train, y_test = train_test_split(X_iris, iris_targets,
random_state=0)
pipe = Pipeline([('scaler', StandardScaler()), ('svc', SVC())])
pipe.fit(X_train, y_train)
pipe.score(X_test, y_test)0.9736842105263158
También se puede aplicar con validación cruzada:
from sklearn.model_selection import cross_val_score
pipe = Pipeline([('scaler', StandardScaler()), ('svc', SVC())])
scores = cross_val_score(pipe, X_iris, iris_targets, cv=5)
print(scores)
print(scores.mean())[0.96666667 0.96666667 0.96666667 0.93333333 1. ]
0.9666666666666666
Por comodidad se puede usar make_pipeline con tantos atributos como procesamientos y/o mdelos.
También se pueden combinar con ColumnTransformer.
from sklearn.model_selection import cross_val_score
trans = make_column_transformer(
(StandardScaler(), ["age"]),
(OneHotEncoder(), ["pelo", "ojos"])
)
trans.fit_transform(data_train_df)
pipe = make_pipeline(trans, SVC())
print(data_train_df)
pipe.fit(data_train_df, [0, 0, 1, 1])
print(data_test_df)
pipe.predict(data_test_df) age pelo ojos
0 30 rubio azules
1 41 moreno verdes
2 42 pelirrojo marrones
3 21 azul marrones
age pelo ojos
0 25 moreno verdes
1 23 pelirrojo azules
array([0, 0])
A veces no nos interesa mostrar si un valor numérico es suficientemente alto o no. Vienen bien para algunos clasificadores, como el Bernoulli Restricted Boltzmann Machine, y son muy populares en procesamiento de texto.
Por ejemplo, a partir del número de cigarros al día identificar si es un fumador habitual.
A menudo no nos interesa un valor numéricos (ej: age) sino convertirlo en un conjunto discreto de valores (joven, adulto, mayor).
La clase K-bins permite discretizar.
from sklearn.preprocessing import KBinsDiscretizer
#Build a discretizer object indicating three bins for every feature
est = KBinsDiscretizer(n_bins=[3, 3, 3, 3], encode='ordinal').fit(X_iris)
#Check feature maximum and minimum values
# print(np.max(X_iris, axis = 0))
# print(np.min(X_iris, axis = 0))
#Check binning intervals
print(est.bin_edges_)[array([4.3, 5.4, 6.3, 7.9]) array([2. , 2.9, 3.2, 4.4])
array([1. , 2.63333333, 4.9 , 6.9 ])
array([0.1 , 0.86666667, 1.6 , 2.5 ])]
#Print discretization results
print(X_iris.iloc[:5,])
discretized_X = pd.DataFrame(est.transform(X_iris), columns=X_iris.columns)
print(discretized_X.iloc[:5,]) sepal length (cm) sepal width (cm) petal length (cm) petal width (cm)
0 5.1 3.5 1.4 0.2
1 4.9 3.0 1.4 0.2
2 4.7 3.2 1.3 0.2
3 4.6 3.1 1.5 0.2
4 5.0 3.6 1.4 0.2
sepal length (cm) sepal width (cm) petal length (cm) petal width (cm)
0 0.0 2.0 0.0 0.0
1 0.0 1.0 0.0 0.0
2 0.0 2.0 0.0 0.0
3 0.0 1.0 0.0 0.0
4 0.0 2.0 0.0 0.0
El criterio de discretización puede ser cambiado con el parámetro strategy.
Una tendencia común sería una uniforme:
est = KBinsDiscretizer(n_bins=5, encode='ordinal', strategy='uniform')
age_disc = est.fit_transform(data_train_df[['age']])
print(est.bin_edges_)
print(age_disc)[array([21. , 25.2, 29.4, 33.6, 37.8, 42. ])]
[[2.]
[4.]
[4.]
[0.]]
No todos los rangos tienen interés, pueden concentrarse.
A menudo la mejor estrategia depende de la frecuencia (comportamiento por defecto).
est = KBinsDiscretizer(n_bins=5, encode='ordinal', strategy='quantile')
age_disc = est.fit_transform(data_train_df[['age']])
print(est.bin_edges_)
print(age_disc)[array([21. , 26.4, 32.2, 38.8, 41.4, 42. ])]
[[1.]
[3.]
[4.]
[0.]]
De esta manera, discretiza más en detalle los intervalos más comunes.
La otra opción es la estrategia kmean que aplica una clasificación kmeans sobre cada algoritmo.
#Build a discretizer object indicating three bins for every feature and using the kmeans strategy
est = KBinsDiscretizer(n_bins=[3, 3, 3, 3], encode='ordinal', strategy='kmeans').fit(X_iris)
#Check binning intervals and results
print(est.bin_edges_)
discretized_X = pd.DataFrame(est.transform(X_iris), columns=X_iris.columns)
print(discretized_X.iloc[:5,])[array([4.3 , 5.53309253, 6.54877049, 7.9 ])
array([2. , 2.85216858, 3.43561538, 4.4 ])
array([1. , 2.87637037, 4.95950081, 6.9 ])
array([0.1 , 0.79151852, 1.70547504, 2.5 ])]
sepal length (cm) sepal width (cm) petal length (cm) petal width (cm)
0 0.0 2.0 0.0 0.0
1 0.0 1.0 0.0 0.0
2 0.0 1.0 0.0 0.0
3 0.0 1.0 0.0 0.0
4 0.0 2.0 0.0 0.0
CAIM es un algoritmo de discretización muy usado. En Python está disponible en el paquete caimcaim:
from caimcaim import CAIMD
caim_dis = CAIMD()
caim_dis.fit(X_scaled, iris_targets)
print(X_scaled.iloc[:5,])
discretized_X = caim_dis.transform(X_scaled)
print(discretized_X.iloc[:5,:])Categorical []
# 0 GLOBAL CAIM 26.636271740334553
# 1 GLOBAL CAIM 17.382507167267576
# 2 GLOBAL CAIM 45.55892255892255
# 3 GLOBAL CAIM 46.16156736446592
sepal length (cm) sepal width (cm) petal length (cm) petal width (cm)
0 -0.900681 1.019004 -1.340227 -1.315444
1 -1.143017 -0.131979 -1.340227 -1.315444
2 -1.385353 0.328414 -1.397064 -1.315444
3 -1.506521 0.098217 -1.283389 -1.315444
4 -1.021849 1.249201 -1.340227 -1.315444
sepal length (cm) sepal width (cm) petal length (cm) petal width (cm)
0 0.0 2.0 0.0 0.0
1 0.0 2.0 0.0 0.0
2 0.0 2.0 0.0 0.0
3 0.0 2.0 0.0 0.0
4 0.0 2.0 0.0 0.0
A veces es útil añadir complejidad a un modelo añadiendo características no lineales. scikit-learn incorpora dos estrategias:
Polinomiales.
Usando splines, trozos polinomiales.
Construye atributos como combinación polinomial de los existentes.
Si la entrada es [a, b] y se usa grado 2, los atributos polinomiales serían [1, a, b, a^2, ab, b^2].
from sklearn.preprocessing import PolynomialFeatures
#Build a polynomial features generator for the squared augmentation, this transforms (X1,X2) to (1,X1,X2,X1^2,X1X2,X2^2)
poly = PolynomialFeatures(degree=2).fit(X_iris)
print(X_iris.iloc[:3,])
poly_X = pd.DataFrame(poly.transform(X_iris))
print(poly_X.iloc[:3,]) sepal length (cm) sepal width (cm) petal length (cm) petal width (cm)
0 5.1 3.5 1.4 0.2
1 4.9 3.0 1.4 0.2
2 4.7 3.2 1.3 0.2
0 1 2 3 4 5 6 7 8 9 10 11 12 \
0 1.0 5.1 3.5 1.4 0.2 26.01 17.85 7.14 1.02 12.25 4.90 0.70 1.96
1 1.0 4.9 3.0 1.4 0.2 24.01 14.70 6.86 0.98 9.00 4.20 0.60 1.96
2 1.0 4.7 3.2 1.3 0.2 22.09 15.04 6.11 0.94 10.24 4.16 0.64 1.69
13 14
0 0.28 0.04
1 0.28 0.04
2 0.26 0.04
A veces interesa solo las interacciones, se puede usar interaction_only.
#Sometimes only interaction terms are required, which can be obtained by setting interaction_only=True
poly = PolynomialFeatures(degree=2, interaction_only=True).fit(X_iris)
print(X_iris.iloc[:5,])
poly_X = pd.DataFrame(poly.transform(X_iris))
print(poly_X.iloc[:5,]) sepal length (cm) sepal width (cm) petal length (cm) petal width (cm)
0 5.1 3.5 1.4 0.2
1 4.9 3.0 1.4 0.2
2 4.7 3.2 1.3 0.2
3 4.6 3.1 1.5 0.2
4 5.0 3.6 1.4 0.2
0 1 2 3 4 5 6 7 8 9 10
0 1.0 5.1 3.5 1.4 0.2 17.85 7.14 1.02 4.90 0.70 0.28
1 1.0 4.9 3.0 1.4 0.2 14.70 6.86 0.98 4.20 0.60 0.28
2 1.0 4.7 3.2 1.3 0.2 15.04 6.11 0.94 4.16 0.64 0.26
3 1.0 4.6 3.1 1.5 0.2 14.26 6.90 0.92 4.65 0.62 0.30
4 1.0 5.0 3.6 1.4 0.2 18.00 7.00 1.00 5.04 0.72 0.28
Index(['id', 'amount_tsh', 'date_recorded', 'funder', 'gps_height',
'installer', 'longitude', 'latitude', 'wpt_name', 'num_private',
'basin', 'subvillage', 'region', 'region_code', 'district_code', 'lga',
'ward', 'population', 'public_meeting', 'recorded_by',
'scheme_management', 'scheme_name', 'permit', 'construction_year',
'extraction_type', 'extraction_type_group', 'extraction_type_class',
'management', 'management_group', 'payment', 'payment_type',
'water_quality', 'quality_group', 'quantity', 'quantity_group',
'source', 'source_type', 'source_class', 'waterpoint_type',
'waterpoint_type_group', 'status_group'],
dtype='object')
Mostrar algún diagrama para mostrar casos de nulos.
Eliminar atributos con un número de nulos mayor que el 30%.
Mostrar distribución de nulos.
Comparar el número de filas eliminando y no eliminando nulos.
Eliminar nulos sobre el original aplicando reemplazo correspondiente.
Eliminar los nulos sobre el original aplicando el KNN.
Comparar los resultados según los pasos 5 y 6.
Este ejercicio se hace sobre el IMDB “IMDB-Movie-Data.csv”.
Index(['Rank', 'Title', 'Genre', 'Description', 'Director', 'Actors', 'Year',
'Runtime (Minutes)', 'Rating', 'Votes', 'Revenue (Millions)',
'Metascore'],
dtype='object')
Discretiza los ingresos en 10 intervalos de igual rango.
Discretiza los ingresos en 10 intervalos según la frecuencia.
Discretizar las películas entre populares (un 8 o más) y menos.
Borrar la descripción.
En el dataset de IMDB etiquetar el género usando el MultiLabel.
Etiquetar el director usando etiquetado.
Etiquetar el director usando HotEncoder.
Comparar resultados entre 3 y 4.
Aplicar con ColumnTransformer y Pipeline para automatizar las transformaciones anteriores con el IMDB (sin aplicar el paso 5 anterior).