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

287 lines
18 KiB
Markdown

<details>
<summary><strong>Aprenda hacking no AWS do zero ao herói com</strong> <a href="https://training.hacktricks.xyz/courses/arte"><strong>htARTE (HackTricks AWS Red Team Expert)</strong></a><strong>!</strong></summary>
Outras formas de apoiar o HackTricks:
* Se você quer ver sua **empresa anunciada no HackTricks** ou **baixar o HackTricks em PDF**, confira os [**PLANOS DE ASSINATURA**](https://github.com/sponsors/carlospolop)!
* Adquira o [**material oficial PEASS & HackTricks**](https://peass.creator-spring.com)
* Descubra [**A Família PEASS**](https://opensea.io/collection/the-peass-family), nossa coleção exclusiva de [**NFTs**](https://opensea.io/collection/the-peass-family)
* **Junte-se ao grupo** 💬 [**Discord**](https://discord.gg/hRep4RUj7f) ou ao grupo [**telegram**](https://t.me/peass) ou **siga-me** no **Twitter** 🐦 [**@carlospolopm**](https://twitter.com/carlospolopm)**.**
* **Compartilhe suas técnicas de hacking enviando PRs para os repositórios do GitHub** [**HackTricks**](https://github.com/carlospolop/hacktricks) e [**HackTricks Cloud**](https://github.com/carlospolop/hacktricks-cloud).
</details>
# Tipos básicos de dados possíveis
Os dados podem ser **contínuos** (**infinitos** valores) ou **categóricos** (nominais) onde a quantidade de valores possíveis é **limitada**.
## Tipos Categóricos
### Binário
Apenas **2 valores possíveis**: 1 ou 0. No caso de um conjunto de dados onde os valores estão em formato de string (por exemplo, "True" e "False"), você atribui números a esses valores com:
```python
dataset["column2"] = dataset.column2.map({"T": 1, "F": 0})
```
### **Ordinal**
Os **valores seguem uma ordem**, como em: 1º lugar, 2º lugar... Se as categorias forem strings (como: "iniciante", "amador", "profissional", "especialista"), você pode mapeá-las para números como vimos no caso binário.
```python
column2_mapping = {'starter':0,'amateur':1,'professional':2,'expert':3}
dataset['column2'] = dataset.column2.map(column2_mapping)
```
* Para **colunas alfabéticas** você pode ordená-las mais facilmente:
```python
# 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 **como valor ordinal** porque há uma ordem, mas isso não significa que um é maior que o outro. Além disso, a **distância entre eles depende da direção** que você está contando. Exemplo: Os dias da semana, domingo não é "maior" que segunda-feira.
* Existem **diferentes maneiras** de codificar características cíclicas, algumas podem funcionar apenas com alguns algoritmos. **Em geral, a codificação dummy pode ser usada**
```python
column2_dummies = pd.get_dummies(dataset.column2, drop_first=True)
dataset_joined = pd.concat([dataset[['column2']], column2_dummies], axis=1)
```
### **Datas**
Datas são **variáveis contínuas**. Podem ser vistas como **cíclicas** (porque se repetem) **ou** como **variáveis ordinais** (porque um momento é maior que o anterior).
* Geralmente, datas são usadas como **índice**
```python
# 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-categoria/nominal
**Mais de 2 categorias** sem ordem relacionada. Use `dataset.describe(include='all')` para obter informações sobre as categorias de cada recurso.
* Uma **string de referência** é uma **coluna que identifica um exemplo** (como o nome de uma pessoa). Isso pode ser duplicado (porque 2 pessoas podem ter o mesmo nome), mas a maioria será única. Esses dados são **inúteis e devem ser removidos**.
* Uma **coluna chave** é usada para **vincular dados entre tabelas**. Neste caso, os elementos são únicos. Esses dados são **inúteis e devem ser removidos**.
Para **codificar colunas de multi-categoria em números** (para que o algoritmo de ML as entenda), **codificação dummy é usada** (e **não codificação one-hot** porque não **evita multicolinearidade perfeita**).
Você pode obter uma **coluna de multi-categoria codificada em one-hot** com `pd.get_dummies(dataset.column1)`. Isso transformará todas as classes em recursos binários, criando assim **uma nova coluna por classe possível** e atribuirá 1 **valor Verdadeiro a uma coluna**, e o restante será falso.
Você pode obter uma **coluna de multi-categoria codificada em dummy** com `pd.get_dummies(dataset.column1, drop_first=True)`. Isso transformará todas as classes em recursos binários, criando assim **uma nova coluna por classe possível menos uma**, já que **as últimas 2 colunas serão refletidas como "1" ou "0" na última coluna binária criada**. Isso evitará multicolinearidade perfeita, reduzindo as relações entre colunas.
# Colinear/Multicolinearidade
Colinear aparece quando **2 recursos estão relacionados um com o outro**. Multicolinearidade aparece quando são mais de 2.
Em ML **você quer que seus recursos estejam relacionados com os resultados possíveis, mas você não quer que eles estejam relacionados entre si**. É por isso que a **codificação dummy mistura as duas últimas colunas** disso e **é melhor do que a codificação one-hot**, que não faz isso, criando uma relação clara entre todos os novos recursos da coluna de multi-categoria.
VIF é o **Fator de Inflação de Variância** que **mede a multicolinearidade dos recursos**. Um valor **acima de 5 significa que um dos dois ou mais recursos colineares deve ser removido**.
```python
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))
```
# Desequilíbrio Categórico
Isso ocorre quando **não há a mesma quantidade de cada categoria** nos dados de treinamento.
```python
# 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())
```
Em um desequilíbrio, sempre há uma **classe ou classes majoritárias** e uma **classe ou classes minoritárias**.
Existem 2 maneiras principais de corrigir esse problema:
* **Undersampling**: Remoção aleatória de dados da classe majoritária para que ela tenha o mesmo número de amostras que a classe minoritária.
```python
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
```
* **Oversampling**: Gerar mais dados para a classe minoritária até que ela tenha tantas amostras quanto a classe majoritária.
```python
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
```
Você pode usar o argumento **`sampling_strategy`** para indicar a **porcentagem** que deseja **subamostrar ou sobreamostrar** (**por padrão é 1 (100%)**, o que significa igualar o número de classes minoritárias com as classes majoritárias)
{% hint style="info" %}
Subamostragem ou Sobreamostragem não são perfeitas se você obter estatísticas (com `.describe()`) dos dados sub/sobreamostrados e compará-los com os originais, você verá **que eles mudaram.** Portanto, sobreamostragem e subamostragem estão modificando os dados de treinamento.
{% endhint %}
## Sobreamostragem com SMOTE
**SMOTE** geralmente é uma **maneira mais confiável de sobreamostrar os dados**.
```python
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
```
# Categorias Raramente Ocorrentes
Imagine um conjunto de dados onde uma das classes alvo **ocorre muito poucas vezes**.
Isso é como o desequilíbrio de categoria da seção anterior, mas a categoria raramente ocorrente está ocorrendo ainda menos que a "classe minoritária" naquele caso. Os métodos de **oversampling** e **undersampling** **puros** também podem ser usados aqui, mas geralmente essas técnicas **não fornecerão resultados realmente bons**.
## Pesos
Em alguns algoritmos é possível **modificar os pesos dos dados alvo** para que alguns deles tenham por padrão mais importância ao gerar o modelo.
```python
weights = {0: 10 1:1} #Assign weight 10 to False and 1 to True
model = LogisticRegression(class_weight=weights)
```
Você pode **misturar os pesos com técnicas de sobreamostragem/subamostragem** para tentar melhorar os resultados.
## PCA - Análise de Componentes Principais
É um método que ajuda a reduzir a dimensionalidade dos dados. Vai **combinar diferentes características** para **reduzir a quantidade** delas, gerando **características mais úteis** (_menos computação é necessária_).
As características resultantes não são compreensíveis por humanos, então também **anonimiza os dados**.
# Categorias de Rótulos Incongruentes
Os dados podem ter erros devido a transformações mal-sucedidas ou simplesmente por erro humano ao escrever os dados.
Portanto, você pode encontrar o **mesmo rótulo com erros de ortografia**, diferentes **capitalizações**, **abreviações** como: _BLUE, Blue, b, bule_. Você precisa corrigir esses erros de rótulo dentro dos dados antes de treinar o modelo.
Você pode limpar esses problemas tornando tudo minúsculo e mapeando rótulos mal escritos para os corretos.
É muito importante verificar que **todos os dados que você possui estão corretamente rotulados**, porque, por exemplo, um erro de ortografia nos dados, ao codificar as classes em dummies, gerará uma nova coluna nas características finais com **consequências ruins para o modelo final**. Este exemplo pode ser detectado facilmente codificando uma coluna em one-hot e verificando os nomes das colunas criadas.
# Dados Ausentes
Alguns dados do estudo podem estar ausentes.
Pode acontecer de alguns dados completamente aleatórios estarem ausentes por algum erro. Esse tipo de dado é **Missing Completely at Random** (**MCAR**).
Pode ser que alguns dados aleatórios estejam ausentes, mas há algo que torna mais provável que alguns detalhes específicos estejam ausentes, por exemplo, é mais frequente que homens revelem sua idade, mas não mulheres. Isso é chamado **Missing at Random** (**MAR**).
Finalmente, pode haver dados **Missing Not at Random** (**MNAR**). O valor dos dados está diretamente relacionado com a probabilidade de ter os dados. Por exemplo, se você quer medir algo embaraçoso, quanto mais embaraçosa for a pessoa, menos provável é que ela compartilhe isso.
As **duas primeiras categorias** de dados ausentes podem ser **ignoráveis**. Mas a **terceira** requer considerar **apenas partes dos dados** que não são impactadas ou tentar **modelar os dados ausentes de alguma forma**.
Uma maneira de descobrir sobre dados ausentes é usar a função `.info()`, pois ela indicará o **número de linhas, mas também o número de valores por categoria**. Se alguma categoria tem menos valores do que o número de linhas, então há alguns dados ausentes:
```bash
# Get info of the dataset
dataset.info()
# Drop all rows where some value is missing
dataset.dropna(how='any', axis=0).info()
```
Geralmente é recomendado que se um recurso estiver **ausente em mais de 20%** do conjunto de dados, a **coluna deve ser removida:**
```bash
# Remove column
dataset.drop('Column_name', axis='columns', inplace=True)
dataset.info()
```
{% hint style="info" %}
Observe que **nem todos os valores ausentes estão faltando no conjunto de dados**. É possível que valores ausentes tenham sido atribuídos o valor "Desconhecido", "n/a", "", -1, 0... Você precisa verificar o conjunto de dados (usando `dataset.column`_`name.value`_`counts(dropna=False)` para verificar os possíveis valores).
{% endhint %}
Se alguns dados estão ausentes no conjunto de dados (e não são muitos), você precisa encontrar a **categoria dos dados ausentes**. Para isso, basicamente precisa saber se os **dados ausentes são aleatórios ou não**, e para isso você precisa descobrir se os **dados ausentes estavam correlacionados com outros dados** do conjunto de dados.
Para descobrir se um valor ausente está correlacionado com outra coluna, você pode criar uma nova coluna que coloque 1s e 0s se o dado está ausente ou não e então calcular a correlação entre eles:
```bash
# 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()
```
Se você decidir ignorar os dados ausentes, ainda precisará decidir o que fazer com eles: Você pode **remover as linhas** com dados ausentes (os dados de treino para o modelo serão menores), pode **remover a característica** completamente, ou poderia **modelá-la**.
Você deve **verificar a correlação entre a característica ausente com a coluna alvo** para ver quão importante essa característica é para o alvo, se for realmente **pequena** você pode **descartá-la ou preenchê-la**.
Para preencher dados **contínuos** ausentes, você poderia usar: a **média**, a **mediana** ou usar um algoritmo de **imputação**. O algoritmo de imputação pode tentar usar outras características para encontrar um valor para a característica ausente:
```python
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 preencher dados categóricos, primeiro você precisa pensar se há algum motivo pelo qual os valores estão ausentes. Se for por **escolha dos usuários** (eles não quiseram fornecer os dados), talvez você possa **criar uma nova categoria** indicando isso. Se for devido a erro humano, você pode **remover as linhas** ou a **característica** (verifique os passos mencionados antes) ou **preenchê-la com a moda, a categoria mais usada** (não recomendado).
# Combinando Características
Se você encontrar **duas características** que estão **correlacionadas** entre si, geralmente você deve **descartar** uma delas (aquela que é menos correlacionada com o alvo), mas você também pode tentar **combiná-las e criar uma nova característica**.
```python
# 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)
```
<details>
<summary><strong>Aprenda hacking no AWS do zero ao herói com</strong> <a href="https://training.hacktricks.xyz/courses/arte"><strong>htARTE (HackTricks AWS Red Team Expert)</strong></a><strong>!</strong></summary>
Outras formas de apoiar o HackTricks:
* Se você quer ver sua **empresa anunciada no HackTricks** ou **baixar o HackTricks em PDF**, confira os [**PLANOS DE ASSINATURA**](https://github.com/sponsors/carlospolop)!
* Adquira o [**material oficial PEASS & HackTricks**](https://peass.creator-spring.com)
* Descubra [**A Família PEASS**](https://opensea.io/collection/the-peass-family), nossa coleção de [**NFTs**](https://opensea.io/collection/the-peass-family) exclusivos
* **Junte-se ao grupo** 💬 [**Discord**](https://discord.gg/hRep4RUj7f) ou ao grupo [**telegram**](https://t.me/peass) ou **siga-me** no **Twitter** 🐦 [**@carlospolopm**](https://twitter.com/carlospolopm)**.**
* **Compartilhe suas técnicas de hacking enviando PRs para os repositórios github do** [**HackTricks**](https://github.com/carlospolop/hacktricks) e [**HackTricks Cloud**](https://github.com/carlospolop/hacktricks-cloud).
</details>