hacktricks/a.i.-exploiting/bra.i.nsmasher-presentation/ml-basics/feature-engineering.md
2023-06-03 01:46:23 +00:00

19 KiB

☁️ HackTricks Cloud ☁️ -🐦 Twitter 🐦 - 🎙️ Twitch 🎙️ - 🎥 Youtube 🎥

Tipos básicos de datos posibles

Los datos pueden ser continuos (con infinitos valores) o categóricos (nominales) donde la cantidad de valores posibles es limitada.

Tipos categóricos

Binario

Solo hay 2 valores posibles: 1 o 0. En caso de que en un conjunto de datos los valores estén en formato de cadena (por ejemplo, "Verdadero" y "Falso"), se asignan números a esos valores con:

dataset["column2"] = dataset.column2.map({"T": 1, "F": 0})

Ordinal

Los valores siguen un orden, como en: 1er lugar, 2do lugar... Si las categorías son cadenas de texto (como: "principiante", "amateur", "profesional", "experto") se pueden asignar números a cada una de ellas como vimos en el caso binario.

column2_mapping = {'starter':0,'amateur':1,'professional':2,'expert':3}
dataset['column2'] = dataset.column2.map(column2_mapping)
  • Para las columnas alfabéticas puedes ordenarlas más fácilmente:
# First get all the uniq values alphabetically sorted
possible_values_sorted = dataset.column2.sort_values().unique().tolist()
# Assign each one a value
possible_values_mapping = {value:idx for idx,value in enumerate(possible_values_sorted)}
dataset['column2'] = dataset.column2.map(possible_values_mapping)

Cíclico

Se parece a un valor ordinal porque hay un orden, pero no significa que uno sea más grande que el otro. Además, la distancia entre ellos depende de la dirección en la que se esté contando. Ejemplo: los días de la semana, el domingo no es "más grande" que el lunes.

  • Hay diferentes formas de codificar características cíclicas, algunas pueden funcionar solo con algunos algoritmos. En general, se puede utilizar la codificación de variables ficticias.
column2_dummies = pd.get_dummies(dataset.column2, drop_first=True)
dataset_joined = pd.concat([dataset[['column2']], column2_dummies], axis=1)

Fechas

Las fechas son variables continuas. Pueden ser vistas como cíclicas (porque se repiten) o como variables ordinales (porque un tiempo es mayor que otro anterior).

  • Usualmente las fechas son usadas como índice.
# Transform dates to datetime
dataset["column_date"] = pd.to_datetime(dataset.column_date)
# Make the date feature the index
dataset.set_index('column_date', inplace=True)
print(dataset.head())

# Sum usage column per day
daily_sum = dataset.groupby(df_daily_usage.index.date).agg({'usage':['sum']})
# Flatten and rename usage column
daily_sum.columns = daily_sum.columns.get_level_values(0)
daily_sum.columns = ['daily_usage']
print(daily_sum.head())

# Fill days with 0 usage
idx = pd.date_range('2020-01-01', '2020-12-31')
daily_sum.index = pd.DatetimeIndex(daily_sum.index)
df_filled = daily_sum.reindex(idx, fill_value=0) # Fill missing values


# Get day of the week, Monday=0, Sunday=6, and week days names
dataset['DoW'] = dataset.transaction_date.dt.dayofweek
# do the same in a different way
dataset['weekday'] = dataset.transaction_date.dt.weekday
# get day names
dataset['day_name'] = dataset.transaction_date.apply(lambda x: x.day_name())

Multi-categoría/nominal

Más de 2 categorías sin un orden relacionado. Use dataset.describe(include='all') para obtener información sobre las categorías de cada característica.

  • Una cadena de referencia es una columna que identifica un ejemplo (como el nombre de una persona). Esto puede estar duplicado (porque 2 personas pueden tener el mismo nombre), pero la mayoría será único. Estos datos son inútiles y deben eliminarse.
  • Una columna clave se utiliza para vincular datos entre tablas. En este caso, los elementos son únicos. Estos datos son inútiles y deben eliminarse.

Para codificar columnas de múltiples categorías en números (para que el algoritmo de ML los entienda), se utiliza la codificación de dummies (y no la codificación one-hot porque no evita la multicolinealidad perfecta).

Puede obtener una columna de múltiples categorías codificada one-hot con pd.get_dummies(dataset.column1). Esto transformará todas las clases en características binarias, por lo que creará una nueva columna por cada clase posible y asignará 1 valor verdadero a una columna, y el resto será falso.

Puede obtener una columna de múltiples categorías codificada en dummies con pd.get_dummies(dataset.column1, drop_first=True). Esto transformará todas las clases en características binarias, por lo que creará una nueva columna por cada clase posible menos una ya que las últimas 2 columnas se reflejarán como "1" o "0" en la última columna binaria creada. Esto evitará la multicolinealidad perfecta, reduciendo las relaciones entre columnas.

Colinealidad/Multicolinealidad

La colinealidad aparece cuando 2 características están relacionadas entre sí. La multicolinealidad aparece cuando hay más de 2.

En ML quieres que tus características estén relacionadas con los posibles resultados, pero no quieres que estén relacionadas entre sí. Es por eso que la codificación de dummies mezcla las últimas dos columnas de eso y es mejor que la codificación one-hot que no lo hace, creando una clara relación entre todas las nuevas características de la columna de múltiples categorías.

VIF es el Factor de Inflación de la Varianza que mide la multicolinealidad de las características. Un valor superior a 5 significa que una de las dos o más características colineales debe eliminarse.

from statsmodels.stats.outliers_influence import variance_inflation_factor
from statsmodels.tools.tools import add_constant

#dummies_encoded = pd.get_dummies(dataset.column1, drop_first=True)
onehot_encoded = pd.get_dummies(dataset.column1)
X = add_constant(onehot_encoded) # Add previously one-hot encoded data
print(pd.Series([variance_inflation_factor(X.values,i) for i in range(X.shape[1])], index=X.columns))

Desequilibrio Categórico

Esto ocurre cuando no hay la misma cantidad de cada categoría en los datos de entrenamiento.

# Get statistic of the features
print(dataset.describe(include='all'))
# Get an overview of the features
print(dataset.info())
# Get imbalance information of the target column
print(dataset.target_column.value_counts())

En un desequilibrio siempre hay una clase o clases mayoritarias y una clase o clases minoritarias.

Hay 2 formas principales de solucionar este problema:

  • Submuestreo: Eliminar datos seleccionados al azar de la clase mayoritaria para que tenga el mismo número de muestras que la clase minoritaria.
from imblearn.under_sampling import RandomUnderSampler
rus = RandomUserSampler(random_state=1337)

X = dataset[['column1', 'column2', 'column3']].copy()
y = dataset.target_column

X_under, y_under = rus.fit_resample(X,y)
print(y_under.value_counts()) #Confirm data isn't imbalanced anymore
  • Sobremuestreo: Generar más datos para la clase minoritaria hasta que tenga tantas muestras como la clase mayoritaria.
from imblearn.under_sampling import RandomOverSampler
ros = RandomOverSampler(random_state=1337)

X = dataset[['column1', 'column2', 'column3']].copy()
y = dataset.target_column

X_over, y_over = ros.fit_resample(X,y)
print(y_over.value_counts()) #Confirm data isn't imbalanced anymore

Puedes usar el argumento sampling_strategy para indicar el porcentaje que deseas submuestrear o sobremuestrear (por defecto es 1 (100%) lo que significa igualar el número de clases minoritarias con las clases mayoritarias).

{% hint style="info" %} El submuestreo o sobremuestreo no son perfectos, si obtienes estadísticas (con .describe()) de los datos sobre/submuestreados y los comparas con los originales, verás que han cambiado. Por lo tanto, el sobremuestreo y el submuestreo modifican los datos de entrenamiento. {% endhint %}

Sobremuestreo SMOTE

SMOTE es generalmente una forma más confiable de sobremuestrear los datos.

from imblearn.over_sampling import SMOTE

# Form SMOTE the target_column need to be numeric, map it if necessary
smote = SMOTE(random_state=1337)
X_smote, y_smote = smote.fit_resample(dataset[['column1', 'column2', 'column3']], dataset.target_column)
dataset_smote = pd.DataFrame(X_smote, columns=['column1', 'column2', 'column3'])
dataset['target_column'] = y_smote
print(y_smote.value_counts()) #Confirm data isn't imbalanced anymore

Categorías de ocurrencia rara

Imagina un conjunto de datos donde una de las clases objetivo ocurre muy pocas veces.

Esto es similar al desequilibrio de categorías de la sección anterior, pero la categoría de ocurrencia rara ocurre incluso menos que la "clase minoritaria" en ese caso. Los métodos de sobremuestreo y submuestreo brutos también podrían usarse aquí, pero generalmente esas técnicas no darán resultados realmente buenos.

Pesos

En algunos algoritmos es posible modificar los pesos de los datos objetivo para que algunos de ellos tengan por defecto más importancia al generar el modelo.

weights = {0: 10 1:1} #Assign weight 10 to False and 1 to True
model = LogisticRegression(class_weight=weights)

Puedes mezclar los pesos con técnicas de sobremuestreo/submuestreo para intentar mejorar los resultados.

PCA - Análisis de Componentes Principales

Es un método que ayuda a reducir la dimensionalidad de los datos. Va a combinar diferentes características para reducir la cantidad de ellas generando características más útiles (se necesita menos cómputo).

Las características resultantes no son comprensibles por los humanos, por lo que también anonimiza los datos.

Categorías de etiquetas incongruentes

Los datos pueden tener errores por transformaciones fallidas o simplemente por errores humanos al escribir los datos.

Por lo tanto, es posible encontrar la misma etiqueta con errores ortográficos, diferentes mayúsculas, abreviaturas como: BLUE, Blue, b, bule. Necesitas corregir estos errores de etiqueta dentro de los datos antes de entrenar el modelo.

Puedes solucionar estos problemas convirtiendo todo en minúsculas y asignando etiquetas mal escritas a las correctas.

Es muy importante comprobar que todos los datos que tienes están etiquetados correctamente, porque por ejemplo, un error de ortografía en los datos, al codificar las clases, generará una nueva columna en las características finales con consecuencias negativas para el modelo final. Este ejemplo se puede detectar muy fácilmente codificando en caliente una columna y comprobando los nombres de las columnas creadas.

Datos faltantes

Puede faltar algún dato del estudio.

Puede suceder que falte algún dato completamente al azar por algún error. Este tipo de dato está Completamente Faltante al Azar (MCAR).

Podría ser que falte algún dato al azar, pero hay algo que hace que algunos detalles específicos sean más probables de faltar, por ejemplo, los hombres suelen decir su edad con más frecuencia que las mujeres. Esto se llama Faltante al Azar (MAR).

Finalmente, podría haber datos Faltantes No al Azar (MNAR). El valor de los datos está directamente relacionado con la probabilidad de tener los datos. Por ejemplo, si quieres medir algo vergonzoso, cuanto más vergonzoso sea alguien, menos probable es que lo comparta.

Las dos primeras categorías de datos faltantes se pueden ignorar. Pero la tercera requiere considerar sólo porciones de los datos que no estén afectadas o intentar modelar los datos faltantes de alguna manera.

Una forma de averiguar sobre los datos faltantes es usar la función .info(), ya que indicará el número de filas pero también el número de valores por categoría. Si alguna categoría tiene menos valores que el número de filas, entonces faltan algunos datos:

# Get info of the dataset
dataset.info()

# Drop all rows where some value is missing
dataset.dropna(how='any', axis=0).info()

Generalmente se recomienda que si una característica falta en más del 20% del conjunto de datos, la columna debe ser eliminada:

# Remove column
dataset.drop('Column_name', axis='columns', inplace=True)
dataset.info()

{% hint style="info" %} Ten en cuenta que no todos los valores faltantes están ausentes en el conjunto de datos. Es posible que los valores faltantes hayan sido reemplazados por "Desconocido", "n/a", "", -1, 0... Debes verificar el conjunto de datos (usando dataset.columnname.valuecounts(dropna=False) para verificar los posibles valores). {% endhint %}

Si falta algún dato en el conjunto de datos (si no es demasiado), debes encontrar la categoría de los datos faltantes. Para ello, básicamente necesitas saber si los datos faltantes están al azar o no, y para ello necesitas encontrar si los datos faltantes estaban correlacionados con otros datos del conjunto de datos.

Para encontrar si un valor faltante está correlacionado con otra columna, puedes crear una nueva columna que ponga 1 y 0 si los datos faltan o no, y luego calcular la correlación entre ellos:

# The closer it's to 1 or -1 the more correlated the data is
# Note that columns are always perfectly correlated with themselves.
dataset[['column_name', 'cloumn_missing_data']].corr()

Si decides ignorar los datos faltantes, aún necesitas decidir qué hacer con ellos: puedes eliminar las filas con datos faltantes (los datos de entrenamiento para el modelo serán más pequeños), puedes eliminar completamente la característica, o puedes modelarla.

Debes verificar la correlación entre la característica faltante y la columna objetivo para ver qué tan importante es esa característica para el objetivo, si es realmente pequeña, puedes eliminarla o llenarla.

Para llenar datos faltantes continuos, puedes usar: la media, la mediana o usar un algoritmo de imputación. El algoritmo de imputación puede intentar usar otras características para encontrar un valor para la característica faltante:

from sklearn.impute import KNNImputer

X = dataset[['column1', 'column2', 'column3']]
y = dataset.column_target

# Create the imputer that will fill the data
imputer = KNNImputer(n_neightbors=2, weights='uniform')
X_imp = imputer.fit_transform(X)

# Check new data
dataset_imp = pd.DataFrame(X_imp)
dataset.columns = ['column1', 'column2', 'column3']
dataset.iloc[10:20] # Get some indexes that contained empty data before

Para rellenar datos categóricos, primero debes pensar si hay alguna razón por la cual los valores estén faltando. Si es por elección de los usuarios (no quisieron proporcionar los datos), tal vez puedas crear una nueva categoría que lo indique. Si es debido a un error humano, puedes eliminar las filas o la característica (verifica los pasos mencionados anteriormente) o rellenarla con la moda, la categoría más utilizada (no recomendado).

Combinando Características

Si encuentras dos características que están correlacionadas entre sí, generalmente deberías eliminar una de ellas (la que está menos correlacionada con el objetivo), pero también podrías intentar combinarlas y crear una nueva característica.

# Create a new feautr combining feature1 and feature2
dataset['new_feature'] = dataset.column1/dataset.column2

# Check correlation with target column
dataset[['new_feature', 'column1', 'column2', 'target']].corr()['target'][:]

# Check for collinearity of the 2 features and the new one
X = add_constant(dataset[['column1', 'column2', 'target']])
# Calculate VIF
pd.Series([variance_inflation_factor(X.values, i) for i in range(X.shape[1])], index=X.columns)
☁️ HackTricks Cloud ☁️ -🐦 Twitter 🐦 - 🎙️ Twitch 🎙️ - 🎥 Youtube 🎥