#+TITLE: Práctica 1
#+SUBTITLE: Inteligencia de Negocio
#+AUTHOR: Amin Kasrou Aouam
#+DATE: 2020-11-10
#+PANDOC_OPTIONS: template:~/.pandoc/templates/eisvogel.latex
#+PANDOC_OPTIONS: listings:t
#+PANDOC_OPTIONS: toc:t
#+PANDOC_METADATA: lang=es
#+PANDOC_METADATA: titlepage:t
#+PANDOC_METADATA: listings-no-page-break:t
#+PANDOC_METADATA: toc-own-page:t
#+PANDOC_METADATA: table-use-row-colors:t
#+PANDOC_METADATA: logo:/home/coolneng/Photos/Logos/UGR.png
* Práctica 1

** Introducción

En esta práctica, usaremos distintos algoritmos de aprendizaje automático para resolver un problema de clasificación.

** Procesado de datos

Antes de proceder con el entrenamiento de los distintos modelos, debemos realizar un preprocesado de los datos, para asegurarnos que nuestros modelos aprenden de un /dataset/ congruente.

La integridad de la lógica del preprocesado se encuentra en el archivo /preprocessing.py/, cuyo contenido mostramos aquí:

#+begin_src python
from pandas import read_csv
from sklearn.preprocessing import LabelEncoder
from sklearn.model_selection import KFold


def replace_values(df):
    columns = ["BI-RADS", "Margin", "Density", "Age"]
    for column in columns:
        df[column].fillna(value=df[column].mean(), inplace=True)
    return df


def process_na(df, action):
    if action == "drop":
        return df.dropna()
    elif action == "fill":
        return replace_values(df)
    else:
        print("Unknown action selected. The choices are: ")
        print("fill: fills the na values with the mean")
        print("drop: drops the na values")
        exit()


def encode_columns(df):
    label_encoder = LabelEncoder()
    encoded_df = df.copy()
    encoded_df["Shape"] = label_encoder.fit_transform(df["Shape"])
    encoded_df["Severity"] = label_encoder.fit_transform(df["Severity"])
    return encoded_df


def split_train_target(df):
    train_data = df.drop(columns=["Severity"])
    target_data = df["Severity"]
    return train_data, target_data


def split_k_sets(df):
    k_fold = KFold(shuffle=True, random_state=42)
    return k_fold.split(df)


def parse_data(source, action):
    df = read_csv(filepath_or_buffer=source, na_values="?")
    processed_df = process_na(df=df, action=action)
    encoded_df = encode_columns(df=processed_df)
    test_data, target_data = split_train_target(df=encoded_df)
    return test_data, target_data
#+end_src

#+RESULTS:


A continuación, mostraremos cada uno de los pasos que realizamos para obtener el /dataset/ final:

*** Valores nulos

Nuestro /dataset/ contiene valores nulos, representados mediante un signo de interrogación (?). Optamos por evaluar 2 estrategias:

**** Eliminar los valores nulos

#+BEGIN_SRC python
df = read_csv(filepath_or_buffer="../data/mamografia.csv", na_values="?")
processed_df = process_na(df=df, action="drop")
print("DataFrame sin preprocesamiento: ")
print(df.describe())
print("DataFrame sin preprocesamiento: ")
print(processed_df.describe())
#+END_SRC

#+begin_example
DataFrame sin preprocesamiento: 
          BI-RADS         Age      Margin     Density
count  959.000000  956.000000  913.000000  885.000000
mean     4.296142   55.487448    2.796276    2.910734
std      0.706291   14.480131    1.566546    0.380444
min      0.000000   18.000000    1.000000    1.000000
25%      4.000000   45.000000    1.000000    3.000000
50%      4.000000   57.000000    3.000000    3.000000
75%      5.000000   66.000000    4.000000    3.000000
max      6.000000   96.000000    5.000000    4.000000
DataFrame sin preprocesamiento: 
          BI-RADS         Age      Margin     Density
count  847.000000  847.000000  847.000000  847.000000
mean     4.322314   55.842975    2.833530    2.909091
std      0.703762   14.603754    1.564049    0.370292
min      0.000000   18.000000    1.000000    1.000000
25%      4.000000   46.000000    1.000000    3.000000
50%      4.000000   57.000000    3.000000    3.000000
75%      5.000000   66.000000    4.000000    3.000000
max      6.000000   96.000000    5.000000    4.000000
#+end_example

Observamos que el número de instancias disminuye considerablemente, hasta un máximo de 112, en el caso del /BI-RADS/. Aún así, los valores de la media y desviación estándar no se ven afectados de forma considerable.

**** Imputar su valor con la media

#+BEGIN_SRC python
df = read_csv(filepath_or_buffer="../data/mamografia.csv", na_values="?")
processed_df = process_na(df=df, action="fill")
print("DataFrame sin preprocesamiento: ")
print(df.describe())
print("DataFrame sin preprocesamiento: ")
print(processed_df.describe())
#+END_SRC

#+begin_example
DataFrame sin preprocesamiento: 
          BI-RADS         Age      Margin     Density
count  961.000000  961.000000  961.000000  961.000000
mean     4.296142   55.487448    2.796276    2.910734
std      0.705555   14.442373    1.526880    0.365074
min      0.000000   18.000000    1.000000    1.000000
25%      4.000000   45.000000    1.000000    3.000000
50%      4.000000   57.000000    3.000000    3.000000
75%      5.000000   66.000000    4.000000    3.000000
max      6.000000   96.000000    5.000000    4.000000
DataFrame sin preprocesamiento: 
          BI-RADS         Age      Margin     Density
count  961.000000  961.000000  961.000000  961.000000
mean     4.296142   55.487448    2.796276    2.910734
std      0.705555   14.442373    1.526880    0.365074
min      0.000000   18.000000    1.000000    1.000000
25%      4.000000   45.000000    1.000000    3.000000
50%      4.000000   57.000000    3.000000    3.000000
75%      5.000000   66.000000    4.000000    3.000000
max      6.000000   96.000000    5.000000    4.000000
#+end_example

Esta alternativa nos permite mantener el número de instancias en todas las columnas, sin alterar la media ni la desviación típica.

*** Valores no númericos

La mayoría de algoritmos de aprendizaje automática trabaja con datos numéricos, desafortunadamente nuestro /dataset/ contiene dos columnas con datos descriptivos.

Procedemos a convertirlos en valores numéricos mediante un /LabelEncoder/:

#+begin_src python
encoded_df = encode_columns(df=processed_df)
print(encoded_df.head())
#+end_src

#+begin_example
   BI-RADS   Age  Shape  Margin   Density  Severity
0      5.0  67.0      1     5.0  3.000000         1
1      4.0  43.0      4     1.0  2.910734         1
2      5.0  58.0      0     5.0  3.000000         1
3      4.0  28.0      4     1.0  3.000000         0
4      5.0  74.0      4     5.0  2.910734         1
#+end_example

Vemos como las columnas *Shape* y *Severity* se componen ahora únicamente de valores numéricos.
*** Separación de datos

Como último paso, separamos la columna objetivo de los demás datos.

#+begin_src python
test_data, target_data = split_train_target(df=encoded_df)
print("Datos de entrenamiento: ")
print(test_data.head())
print("Datos objetivo: ")
print(target_data.head())
#+end_src

#+begin_example
Datos de entrenamiento: 
   BI-RADS   Age  Shape  Margin   Density
0      5.0  67.0      1     5.0  3.000000
1      4.0  43.0      4     1.0  2.910734
2      5.0  58.0      0     5.0  3.000000
3      4.0  28.0      4     1.0  3.000000
4      5.0  74.0      4     5.0  2.910734
Datos objetivo: 
0    1
1    1
2    1
3    0
4    1
Name: Severity, dtype: int64
#+end_example
** Configuración de algoritmos

Elegimos 5 algoritmos distintos:

1. Naive Bayes
2. Linear Support Vector Classification
3. K Nearest Neighbors
4. Árbol de decisión
5. Perceptrón multicapa (red neuronal)

Procedemos  a evaluar el rendimiento de cada algoritmo, usando las siguientes métricas:

- Accuracy score
- Matriz de confusión
- Cross validation score
- Area under the curve (AUC)

Vamos a realizar 2 ejecuciones por algoritmo, para evaluar las diferencias que obtenemos según el preprocesado utilizado (eliminación de valores nulos o imputación).

La implementación se encuentra en el archivo /processing.py/, cuyo contenido mostramos  a continuación:

#+begin_src python
from numpy import mean
from sklearn.metrics import confusion_matrix, accuracy_score, roc_auc_score
from sklearn.model_selection import cross_val_score
from sklearn.naive_bayes import GaussianNB
from sklearn.neural_network import MLPClassifier
from sklearn.neighbors import KNeighborsClassifier
from sklearn.preprocessing import scale
from sklearn.svm import LinearSVC
from sklearn.tree import DecisionTreeClassifier

from sys import argv

from preprocessing import parse_data, split_k_sets


def choose_model(model):
    if model == "gnb":
        return GaussianNB()
    elif model == "svc":
        return LinearSVC(random_state=42)
    elif model == "knn":
        return KNeighborsClassifier(n_neighbors=10)
    elif model == "tree":
        return DecisionTreeClassifier(random_state=42)
    elif model == "neuralnet":
        return MLPClassifier(hidden_layer_sizes=10)
    else:
        print("Unknown model selected. The choices are: ")
        print("gnb: Gaussian Naive Bayes")
        print("svc: Linear Support Vector Classification")
        print("knn: K-neighbors")
        print("tree: Decision tree")
        print("neuralnet: MLP Classifier")
        exit()


def predict_data(data, target, model):
    model = choose_model(model)
    if model == "knn":
        data = scale(data)
    accuracy_scores = []
    confusion_matrices = []
    auc = []
    for train_index, test_index in split_k_sets(data):
        model.fit(data.iloc[train_index], target.iloc[train_index])
        prediction = model.predict(data.iloc[test_index])
        accuracy_scores.append(accuracy_score(target.iloc[test_index], prediction))
        confusion_matrices.append(confusion_matrix(target.iloc[test_index], prediction))
        auc.append(roc_auc_score(target.iloc[test_index], prediction))
    cv_score = cross_val_score(model, data, target, cv=10)
    evaluate_performance(
        confusion_matrix=mean(confusion_matrices, axis=0),
        accuracy=mean(accuracy_scores),
        cv_score=mean(cv_score),
        auc=mean(auc),
    )


def evaluate_performance(confusion_matrix, accuracy, cv_score, auc):
    print("Accuracy Score: " + str(accuracy))
    print("Confusion matrix: ")
    print(str(confusion_matrix))
    print("Cross validation score: " + str(cv_score))
    print("AUC: " + str(auc))
#+end_src

** Resultados obtenidos
*** Naives Bayes

Los resultados que obtenemos son los siguientes:

#+CAPTION: Naive Bayes
[[./assets/gnb.png]]
*** Linear SVC

Los resultados que obtenemos son los siguientes:

#+CAPTION: Linear SVC con eliminación
[[./assets/svc_drop.png]]

#+CAPTION: Linear SVC con imputación
[[./assets/svc_fill.png]]
*** KNN
Antes de ejecutar este algoritmo, normalizamos los datos dado que el /KNN/ es un algoritmo basado en distancia.

Los resultados que obtenemos son los siguientes:

#+CAPTION: KNN
[[./assets/knn.png]]
*** Árbol de decisión

Los resultados que obtenemos son los siguientes:

#+CAPTION: Árbol de decisión
[[./assets/tree.png]]
*** Perceptrón multicapa

Los resultados que obtenemos son los siguientes:

#+CAPTION: Perceptrón multicapa con eliminación
[[./assets/neuralnet_drop.png]]

#+CAPTION: Perceptrón multicapa con imputación
[[./assets/neuralnet_fill.png]]
** Análisis de resultados