Prevendo o grau de dano a imóveis que foram atingidos por terremotos

Criador: Eduardo Gonçalves (https://edugvs.github.io/)

  • Objetivo: Prever qual seria o grau do dano (Pouco dano, Dano moderado e Quase completamente destruido) em um edifício/imóvel atingido por um terremoto.
  • Tipo de problema de ML: Aprendizagem Supervisionada (Classificação Multiclasse).
  • Onde estão os dados: Arquivo csv contendo os dados históricos.

Vamos importar as bibliotecas necessárias.

In [1]:
# Para manipulação dos dados
import pandas as pd
import numpy as np

# Para visualização dos dados
import seaborn as sns
import plotly.graph_objs as go
import plotly.offline as py
import plotly.express as px
import matplotlib.pyplot as plt
%matplotlib inline

# Para o cálculo de algumas estatísticas
from scipy.stats import kurtosis, skew

# Para a etapa de pré-processamento dos dados.
from sklearn.preprocessing import MinMaxScaler

# Para balancear o conjunto de dados.
from imblearn.over_sampling import SMOTE
#pip install -U imbalanced-learn
#conda install -c conda-forge imbalanced-learn

# Para a fase de Feature Selection.
from sklearn.feature_selection import SelectKBest, SelectPercentile, mutual_info_classif, f_classif, RFE, chi2
from sklearn.model_selection import KFold, cross_val_score, RepeatedStratifiedKFold, train_test_split, GridSearchCV

# Para a etapa de modelagem preditiva.
from sklearn import svm
from sklearn.tree import DecisionTreeClassifier
from sklearn.neighbors import KNeighborsClassifier
from sklearn.linear_model import LogisticRegression
from sklearn.ensemble import RandomForestClassifier, ExtraTreesClassifier
import sklearn

# Para a avaliação dos modelos
import time
from sklearn.metrics import precision_score, recall_score, f1_score, accuracy_score, roc_auc_score
from sklearn.metrics import confusion_matrix
from sklearn.metrics import classification_report

# Para ignorar os avisos dentro deste notebook
import warnings
warnings.filterwarnings("ignore")
In [2]:
# Instala o pacote watermark. 
# Esse pacote é usado para gravar as versões de outros pacotes usados neste jupyter notebook.
#!pip install -q -U watermark
In [3]:
# Versões dos pacotes usados neste jupyter notebook
%reload_ext watermark
%watermark -a "Eduardo Gonçalves" --iversions
Author: Eduardo Gonçalves

sklearn   : 0.0
seaborn   : 0.9.0
numpy     : 1.16.5
pandas    : 0.25.1
matplotlib: 3.1.1
plotly    : 4.14.3

In [4]:
start_notebook = time.time()

Dicionário dos dados:

Variable Description Type
geo_level_1_id, geo_level_2_id, geo_level_3_id geographic region in which building exists, from largest (level 1) to most specific sub-region (level 3). Possible values: level 1: 0-30, level 2: 0-1427, level 3: 0-12567. (type: int)
count_floors_pre_eq number of floors in the building before the earthquake. (type: int)
age age of the building in years. (type: int)
area_percentage normalized area of the building footprint. (type: int)
height_percentage normalized height of the building footprint. (type: int)
land_surface_condition surface condition of the land where the building was built. Possible values: n, o, t. (type: categorical)
foundation_type type of foundation used while building. Possible values: h, i, r, u, w. (type: categorical)
roof_type type of roof used while building. Possible values: n, q, x. (type: categorical)
ground_floor_type type of constructions used in higher than the ground floors (except of roof). Possible values: j, q, s, x. (type: categorical)
position position of the building. Possible values: j, o, s, t. (type: categorical)
plan_configuration building plan configuration. Possible values: a, c, d, f, m, n, o, q, s, u. (type: categorical)
has_superstructure_adobe_mud flag variable that indicates if the superstructure was made of Adobe/Mud. (type: binary)
has_superstructure_mud_mortar_stone flag variable that indicates if the superstructure was made of Mud Mortar - Stone. (type: binary)
has_superstructure_stone_flag flag variable that indicates if the superstructure was made of Stone. (type: binary)
has_superstructure_cement_mortar_stone flag variable that indicates if the superstructure was made of Cement Mortar - Stone. (type: binary)
has_superstructure_mud_mortar_brick flag variable that indicates if the superstructure was made of Mud Mortar - Brick. (type: binary)
has_superstructure_cement_mortar_brick flag variable that indicates if the superstructure was made of Cement Mortar - Brick. (type: binary)
has_superstructure_timber flag variable that indicates if the superstructure was made of Timber. (type: binary)
has_superstructure_bamboo flag variable that indicates if the superstructure was made of Bamboo. (type: binary)
has_superstructure_rc_non_engineered flag variable that indicates if the superstructure was made of non-engineered reinforced concrete. (type: binary)
has_superstructure_other flag variable that indicates if the superstructure was made of any other material. (type: binary)
legal_ownership_status legal ownership status of the land where building was built. Possible values: a, r, v, w. (type: categorical)
count_families number of families that live in the building. (type: int)
has_secondary_use flag variable that indicates if the building was used for any secondary purpose. (type: binary)
has_secondary_use_agriculture flag variable that indicates if the building was used for agricultural purposes. (type: binary)
has_secondary_use_hotel flag variable that indicates if the building was used as a hotel. (type: binary)
has_secondary_use_rental flag variable that indicates if the building was used for rental purposes. (type: binary)
has_secondary_use_institution flag variable that indicates if the building was used as a location of any institution. (type: binary)
has_secondary_use_school flag variable that indicates if the building was used as a school. (type: binary)
has_secondary_use_industry flag variable that indicates if the building was used for industrial purposes. (type: binary)
has_secondary_use_health_post flag variable that indicates if the building was used as a health post. (type: binary)
has_secondary_use_gov_office flag variable that indicates if the building was used fas a government office. (type: binary)
has_secondary_use_use_police flag variable that indicates if the building was used as a police station. (type: binary)
has_secondary_use_other flag variable that indicates if the building was secondarily used for other purposes (type: binary)
damage_grade [TARGET] represents a level of damage to the building that was hit by the earthquake. There are 3 grades of the damage: 1 = low damage; 2 = a medium amount of damage; 3 = almost complete destruction. (type: int)

Estamos tentando prever a variável ordinal damage_grade, que representa o nível de dano ao imóvel que foi atingido pelo terremoto.

Existem 3 graus de dano:

  • 1 representa dano baixo.
  • 2 representa uma quantidade média de dano .
  • 3 representa destruição quase completa.

1. Carregando o conjunto de dados

In [5]:
# Carregando os dados de treino
dataset = pd.read_csv("train_data.csv", sep ='|')
In [6]:
# Criando uma cópia do dataset
df = dataset
display(df)
building_id geo_level_1_id geo_level_2_id geo_level_3_id count_floors_pre_eq age area_percentage height_percentage land_surface_condition foundation_type ... has_secondary_use_hotel has_secondary_use_rental has_secondary_use_institution has_secondary_use_school has_secondary_use_industry has_secondary_use_health_post has_secondary_use_gov_office has_secondary_use_use_police has_secondary_use_other damage_grade
0 802906 6 487 12198 2 30 6 5 t r ... 0 0 0 0 0 0 0 0 0 3
1 28830 8 900 2812 2 10 8 7 o r ... 0 0 0 0 0 0 0 0 0 2
2 94947 21 363 8973 2 10 5 5 t r ... 0 0 0 0 0 0 0 0 0 3
3 590882 22 418 10694 2 10 6 5 t r ... 0 0 0 0 0 0 0 0 0 2
4 201944 11 131 1488 3 30 8 9 t r ... 0 0 0 0 0 0 0 0 0 3
... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ...
260596 688636 25 1335 1621 1 55 6 3 n r ... 0 0 0 0 0 0 0 0 0 2
260597 669485 17 715 2060 2 0 6 5 t r ... 0 0 0 0 0 0 0 0 0 3
260598 602512 17 51 8163 3 55 6 7 t r ... 0 0 0 0 0 0 0 0 0 3
260599 151409 26 39 1851 2 10 14 6 t r ... 0 0 0 0 0 0 0 0 0 2
260600 747594 21 9 9101 3 10 7 6 n r ... 0 0 0 0 0 0 0 0 0 3

260601 rows × 40 columns

2. Entendendo os dados

In [7]:
# Visualizando qual o tipo de objeto
type(df)
Out[7]:
pandas.core.frame.DataFrame
In [8]:
# Checando se há dados duplicados
df.duplicated().sum()
Out[8]:
0
In [9]:
# Checando se há valores ausentes
df.isna().sum()
Out[9]:
building_id                               0
geo_level_1_id                            0
geo_level_2_id                            0
geo_level_3_id                            0
count_floors_pre_eq                       0
age                                       0
area_percentage                           0
height_percentage                         0
land_surface_condition                    0
foundation_type                           0
roof_type                                 0
ground_floor_type                         0
other_floor_type                          0
position                                  0
plan_configuration                        0
has_superstructure_adobe_mud              0
has_superstructure_mud_mortar_stone       0
has_superstructure_stone_flag             0
has_superstructure_cement_mortar_stone    0
has_superstructure_mud_mortar_brick       0
has_superstructure_cement_mortar_brick    0
has_superstructure_timber                 0
has_superstructure_bamboo                 0
has_superstructure_rc_non_engineered      0
has_superstructure_rc_engineered          0
has_superstructure_other                  0
legal_ownership_status                    0
count_families                            0
has_secondary_use                         0
has_secondary_use_agriculture             0
has_secondary_use_hotel                   0
has_secondary_use_rental                  0
has_secondary_use_institution             0
has_secondary_use_school                  0
has_secondary_use_industry                0
has_secondary_use_health_post             0
has_secondary_use_gov_office              0
has_secondary_use_use_police              0
has_secondary_use_other                   0
damage_grade                              0
dtype: int64
In [10]:
# Verificando as dimensões do objeto
df.shape
Out[10]:
(260601, 40)
In [11]:
# Contando valores únicos
info = df.nunique().sort_values()

# Determinando o tipo de dados para cada variável
info = pd.DataFrame(info.values, index = info.index, columns = ['NUniques'])

# Atribuindo as informações sobre o tipo de dados das variáveis a um DataFrame.
info['dtypes'] = df.dtypes

# Exibe o dataframe
display(info)
NUniques dtypes
has_superstructure_mud_mortar_brick 2 int64
has_secondary_use_other 2 int64
has_superstructure_cement_mortar_stone 2 int64
has_superstructure_stone_flag 2 int64
has_superstructure_mud_mortar_stone 2 int64
has_superstructure_adobe_mud 2 int64
has_superstructure_bamboo 2 int64
has_superstructure_rc_non_engineered 2 int64
has_superstructure_rc_engineered 2 int64
has_superstructure_other 2 int64
has_secondary_use 2 int64
has_secondary_use_agriculture 2 int64
has_secondary_use_hotel 2 int64
has_secondary_use_rental 2 int64
has_secondary_use_institution 2 int64
has_secondary_use_school 2 int64
has_secondary_use_industry 2 int64
has_secondary_use_health_post 2 int64
has_secondary_use_gov_office 2 int64
has_secondary_use_use_police 2 int64
has_superstructure_cement_mortar_brick 2 int64
has_superstructure_timber 2 int64
damage_grade 3 int64
roof_type 3 object
land_surface_condition 3 object
position 4 object
other_floor_type 4 object
legal_ownership_status 4 object
ground_floor_type 5 object
foundation_type 5 object
count_floors_pre_eq 9 int64
count_families 10 int64
plan_configuration 10 object
height_percentage 27 int64
geo_level_1_id 31 int64
age 42 int64
area_percentage 84 int64
geo_level_2_id 1414 int64
geo_level_3_id 11595 int64
building_id 260601 int64

Variáveis contendo muitos valores únicos podem atrapalhar a classificação. Uma boa estratégia é utilizar a técnica de engenharia de atributos. Neste caso, podemos criar um range de valores e transformar a variável em categórica (fator), processo também conhecido como "quantization". Por exemplo, podemos pegar uma variável que representa a idade de clientes e que possua muitos valores únicos e então criar um range de idade, ou faixas etárias. Essa etapa também pode ser aplicada durante o pré-processamento, tudo irá depender do problema de negócio.

3. Manipulando os dados - Limpeza e Transformação

In [12]:
# Definindo uma função, para converter variáveis para o tipo categórico, e criar suas respectivas versões dummy.
def categoryToDummyVariables(data, columnsName):

    # Criando um dicionário vazio.
    newTypes = dict()
    
    # Criando o nome das variáveis dummy.
    newColumnsName = [n + '_dummy' for n in columnsName]

    # Definindo que cada variável especificada, deve ser convertida para o tipo de dado categórico.
    for i in range(0, len(columnsName)):
        newTypes.update({columnsName[i]: 'category'}) 

    # Convertendo o tipo de dado das variáveis especificadas.
    data = df.astype(newTypes)

    # Criando variáveis dummy.
    for i in range(0, len(columnsName)):
        data[newColumnsName[i]] = data[columnsName[i]].cat.codes

    # Retornando o DataFrame modificado.
    return data
In [13]:
# Definindo uma função, para realizar as tarefas de Data Munging, necessárias para o conjunto de dados em análise.
def organizeData(data):
          
    # Criando variáveis dummy para o conjunto de dados.
    data = categoryToDummyVariables(data = data, columnsName = data.select_dtypes(include = np.object).columns)

    # Eliminando as variáveis "building_id", "geo_level_1_id", "geo_level_2_id", "geo_level_3_id" de índice do conjunto de dados e as variáveis categóricas (com letras)
    data = data.drop(columns = data.columns[[0,1,2,3,8,9,10,11,12,13,14,26]], axis = 1)
    
    # Retornando o DataFrame modificado.
    return data
In [14]:
# Limpando e organizando o conjunto de dados de treino.
df = organizeData(data = df)
In [15]:
display(df)
count_floors_pre_eq age area_percentage height_percentage has_superstructure_adobe_mud has_superstructure_mud_mortar_stone has_superstructure_stone_flag has_superstructure_cement_mortar_stone has_superstructure_mud_mortar_brick has_superstructure_cement_mortar_brick ... has_secondary_use_other damage_grade land_surface_condition_dummy foundation_type_dummy roof_type_dummy ground_floor_type_dummy other_floor_type_dummy position_dummy plan_configuration_dummy legal_ownership_status_dummy
0 2 30 6 5 1 1 0 0 0 0 ... 0 3 2 2 0 0 1 3 2 2
1 2 10 8 7 0 1 0 0 0 0 ... 0 2 1 2 0 3 1 2 2 2
2 2 10 5 5 0 1 0 0 0 0 ... 0 3 2 2 0 0 3 3 2 2
3 2 10 6 5 0 1 0 0 0 0 ... 0 2 2 2 0 0 3 2 2 2
4 3 30 8 9 1 0 0 0 0 0 ... 0 3 2 2 0 0 3 2 2 2
... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ...
260596 1 55 6 3 0 1 0 0 0 0 ... 0 2 0 2 0 0 0 2 7 2
260597 2 0 6 5 0 1 0 0 0 0 ... 0 3 2 2 0 0 1 2 2 2
260598 3 55 6 7 0 1 0 0 0 0 ... 0 3 2 2 1 0 1 2 2 2
260599 2 10 14 6 0 0 0 0 0 1 ... 0 2 2 2 2 2 2 0 2 2
260600 3 10 7 6 0 1 0 0 0 0 ... 0 3 0 2 0 0 1 0 2 2

260601 rows × 36 columns

In [16]:
# Checando os tipo de dados
df.dtypes
Out[16]:
count_floors_pre_eq                       int64
age                                       int64
area_percentage                           int64
height_percentage                         int64
has_superstructure_adobe_mud              int64
has_superstructure_mud_mortar_stone       int64
has_superstructure_stone_flag             int64
has_superstructure_cement_mortar_stone    int64
has_superstructure_mud_mortar_brick       int64
has_superstructure_cement_mortar_brick    int64
has_superstructure_timber                 int64
has_superstructure_bamboo                 int64
has_superstructure_rc_non_engineered      int64
has_superstructure_rc_engineered          int64
has_superstructure_other                  int64
count_families                            int64
has_secondary_use                         int64
has_secondary_use_agriculture             int64
has_secondary_use_hotel                   int64
has_secondary_use_rental                  int64
has_secondary_use_institution             int64
has_secondary_use_school                  int64
has_secondary_use_industry                int64
has_secondary_use_health_post             int64
has_secondary_use_gov_office              int64
has_secondary_use_use_police              int64
has_secondary_use_other                   int64
damage_grade                              int64
land_surface_condition_dummy               int8
foundation_type_dummy                      int8
roof_type_dummy                            int8
ground_floor_type_dummy                    int8
other_floor_type_dummy                     int8
position_dummy                             int8
plan_configuration_dummy                   int8
legal_ownership_status_dummy               int8
dtype: object

4. Análise Exploratória dos dados

Criando algumas funções auxiliares

In [17]:
# Definindo uma função para gerar um dataframe com estatísticas de variáveis numéricas.
def varStats(col, data, target = ''):

    if target == '':

        stats = pd.DataFrame({
            'Min'   : data[col].min(),
            'Q1'    : data[col].quantile(.25),
            'Median': data[col].median(),
            'Mean'  : data[col].mean(),
            'Q3'    : data[col].quantile(.75),
            'Max'   : data[col].max(),
            'SD'    : data[col].std(),
            'SK'    : skew(data[col]),
            'KU'    : kurtosis(data[col])
        }, index = [col])

    else:

        stats = pd.concat([
            df[[col, target]].groupby(target).min(),
            df[[col, target]].groupby(target).quantile(.25),
            df[[col, target]].groupby(target).median(),
            df[[col, target]].groupby(target).mean(),
            df[[col, target]].groupby(target).quantile(.75),
            df[[col, target]].groupby(target).max(),
            df[[col, target]].groupby(target).std(),
            df[[col, target]].groupby(target).skew(),
            df[[col, target]].groupby(target).apply(lambda group: kurtosis(group)[0])

        ], axis = 1)

        stats.columns = ['Min', 'Q1', 'Median', 'Mean', 'Q3', 'Max', 'SD', 'SK', 'KU']

    return stats
In [18]:
# Definindo uma função para traçar gráficos interativos em um ambiente jupyter não padrão.
def configure_plotly_browser_state():
  
  import IPython
  
  display(IPython.core.display.HTML('''
        <script src="/static/components/requirejs/require.js"></script>
        <script>
          requirejs.config({
            paths: {
              base: '/static/base',
              plotly: 'https://cdn.plot.ly/plotly-1.43.1.min.js?noext',
            },
          });
        </script>
        '''))
In [19]:
# Definindo uma função, para criar gráficos de Barra interativos com o plotly.
def plotBar(data, col = '', target = '', title = '', yaxis = '', xaxis = '', kind = 'normal', 
            color = ['#8783D1', '#FADF63', '#FF9F43', '#EE6352', '#FC7A1E'], opacity = 0.65, 
            template = 'plotly_white', orientation = 'v', barmode = 'group'):
    
    # Realizando as pré-configurações necessárias para a plotagem do gráfico interativo.
    configure_plotly_browser_state()

    # Criando gráficos na vertical.
    if orientation == 'v':

        # Plotando gráfico de barras simples.
        if kind == 'normal':
            
            # Definindo os dados, a cor, orientação e a transparência que serão utilizados para criar as barras.
            dataTrace = go.Bar(
                x           = data.index, 
                y           = data.values, 
                marker      = {'color': color[2], "opacity": opacity}, 
                orientation = orientation
            ) 
        
        # Plotando gráfico de barras agrupado por uma variável categórica.
        elif kind == 'groups':
            
            # Captura os registros pertencentes a cada categoria, da variável categórica.
            g = [data[data[target] == cat] for cat in data[target].cat.categories]

            # Definindo os dados, a cor, orientação e a transparência que serão utilizados para criar as barras.
            dataTrace = [
                go.Bar(
                    x           = g[cat][col], 
                    y           = g[cat]['count'], 
                    name        = data[target].cat.categories[cat].capitalize(),
                    marker      = {'color': color[cat], "opacity": opacity}, 
                    orientation = orientation
                ) for cat in range(0,len(g))
            ]
    
    # Criando gráficos na horizontal.
    else:

        # Plotando gráfico de barras simples.      
        if kind == 'normal':
            
            # Definindo os dados, a cor, orientação e a transparência que serão utilizados para criar as barras.

            dataTrace = go.Bar(
                x           = data.values, 
                y           = data.index, 
                marker      = {'color': color[3], "opacity": opacity}, 
                orientation = orientation
            ) 
        
        # Plotando gráfico de barras agrupado por uma variável categórica.
        elif kind == 'groups': 

            # Captura os registros pertencentes a cada categoria, da variável categórica.
            g = [data[data[target] == cat] for cat in data[target].cat.categories]

            # Definindo os dados, a cor, orientação e a transparência que serão utilizados para criar as barras.
            dataTrace = [
                go.Bar(
                    x           = g[cat]['count'], 
                    y           = g[cat][col], 
                    name        = data[target].cat.categories[cat].capitalize(), 
                    marker      = {'color': color[cat], "opacity": opacity}, 
                    orientation = orientation
                ) for cat in range(0,len(g))
            ]

    # Defindo as configurações de layout.
    layout = go.Layout(
        title       = title,
        yaxis       =  {'title': yaxis},
        xaxis       =  {'title': xaxis},
        template    = template
    )

    # Criando uma Figure, com os dados e o layout defindos.
    fig = go.Figure(data = dataTrace, layout = layout)

    # Definindo que as barras devem ser dispostas uma ao lado da outra caso estejam agrupadas por categoria.
    # Para criar Stacked Bars, utilize: 'stack'.
    fig.update_layout(barmode = barmode) 
    
    # Plotando o Figure com o pyplot.
    fig.show()

Variável: damage_grade [TARGET]

In [20]:
configure_plotly_browser_state()

# Definindo a variável a ser analisada
col = 'damage_grade'

# Definindo a label
label = 'damage_grade'

# Criando o plot
fig = px.histogram(df, 
             x = col,
             template = 'plotly_white',
             title = 'Absolute frequency of ' + col,
             labels = {col: label},
             opacity = 0.70,
             color = "damage_grade"
            )
   
# Exibe o gráfico
fig.show()
In [21]:
# Obtendo a frequência relativa
display(df['damage_grade'].value_counts(normalize=True).map('{:.2%}'.format))
2    56.89%
3    33.47%
1     9.64%
Name: damage_grade, dtype: object

Podemos observar um certo desbalanceamento entre as classes da variável target. Talvez seja necessário balancear antes da etapa de modelagem preditiva.

Variável: age

In [22]:
configure_plotly_browser_state()

# Criando o boxplot
fig1 = px.box(df, 
              x = 'age',      
              template = 'plotly_white',
              title = 'Building age (years)',
              color = "damage_grade",
             )

# Exbie o gráfico
fig1.show()

Vamos tratar os valores extremos (outliers). O critério para identificar um outlier é quando um ponto de dado fica 1,5 vezes fora de um intervalo interquartil acima do 3º quartil (Q3) e abaixo do 1º quartil (Q1)

In [23]:
# Verificando o shape dos dados
df['age'].shape
Out[23]:
(260601,)
In [24]:
# Obtendo o intervalo entre o 1 e o 3 quartil
Q1 = df['age'].quantile(0.25)
Q3 = df['age'].quantile(0.75)
IQR = Q3 - Q1
print(Q1, Q3, IQR)
10.0 30.0 20.0
In [25]:
# Fazendo cálculo para filtrar os dados que estiverem 1,5x abaixo do 1Q ou acima do 3Q
Lower_Whisker = Q1 - 1.5 * IQR
Upper_Whisker = Q3 + 1.5 * IQR
print(Lower_Whisker, Upper_Whisker)
-20.0 60.0
In [26]:
# Limpando os dados
df = df[df['age']< Upper_Whisker]
In [27]:
# Verificando o shape após o tratamento dos outliers
df['age'].shape
Out[27]:
(244490,)
In [28]:
configure_plotly_browser_state()

# Definindo a variável a ser analisada
col = 'age'

# Definindo a label
label = 'Building age (years)'

# Criando o plot
fig = px.histogram(df, 
             x = col,
             template = 'plotly_white',
             title = 'Absolute frequency of ' + col,
             labels = {col: label},
             opacity = 0.70,
             color = "damage_grade"
            )
   
# Exibe o gráfico
fig.show()
In [29]:
# Obtendo a frequência relativa
display(df['age'].value_counts(normalize=True).map('{:.2%}'.format))
10    15.91%
15    14.73%
5     13.78%
20    13.16%
0     10.65%
25     9.97%
30     7.37%
35     4.38%
40     4.32%
50     2.97%
45     1.93%
55     0.83%
Name: age, dtype: object
In [30]:
# Definindo a variável a ser calculada
col = 'age'

# Aplicando a função para calcular as estatísticas
varStats(col, target = 'damage_grade', data = df)
Out[30]:
Min Q1 Median Mean Q3 Max SD SK KU
damage_grade
1 0 0.0 5 9.754609 15.0 55 10.296220 1.427454 2.252384
2 0 10.0 15 18.268094 25.0 55 13.143263 0.712772 -0.086126
3 0 10.0 20 19.736826 30.0 55 13.235907 0.603810 -0.246108

Variável: count_floors_pre_eq

In [31]:
configure_plotly_browser_state()

# Definindo a variável a ser analisada
col = 'count_floors_pre_eq'

# Definindo a label
label = 'Number of floors in the building'

# Criando o plot
fig = px.histogram(df, 
             x = col,
             template = 'plotly_white',
             title = 'Absolute frequency of ' + col,
             labels = {col: label},
             opacity = 0.70,
             color = "damage_grade"
            )
   
# Exibe o gráfico
fig.show()
In [32]:
# Obtendo a frequência relativa
display(df['count_floors_pre_eq'].value_counts(normalize=True).map('{:.2%}'.format))
2    61.10%
3    21.06%
1    16.10%
4     1.22%
5     0.45%
6     0.06%
7     0.01%
9     0.00%
8     0.00%
Name: count_floors_pre_eq, dtype: object
In [33]:
# Filtrando os dados da variável count_floors_pre_eq para selecionar somente os imóveis de até 4 andares.
df['count_floors_pre_eq'] = df.query('count_floors_pre_eq < 5') 
In [34]:
# Definindo a variável a ser calculada
col = 'count_floors_pre_eq'

# Aplicando a função para calcular as estatísticas
varStats(col, target = 'damage_grade', data = df)
Out[34]:
Min Q1 Median Mean Q3 Max SD SK KU
damage_grade
1 1.0 1.0 2.0 1.795021 2.0 4.0 0.733914 0.775627 NaN
2 1.0 2.0 2.0 2.068986 2.0 4.0 0.618544 0.273580 NaN
3 1.0 2.0 2.0 2.167689 3.0 4.0 0.638114 -0.000443 NaN

Variável: area_percentage

In [35]:
configure_plotly_browser_state()

# Cria o boxplot
fig1 = px.box(df, 
              x = 'area_percentage',      
              template = 'plotly_white',
              title = 'Normalized area of the building footprint',
              color = "damage_grade",
             )

# Exibe o gráfico
fig1.show()
In [36]:
configure_plotly_browser_state()

# Definindo a variável a ser analisada
col = 'area_percentage'

# Definindo a label
label = 'Normalized area of the building footprint'

# Criando o plot
fig = px.histogram(df, 
             x = col,
             template = 'plotly_white',
             title = 'Absolute frequency of ' + col,
             labels = {col: label},
             opacity = 0.70,
             color = "damage_grade"
            )
   
# Exibe o gráfico
fig.show()
In [37]:
# Obtendo a frequência relativa
display(df['area_percentage'].value_counts(normalize=True).map('{:.2%}'.format))
6      16.18%
7      14.11%
5      12.56%
8      10.94%
9       8.54%
        ...  
82      0.00%
80      0.00%
78      0.00%
75      0.00%
100     0.00%
Name: area_percentage, Length: 84, dtype: object
In [38]:
# Definindo a variável a ser calculada
col = 'area_percentage'

# Aplicando a função para calcular as estatísticas
varStats(col, target = 'damage_grade', data = df)
Out[38]:
Min Q1 Median Mean Q3 Max SD SK KU
damage_grade
1 1 6.0 8 9.728614 12.0 96 6.309193 2.639265 14.846705
2 1 6.0 7 8.050565 9.0 100 4.257775 3.555296 32.104265
3 1 5.0 7 7.484262 9.0 96 3.759495 3.753068 40.201653

Variável: height_percentage

In [39]:
configure_plotly_browser_state()

# Cria o boxplot
fig1 = px.box(df, 
              x = 'height_percentage',      
              template = 'plotly_white',
              title = 'Normalized height of the building footprint',
              color = "damage_grade",
             )

# Exibe o gráfico
fig1.show()
In [40]:
configure_plotly_browser_state()

# Definindo a variável a ser analisada
col = 'height_percentage'

# Definindo a label
label = 'Normalized height of the building footprint'

# Criando o plot
fig = px.histogram(df, 
             x = col,
             template = 'plotly_white',
             title = 'Absolute frequency of ' + col,
             labels = {col: label},
             opacity = 0.70,
             color = "damage_grade"
            )
   
# Exibe o gráfico
fig.show()
In [41]:
# Obtendo a frequência relativa
display(df['height_percentage'].value_counts(normalize=True).map('{:.2%}'.format))
5     30.64%
6     17.97%
4     14.73%
7     13.56%
3     10.35%
8      4.82%
2      3.67%
9      1.85%
10     1.34%
12     0.32%
13     0.24%
11     0.23%
15     0.10%
16     0.06%
32     0.03%
18     0.03%
14     0.02%
20     0.01%
21     0.01%
23     0.00%
17     0.00%
19     0.00%
24     0.00%
25     0.00%
26     0.00%
28     0.00%
31     0.00%
Name: height_percentage, dtype: object
In [42]:
# Definindo a variável a ser calculada
col = 'height_percentage'

# Aplicando a função para calcular as estatísticas
varStats(col, target = 'damage_grade', data = df)
Out[42]:
Min Q1 Median Mean Q3 Max SD SK KU
damage_grade
1 2 3.0 5 5.148367 6.0 24 2.517017 1.686811 4.303471
2 2 4.0 5 5.348439 6.0 32 1.752329 1.117980 5.239259
3 2 5.0 5 5.452997 6.0 32 1.813670 3.195479 41.411091

Variável: count_families

In [43]:
configure_plotly_browser_state()

# Definindo a variável a ser analisada
col = 'count_families'

# Definindo a label
label = 'Number of families that live in the building'

# Criando o plot
fig = px.histogram(df, 
             x = col,
             template = 'plotly_white',
             title = 'Absolute frequency of ' + col,
             labels = {col: label},
             opacity = 0.70,
             color = "damage_grade"
            )
   
# Exibe o gráfico
fig.show()
In [44]:
# Obtendo a frequência relativa
display(df['count_families'].value_counts(normalize=True).map('{:.2%}'.format))
1    86.91%
0     8.00%
2     4.25%
3     0.66%
4     0.13%
5     0.03%
6     0.01%
7     0.00%
9     0.00%
8     0.00%
Name: count_families, dtype: object
In [45]:
# Definindo a variável a ser calculada
col = 'count_families'

# Aplicando a função para calcular as estatísticas
varStats(col, target = 'damage_grade', data = df)
Out[45]:
Min Q1 Median Mean Q3 Max SD SK KU
damage_grade
1 0 1.0 1 0.915225 1.0 9 0.497953 1.593388 16.911185
2 0 1.0 1 0.980378 1.0 9 0.396833 1.445391 18.302648
3 0 1.0 1 1.004788 1.0 7 0.407969 1.697361 15.502313

Variável: land_surface_condition

In [46]:
# Definindo o nome da variável a ser analisada.
col = 'land_surface_condition'

# Definindo o nome da variável Target.
target = 'damage_grade'

# Capturando variáveis especificadas do Dataset.
data = dataset[[col, target]]

# Criando uma variável count para contabilizar as ocorrências de cada registro.
data['count'] = 1

# Agrupando dados e contabilizando o número de ocorrências.
data = data.groupby(by = [target, col]).sum()

# Reorganizando DataFrame. 
data = data.reset_index()

# Plotando um gráfico de barras para a variável especificada.
fig = px.bar(data, x=col, y='count', title='Absolute frequency of ' + col, color = target, template = 'plotly_white', opacity = 0.70)

fig.show()
In [47]:
# Obtendo a frequência relativa
display(dataset['land_surface_condition'].value_counts(normalize=True).map('{:.2%}'.format))
t    83.18%
n    13.63%
o     3.19%
Name: land_surface_condition, dtype: object

Variável: foundation_type

In [48]:
# Definindo o nome da variável a ser analisada.
col = 'foundation_type'

# Definindo o nome da variável Target.
target = 'damage_grade'

# Capturando variáveis especificadas do Dataset.
data = dataset[[col, target]]

# Criando uma variável count para contabilizar as ocorrências de cada registro.
data['count'] = 1

# Agrupando dados e contabilizando o número de ocorrências.
data = data.groupby(by = [target, col]).sum()

# Reorganizando DataFrame. 
data = data.reset_index()

# Plotando um gráfico de barras para a variável especificada.
fig = px.bar(data, x=col, y='count', title='Absolute frequency of ' + col, color = target, template = 'plotly_white', opacity = 0.70)

fig.show()
In [49]:
# Obtendo a frequência relativa
display(dataset['foundation_type'].value_counts(normalize=True).map('{:.2%}'.format))
r    84.11%
w     5.80%
u     5.47%
i     4.06%
h     0.56%
Name: foundation_type, dtype: object

Variável: roof_type

In [50]:
# Definindo o nome da variável a ser analisada.
col = 'roof_type'

# Definindo o nome da variável Target.
target = 'damage_grade'

# Capturando variáveis especificadas do Dataset.
data = dataset[[col, target]]

# Criando uma variável count para contabilizar as ocorrências de cada registro.
data['count'] = 1

# Agrupando dados e contabilizando o número de ocorrências.
data = data.groupby(by = [target, col]).sum()

# Reorganizando DataFrame. 
data = data.reset_index()

# Plotando um gráfico de barras para a variável especificada.
fig = px.bar(data, x=col, y='count', title='Absolute frequency of ' + col, color = target, template = 'plotly_white', opacity = 0.70)

fig.show()
In [51]:
# Obtendo a frequência relativa
display(dataset['roof_type'].value_counts(normalize=True).map('{:.2%}'.format))
n    70.16%
q    23.63%
x     6.21%
Name: roof_type, dtype: object

Variável: ground_floor_type

In [52]:
# Definindo o nome da variável a ser analisada.
col = 'ground_floor_type'

# Definindo o nome da variável Target.
target = 'damage_grade'

# Capturando variáveis especificadas do Dataset.
data = dataset[[col, target]]

# Criando uma variável count para contabilizar as ocorrências de cada registro.
data['count'] = 1

# Agrupando dados e contabilizando o número de ocorrências.
data = data.groupby(by = [target, col]).sum()

# Reorganizando DataFrame. 
data = data.reset_index()

# Plotando um gráfico de barras para a variável especificada.
fig = px.bar(data, x=col, y='count', title='Absolute frequency of ' + col, color = target, template = 'plotly_white', opacity = 0.70)

fig.show()
In [53]:
# Obtendo a frequência relativa
display(dataset['ground_floor_type'].value_counts(normalize=True).map('{:.2%}'.format))
f    80.44%
x     9.55%
v     9.44%
z     0.39%
m     0.19%
Name: ground_floor_type, dtype: object

Variável: other_floor_type

In [54]:
# Definindo o nome da variável a ser analisada.
col = 'other_floor_type'

# Definindo o nome da variável Target.
target = 'damage_grade'

# Capturando variáveis especificadas do Dataset.
data = dataset[[col, target]]

# Criando uma variável count para contabilizar as ocorrências de cada registro.
data['count'] = 1

# Agrupando dados e contabilizando o número de ocorrências.
data = data.groupby(by = [target, col]).sum()

# Reorganizando DataFrame. 
data = data.reset_index()

# Plotando um gráfico de barras para a variável especificada.
fig = px.bar(data, x=col, y='count', title='Absolute frequency of ' + col, color = target, template = 'plotly_white', opacity = 0.70)

fig.show()
In [55]:
# Obtendo a frequência relativa
display(dataset['other_floor_type'].value_counts(normalize=True).map('{:.2%}'.format))
q    63.42%
x    16.67%
j    15.29%
s     4.62%
Name: other_floor_type, dtype: object

Variável: position

In [56]:
# Definindo o nome da variável a ser analisada.
col = 'position'

# Definindo o nome da variável Target.
target = 'damage_grade'

# Capturando variáveis especificadas do Dataset.
data = dataset[[col, target]]

# Criando uma variável count para contabilizar as ocorrências de cada registro.
data['count'] = 1

# Agrupando dados e contabilizando o número de ocorrências.
data = data.groupby(by = [target, col]).sum()

# Reorganizando DataFrame. 
data = data.reset_index()

# Plotando um gráfico de barras para a variável especificada.
fig = px.bar(data, x=col, y='count', title='Absolute frequency of ' + col, color = target, template = 'plotly_white', opacity = 0.70)

fig.show()
In [57]:
# Obtendo a frequência relativa
display(dataset['position'].value_counts(normalize=True).map('{:.2%}'.format))
s    77.55%
t    16.46%
j     5.10%
o     0.90%
Name: position, dtype: object

Variável: plan_configuration

In [58]:
# Definindo o nome da variável a ser analisada.
col = 'plan_configuration'

# Definindo o nome da variável Target.
target = 'damage_grade'

# Capturando variáveis especificadas do Dataset.
data = dataset[[col, target]]

# Criando uma variável count para contabilizar as ocorrências de cada registro.
data['count'] = 1

# Agrupando dados e contabilizando o número de ocorrências.
data = data.groupby(by = [target, col]).sum()

# Reorganizando DataFrame. 
data = data.reset_index()

# Plotando um gráfico de barras para a variável especificada.
fig = px.bar(data, x=col, y='count', title='Absolute frequency of ' + col, color = target, template = 'plotly_white', opacity = 0.70)

fig.show()
In [59]:
# Obtendo a frequência relativa
display(dataset['plan_configuration'].value_counts(normalize=True).map('{:.2%}'.format))
d    95.96%
q     2.18%
u     1.40%
s     0.13%
c     0.12%
a     0.10%
o     0.06%
m     0.02%
n     0.01%
f     0.01%
Name: plan_configuration, dtype: object

4.1 Análise de correlação

In [60]:
# Criando um dataframe somente com as variáveis de interesse
numerics = ['int16', 'int32', 'int64', 'int8', 'float16', 'float32', 'float64']
corr_features = df.select_dtypes(include=numerics)

# Criando um clustermap para a análise correlação em grupos
clustermap = sns.clustermap(corr_features.corr(), vmin=-1, vmax=1, annot=True, figsize=(30, 30),cmap="mako", method='centroid', metric='euclidean')

Não há nenhuma correlação significativa nos dados quando olhamos para a variável target, isso pode representar um problema para os modelos, afetando a precisão.

5. Aplicando técnicas de feature selection

5.1 Normalizando os dados (ajustando a escala entre 0 e 1)

Alguns algorítmos, como o kNN e o SVM, requerem a normalização dos dados.

In [62]:
# Criando um objeto da classe StandardScaler().        
scaler = MinMaxScaler()

# Selecionando as variáveis a serem normalizadas
features = df[["count_floors_pre_eq", "count_families", "height_percentage", "age", "area_percentage"]]

# Aplicando a escala nas Features e capturando o resultado obtido.
featuresTransformed = scaler.fit_transform(features)

# Criando um DataFrame com os resultados obtidos.
featuresTransformed = pd.DataFrame(data = featuresTransformed, columns = features.columns)

# Removendo as colunas não normalizadas
data = df.drop(columns = df.columns[[0,1,2,3,15]], axis = 1)

# Concatenando os dataframes
df_norm = pd.concat([featuresTransformed, data], axis = 1)
In [63]:
display(df_norm)
count_floors_pre_eq count_families height_percentage age area_percentage has_superstructure_adobe_mud has_superstructure_mud_mortar_stone has_superstructure_stone_flag has_superstructure_cement_mortar_stone has_superstructure_mud_mortar_brick ... has_secondary_use_other damage_grade land_surface_condition_dummy foundation_type_dummy roof_type_dummy ground_floor_type_dummy other_floor_type_dummy position_dummy plan_configuration_dummy legal_ownership_status_dummy
0 0.333333 0.111111 0.100000 0.545455 0.050505 1.0 1.0 0.0 0.0 0.0 ... 0.0 3.0 2.0 2.0 0.0 0.0 1.0 3.0 2.0 2.0
1 0.333333 0.111111 0.166667 0.181818 0.070707 0.0 1.0 0.0 0.0 0.0 ... 0.0 2.0 1.0 2.0 0.0 3.0 1.0 2.0 2.0 2.0
2 0.333333 0.111111 0.100000 0.181818 0.040404 0.0 1.0 0.0 0.0 0.0 ... 0.0 3.0 2.0 2.0 0.0 0.0 3.0 3.0 2.0 2.0
3 0.333333 0.111111 0.100000 0.181818 0.050505 0.0 1.0 0.0 0.0 0.0 ... 0.0 2.0 2.0 2.0 0.0 0.0 3.0 2.0 2.0 2.0
4 0.666667 0.111111 0.233333 0.545455 0.070707 1.0 0.0 0.0 0.0 0.0 ... 0.0 3.0 2.0 2.0 0.0 0.0 3.0 2.0 2.0 2.0
... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ...
260596 NaN NaN NaN NaN NaN 0.0 1.0 0.0 0.0 0.0 ... 0.0 2.0 0.0 2.0 0.0 0.0 0.0 2.0 7.0 2.0
260597 NaN NaN NaN NaN NaN 0.0 1.0 0.0 0.0 0.0 ... 0.0 3.0 2.0 2.0 0.0 0.0 1.0 2.0 2.0 2.0
260598 NaN NaN NaN NaN NaN 0.0 1.0 0.0 0.0 0.0 ... 0.0 3.0 2.0 2.0 1.0 0.0 1.0 2.0 2.0 2.0
260599 NaN NaN NaN NaN NaN 0.0 0.0 0.0 0.0 0.0 ... 0.0 2.0 2.0 2.0 2.0 2.0 2.0 0.0 2.0 2.0
260600 NaN NaN NaN NaN NaN 0.0 1.0 0.0 0.0 0.0 ... 0.0 3.0 0.0 2.0 0.0 0.0 1.0 0.0 2.0 2.0

259599 rows × 36 columns

5.2 Limpeza de valores ausentes e divisão em dados de treino, teste e validação

In [64]:
# Verificando valores ausentes
df_norm.isna().sum()
Out[64]:
count_floors_pre_eq                       16375
count_families                            15109
height_percentage                         15109
age                                       15109
area_percentage                           15109
has_superstructure_adobe_mud              15109
has_superstructure_mud_mortar_stone       15109
has_superstructure_stone_flag             15109
has_superstructure_cement_mortar_stone    15109
has_superstructure_mud_mortar_brick       15109
has_superstructure_cement_mortar_brick    15109
has_superstructure_timber                 15109
has_superstructure_bamboo                 15109
has_superstructure_rc_non_engineered      15109
has_superstructure_rc_engineered          15109
has_superstructure_other                  15109
has_secondary_use                         15109
has_secondary_use_agriculture             15109
has_secondary_use_hotel                   15109
has_secondary_use_rental                  15109
has_secondary_use_institution             15109
has_secondary_use_school                  15109
has_secondary_use_industry                15109
has_secondary_use_health_post             15109
has_secondary_use_gov_office              15109
has_secondary_use_use_police              15109
has_secondary_use_other                   15109
damage_grade                              15109
land_surface_condition_dummy              15109
foundation_type_dummy                     15109
roof_type_dummy                           15109
ground_floor_type_dummy                   15109
other_floor_type_dummy                    15109
position_dummy                            15109
plan_configuration_dummy                  15109
legal_ownership_status_dummy              15109
dtype: int64
In [65]:
# Verificando o shape
df_norm.shape
Out[65]:
(259599, 36)
In [66]:
# Deletando valores nulos
df_norm = df_norm.dropna(how='any')
In [67]:
# Verificando o shape
df_norm.shape
Out[67]:
(228204, 36)
In [68]:
# Contando valores únicos
info = df_norm.nunique().sort_values()

# Determinando o tipo de dados para cada variável
info = pd.DataFrame(info.values, index = info.index, columns = ['NUniques'])

# Atribuição de informações sobre o tipo de dados das variáveis a um DataFrame.
info['dtypes'] = df_norm.dtypes

# Exibe o dataframe
display(info)
NUniques dtypes
has_secondary_use_agriculture 2 float64
has_secondary_use_gov_office 2 float64
has_secondary_use_health_post 2 float64
has_secondary_use_industry 2 float64
has_secondary_use_school 2 float64
has_secondary_use_institution 2 float64
has_secondary_use_rental 2 float64
has_secondary_use_hotel 2 float64
has_secondary_use 2 float64
has_superstructure_other 2 float64
has_superstructure_rc_engineered 2 float64
has_secondary_use_use_police 2 float64
has_superstructure_rc_non_engineered 2 float64
has_superstructure_timber 2 float64
has_superstructure_cement_mortar_brick 2 float64
has_superstructure_mud_mortar_brick 2 float64
has_superstructure_cement_mortar_stone 2 float64
has_superstructure_stone_flag 2 float64
has_superstructure_mud_mortar_stone 2 float64
has_superstructure_adobe_mud 2 float64
has_superstructure_bamboo 2 float64
has_secondary_use_other 2 float64
damage_grade 3 float64
land_surface_condition_dummy 3 float64
roof_type_dummy 3 float64
other_floor_type_dummy 4 float64
position_dummy 4 float64
count_floors_pre_eq 4 float64
legal_ownership_status_dummy 4 float64
foundation_type_dummy 5 float64
ground_floor_type_dummy 5 float64
count_families 10 float64
plan_configuration_dummy 10 float64
age 12 float64
height_percentage 25 float64
area_percentage 82 float64
In [69]:
# Ajustando a variável target para o final do dataframe
target = df_norm.pop('damage_grade') 
df_norm['damage_grade'] = target

df_final = df_norm
In [70]:
display(df_final)
count_floors_pre_eq count_families height_percentage age area_percentage has_superstructure_adobe_mud has_superstructure_mud_mortar_stone has_superstructure_stone_flag has_superstructure_cement_mortar_stone has_superstructure_mud_mortar_brick ... has_secondary_use_other land_surface_condition_dummy foundation_type_dummy roof_type_dummy ground_floor_type_dummy other_floor_type_dummy position_dummy plan_configuration_dummy legal_ownership_status_dummy damage_grade
0 0.333333 0.111111 0.100000 0.545455 0.050505 1.0 1.0 0.0 0.0 0.0 ... 0.0 2.0 2.0 0.0 0.0 1.0 3.0 2.0 2.0 3.0
1 0.333333 0.111111 0.166667 0.181818 0.070707 0.0 1.0 0.0 0.0 0.0 ... 0.0 1.0 2.0 0.0 3.0 1.0 2.0 2.0 2.0 2.0
2 0.333333 0.111111 0.100000 0.181818 0.040404 0.0 1.0 0.0 0.0 0.0 ... 0.0 2.0 2.0 0.0 0.0 3.0 3.0 2.0 2.0 3.0
3 0.333333 0.111111 0.100000 0.181818 0.050505 0.0 1.0 0.0 0.0 0.0 ... 0.0 2.0 2.0 0.0 0.0 3.0 2.0 2.0 2.0 2.0
4 0.666667 0.111111 0.233333 0.545455 0.070707 1.0 0.0 0.0 0.0 0.0 ... 0.0 2.0 2.0 0.0 0.0 3.0 2.0 2.0 2.0 3.0
... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ...
244482 0.333333 0.111111 0.100000 0.090909 0.121212 0.0 0.0 0.0 0.0 0.0 ... 0.0 2.0 1.0 2.0 2.0 0.0 2.0 2.0 2.0 3.0
244486 0.333333 0.111111 0.100000 0.000000 0.050505 0.0 1.0 0.0 0.0 0.0 ... 0.0 2.0 2.0 1.0 3.0 1.0 2.0 2.0 2.0 3.0
244487 0.666667 0.111111 0.166667 1.000000 0.050505 0.0 1.0 0.0 0.0 0.0 ... 0.0 2.0 2.0 0.0 0.0 1.0 2.0 2.0 2.0 3.0
244488 0.333333 0.111111 0.133333 0.181818 0.131313 0.0 0.0 0.0 0.0 0.0 ... 0.0 0.0 1.0 2.0 2.0 2.0 3.0 2.0 2.0 2.0
244489 0.666667 0.333333 0.133333 0.181818 0.060606 0.0 0.0 0.0 0.0 0.0 ... 0.0 2.0 3.0 2.0 2.0 0.0 2.0 7.0 2.0 2.0

228204 rows × 36 columns

In [71]:
df_final.shape
Out[71]:
(228204, 36)
In [72]:
# Salvando o dataset final como csv
df_final.to_csv(r'D:\OneDrive\DS_PROJECTS\Project03-damage-earthquake\df_final.csv', index=False)

5.3 Balanceando as classes da variável target (damage_grade)

SMOTE (técnica de sobreamostragem minoritária sintética) é um dos métodos de sobreamostragem (oversampling) mais comumente usados para resolver o problema de desequilíbrio entre classes de uma variável. Seu objetivo é equilibrar a distribuição de classes, aumentando aleatoriamente os exemplos de classes minoritárias, replicando os dados. O SMOTE sintetiza novas instâncias minoritárias entre instâncias minoritárias existentes.

In [73]:
df_final.columns
Out[73]:
Index(['count_floors_pre_eq', 'count_families', 'height_percentage', 'age',
       'area_percentage', 'has_superstructure_adobe_mud',
       'has_superstructure_mud_mortar_stone', 'has_superstructure_stone_flag',
       'has_superstructure_cement_mortar_stone',
       'has_superstructure_mud_mortar_brick',
       'has_superstructure_cement_mortar_brick', 'has_superstructure_timber',
       'has_superstructure_bamboo', 'has_superstructure_rc_non_engineered',
       'has_superstructure_rc_engineered', 'has_superstructure_other',
       'has_secondary_use', 'has_secondary_use_agriculture',
       'has_secondary_use_hotel', 'has_secondary_use_rental',
       'has_secondary_use_institution', 'has_secondary_use_school',
       'has_secondary_use_industry', 'has_secondary_use_health_post',
       'has_secondary_use_gov_office', 'has_secondary_use_use_police',
       'has_secondary_use_other', 'land_surface_condition_dummy',
       'foundation_type_dummy', 'roof_type_dummy', 'ground_floor_type_dummy',
       'other_floor_type_dummy', 'position_dummy', 'plan_configuration_dummy',
       'legal_ownership_status_dummy', 'damage_grade'],
      dtype='object')
In [74]:
# Para reproduzir os mesmo resultados
seed = 100

# Separa X e y
X = df_final.iloc[:,0:35]
y = df_final.iloc[:,35] 

# Cria o balanceador SMOTE
smt = SMOTE(random_state = seed)

# Aplica o balanceador
X_res, y_res = smt.fit_resample(X, y)
In [75]:
# Plot 
sns.countplot(y_res, palette = "OrRd")
plt.box(False)
plt.xlabel('Grau de dano: Pouco(1) / Médio (2) / Quase destruído (3)', fontsize = 11)
plt.ylabel('count', fontsize = 11)
plt.title('Contagem de Classes\n')
plt.show()
In [76]:
df_final.shape
Out[76]:
(228204, 36)
In [77]:
X_res.shape
Out[77]:
(387768, 35)
In [78]:
y_res.shape
Out[78]:
(387768,)
In [79]:
# Ajustando X e y
X = X_res
y = y_res
In [80]:
# Criando conjunto de dados de treino e de teste.
trainFeatures, testFeatures, trainTarget, testTarget = train_test_split(X, y, test_size = 0.30, random_state = 101)
In [81]:
# Divisão dos dados de treino em dados de treino e dados de validação
trainData, validData, trainLabels, validLabels = train_test_split(trainFeatures, 
                                                                    trainTarget, 
                                                                    test_size = 0.1, 
                                                                    random_state = 84)
In [82]:
# Imprimindo o número de exemplos (observações) em cada dataset
print("Exemplos de Treino: {}".format(len(trainData)))
print("Exemplos de Validação: {}".format(len(validLabels)))
print("Exemplos de Teste: {}".format(len(testTarget)))
Exemplos de Treino: 244293
Exemplos de Validação: 27144
Exemplos de Teste: 116331
In [83]:
# Shape dos datasets
print(trainData.shape, validData.shape, testFeatures.shape)
(244293, 35) (27144, 35) (116331, 35)

5.4 Feature Selection usando ANOVA F-value

Se os recursos forem categóricos, calcularemos uma estatística qui-quadrado entre cada recurso e a variável target. No entanto, se os recursos forem quantitativos, calcularemos a ANOVA F-Value entre cada recurso e a variável target.

As pontuações do F-Value examinam se, quando agrupamos a característica numérica pela variável target, as médias para cada grupo se tornam significativamente diferentes.

In [84]:
# Definindo qual conjunto de dados, já escalado, deve ser utilizado.
tFeatures = trainFeatures

# Instanciando um objeto da classe SelectKBest para selecionar as melhores variáveis preditoras a partir dos scores ANOVA F-Values.
skb = SelectKBest(f_classif, k = 8)

# Capturando as variáveis preditoras.
bestFeatuesANOVA = skb.fit_transform(tFeatures, trainTarget)

# Capturando o nome das variáveis preditoras.
bfAnova = tFeatures.columns[skb.get_support()]

# Exibindo o nome das variáveis preditoras.
bfAnova
Out[84]:
Index(['has_superstructure_mud_mortar_stone',
       'has_superstructure_cement_mortar_brick',
       'has_superstructure_rc_non_engineered',
       'has_superstructure_rc_engineered', 'has_secondary_use_hotel',
       'roof_type_dummy', 'ground_floor_type_dummy',
       'legal_ownership_status_dummy'],
      dtype='object')
In [85]:
# Criando uma Série Temporal com os scores obtidos para cada uma das Features segundo a técnica utilizada.
sc = pd.Series(skb.scores_, index = tFeatures.columns)

# Capturando os scores das variáveis preditoras.
sc = sc[skb.get_support()]

# Ordenando a Série Temporal em ordem decrescente dos scores.
sc = sc.sort_values(ascending = False)
In [86]:
# Plotando um gráfico de barras, dos scores gerados para as features, a partir da técnica utilizada.
plotBar (
    data        = sc,
    title       = 'Scores das melhores features com o ANOVA F-value', 
    yaxis       = 'Features', 
    xaxis       = 'Scores', 
    orientation = 'h'
)
In [87]:
# Capturando o nome das variáveis preditoras.
bfAnv = sc.index

# Exibindo o nome das variáveis preditoras.
bfAnv
Out[87]:
Index(['has_superstructure_mud_mortar_stone',
       'has_superstructure_cement_mortar_brick', 'ground_floor_type_dummy',
       'roof_type_dummy', 'has_superstructure_rc_engineered',
       'has_superstructure_rc_non_engineered', 'has_secondary_use_hotel',
       'legal_ownership_status_dummy'],
      dtype='object')

5.5 Extra Trees Classifier

O Extremely Randomized Trees Classifier (Extra Trees Classifier) é um tipo de técnica de aprendizagem de conjunto que agrega os resultados de várias árvores de decisão descorrelacionadas coletadas em uma “floresta” para produzir seu resultado de classificação. Em conceito, é muito semelhante a um classificador Random Forest e só difere na forma de construção das árvores de decisão na floresta.

Cada árvore de decisão na floresta de árvores extras é construída a partir da amostra de treinamento original. Então, em cada nó de teste, cada árvore é fornecida com uma amostra aleatória de k recursos do conjunto de recursos a partir do qual cada árvore de decisão deve selecionar o melhor recurso para dividir os dados com base em alguns critérios matemáticos (normalmente o índice de Gini). Essa amostra aleatória de recursos leva à criação de várias árvores de decisão não correlacionadas.

Para realizar a seleção de características usando a estrutura de floresta acima, durante a construção da floresta, para cada característica, a redução total normalizada nos critérios matemáticos usados na decisão da característica de divisão (Índice de Gini se for usado na construção de floresta) é computado. Esse valor é chamado de Importância Gini do recurso. Para realizar a seleção de recursos, cada recurso é ordenado em ordem decrescente de acordo com a Importância Gini de cada recurso e o usuário seleciona os k principais recursos de acordo com sua escolha.

In [88]:
# Definindo qual conjunto de dados, já escalado, deve ser utilizado.
tFeatures = trainFeatures

# Instanciando um objeto da classe ExtraTreesClassifier.
modelETC = ExtraTreesClassifier()

# Computando os scores de cada feature.
modelETC.fit (X = tFeatures, y = trainTarget)

# Inserindo Scores obtidos em uma Série Temporal.
featuresImpETC = pd.Series(data = modelETC.feature_importances_, index = tFeatures.columns)

# Ordenando o nome das variáveis preditoras segundo seu score em ordem decrescente.
bfEtc = featuresImpETC.sort_values(ascending = False)
In [89]:
# Plotando um gráfico de barras, dos scores gerados para as features, a partir da técnica utilizada.
plotBar (
    data        = bfEtc, 
    title       = 'Scores das melhores features com o Extra Trees Classifier', 
    yaxis       = 'Features', 
    xaxis       = 'Scores', 
    orientation = 'h'
)
In [90]:
# Capturando o nome das variáveis preditoras.
bfEtc = bfEtc.index

# Exibindo o nome das variáveis preditoras.
bfEtc
Out[90]:
Index(['area_percentage', 'age', 'height_percentage',
       'has_superstructure_mud_mortar_stone', 'count_floors_pre_eq',
       'count_families', 'foundation_type_dummy', 'roof_type_dummy',
       'ground_floor_type_dummy', 'has_superstructure_cement_mortar_brick',
       'other_floor_type_dummy', 'position_dummy',
       'has_superstructure_adobe_mud', 'has_superstructure_timber',
       'land_surface_condition_dummy', 'has_superstructure_rc_non_engineered',
       'has_superstructure_mud_mortar_brick',
       'has_superstructure_rc_engineered', 'has_superstructure_stone_flag',
       'legal_ownership_status_dummy', 'plan_configuration_dummy',
       'has_superstructure_bamboo', 'has_secondary_use',
       'has_superstructure_cement_mortar_stone', 'has_superstructure_other',
       'has_secondary_use_hotel', 'has_secondary_use_agriculture',
       'has_secondary_use_rental', 'has_secondary_use_other',
       'has_secondary_use_industry', 'has_secondary_use_institution',
       'has_secondary_use_school', 'has_secondary_use_health_post',
       'has_secondary_use_gov_office', 'has_secondary_use_use_police'],
      dtype='object')

5.6 Random Forrest Importance

O Random Forest, é um dos algoritmos de aprendizado de máquina mais populares. É um dos mais bem-sucedidos porque fornece, em geral, um bom desempenho preditivo, baixo overfitting e é de fácil interpretabilidade.

Essa interpretabilidade é dada pela facilidade de se derivar a importância de cada variável na árvore de decisão. Em outras palavras, é fácil calcular o quanto cada variável está contribuindo para a decisão do modelo.

O Random Forest consiste em 4-12 centenas de árvores de decisão, cada uma delas construída sobre uma extração aleatória das observações do conjunto de dados e uma extração aleatória das características. Nem toda árvore vê todas as características ou todas as observações, e isso garante que as árvores sejam descorrelacionadas e, portanto, menos sujeitas a sobreajuste. Cada árvore também é uma sequência de perguntas sim-não com base em um único recurso ou em uma combinação de recursos. Em cada nó (isto é em cada questão), os três dividem o conjunto de dados em 2 depósitos, cada um deles hospedando observações que são mais semelhantes entre si e diferentes das do outro bloco. Portanto, a importância de cada recurso é derivada do quão "puro" cada um dos blocos é.

Para classificação, a medida de impureza é a impureza de Gini ou o ganho/entropia de informação. Para regressão, a medida de impureza é a variância. Portanto, ao treinar uma árvore, é possível calcular o quanto cada recurso diminui a impureza. Quanto maior for a diminuição da impureza que um recurso gerar, mais importante ele será. Em florestas aleatórias, a diminuição da impureza de cada recurso pode ser calculada em média entre as árvores para determinar a importância final da variável.

In [91]:
# Definindo qual conjunto de dados, já escalado, deve ser utilizado.
tFeatures = trainFeatures

# Instanciando um objeto da classe RandomForestClassifier.
rfImp = RandomForestClassifier (n_estimators = 200, random_state = 0)

# Treinando o classificador com o conjunto de dados de treino.
rfImp.fit(X = tFeatures, y = trainTarget)

# Prevendo os scores das features dos dados de treino.
pred = rfImp.predict(tFeatures)

# Convertendo os scores para um DataFrame.
featuresImpRf = pd.Series(data = rfImp.feature_importances_, index = tFeatures.columns)

# Capturando os scores de cada uma das features.
bfRf = featuresImpRf.nlargest(13)
In [92]:
# Plotando um gráfico de barras, dos scores gerados para as features, a partir da técnica utilizada.
plotBar (
    data        = bfRf,
    title       = 'Scores das melhores features com o Random Forest', 
    yaxis       = 'Features', 
    xaxis       = 'Scores', 
    orientation = 'h'
)
In [93]:
# Capturando o nome das variáveis preditoras.
bfRf = bfRf.index

# Exibindo o nome das variáveis preditoras.
bfRf
Out[93]:
Index(['area_percentage', 'age', 'height_percentage',
       'has_superstructure_mud_mortar_stone', 'foundation_type_dummy',
       'count_floors_pre_eq', 'roof_type_dummy', 'ground_floor_type_dummy',
       'other_floor_type_dummy', 'count_families',
       'has_superstructure_cement_mortar_brick', 'position_dummy',
       'land_surface_condition_dummy'],
      dtype='object')

6. Modelagem preditiva

6.1 Criando um modelo utilizando kNN

O k-Nearest-Neighbours (kNN) é um método de classificação não paramétrico, que é simples mas eficaz em muitos casos. Para que um registro de dados seja classificado, seus "k" vizinhos mais próximos são recuperados e isso forma uma vizinhança ao redor dos dados. O algoritmo kNN utiliza medidas matemáticas de distância a fim de comparar os pontos de dados, geralmente o método mais comum é a distância euclidiana, entretanto, há outras maneiras como: Distância Manhattan, Distância de Minkowsky e Distância de Hamming. Para aplicar o kNN, precisamos escolher um valor apropriado para k, e o sucesso da classificação depende muito desse valor. Em certo sentido, o método kNN é influenciado por k. Existem muitas maneiras de escolher o valor k, mas a maneira mais simples é executar o algoritmo várias vezes com valores k diferentes e escolher aquele com o melhor desempenho.

Se o valor de K for muito alto, o algoritmo kNN pode resultar em um modelo com baixa precisão. Se o valor de K for muito baixo, o modelo será muito sensível a outliers, gerando classificações incorretas. O objetivo é definir um valor de k que gere o modelo mais generalizável possível.

In [94]:
# Range de valores de k que iremos testar
kVals = range(1, 15, 2)

Números ímpares tendem a evitar o problema de empate na votação do algorítmo para determinar o vizinho mais próximo.

In [95]:
# Lista vazia para receber as acurácias
acuracias = []
In [96]:
# Loop em todos os valores de k para testar cada um deles
start = time.time()

for k in kVals:
    
    # Treinando o modelo KNN com cada valor de k com as melhores variáveis selecionadas pelo Random Forest
    modeloKNN = KNeighborsClassifier(n_neighbors = k)
    modeloKNN.fit(trainFeatures[bfRf], trainTarget)
          
    # Avaliando o modelo e atualizando a lista de acurácias
    score = modeloKNN.score(validData[bfRf], validLabels)
    print("Com valor de k = %d, a acurácia é = %.2f%%" % (k, score * 100))
    acuracias.append(score)

end = time.time()
print('Tempo de Treinamento do Modelo:', end - start)
Com valor de k = 1, a acurácia é = 81.95%
Com valor de k = 3, a acurácia é = 71.69%
Com valor de k = 5, a acurácia é = 69.05%
Com valor de k = 7, a acurácia é = 67.08%
Com valor de k = 9, a acurácia é = 65.66%
Com valor de k = 11, a acurácia é = 64.33%
Com valor de k = 13, a acurácia é = 63.38%
Tempo de Treinamento do Modelo: 1471.0139465332031
In [97]:
# Obtendo o valor de k que apresentou a maior acurácia
i = np.argmax(acuracias)
print("O valor de k = %d alcançou a mais alta acurácia de %.2f%% nos dados de validação!" % (kVals[i], acuracias[i] * 100))
O valor de k = 1 alcançou a mais alta acurácia de 81.95% nos dados de validação!
In [98]:
# Criando a versão final do modelo com o maior valor de k
modeloFinal = KNeighborsClassifier(n_neighbors = kVals[i])
In [99]:
# Treinamento do modelo
start = time.time()
modeloFinal.fit(trainFeatures[bfRf], trainTarget)
end = time.time()
print('Tempo de Treinamento do Modelo:', end - start)
Tempo de Treinamento do Modelo: 194.13200688362122

6.2 Avaliando o modelo utilizando kNN

In [100]:
# Previsões com os dados de teste
predictions = modeloFinal.predict(testFeatures[bfRf])
In [101]:
# Performance do modelo nos dados de teste
print("Avaliação do Modelo nos Dados de Teste")
print(classification_report(testTarget, predictions))
Avaliação do Modelo nos Dados de Teste
              precision    recall  f1-score   support

         1.0       0.78      0.77      0.78     38752
         2.0       0.50      0.51      0.51     38961
         3.0       0.52      0.53      0.53     38618

    accuracy                           0.60    116331
   macro avg       0.60      0.60      0.60    116331
weighted avg       0.60      0.60      0.60    116331

  • Accuracy: mede a exatidão do modelo de classificação, como a proporção de resultados verdadeiros em relação ao total de casos analisados. Quanto maior, melhor!

  • Precision: é a proporção de resultados verdadeiros sobre os resultados positivos. Quanto maior, melhor!

  • Recall: é a fração de resultados corretos retornados pelo modelo. Quanto maior, melhor!

  • F-Score: é a média ponderada entre a precisão e o recall. O valor ideal para o F-Score é igual a 1.

  • Support: é o número de ocorrências reais da classe no conjunto de dados especificado. O suporte desequilibrado nos dados de treinamento pode indicar fraquezas estruturais nas pontuações relatadas do classificador e pode indicar a necessidade de amostragem estratificada ou rebalanceamento.

As médias relatadas incluem macro média (média da média não ponderada por label), média ponderada (média da média ponderada de suporte por label) e média da amostra (apenas para classificação multiclasse). A micro média (calculando a média do total de positivos verdadeiros, falsos negativos e falsos positivos) só é exibida para várias labels ou várias classes com um subconjunto de classes, porque corresponde à precisão de outra forma e seria o mesmo para todas as métricas. Para mais informações acesse: https://scikit-learn.org/stable/modules/generated/sklearn.metrics.classification_report.html

In [102]:
# Confusion Matrix do Modelo Final
print ("Confusion matrix")
print(confusion_matrix(testTarget, predictions))
Confusion matrix
[[29980  4809  3963]
 [ 4652 19920 14389]
 [ 3603 14734 20281]]

6.3 Criando um modelo utilizando Regressão Logística

Um hiperparâmetro importante a ser ajustado para regressão logística multinomial é o termo de penalidade. Este termo impõe pressão sobre o modelo para buscar pesos de modelo menores. Isso é obtido adicionando uma soma ponderada dos coeficientes do modelo à função de perda, encorajando o modelo a reduzir o tamanho dos pesos junto com o erro durante o ajuste do modelo. Um tipo popular de penalidade é a penalidade L2 que adiciona a soma (ponderada) dos coeficientes quadrados à função de perda. Uma ponderação dos coeficientes pode ser usada para reduzir a força da penalidade de penalidade total para uma penalidade muito leve. Por padrão, a classe LogisticRegression usa a penalidade L2 com uma ponderação de coeficientes definida como 1,0. O tipo de penalidade pode ser definido por meio do argumento "penalidade" com valores de "l1", "l2", "elasticnet" (por exemplo, ambos), embora nem todos os solucionadores suportem todos os tipos de penalidade. A ponderação dos coeficientes na penalidade pode ser definida por meio do argumento “C”.

Isso significa que valores próximos a 1,0 indicam uma penalidade muito pequena e valores próximos de zero indicam uma penalidade forte. Um valor C de 1,0 pode indicar nenhuma penalidade.

  • C próximo de 1.0: Penalidade leve.
  • C próximo de 0.0: Penalidade forte.

A penalidade pode ser desabilitada definindo o argumento “penalidade” para a string “nenhum“.

In [103]:
# Obtendo uma lista de modelos para avaliar
def get_models():
    models = dict()
    for p in [0.0, 0.0001, 0.001, 0.01, 0.1, 1.0]:
        # cria o nome do modelo
        key = '%.4f' % p
        # desliga a penalidade em alguns casos
        if p == 0.0:
            # Sem penalidade neste caso
            models[key] = LogisticRegression(multi_class='multinomial', solver='lbfgs', penalty='none')
        else:
            models[key] = LogisticRegression(multi_class='multinomial', solver='lbfgs', penalty='l2', C=p)
    return models
 
# avaliando o modelo usando validação cruzada
def evaluate_model(model, X, y):
    # define o procedimento de validação
    cv = RepeatedStratifiedKFold(n_splits=10, n_repeats=3, random_state=1)
    # avalia o modelo
    scores = cross_val_score(model, X, y, scoring='accuracy', cv=cv, n_jobs=-1)
    return scores
In [104]:
start = time.time()

# define o conjunto de dados
X, y = trainFeatures[bfRf], trainTarget

# obtem os modelos para avaliação
models = get_models()

# avalia os modelos e armazena os resultados
results, names = list(), list()
for name, model in models.items():
    # avalia os modelos e coleta os scores
    scores = evaluate_model(model, X, y)
    # armazena os resultados nas listas
    results.append(scores)
    names.append(name)
    
# resume o progresso ao longo do caminho
print('>%s %.3f (%.3f)' % (name, np.mean(scores), np.std(scores)))

end = time.time()
print('Tempo de Treinamento do Modelo:', end - start)
>1.0000 0.523 (0.002)
Tempo de Treinamento do Modelo: 353.1663980484009
In [105]:
# plot do desempenho dos modelos para comparação de acordo com as penalidades
plt.boxplot(results, labels=names, showmeans=True)
plt.show()

Nesse caso, podemos ver que um valor C de 1,0 tem a melhor pontuação de cerca de 52%, que é o mesmo score do modelo com a penalidade mais forte (0,0)

6.4 Criando um modelo utilizando SVM

O objetivo do algoritmo da máquina de vetores de suporte (SVM – Support Vector Machine) é encontrar um hiperplano em um espaço N-dimensional (N - o número de recursos ou atributos) que classifica distintamente os pontos de dados. Para separar as duas classes de pontos de dados, existem muitos hiperplanos possíveis que podem ser escolhidos. Nosso objetivo é encontrar um hiperplano com a margem máxima, ou seja, a distância máxima entre os pontos de dados das duas classes. A maximização da distância da margem fornece um limite para que os pontos de dados futuros possam ser classificados com mais confiança. Hiperplanos são limites de decisão que ajudam a classificar os pontos de dados.

A dimensão do hiperplano depende do número de recursos. Se o número de recursos de entrada for 2, o hiperplano será apenas uma linha. Se o número de recursos de entrada for 3, o hiperplano se tornará um plano bidimensional. Os vetores de suporte são pontos de dados que estão mais próximos do hiperplano e influenciam a posição e a orientação do hiperplano. Usando esses vetores de suporte, maximizamos a margem do classificador. A exclusão dos vetores de suporte alterará a posição do hiperplano. Esses são os pontos que nos ajudam a criar nosso modelo SVM.

In [106]:
# Cria o modelo
modelo_svm_v1 = svm.SVC(kernel = 'linear')
In [107]:
# Selecionando uma amostragem aleatória do conjuntos de dados
train_sample = trainFeatures[bfRf].sample(n=20000, random_state=59)
In [108]:
# Selecionando uma amostragem aleatória do conjuntos de dados
target_sample = trainTarget.sample(n=20000, random_state=59)
In [109]:
# Treinamento
start = time.time()
modelo_svm_v1.fit(train_sample, target_sample)
end = time.time()
print('Tempo de Treinamento do Modelo:', end - start)
Tempo de Treinamento do Modelo: 18.599387645721436
In [110]:
# Selecionando uma amostragem aleatória do conjuntos de dados
test_sample = testFeatures[bfRf].sample(n=10000, random_state=59)

# Previsões
previsoes_v1 = modelo_svm_v1.predict(test_sample)
In [111]:
# Selecionando uma amostragem aleatória do conjuntos de dados
test_target_sample = testTarget.sample(n=10000, random_state=59)

#y_class = np.argmax(y_pred, axis = 0)

# Dicionário de métricas e metadados
SVM_dict_v1 = {'Modelo':'SVM',
               'Versão':'1',
               'Kernel':'Linear com dados normalizados',
               'Precision':precision_score(previsoes_v1, test_target_sample, average='weighted'),
               'Recall':recall_score(previsoes_v1, test_target_sample, average='weighted'),
               'F1 Score':f1_score(previsoes_v1, test_target_sample, average='weighted'),
               'Acurácia':accuracy_score(previsoes_v1, test_target_sample)},
               #'AUC':roc_auc_score(test_target_sample, previsoes_v1, average='weighted', multi_class='ovo')}
In [112]:
# Print
print("Métricas em Teste:\n")
SVM_dict_v1
Métricas em Teste:

Out[112]:
({'Modelo': 'SVM',
  'Versão': '1',
  'Kernel': 'Linear com dados normalizados',
  'Precision': 0.7809766388658902,
  'Recall': 0.5139,
  'F1 Score': 0.598248623096281,
  'Acurácia': 0.5139},)

6.5 Otimização de Hiperparâmetros com Grid Search e Kernel RBF

As funções de Kernel são usadas para mapear o conjunto de dados original (linear / não linear) em um espaço dimensional mais alto, com vista a torná-lo linear. O Kernel Linear, Polinomial e RBF (ou Gaussiano) são simplesmente diferentes opções para estabelecer o limite de decisão do hiperplano entre as classes. Geralmente, os Kernels ineares e Polinomiais consomem menos tempo e fornecem menos precisão do que os Kernels RBF.

O Kernel RBF é o mais amplamente usado ao treinar modelos SVM e dois Hiperparâmetros nos ajudam a ajustar o modelo ideal: C e gama. Intuitivamente, o parâmetro gama define até que ponto a influência de um único exemplo de treinamento alcança, com valores baixos significando 'longe' e valores altos significando 'próximos'. Os parâmetros gama podem ser vistos como o inverso do raio de influência das amostras selecionadas pelo modelo como vetores de suporte. O parâmetro C negocia a lassificação correta dos exemplos de treinamento contra a maximização da margem da função de decisão.

Para valores maiores de C, uma margem menor será aceita se a função de decisão for melhor na classificação correta de todos os pontos de treinamento. Um C mais baixo incentivará uma margem maior, portanto, uma função de decisão mais simples, ao custo da precisão do treinamento. Em outras palavras, “C” se comporta como um parâmetro de regularização no SVM

In [114]:
# Cria o modelo
modelo_svm_v2 = svm.SVC(kernel = 'rbf')

# Valores para o grid
C_range = np.array([50., 100., 200.])
gamma_range = np.array([0.3*0.001,0.001,3*0.001])

# Grid de hiperparâmetros
svm_param_grid = dict(gamma = gamma_range, C = C_range)

# Grid Search
modelo_svm_v2_grid_search_rbf = GridSearchCV(modelo_svm_v2, svm_param_grid, cv = 3)

# Treinamento
start = time.time()
modelo_svm_v2_grid_search_rbf.fit(train_sample, target_sample)
end = time.time()
print('Tempo de Treinamento do Modelo com Grid Search:', end - start)
print("")

# Acurácia em Treino
print(f"Acurácia em Treinamento: {modelo_svm_v2_grid_search_rbf.best_score_ :.2%}")
print("")
print(f"Hiperparâmetros Ideais: {modelo_svm_v2_grid_search_rbf.best_params_}")
Tempo de Treinamento do Modelo com Grid Search: 639.8525521755219

Acurácia em Treinamento: 53.97%

Hiperparâmetros Ideais: {'C': 200.0, 'gamma': 0.003}
In [115]:
# Previsões
previsoes_v2 = modelo_svm_v2_grid_search_rbf.predict(test_sample)
In [116]:
# Dicionário de métricas e metadados
SVM_dict_v2 = {'Modelo':'SVM',
               'Versão':'2',
               'Kernel':'RBF com dados normalizados',
               'Precision':precision_score(previsoes_v2, test_target_sample, average='weighted'),
               'Recall':recall_score(previsoes_v2, test_target_sample, average='weighted'),
               'F1 Score':f1_score(previsoes_v2, test_target_sample, average='weighted'),
               'Acurácia':accuracy_score(previsoes_v2, test_target_sample)},
               #'AUC':roc_auc_score(test_target_sample, previsoes_v2)}
In [117]:
# Print
print("Métricas em Teste:\n")
SVM_dict_v2
Métricas em Teste:

Out[117]:
({'Modelo': 'SVM',
  'Versão': '2',
  'Kernel': 'RBF com dados normalizados',
  'Precision': 0.829554609069602,
  'Recall': 0.5309,
  'F1 Score': 0.6086720519154126,
  'Acurácia': 0.5309},)

6.6 Otimização de Hiperparâmetros com Grid Search e Kernel Polinomial

In [118]:
# Cria o modelo
modelo_v3 = svm.SVC(kernel = 'poly')

# Valores para o grid
r_range =  np.array([0.5, 1])
gamma_range =  np.array([0.001, 0.01])
d_range = np.array([2, 3, 4])

# Grid de hiperparâmetros
param_grid_poly = dict(gamma = gamma_range, degree = d_range, coef0 = r_range)

# Grid Search
modelo_v3_grid_search_poly = GridSearchCV(modelo_v3, param_grid_poly, cv = 3)

# Treinamento
start = time.time()
modelo_v3_grid_search_poly.fit(train_sample, target_sample)
end = time.time()
print('Tempo de Treinamento do Modelo com Grid Search:', end - start)

# Acurácia em Treino
print(f"Acurácia em Treinamento: {modelo_v3_grid_search_poly.best_score_ :.2%}")
print("")
print(f"Hiperparâmetros Ideais: {modelo_v3_grid_search_poly.best_params_}")
Tempo de Treinamento do Modelo com Grid Search: 413.4537773132324
Acurácia em Treinamento: 53.48%

Hiperparâmetros Ideais: {'coef0': 1.0, 'degree': 4, 'gamma': 0.01}
In [119]:
# Previsões
previsoes_v3 = modelo_v3_grid_search_poly.predict(test_sample)
In [120]:
# Dicionário de métricas e metadados
SVM_dict_v3 = {'Modelo':'SVM',
               'Versão':'3',
               'Kernel':'Polinomial com dados normalizados',
               'Precision':precision_score(previsoes_v3, test_target_sample, average='weighted'),
               'Recall':recall_score(previsoes_v3, test_target_sample, average='weighted'),
               'F1 Score':f1_score(previsoes_v3, test_target_sample, average='weighted'),
               'Acurácia':accuracy_score(previsoes_v3, test_target_sample)},
               #'AUC':roc_auc_score(testTarget, previsoes_v3)}
In [121]:
# Print
print("Métricas em Teste:\n")
SVM_dict_v3
Métricas em Teste:

Out[121]:
({'Modelo': 'SVM',
  'Versão': '3',
  'Kernel': 'Polinomial com dados normalizados',
  'Precision': 0.8363362030019112,
  'Recall': 0.5298,
  'F1 Score': 0.6108729508478477,
  'Acurácia': 0.5298},)
In [122]:
# Criando dataframes com os resultados dos modelos
df1 = pd.DataFrame.from_dict(SVM_dict_v1)
df2 = pd.DataFrame.from_dict(SVM_dict_v2)
df3 = pd.DataFrame.from_dict(SVM_dict_v3)

# Concatenando os dataframes
resumo_svm = pd.concat([df1, df2, df3], axis = 0)

# Exibe o df
display(resumo_svm)
Modelo Versão Kernel Precision Recall F1 Score Acurácia
0 SVM 1 Linear com dados normalizados 0.780977 0.5139 0.598249 0.5139
0 SVM 2 RBF com dados normalizados 0.829555 0.5309 0.608672 0.5309
0 SVM 3 Polinomial com dados normalizados 0.836336 0.5298 0.610873 0.5298

7. Conclusões

  • O conjunto de dados original contém 260601 registros e 40 variáveis.
  • As classes da variável target (damage_grade) estavam desbalanceadas e foram tratadas usando uma técnica de oversampling (SMOTE).
  • Foi observado muitos outliers na variável ‘age’ que mostra a idade do imóvel em anos. Esses valores extremos foram tratados e removidos. As idades (em anos) mais frequêntes observadas foram: 10 (15,91%), 15 (14,73%), 5 (13,78%). 78,88% dos imóveis nesse conjunto de dados têm até 25 anos de idade. Ao observar a distribuição das idades, podemos observar que conforme a idade vai aumentando, a proporção da classe 1, que representa pouco dano a estrutura, vai perdendo relevância. Para confirmar essa hipótese, seriam necessários alguns testes estatísticos não-paramétricos, visto que a distribuição dos dados não segue uma curva normal, que mostrassem o peso dessa variável para o resultado observado.
  • 99,48% dos imóveis têm até 4 andares.
  • Em 86,91% dos dados da variável ‘count_families’, o número de famílias vivendo dentro do imóvel era de apenas uma. Também é interessante observar que em 8,00% dos dados não havia nenhuma família morando. Seriam prédios comerciais ou abandonados?
  • A condição “t” para ‘land_surface_condition’ prevaleceu com uma proporção de 83,18% dos dados.
  • O tipo de fundação “r”, em ‘foundation_type’, foi a mais frequente (84,11%).
  • O tipo de telhado utilizado mais frequente foi o tipo “n” (70,16%), seguido do tipo “q” (23,63%) e do tipo “x” (6,21%).
  • 80,44% do piso térreo era do tipo “f”.
  • A posição do imóvel mais frequente foi a do tipo “s” com a proporção de 77,55% dos dados.
  • Na variável ‘plan_configuration’ quase todos os dados estão concentrados no tipo “d” (95,96%).
  • Não foi encontrada nenhuma correlação relevante entre as variáveis. Isso pode explicar uma baixa precisão dos modelos de ML. Pelo fato de as variáveis não serem correlacionadas entre si, os modelos enfrentam uma dificuldade ao tentar achar a melhor aproximação da fórmula matemática que explica a organização e distribuição dos dados. A variável target “damage_gradenão mostrou ser fortemente correlacionada com nenhuma outra variável do conjunto de dados.
  • Valores ausentes causados pela manipulação e transformação dos dados foram tratados simplesmente removendo os registros, levando a uma perda minimamente considerável dos dados. Essa decisão foi tomada por motivos de capacidade computacional, tempo hábil para a execução do projeto e pelo fato de ainda existir mais de 200,000 registros no conjunto de dados para treino e teste dos modelos.
  • Para selecionar as melhores variáveis para treinar os modelos foram utilizadas três metodologias: ANOVA F-value, Extra Trees Classifier e Random Forest. Foram selecionadas as variáveis utilizando o Random Forest por conta dos scores que as variáveis obtiveram e por fazerem mais sentido em explicar a variável target, ou seja, o grau de dano ao imóvel após um terremoto.
  • Utilizando o algoritmo kNN encontramos um valor ideal de k = 1. O modelo apresentou os seguintes resultados:
    • Para a classe 1 (pouco dano): Precision = 0.78; Recall = 0.77; F1-Score = 0.78
    • Para a classe 2 (Dano moderado): Precision = 0.50; Recall = 0.51; F1-Score = 0.51
    • Para a classe 3 (Quase completamente destruído): Precision = 0.52; Recall = 0.53; F1-Score = 0.53.
  • O modelo utilizando Regressão Logística apresentou uma acurácia menor do que 55%, portanto, as previsões não foram feitas. O modelo ficou no projeto para fins de comparação de performance.
  • Foram treinados três algoritmos SVM, otimizando os hiperparâmetros e modificando o tipo de kernel (Linear, RBF e Polinomial). Dentre os modelos, o que apresentou a melhor performance foi o SVM utilizando Kernel RBF com os dados normalizados.
Possíveis melhorias no desenvolvimento deste projeto:
  • Explorar todas as variáveis binárias e rodar testes qui-quadrado, visto que estaremos comparando grupos utilizando variáveis categóricas, para verificar se a variável é estatisticamente significante em explicar a classe da variável target.
  • Testar outros algorítmos de classificação, como NaiveBayes e Árvores de decisão.
  • Modificar os hiperparâmetros dos algorítmos utilizados, selecionar outras variáveis na etapa de feature selection ou até mesmo mudar o split dos dados treino/teste.
  • Padronizar os dados ( deixar a distribuição com média = 0 e desvio padrão = 1).
  • Usar todo o conjunto de dados disponível para treinar os modelos (requer mais capacidade de processamento e tempo).
  • Aplicar engenharia de atributos, transformando variáveis numéricas com muitos valores únicos em um range de valores.
  • Buscar novos dados que expliquem e resolvam o problema de negócio.
In [124]:
end_notebook = time.time()
print('Tempo de Execução do Notebook:', end_notebook - start_notebook)
Tempo de Execução do Notebook: 4131.7495033741
Versionamento
Versão Editor Data Observação
1.0 Eduardo Gonçalves 05/06/2021 Primeira versão do projeto.