4  Árboles de decisión

Los árboles de decisión son modelos de aprendizaje simples e intuitivos que pueden utilizarse para tanto para predecir variables cuantitativas (regresión) como categóricas (clasificación). Esta práctica contiene ejercicios que muestran como construir modelos de aprendizaje basados en árboles de decisión con Julia.

4.1 Ejercicios Resueltos

Para la realización de esta práctica se requieren los siguientes paquetes:

using CSV  # Para la lectura de archivos CSV.
using DataFrames  # Para el manejo de datos tabulares.
using Tidier # Para el preprocesamiento de datos.
using PrettyTables  # Para mostrar tablas formateadas.
using GLMakie  # Para obtener gráficos interactivos.
using AlgebraOfGraphics # Para generar gráficos mediante la gramática de gráficos.
using DecisionTree # Para construir árboles de decisión.
using GraphMakie # Para la visualización de árboles de decisión.

Ejercicio 4.1 El conjunto de datos tenis.csv contiene información sobre las condiciones meteorológicas de varios días y si se pudo jugar al tenis o no.

  1. Cargar los datos del archivo tenis.csv en un data frame.

    using CSV, DataFrames
    df = CSV.read(download("https://aprendeconalf.es/aprendizaje-automatico-practicas-julia/datos/tenis.csv"), DataFrame)
    14×5 DataFrame
    Row Cielo Temperatura Humedad Viento Tenis
    String15 String15 String7 String7 String3
    1 Soleado Caluroso Alta Suave No
    2 Soleado Caluroso Alta Fuerte No
    3 Nublado Caluroso Alta Suave
    4 Lluvioso Moderado Alta Suave
    5 Lluvioso Frío Normal Suave
    6 Lluvioso Frío Normal Fuerte No
    7 Nublado Frío Normal Fuerte
    8 Soleado Moderado Alta Suave No
    9 Soleado Frío Normal Suave
    10 Lluvioso Moderado Normal Suave
    11 Soleado Moderado Normal Fuerte
    12 Nublado Moderado Alta Fuerte
    13 Nublado Caluroso Normal Suave
    14 Lluvioso Moderado Alta Fuerte No
  2. Crear un diagrama de barras que muestre la distribución de frecuencias de cada variable meteorológica según si se pudo jugar al tenis o no. ¿Qué variable meteorológica parece tener más influencia en la decisión de jugar al tenis?

    using GLMakie, AlgebraOfGraphics
    
    function frecuencias(df::DataFrame, var::Symbol)
        # Calculamos el número de días de cada clase que se juega al tenis.
        frec = combine(groupby(df, [var, :Tenis]), nrow => :Días)
        # Dibujamos el diagrama de barras.
        plt = data(frec) * 
        mapping(var, :Días, stack = :Tenis, color = :Tenis, ) * 
        visual(BarPlot) 
        # Devolvemos el gráfico.
        return plt
    end
    
    fig = Figure()
    draw!(fig[1, 1], frecuencias(df, :Cielo))
    draw!(fig[1, 2], frecuencias(df, :Temperatura))
    draw!(fig[1, 3], frecuencias(df, :Humedad))
    draw!(fig[1, 4], frecuencias(df, :Viento))
    fig
    ┌ Warning: Found `resolution` in the theme when creating a `Scene`. The `resolution` keyword for `Scene`s and `Figure`s has been deprecated. Use `Figure(; size = ...` or `Scene(; size = ...)` instead, which better reflects that this is a unitless size and not a pixel resolution. The key could also come from `set_theme!` calls or related theming functions.
    └ @ Makie ~/.julia/packages/Makie/ux0Te/src/scenes.jl:238

    A la vista de las frecuencias de cada variable, las variable Cielo y Humedad parecen ser las que más influye en la decisión de jugar al tenis.

  3. Calcular la impureza del conjunto de datos utilizando el índice de Gini. ¿Qué variable meteorológica parece tener más influencia en la decisión de jugar al tenis?

    El índice de Gini se calcula mediante la fórmula

    GI=1i=1npi2

    donde pi es la proporción de cada clase en el conjunto de datos y n es el número de clases.

    El índice de Gini toma valores entre 0 y 11n (0.5 en el caso de clasificación binaria), donde 0 indica que todas las instancias pertenecen a una sola clase (mínima impureza) y 11n indica que las instancias están distribuidas uniformemente entre todas las clases (máxima impureza).

    function gini(df::DataFrame, var::Symbol)
        # Calculamos el número de ejemplos.
        n = nrow(df)
        # Calculamos las frecuencias absolutas de cada clase.
        frec = combine(groupby(df, var), nrow => :ni)
        # Calculamos la proporción de cada clase.
        frec.p = frec.ni ./ n
        # Calculamos el índice de Gini.
        gini = 1 - sum(frec.p .^ 2)
        return gini
    end
    
    g0 = gini(df, :Tenis)
    0.4591836734693877
  4. ¿Qué reducción del índice Gini se obtiene si dividimos el conjunto de ejemplos según la variable Humedad? ¿Y si dividimos el conjunto con respecto a la variable Viento?

    La reducción del índice de Gini se calcula como la diferencia entre el índice de Gini del conjunto original y el índice de Gini del conjunto dividido.

    ΔGI=GIoriginalGIdividido

    donde el índice de Gini del conjunto dividido es la media ponderada de los índices de Gini de los subconjuntos resultantes de la división.

    Calculamos primero la reducción del índice de Gini al dividir el conjunto de ejemplos según la variable Humedad.

    using Tidier
    # Dividimos el conjunto de ejemplos según la variable Humedad.
    df_humedad_alta = @filter(df, Humedad == "Alta")
    df_humedad_normal = @filter(df, Humedad == "Normal")
    # Calculamos los tamaños de los subconjuntos de ejemplos.
    n = nrow(df_humedad_alta), nrow(df_humedad_normal)
    # Calculamos el índice de Gini de cada subconjunto.
    gis = gini(df_humedad_alta, :Tenis), gini(df_humedad_normal, :Tenis)
    # Calculamos media ponderada de los índices de Gini de los subconjuntos 
    g_humedad = sum(gis .* n) / sum(n)
    # Calculamos la reducción del índice de Gini.
    g0 - g_humedad
    0.09183673469387743

    Calculamos ahora la reducción del índice de Gini al dividir el conjunto de ejemplos según la variable Viento.

    # Dividimos el conjunto de ejemplos según la variable `Viento`
    df_viento_fuerte = @filter(df, Viento == "Fuerte")
    df_viento_suave = @filter(df, Viento == "Suave")
    # Calculamos los tamaños de los subconjuntos de ejemplos
    n = nrow(df_viento_fuerte), nrow(df_viento_suave)
    # Calculamos el índice de Gini de cada subconjunto
    gis = gini(df_viento_fuerte, :Tenis), gini(df_viento_suave, :Tenis)
    # Calculamos media ponderada de los índices de Gini de los subconjuntos
    g_viento = sum(gis .* n) / sum(n)
    # Calculamos la reducción del índice de Gini
    g0 - g_viento
    0.030612244897959162

    Como se puede observar, la reducción del índice de Gini al dividir el conjunto de ejemplos según la variable Humedad es mayor que la reducción del índice de Gini al dividir el conjunto con respecto a la variable Viento. Por lo tanto, la variable Humedad parece tener más influencia en la decisión de jugar al tenis y sería la variable que se debería elegir para dividir el conjunto de ejemplos.

  5. Construir un árbol de decisión que explique si se puede jugar al tenis en función de las variables meteorológicas.

    Usar la función DecisionTreeClassifier del paquete DecisionTree.jl.

    Los parámetros más importantes de esta función son:

    • max_depth: Profundidad máxima del árbol. Si no se indica, el árbol crecerá hasta que todas las hojas sean puras o hasta que todas las hojas contengan menos de min_samples_split ejemplos.
    • min_samples_leaf: Número mínimo de ejemplos en una hoja (1 por defecto).
    • min_samples_split: Número mínimo de ejemplos para dividir un nodo (2 por defecto).
    • min_impurity_decrease: Reducción mínima de la impureza para dividir un nodo (0 por defecto).
    • post-prune: Si se indica true, se poda el árbol después de que se ha construido. La poda reduce el tamaño del árbol eliminando nodos que no aportan información útil.
    • merge_purity_threshold: Umbral de pureza para fusionar nodos. Si se indica, se fusionan los nodos que tienen una pureza menor que este umbral.
    • feature_importance: Indica la medida para calcular la importancia de las variables a la hora de dividir el conjunto de datos. Puede ser :impurity o :split. Si no se indica, se utiliza la impureza de Gini.
    • rng: Indica la semilla para la generación de números aleatorios. Si no se indica, se utiliza el generador de números aleatorios por defecto.
    using DecisionTree, CategoricalArrays
    # Variables predictoras.
    X = Matrix(select(df, Not(:Tenis)))
    # Variable objetivo.
    y = df.Tenis
    # Convertir las variables categóricas a enteros.
    X = hcat([levelcode.(categorical(X[:, j])) for j in 1:size(X, 2)]...)
    # Convertir la variable objetivo a enteros.
    y = levelcode.(categorical(y))
    tree = DecisionTreeClassifier(max_depth=3)
    fit!(tree, X, y)
    DecisionTreeClassifier
    max_depth:                3
    min_samples_leaf:         1
    min_samples_split:        2
    min_purity_increase:      0.0
    pruning_purity_threshold: 1.0
    n_subfeatures:            0
    classes:                  [1, 2]
    root:                     Decision Tree
    Leaves: 6
    Depth:  3
  6. Visualizar el árbol de decisión construido.

    Usar la función plot_tree del paquete DecisionTree.jl.

    print_tree(tree, feature_names=names(df)[1:end-1])
    Feature 3: "Humedad" < 2.0 ?
    ├─ Feature 1: "Cielo" < 3.0 ?
        ├─ Feature 4: "Viento" < 2.0 ?
            ├─ 2 : 1/2
            └─ 2 : 2/2
        └─ 1 : 3/3
    └─ Feature 1: "Cielo" < 2.0 ?
        ├─ Feature 4: "Viento" < 2.0 ?
            ├─ 1 : 1/1
            └─ 2 : 2/2
        └─ 2 : 4/4

Ejercicio 4.2 El conjunto de datos pingüinos.csv contiene un conjunto de datos sobre tres especies de pingüinos con las siguientes variables:

  • Especie: Especie de pingüino, comúnmente Adelie, Chinstrap o Gentoo.
  • Isla: Isla del archipiélago Palmer donde se realizó la observación.
  • Longitud_pico: Longitud del pico en mm.
  • Profundidad_pico: Profundidad del pico en mm
  • Longitud_ala: Longitud de la aleta en mm.
  • Peso: Masa corporal en gramos.
  • Sexo: Sexo
  1. Cargar los datos del archivo pinguïnos.csv en un data frame.

    using CSV, DataFrames
    df = CSV.read(download("https://aprendeconalf.es/aprendizaje-automatico-practicas-julia/datos/pingüinos.csv"), DataFrame, missingstring="NA")
    344×7 DataFrame
    319 rows omitted
    Row Especie Isla Longitud_pico Profundidad_pico Longitud_ala Peso Sexo
    String15 String15 Float64? Float64? Int64? Int64? String7?
    1 Adelie Torgersen 39.1 18.7 181 3750 macho
    2 Adelie Torgersen 39.5 17.4 186 3800 hembra
    3 Adelie Torgersen 40.3 18.0 195 3250 hembra
    4 Adelie Torgersen missing missing missing missing missing
    5 Adelie Torgersen 36.7 19.3 193 3450 hembra
    6 Adelie Torgersen 39.3 20.6 190 3650 macho
    7 Adelie Torgersen 38.9 17.8 181 3625 hembra
    8 Adelie Torgersen 39.2 19.6 195 4675 macho
    9 Adelie Torgersen 34.1 18.1 193 3475 missing
    10 Adelie Torgersen 42.0 20.2 190 4250 missing
    11 Adelie Torgersen 37.8 17.1 186 3300 missing
    12 Adelie Torgersen 37.8 17.3 180 3700 missing
    13 Adelie Torgersen 41.1 17.6 182 3200 hembra
    333 Chinstrap Dream 45.2 16.6 191 3250 hembra
    334 Chinstrap Dream 49.3 19.9 203 4050 macho
    335 Chinstrap Dream 50.2 18.8 202 3800 macho
    336 Chinstrap Dream 45.6 19.4 194 3525 hembra
    337 Chinstrap Dream 51.9 19.5 206 3950 macho
    338 Chinstrap Dream 46.8 16.5 189 3650 hembra
    339 Chinstrap Dream 45.7 17.0 195 3650 hembra
    340 Chinstrap Dream 55.8 19.8 207 4000 macho
    341 Chinstrap Dream 43.5 18.1 202 3400 hembra
    342 Chinstrap Dream 49.6 18.2 193 3775 macho
    343 Chinstrap Dream 50.8 19.0 210 4100 macho
    344 Chinstrap Dream 50.2 18.7 198 3775 hembra
  2. Hacer un análisis de los datos perdidos en el data frame.

    describe(df, :nmissing)
    7×2 DataFrame
    Row variable nmissing
    Symbol Int64
    1 Especie 0
    2 Isla 0
    3 Longitud_pico 2
    4 Profundidad_pico 2
    5 Longitud_ala 2
    6 Peso 2
    7 Sexo 11
  3. Eliminar del data frame los casos con valores perdidos.

    dropmissing!(df)
    333×7 DataFrame
    308 rows omitted
    Row Especie Isla Longitud_pico Profundidad_pico Longitud_ala Peso Sexo
    String15 String15 Float64 Float64 Int64 Int64 String7
    1 Adelie Torgersen 39.1 18.7 181 3750 macho
    2 Adelie Torgersen 39.5 17.4 186 3800 hembra
    3 Adelie Torgersen 40.3 18.0 195 3250 hembra
    4 Adelie Torgersen 36.7 19.3 193 3450 hembra
    5 Adelie Torgersen 39.3 20.6 190 3650 macho
    6 Adelie Torgersen 38.9 17.8 181 3625 hembra
    7 Adelie Torgersen 39.2 19.6 195 4675 macho
    8 Adelie Torgersen 41.1 17.6 182 3200 hembra
    9 Adelie Torgersen 38.6 21.2 191 3800 macho
    10 Adelie Torgersen 34.6 21.1 198 4400 macho
    11 Adelie Torgersen 36.6 17.8 185 3700 hembra
    12 Adelie Torgersen 38.7 19.0 195 3450 hembra
    13 Adelie Torgersen 42.5 20.7 197 4500 macho
    322 Chinstrap Dream 45.2 16.6 191 3250 hembra
    323 Chinstrap Dream 49.3 19.9 203 4050 macho
    324 Chinstrap Dream 50.2 18.8 202 3800 macho
    325 Chinstrap Dream 45.6 19.4 194 3525 hembra
    326 Chinstrap Dream 51.9 19.5 206 3950 macho
    327 Chinstrap Dream 46.8 16.5 189 3650 hembra
    328 Chinstrap Dream 45.7 17.0 195 3650 hembra
    329 Chinstrap Dream 55.8 19.8 207 4000 macho
    330 Chinstrap Dream 43.5 18.1 202 3400 hembra
    331 Chinstrap Dream 49.6 18.2 193 3775 macho
    332 Chinstrap Dream 50.8 19.0 210 4100 macho
    333 Chinstrap Dream 50.2 18.7 198 3775 hembra
  4. Crear diagramas que muestren la distribución de frecuencias de cada variable según la especie de pingüino. ¿Qué variable parece tener más influencia en la especie de pingüino?

    Para las variables cualitativas dibujamos diagramas de barras.

    using GLMakie, AlgebraOfGraphics
    
    frec_isla = combine(groupby(df, [:Isla, :Especie]), nrow => :Frecuencia)
    data(frec_isla) * 
        mapping(:Isla, :Frecuencia, stack = :Especie, color =:Especie) *
        visual(BarPlot) |> draw
    ┌ Warning: Found `resolution` in the theme when creating a `Scene`. The `resolution` keyword for `Scene`s and `Figure`s has been deprecated. Use `Figure(; size = ...` or `Scene(; size = ...)` instead, which better reflects that this is a unitless size and not a pixel resolution. The key could also come from `set_theme!` calls or related theming functions.
    └ @ Makie ~/.julia/packages/Makie/ux0Te/src/scenes.jl:238

    frec_sexo = combine(groupby(df, [:Sexo, :Especie]), nrow => :Frecuencia)
    data(frec_sexo) * 
        mapping(:Sexo, :Frecuencia, stack = :Especie, color =:Especie) *
        visual(BarPlot) |> draw
    ┌ Warning: Found `resolution` in the theme when creating a `Scene`. The `resolution` keyword for `Scene`s and `Figure`s has been deprecated. Use `Figure(; size = ...` or `Scene(; size = ...)` instead, which better reflects that this is a unitless size and not a pixel resolution. The key could also come from `set_theme!` calls or related theming functions.
    └ @ Makie ~/.julia/packages/Makie/ux0Te/src/scenes.jl:238

    Para las variables cuantitativas dibujamos diagramas de cajas.

    function cajas(df, var, clase)
        data(df) *
            mapping(clase, var, color = clase) *
            visual(BoxPlot) |> 
            draw
    end
    
    cajas(df, :Longitud_pico, :Especie)
    ┌ Warning: Found `resolution` in the theme when creating a `Scene`. The `resolution` keyword for `Scene`s and `Figure`s has been deprecated. Use `Figure(; size = ...` or `Scene(; size = ...)` instead, which better reflects that this is a unitless size and not a pixel resolution. The key could also come from `set_theme!` calls or related theming functions.
    └ @ Makie ~/.julia/packages/Makie/ux0Te/src/scenes.jl:238

    cajas(df, :Profundidad_pico, :Especie)
    ┌ Warning: Found `resolution` in the theme when creating a `Scene`. The `resolution` keyword for `Scene`s and `Figure`s has been deprecated. Use `Figure(; size = ...` or `Scene(; size = ...)` instead, which better reflects that this is a unitless size and not a pixel resolution. The key could also come from `set_theme!` calls or related theming functions.
    └ @ Makie ~/.julia/packages/Makie/ux0Te/src/scenes.jl:238

    cajas(df, :Longitud_ala, :Especie)
    ┌ Warning: Found `resolution` in the theme when creating a `Scene`. The `resolution` keyword for `Scene`s and `Figure`s has been deprecated. Use `Figure(; size = ...` or `Scene(; size = ...)` instead, which better reflects that this is a unitless size and not a pixel resolution. The key could also come from `set_theme!` calls or related theming functions.
    └ @ Makie ~/.julia/packages/Makie/ux0Te/src/scenes.jl:238

    cajas(df, :Peso, :Especie)
    ┌ Warning: Found `resolution` in the theme when creating a `Scene`. The `resolution` keyword for `Scene`s and `Figure`s has been deprecated. Use `Figure(; size = ...` or `Scene(; size = ...)` instead, which better reflects that this is a unitless size and not a pixel resolution. The key could also come from `set_theme!` calls or related theming functions.
    └ @ Makie ~/.julia/packages/Makie/ux0Te/src/scenes.jl:238

  5. ¿Cuál es la reducción de la impureza del conjunto de datos si dividimos el conjunto de datos en dos conjuntos según si la longitud del pico es mayor o menor que 44 mm?

    using Tidier
    function gini(df::DataFrame, var::Symbol)
        n = nrow(df)
        frec = combine(groupby(df, var), nrow => :ni)
        frec.p = frec.ni ./ n
        gini = 1 - sum(frec.p .^ 2)
        return gini
    end
    
    function reduccion_impureza(df::DataFrame, var::Symbol, val::Number)
        # Dividimos el conjunto de ejemplos según la longitud del pico es menor de 44.
        df_menor = @eval @filter($df, $var <= $val)
        df_mayor = @eval @filter($df, $var > $val)
        # Calculamos los tamaños de los subconjuntos de ejemplos.
        n = nrow(df_menor), nrow(df_mayor)
        # Calculamos el índice de Gini de cada subconjunto.
        gis = gini(df_menor, :Especie), gini(df_mayor, :Especie)
        # Calculamos media ponderada de los índices de Gini de los subconjuntos.
        g1 = sum(gis .* n) / sum(n)
        # Calculamos la reducción del índice de Gini.
        gini(df, :Especie) - g1
    end
    
    reduccion_impureza(df, :Longitud_pico, 44)
    0.26577182779353914
  6. Determinar el valor óptimo de división del conjunto de datos según la longitud del pico. Para ello, calcular la reducción de la impureza para cada valor de longitud del pico y dibujar el resultado.

    Dibujamos la reducción de la impureza en función de la longitud del pico.

    # Valores únicos de longitud del pico.
    valores = unique(df.Longitud_pico)
    # Reducción de la impureza para cada valor.
    reducciones = [reduccion_impureza(df, :Longitud_pico, val) for val in valores]
    # Graficamos el resultado.
    using GLMakie
    fig = Figure()
    ax = Axis(fig[1, 1], title = "Reducción de la impureza según la longitud del pico", xlabel = "Longitud del pico", ylabel = "Reducción de la impureza")
    scatter!(ax, valores, reducciones)
    ┌ Warning: Found `resolution` in the theme when creating a `Scene`. The `resolution` keyword for `Scene`s and `Figure`s has been deprecated. Use `Figure(; size = ...` or `Scene(; size = ...)` instead, which better reflects that this is a unitless size and not a pixel resolution. The key could also come from `set_theme!` calls or related theming functions.
    └ @ Makie ~/.julia/packages/Makie/ux0Te/src/scenes.jl:238
    Scatter{Tuple{Vector{Point{2, Float64}}}}

    Y ahora obtenemos el valor óptimo de división del conjunto de datos según la longitud del pico.

    val_optimo = valores[argmax(reducciones)]
    42.3
  7. Dividir aleatoriamente el dataframe en un conjunto de entrenamiento y un conjunto de test con proporciones 3/4 y 1/4 respectivamente.

    Utilizar la función shuffle del paquete Random para barajar el dataframe y luego dividirlo en dos subconjuntos.

    using Random
    # Establecemos la semilla para la reproducibilidad.
    Random.seed!(1234)
    # Barajamos el dataframe.
    df = shuffle(df)
    # Dividimos el dataframe en un conjunto de entrenamiento y un conjunto de test.
    n = nrow(df)
    df_test = df[1:div(n, 4), :]
    df_train = df[div(n, 4)+1:end, :]
    250×7 DataFrame
    225 rows omitted
    Row Especie Isla Longitud_pico Profundidad_pico Longitud_ala Peso Sexo
    String15 String15 Float64 Float64 Int64 Int64 String7
    1 Adelie Dream 39.0 18.7 185 3650 macho
    2 Chinstrap Dream 52.8 20.0 205 4550 macho
    3 Chinstrap Dream 55.8 19.8 207 4000 macho
    4 Adelie Torgersen 35.1 19.4 193 4200 macho
    5 Adelie Torgersen 34.6 21.1 198 4400 macho
    6 Gentoo Biscoe 50.0 15.2 218 5700 macho
    7 Chinstrap Dream 50.6 19.4 193 3800 macho
    8 Chinstrap Dream 43.5 18.1 202 3400 hembra
    9 Adelie Dream 36.9 18.6 189 3500 hembra
    10 Adelie Dream 36.6 18.4 184 3475 hembra
    11 Chinstrap Dream 46.6 17.8 193 3800 hembra
    12 Gentoo Biscoe 50.8 17.3 228 5600 macho
    13 Chinstrap Dream 52.2 18.8 197 3450 macho
    239 Adelie Dream 39.8 19.1 184 4650 macho
    240 Adelie Torgersen 43.1 19.2 197 3500 macho
    241 Chinstrap Dream 49.8 17.3 198 3675 hembra
    242 Gentoo Biscoe 49.8 15.9 229 5950 macho
    243 Chinstrap Dream 50.8 18.5 201 4450 macho
    244 Gentoo Biscoe 50.7 15.0 223 5550 macho
    245 Gentoo Biscoe 46.2 14.1 217 4375 hembra
    246 Adelie Torgersen 35.5 17.5 190 3700 hembra
    247 Adelie Biscoe 39.7 18.9 184 3550 macho
    248 Gentoo Biscoe 47.7 15.0 216 4750 hembra
    249 Adelie Torgersen 42.9 17.6 196 4700 macho
    250 Adelie Dream 40.8 18.9 208 4300 macho
  8. Construir un árbol de decisión con el conjunto de entrenamiento sin tener en cuenta la variable Isla y visualizarlo.

    using DecisionTree, CategoricalArrays
    # Variables predictivas.
    X_train = Matrix(select(df_train, Not(:Isla, :Especie)))
    # Variable objetivo.
    y_train = df_train.Especie
    # Convertir las variables categóricas a enteros.
    X_train = hcat([levelcode.(categorical(X_train[:, j])) for j in 1:size(X_train, 2)]...)
    # Convertir la variable objetivo a enteros
    y_train = levelcode.(categorical(y_train))
    
    # Construimos el árbol de decisión con profundidad máxima 3.
    tree = DecisionTreeClassifier(max_depth = 3)
    fit!(tree, X_train, y_train)
    print_tree(tree, feature_names=names(df)[3:end])
    Feature 3: "Longitud_ala" < 29.0 ?
    ├─ Feature 1: "Longitud_pico" < 62.0 ?
        ├─ 1 : 96/96
        └─ Feature 1: "Longitud_pico" < 87.0 ?
            ├─ 2 : 10/20
            └─ 2 : 37/38
    └─ Feature 2: "Profundidad_pico" < 46.0 ?
        ├─ 3 : 90/90
        └─ Feature 1: "Longitud_pico" < 109.0 ?
            ├─ 1 : 2/2
            └─ 2 : 4/4
  9. Predecir la especie de los pingüinos del conjunto de test y calcular la matriz de confusión de las predicciones.

    Utilizar la función confmat del paquete StatisticalMeaures para barajar el dataframe y luego dividirlo en dos subconjuntos.

    using StatisticalMeasures
    # Variables predictivas
    X_test = Matrix(select(df_test, Not(:Isla, :Especie)))
    # Variable objetivo
    y_test = df_test.Especie
    # Convertir las variables categóricas a enteros
    X_test = hcat([levelcode.(categorical(X_test[:, j])) for j in 1:size(X_test, 2)]...)
    # Convertir la variable objetivo a enteros
    y_test = levelcode.(categorical(y_test))
    # Predecimos la especie de pingüino del conjunto de test
    y_pred = predict(tree, X_test)
    # Calculamos la precisión del modelo
    confmat(y_pred, y_test)
              ┌──────────────┐
              │ Ground Truth │
    ┌─────────┼────┬────┬────┤
    │Predicted│ 1  │ 2  │ 3  │
    ├─────────┼────┼────┼────┤
    │    1    │ 38 │ 11 │ 9  │
    ├─────────┼────┼────┼────┤
    │    2    │ 0  │ 6  │ 0  │
    ├─────────┼────┼────┼────┤
    │    3    │ 0  │ 0  │ 19 │
    └─────────┴────┴────┴────┘
  10. Calcular la precisión del modelo.

    La precisión es la proporción de predicciones correctas sobre el total de predicciones.

    Utilizar la función accuracy del paquete StatisticalMeaures para calcular la precisión del modelo.

    # Calculamos la precisión del modelo
    accuracy(y_pred, y_test)
    0.7590361445783133

Ejercicio 4.3 El fichero vinos.csv contiene información sobre las características de una muestra de vinos portugueses de la denominación “Vinho Verde”. Las variables que contiene son:

Variable Descripción Tipo (unidades)
tipo Tipo de vino Categórica (blanco, tinto)
meses.barrica Mesesde envejecimiento en barrica Numérica(meses)
acided.fija Cantidadde ácidotartárico Numérica(g/dm3)
acided.volatil Cantidad de ácido acético Numérica(g/dm3)
acido.citrico Cantidad de ácidocítrico Numérica(g/dm3)
azucar.residual Cantidad de azúcarremanente después de la fermentación Numérica(g/dm3)
cloruro.sodico Cantidad de clorurosódico Numérica(g/dm3)
dioxido.azufre.libre Cantidad de dióxido de azufreen formalibre Numérica(mg/dm3)
dioxido.azufre.total Cantidadde dióxido de azufretotal en forma libre o ligada Numérica(mg/dm3)
densidad Densidad Numérica(g/cm3)
ph pH Numérica(0-14)
sulfatos Cantidadde sulfato de potasio Numérica(g/dm3)
alcohol Porcentajede contenidode alcohol Numérica(0-100)
calidad Calificación otorgada porun panel de expertos Numérica(0-10)
  1. Crear un data frame con los datos de los vinos a partir del fichero vinos.csv.

    using CSV, DataFrames
    df = CSV.read(download("https://aprendeconalf.es/aprendizaje-automatico-practicas-julia/datos/vinos.csv"), DataFrame)
    5320×14 DataFrame
    5295 rows omitted
    Row tipo meses_barrica acided_fija acided_volatil acido_citrico azucar_residual cloruro_sodico dioxido_azufre_libre dioxido_azufre_total densidad ph sulfatos alcohol calidad
    String7 Int64 Float64 Float64 Float64 Float64 Float64 Float64 Float64 Float64 Float64 Float64 Float64 Int64
    1 blanco 0 7.0 0.27 0.36 20.7 0.045 45.0 170.0 1.001 3.0 0.45 8.8 6
    2 blanco 0 6.3 0.3 0.34 1.6 0.049 14.0 132.0 0.994 3.3 0.49 9.5 6
    3 blanco 0 8.1 0.28 0.4 6.9 0.05 30.0 97.0 0.9951 3.26 0.44 10.1 6
    4 blanco 0 7.2 0.23 0.32 8.5 0.058 47.0 186.0 0.9956 3.19 0.4 9.9 6
    5 blanco 0 6.2 0.32 0.16 7.0 0.045 30.0 136.0 0.9949 3.18 0.47 9.6 6
    6 blanco 0 8.1 0.22 0.43 1.5 0.044 28.0 129.0 0.9938 3.22 0.45 11.0 6
    7 blanco 0 8.1 0.27 0.41 1.45 0.033 11.0 63.0 0.9908 2.99 0.56 12.0 5
    8 blanco 0 8.6 0.23 0.4 4.2 0.035 17.0 109.0 0.9947 3.14 0.53 9.7 5
    9 blanco 0 7.9 0.18 0.37 1.2 0.04 16.0 75.0 0.992 3.18 0.63 10.8 5
    10 blanco 0 6.6 0.16 0.4 1.5 0.044 48.0 143.0 0.9912 3.54 0.52 12.4 7
    11 blanco 0 8.3 0.42 0.62 19.25 0.04 41.0 172.0 1.0002 2.98 0.67 9.7 5
    12 blanco 0 6.6 0.17 0.38 1.5 0.032 28.0 112.0 0.9914 3.25 0.55 11.4 7
    13 blanco 0 6.3 0.48 0.04 1.1 0.046 30.0 99.0 0.9928 3.24 0.36 9.6 6
    5309 tinto 7 7.5 0.31 0.41 2.4 0.065 34.0 60.0 0.99492 3.34 0.85 11.4 6
    5310 tinto 7 5.8 0.61 0.11 1.8 0.066 18.0 28.0 0.99483 3.55 0.66 10.9 6
    5311 tinto 10 7.2 0.66 0.33 2.5 0.068 34.0 102.0 0.99414 3.27 0.78 12.8 6
    5312 tinto 3 6.6 0.725 0.2 7.8 0.073 29.0 79.0 0.9977 3.29 0.54 9.2 5
    5313 tinto 7 6.3 0.55 0.15 1.8 0.077 26.0 35.0 0.99314 3.32 0.82 11.6 6
    5314 tinto 9 5.4 0.74 0.09 1.7 0.089 16.0 26.0 0.99402 3.67 0.56 11.6 6
    5315 tinto 3 6.3 0.51 0.13 2.3 0.076 29.0 40.0 0.99574 3.42 0.75 11.0 6
    5316 tinto 3 6.8 0.62 0.08 1.9 0.068 28.0 38.0 0.99651 3.42 0.82 9.5 6
    5317 tinto 5 6.2 0.6 0.08 2.0 0.09 32.0 44.0 0.9949 3.45 0.58 10.5 5
    5318 tinto 10 5.9 0.55 0.1 2.2 0.062 39.0 51.0 0.99512 3.52 0.76 11.2 6
    5319 tinto 6 5.9 0.645 0.12 2.0 0.075 32.0 44.0 0.99547 3.57 0.71 10.2 5
    5320 tinto 3 6.0 0.31 0.47 3.6 0.067 18.0 42.0 0.99549 3.39 0.66 11.0 6
  2. Mostrar los tipos de cada variable del data frame.

    Usar la función schema del paquete MLJ.

    using MLJ
    schema(df)
    WARNING: using MLJ.fit! in module Main conflicts with an existing identifier.
    WARNING: using MLJ.predict in module Main conflicts with an existing identifier.
    ┌──────────────────────┬────────────┬─────────┐
    │ names                │ scitypes   │ types   │
    ├──────────────────────┼────────────┼─────────┤
    │ tipo                 │ Textual    │ String7 │
    │ meses_barrica        │ Count      │ Int64   │
    │ acided_fija          │ Continuous │ Float64 │
    │ acided_volatil       │ Continuous │ Float64 │
    │ acido_citrico        │ Continuous │ Float64 │
    │ azucar_residual      │ Continuous │ Float64 │
    │ cloruro_sodico       │ Continuous │ Float64 │
    │ dioxido_azufre_libre │ Continuous │ Float64 │
    │ dioxido_azufre_total │ Continuous │ Float64 │
    │ densidad             │ Continuous │ Float64 │
    │ ph                   │ Continuous │ Float64 │
    │ sulfatos             │ Continuous │ Float64 │
    │ alcohol              │ Continuous │ Float64 │
    │ calidad              │ Count      │ Int64   │
    └──────────────────────┴────────────┴─────────┘
    
  3. Hacer un análisis de los datos perdidos en el data frame.

    describe(df, :nmissing)
    14×2 DataFrame
    Row variable nmissing
    Symbol Int64
    1 tipo 0
    2 meses_barrica 0
    3 acided_fija 0
    4 acided_volatil 0
    5 acido_citrico 0
    6 azucar_residual 0
    7 cloruro_sodico 0
    8 dioxido_azufre_libre 0
    9 dioxido_azufre_total 0
    10 densidad 0
    11 ph 0
    12 sulfatos 0
    13 alcohol 0
    14 calidad 0
  4. Se considera que un vino es bueno si tiene una puntuación de calidad mayor que 6.5. Recodificar la variable calidad en una variable categórica que tome el valor 1 si la calidad es mayor que 6.5 y 0 en caso contrario.

    using CategoricalArrays
    # Recodificamos la variable calidad.
    df.calidad = cut(df.calidad, [0, 6.5, 10], labels = [" ☹️ ", " 😊 "])
    5320-element CategoricalArray{String,1,UInt32}:
     " ☹️ "
     " ☹️ "
     " ☹️ "
     " ☹️ "
     " ☹️ "
     " ☹️ "
     " ☹️ "
     " ☹️ "
     " ☹️ "
     " 😊 "
     " ☹️ "
     " 😊 "
     " ☹️ "
     ⋮
     " ☹️ "
     " ☹️ "
     " ☹️ "
     " ☹️ "
     " ☹️ "
     " ☹️ "
     " ☹️ "
     " ☹️ "
     " ☹️ "
     " ☹️ "
     " ☹️ "
     " ☹️ "
  5. Dividir el data frame en un data frame con las variables predictivas y un vector con la variable objetivo bueno.

    Usar la función unpack del paquete MLJ para dividir el data frame en dos partes, una con las columnas de entrada del modelo y otra con la columna de salida.

    y, X = unpack(df, ==(:calidad), rng = 123)
    (CategoricalValue{String, UInt32}[" ☹️ ", " ☹️ ", " ☹️ ", " ☹️ ", " ☹️ ", " 😊 ", " ☹️ ", " ☹️ ", " ☹️ ", " ☹️ "  …  " ☹️ ", " ☹️ ", " ☹️ ", " ☹️ ", " ☹️ ", " ☹️ ", " ☹️ ", " ☹️ ", " ☹️ ", " ☹️ "], 5320×13 DataFrame
      Row  tipo     meses_barrica  acided_fija  acided_volatil  acido_citrico  az ⋯
          │ String7  Int64          Float64      Float64         Float64        Fl ⋯
    ──────┼─────────────────────────────────────────────────────────────────────────
        1 │ blanco               0          6.7           0.5             0.36     ⋯
        2 │ blanco               0          6.3           0.2             0.3
        3 │ blanco               0          6.2           0.35            0.03
        4 │ tinto                3          8.0           0.39            0.3
        5 │ blanco               0          7.9           0.255           0.26     ⋯
        6 │ blanco               0          6.1           0.31            0.37
        7 │ blanco               0          6.8           0.28            0.36
        8 │ blanco               0          8.2           0.34            0.49
        9 │ tinto                0          6.7           0.48            0.02     ⋯
       10 │ blanco               0          7.4           0.35            0.2
       11 │ tinto                5          7.5           0.53            0.06
      ⋮   │    ⋮           ⋮             ⋮             ⋮               ⋮           ⋱
     5311 │ blanco               0          7.2           0.14            0.35
     5312 │ tinto                3          7.6           0.41            0.24     ⋯
     5313 │ tinto                0          7.3           0.4             0.3
     5314 │ tinto                4          7.1           0.48            0.28
     5315 │ blanco               0          6.4           0.29            0.2
     5316 │ blanco               0          9.4           0.24            0.29     ⋯
     5317 │ blanco               0          6.3           0.25            0.27
     5318 │ blanco               0          5.5           0.16            0.26
     5319 │ blanco               0          7.4           0.36            0.32
     5320 │ blanco               0          7.6           0.51            0.24     ⋯
                                                     8 columns and 5299 rows omitted)
  6. Para poder entrenar un modelo de un arbol de decisión, las variables predictivas deben ser cuantitativas. Transmformar las variables categóricas en variables numéricas.

    Usar la función coerce! del paquete MLJ para transformar las variables categóricas en variables numéricas.

    # Convertir las variables categóricas a enteros.
    coerce!(X, :tipo => OrderedFactor, :meses_barrica => Continuous)
    schema(X)
    ┌──────────────────────┬──────────────────┬───────────────────────────────────┐
    │ names                │ scitypes         │ types                             │
    ├──────────────────────┼──────────────────┼───────────────────────────────────┤
    │ tipo                 │ OrderedFactor{2} │ CategoricalValue{String7, UInt32} │
    │ meses_barrica        │ Continuous       │ Float64                           │
    │ acided_fija          │ Continuous       │ Float64                           │
    │ acided_volatil       │ Continuous       │ Float64                           │
    │ acido_citrico        │ Continuous       │ Float64                           │
    │ azucar_residual      │ Continuous       │ Float64                           │
    │ cloruro_sodico       │ Continuous       │ Float64                           │
    │ dioxido_azufre_libre │ Continuous       │ Float64                           │
    │ dioxido_azufre_total │ Continuous       │ Float64                           │
    │ densidad             │ Continuous       │ Float64                           │
    │ ph                   │ Continuous       │ Float64                           │
    │ sulfatos             │ Continuous       │ Float64                           │
    │ alcohol              │ Continuous       │ Float64                           │
    └──────────────────────┴──────────────────┴───────────────────────────────────┘
    
  7. Definir un modelo de árbol de decisión con profundidad máxima 3.

    Cargar el modelo DecisionTreeClassifier del paquete DecisionTree con la macros @iload.

    # Cargamos el tipo de modelo.
    Tree = @iload DecisionTreeClassifier pkg = "DecisionTree"
    # Instanciamos el modelo con sus parámetros.
    arbol = Tree(max_depth =3, rng = 123)
    import MLJDecisionTreeInterface ✔
    [ Info: For silent loading, specify `verbosity=0`. 
    DecisionTreeClassifier(
      max_depth = 3, 
      min_samples_leaf = 1, 
      min_samples_split = 2, 
      min_purity_increase = 0.0, 
      n_subfeatures = 0, 
      post_prune = false, 
      merge_purity_threshold = 1.0, 
      display_depth = 5, 
      feature_importance = :impurity, 
      rng = 123)
  8. Evaluar el modelo tomando un 70% de ejemplos en el conjunto de entrenamiento y un 30% en el conjunto de test. Utilizar como métrica la precisión.

    Usar la función evaluate del paquete MLJ para evaluar el modelo. Los parámetros más importantes de esta función son:

    • resampling: Indica el método de muestreo para definir los conjuntos de entrenamiento y test. Los métodos más habituales son:
      • Holdout(fraction_train = p): Divide el conjunto de datos tomando una proporción de p ejemplos en el conjunto de entrenamiento y 1p en el conjunto de test.
      • CV(nfolds = n, shuffle = true|false): Utiliza validación cruzada con n iteraciones. Si se indica shuffle = true, se utiliza validación cruzada aleatoria.
      • StratifiedCV(nfolds = n, shuffle = true|false): Utiliza validación cruzada estratificada con n iteraciones. Si se indica shuffle = true, se utiliza validación cruzada estratificada aleatoria.
      • InSample(): Utiliza el conjunto de entrenamiento como conjunto de test.
    • measures: Indica las métricas a utilizar para evaluar el modelo. Las métricas más habituales son:
      • cross_entropy: Pérdida de entropía cruzada.
      • confusion_matrix: Matriz de confusión.
      • true_positive_rate: Tasa de verdaderos positivos.
      • true_negative_rate: Tasa de verdaderos negativos.
      • ppv: Valor predictivo positivo.
      • npv: Valor predictivo negativo.
      • accuracy: Precisión.
      Se puede indicar más de una en un vector.
    evaluate(arbol, X, y, resampling = Holdout(fraction_train = 0.7, rng = 123), measures = accuracy)
    PerformanceEvaluation object with these fields:
      model, measure, operation,
      measurement, per_fold, per_observation,
      fitted_params_per_fold, report_per_fold,
      train_test_rows, resampling, repeats
    Extract:
    ┌────────────┬──────────────┬─────────────┐
    │ measure    │ operation    │ measurement │
    ├────────────┼──────────────┼─────────────┤
    │ Accuracy() │ predict_mode │ 0.843       │
    └────────────┴──────────────┴─────────────┘
    
  9. Evaluar el modelo mediante validación cruzada estratificada usando las métricas de la pérdida de entropía cruzada, la matriz de confusión, la tasa de verdaderos positivos, la tasa de verdaderos negativos, el valor predictivo positivo, el valor predictivo negativo y la precisión. ¿Es un buen modelo?

    evaluate(arbol, X, y, resampling = StratifiedCV(rng = 123), measures = [cross_entropy, confusion_matrix, true_positive_rate, true_negative_rate, ppv, npv, accuracy])
    Evaluating over 6 folds:  33%[========>                ]  ETA: 0:00:02Evaluating over 6 folds: 100%[=========================] Time: 0:00:01
    PerformanceEvaluation object with these fields:
      model, measure, operation,
      measurement, per_fold, per_observation,
      fitted_params_per_fold, report_per_fold,
      train_test_rows, resampling, repeats
    Extract:
    ┌───┬──────────────────────────┬──────────────┬─────────────────────────────────
    │   │ measure                  │ operation    │ measurement                    ⋯
    ├───┼──────────────────────────┼──────────────┼─────────────────────────────────
    │ A │ LogLoss(                 │ predict      │ 0.375                          ⋯
    │   │   tol = 2.22045e-16)     │              │                                ⋯
    │ B │ ConfusionMatrix(         │ predict_mode │ ConfusionMatrix{2}([3821534 78 ⋯
    │   │   levels = nothing,      │              │                                ⋯
    │   │   perm = nothing,        │              │                                ⋯
    │   │   rev = nothing,         │              │                                ⋯
    │   │   checks = true)         │              │                                ⋯
    │ C │ TruePositiveRate(        │ predict_mode │ 0.128                          ⋯
    │   │   levels = nothing,      │              │                                ⋯
    │   │   rev = nothing,         │              │                                ⋯
    │   │   checks = true)         │              │                                ⋯
    │ D │ TrueNegativeRate(        │ predict_mode │ 1.0                            ⋯
    │   │   levels = nothing,      │              │                                ⋯
    │   │   rev = nothing,         │              │                                ⋯
    │   │   checks = true)         │              │                                ⋯
    │ E │ PositivePredictiveValue( │ predict_mode │ 0.994                          ⋯
    │   │   levels = nothing,      │              │                                ⋯
    │   │   rev = nothing,         │              │                                ⋯
    │   │   checks = true)         │              │                                ⋯
    │ F │ NegativePredictiveValue( │ predict_mode │ 0.83                           ⋯
    │   │   levels = nothing,      │              │                                ⋯
    │   │   rev = nothing,         │              │                                ⋯
    │ ⋮ │            ⋮             │      ⋮       │                        ⋮       ⋱
    └───┴──────────────────────────┴──────────────┴─────────────────────────────────
                                                         1 column and 2 rows omitted
    ┌───┬───────────────────────────────────────────────────────────────────────────
    │   │ per_fold                                                                 ⋯
    ├───┼───────────────────────────────────────────────────────────────────────────
    │ A │ [0.391, 0.394, 0.35, 0.358, 0.365, 0.391]                                ⋯
    │ B │ ConfusionMatrix{2, true, CategoricalValue{String, UInt32}}[ConfusionMatr ⋯
    │ C │ [0.125, 0.167, 0.155, 0.112, 0.113, 0.0952]                              ⋯
    │ D │ [1.0, 0.999, 1.0, 1.0, 1.0, 1.0]                                         ⋯
    │ E │ [1.0, 0.966, 1.0, 1.0, 1.0, 1.0]                                         ⋯
    │ F │ [0.83, 0.837, 0.835, 0.827, 0.828, 0.825]                                ⋯
    │ G │ [0.834, 0.841, 0.84, 0.831, 0.832, 0.828]                                ⋯
    └───┴───────────────────────────────────────────────────────────────────────────
                                                                   2 columns omitted
    

    La precisión del modelo es de 0.834 que no está mal, pero si consdieramos la tasa de verdadero positivos, que es 0.13 y la tasa de verdaderos negativos, que es prácticamente 1, el modelo tiene un buen rendimiento en la clasificación de los vinos malos, pero un mal rendimiento en la clasificación de los vinos buenos. Por lo tanto, no podemos decir que sea un buen modelo.

  10. Construir árboles de decisión con profundidades máximas de 2 a 10 y evaluar el modelo con validación cruzada estratificada. ¿Cuál es la profundidad máxima que da mejor resultado?

    Usar la función TunedModel del paquete MLJ para ajustar los parámetros del modelo.

    Los parámetros más importantes de esta función son:
    • model: Indica el modelo a ajustar.
    • resampling: Indica el método de muestreo para definir los conjuntos de entrenamiento y test.
    • tuning: Indica el método de ajuste de los parámetros del modelo. Los métodos más habituales son:
      • Grid(resolution = n): Ajusta los parámetros del modelo utilizando una cuadrícula de búsqueda con n valores.
      • RandomSearch(resolution = n): Ajusta los parámetros del modelo utilizando una búsqueda aleatoria con n valores.
    • range: Indica el rango de valores a utilizar para ajustar los parámetros del modelo. Se puede indicar un rango de valores o un vector de valores.
    • measure: Indica la métrica a utilizar para evaluar el modelo.
    # Instanciamos el modelo de árbol de decisión.
    arbol = Tree()
    # Definimos el rango de valores a utilizar para ajustar los parámetros del modelo.
    r = range(arbol, :max_depth, lower=2, upper=10)
    # Ajustamos los parámetros del modelo utilizando una cuadrícula de búsqueda con 9 valores.
    arbol_parametrizado = TunedModel(
        model = arbol,
        resampling = StratifiedCV(rng = 123),
        tuning = Grid(resolution = 9),
        range = r,
        measure = accuracy)
    # Definimos una máquina de aprendizaje con el modelo, las variables predictivas y la variable objetivo.
    mach = machine(arbol_parametrizado, X, y)
    # Ajustamos los parámetros del modelo.
    MLJ.fit!(mach)
    # Mostramos los parámetros del mejor modelo.
    fitted_params(mach).best_model
    [ Info: Training machine(ProbabilisticTunedModel(model = DecisionTreeClassifier(max_depth = -1, …), …), …).
    [ Info: Attempting to evaluate 9 models.
    Evaluating over 9 metamodels:   0%[>                        ]  ETA: N/AEvaluating over 9 metamodels:  11%[==>                      ]  ETA: 0:00:03Evaluating over 9 metamodels:  22%[=====>                   ]  ETA: 0:00:03Evaluating over 9 metamodels:  33%[========>                ]  ETA: 0:00:02Evaluating over 9 metamodels:  44%[===========>             ]  ETA: 0:00:01Evaluating over 9 metamodels:  56%[=============>           ]  ETA: 0:00:01Evaluating over 9 metamodels:  67%[================>        ]  ETA: 0:00:01Evaluating over 9 metamodels:  78%[===================>     ]  ETA: 0:00:01Evaluating over 9 metamodels:  89%[======================>  ]  ETA: 0:00:00Evaluating over 9 metamodels: 100%[=========================] Time: 0:00:02
    DecisionTreeClassifier(
      max_depth = 5, 
      min_samples_leaf = 1, 
      min_samples_split = 2, 
      min_purity_increase = 0.0, 
      n_subfeatures = 0, 
      post_prune = false, 
      merge_purity_threshold = 1.0, 
      display_depth = 5, 
      feature_importance = :impurity, 
      rng = TaskLocalRNG())
  11. Dibujar la curva de aprendizaje del modelo en función de la profundidad del árbol de decisión.

    Usar la función learning_curve del paquete MLJ para dibujar la curva de aprendizaje. Los parámetros más importantes de esta función son:
    • mach: Indica la máquina de aprendizaje a utilizar.
    • range: Indica el rango de valores a utilizar para ajustar los parámetros del modelo.
    • resampling: Indica el método de muestreo para definir los conjuntos de entrenamiento y test.
    • measure: Indica la métrica a utilizar para evaluar el modelo.
    • rngs: Indica la semilla para la generación de números aleatorios. Se pueden indicar varias semillas en un vector y se genera una curva de aprendizaje para cada semilla.
    # Instanciamos el modelo de árbol de decisión.
    arbol = Tree()
    # Definimos una máquina de aprendizaje con el modelo, las variables predictivas y la variable objetivo.
    mach = machine(arbol, X, y)
    # Definimos el rango de valores a utilizar para ajustar los parámetros del modelo.
    r = range(arbol, :max_depth, lower=2, upper=10)
    # Dibujamos la curva de aprendizaje.
    curva = learning_curve(mach, range = r, resampling = StratifiedCV(rng = 123), measure = accuracy)
    # Dibujamos la curva de aprendizaje.
    fig = Figure()
    ax = Axis(fig[1, 1], title = "Curva de aprendizaje", xlabel = "Profundidad del árbol", ylabel = "Precisión")
    Makie.scatter!(ax, curva.parameter_values, curva.measurements)
    fig
    [ Info: Training machine(ProbabilisticTunedModel(model = DecisionTreeClassifier(max_depth = -1, …), …), …).
    [ Info: Attempting to evaluate 9 models.
    Evaluating over 9 metamodels:  22%[=====>                   ]  ETA: 0:00:01Evaluating over 9 metamodels:  33%[========>                ]  ETA: 0:00:01Evaluating over 9 metamodels:  44%[===========>             ]  ETA: 0:00:01Evaluating over 9 metamodels:  56%[=============>           ]  ETA: 0:00:01Evaluating over 9 metamodels:  67%[================>        ]  ETA: 0:00:01Evaluating over 9 metamodels:  78%[===================>     ]  ETA: 0:00:00Evaluating over 9 metamodels:  89%[======================>  ]  ETA: 0:00:00Evaluating over 9 metamodels: 100%[=========================] Time: 0:00:01
    ┌ Warning: Found `resolution` in the theme when creating a `Scene`. The `resolution` keyword for `Scene`s and `Figure`s has been deprecated. Use `Figure(; size = ...` or `Scene(; size = ...)` instead, which better reflects that this is a unitless size and not a pixel resolution. The key could also come from `set_theme!` calls or related theming functions.
    └ @ Makie ~/.julia/packages/Makie/ux0Te/src/scenes.jl:238

  12. Construir un árbol de decisión con la profundidad máxima que da mejor resultado y visualizarlo.

    # Instanciamos el modelo de árbol de decisión.
    arbol = Tree(max_depth = 4)
    # Definimos una máquina de aprendizaje con el modelo, las variables predictivas y la variable objetivo.
    mach = machine(arbol, X, y)
    # Ajustamos los parámetros del modelo.
    MLJ.fit!(mach)
    # Visualizamos el árbol de decisión.
    fitted_params(mach).tree
    [ Info: Training machine(DecisionTreeClassifier(max_depth = 4, …), …).
    alcohol < 10.62
    ├─ meses_barrica < 8.5
    │  ├─ acided_volatil < 0.3125
    │  │  ├─ acided_volatil < 0.2025
    │  │  │  ├─  ☹️  (408/496)
    │  │  │  └─  ☹️  (1095/1172)
    │  │  └─ meses_barrica < 5.5
    │  │     ├─  ☹️  (1334/1345)
    │  │     └─  ☹️  (51/58)
    │  └─  😊  (25/25)
    └─ meses_barrica < 12.5
       ├─ cloruro_sodico < 0.0455
       │  ├─ alcohol < 12.55
       │  │  ├─  ☹️  (751/1160)
       │  │  └─  😊  (185/286)
       │  └─ meses_barrica < 10.5
       │     ├─  ☹️  (552/629)
       │     └─  😊  (25/43)
       └─ alcohol < 14.45
          ├─  😊  (105/105)
          └─  ☹️  (1/1)
  13. ¿Cuál es la importancia de cada variable en el modelo?

    Usar la función feature_importances del paquete DecisionTree para calcular la importancia de cada variable.

    # Calculamos la importancia de cada variable.
    feature_importances(mach)
    13-element Vector{Pair{Symbol, Float64}}:
                  :alcohol => 0.5303315899204789
            :meses_barrica => 0.26854115615561525
           :acided_volatil => 0.1040970236546446
           :cloruro_sodico => 0.09703023026926123
                     :tipo => 0.0
              :acided_fija => 0.0
            :acido_citrico => 0.0
          :azucar_residual => 0.0
     :dioxido_azufre_libre => 0.0
     :dioxido_azufre_total => 0.0
                 :densidad => 0.0
                       :ph => 0.0
                 :sulfatos => 0.0
  14. Predecir la calidad de los 10 primeros vinos del conjunto de ejemplos.

    Usar la función predict del paquete DecisionTree para predecir las probabilidades de pertenecer a cada clase un ejemplo o conjunto de ejemplos.

    Usar la función predict_mode del paquete DecisionTree para predecir la clase de un ejemplo o conjunto de ejemplos.

    Primero calculamos las probabilidades de cada clase.

    MLJ.predict(mach, X[1:10, :])
    10-element CategoricalDistributions.UnivariateFiniteVector{OrderedFactor{2}, String, UInt32, Float64}:
     UnivariateFinite{OrderedFactor{2}}( ☹️ =>0.992,  😊 =>0.00818)
     UnivariateFinite{OrderedFactor{2}}( ☹️ =>0.823,  😊 =>0.177)
     UnivariateFinite{OrderedFactor{2}}( ☹️ =>0.992,  😊 =>0.00818)
     UnivariateFinite{OrderedFactor{2}}( ☹️ =>0.992,  😊 =>0.00818)
     UnivariateFinite{OrderedFactor{2}}( ☹️ =>0.647,  😊 =>0.353)
     UnivariateFinite{OrderedFactor{2}}( ☹️ =>0.647,  😊 =>0.353)
     UnivariateFinite{OrderedFactor{2}}( ☹️ =>0.647,  😊 =>0.353)
     UnivariateFinite{OrderedFactor{2}}( ☹️ =>0.878,  😊 =>0.122)
     UnivariateFinite{OrderedFactor{2}}( ☹️ =>0.992,  😊 =>0.00818)
     UnivariateFinite{OrderedFactor{2}}( ☹️ =>0.992,  😊 =>0.00818)

    Y ahora predecimos la clase.

    predict_mode(mach, X[1:10, :])
    10-element CategoricalArray{String,1,UInt32}:
     " ☹️ "
     " ☹️ "
     " ☹️ "
     " ☹️ "
     " ☹️ "
     " ☹️ "
     " ☹️ "
     " ☹️ "
     " ☹️ "
     " ☹️ "