hacktricks/a.i.-exploiting/bra.i.nsmasher-presentation/ml-basics/feature-engineering.md

18 KiB

Aprende hacking en AWS desde cero hasta convertirte en un héroe con htARTE (Experto en Red Team de AWS de HackTricks)!

Otras formas de apoyar a HackTricks:

Tipos básicos de datos posibles

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

Tipos categóricos

Binario

Solo 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") asignas 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") puedes mapearlos a números 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

Parece 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 cuentan. 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 dummy.
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 se utilizan 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. Utiliza 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 ser eliminados.
  • Una columna clave se utiliza para vincular datos entre tablas. En este caso, los elementos son únicos. Estos datos son inútiles y deben ser eliminados.

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

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

Puedes 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, creando 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.

Colineal/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í. Por eso la codificación dummy mezcla las últimas dos columnas de eso y es mejor que la codificación one-hot que no hace eso 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 ser eliminada.

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 sobremuestreados o submuestreados y los comparas con los originales, verás que han cambiado. Por lo tanto, el sobremuestreo y 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 que ocurren raramente

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 que ocurre raramente ocurre incluso menos que la "clase minoritaria" en ese caso. Los métodos de sobremuestreo y submuestreo brutos también podrían ser utilizados 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 sobre/muestreo 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álculo).

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

Categorías de Etiquetas Incongruentes

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

Por lo tanto, es posible encontrar la misma etiqueta con errores de ortografía, diferentes mayúsculas, abreviaturas como: AZUL, Azul, a, azul. Debes corregir estos errores de etiqueta dentro de los datos antes de entrenar el modelo.

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

Es muy importante verificar que todos los datos que tienes estén etiquetados correctamente, porque por ejemplo, un error de ortografía en los datos, al codificar en dummies 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 one-hot una columna y verificando los nombres de las columnas creadas.

Datos Faltantes

Algunos datos del estudio pueden faltar.

Puede suceder que falten algunos datos aleatorios por algún error. Este tipo de datos faltantes es Faltante Completamente al Azar (MCAR).

Podría ser que falten algunos datos aleatorios pero haya algo que haga que algunos detalles específicos sean más probables de faltar, por ejemplo, es más probable que un hombre diga su edad pero no una mujer. 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 vergonzosa sea una persona, menos probable es que lo comparta.

Las dos primeras categorías de datos faltantes pueden ser ignoradas. Pero la tercera requiere considerar solo porciones de los datos que no se ven afectadas o intentar modelar de alguna manera los datos faltantes.

Una forma de detectar 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 asignados con el valor "Desconocido", "n/a", "", -1, 0... Necesitas verificar el conjunto de datos (usando conjunto_de_datos.nombre_de_columna.valor_contados(dropna=False) para verificar los posibles valores). {% endhint %}

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

Para determinar si un valor faltante está correlacionado con otra columna, puedes crear una nueva columna que ponga 1s y 0s 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 del modelo serán más pequeños), puedes eliminar por completo 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 completarla.

Para completar datos faltantes continuos podrías usar: la media, la mediana o utilizar 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 completar los datos categóricos, primero debes pensar si hay alguna razón por la cual faltan los valores. Si es por elección de los usuarios (no quisieron proporcionar los datos), tal vez puedas crear una nueva categoría indicándolo. Si es debido a un error humano, puedes eliminar las filas o la característica (verificar los pasos mencionados anteriormente) o rellenarla con la moda, la categoría más utilizada (no recomendado).

Combinación de Características

Si encuentras dos características que están correlacionadas entre sí, generalmente deberías eliminar una de ellas (la 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)
Aprende hacking en AWS desde cero hasta experto con htARTE (HackTricks AWS Red Team Expert)!

Otras formas de apoyar a HackTricks: