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 CategoricalArrays # Para codificar las categorías como números enteros.
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 MLJ # Para la construcción y evaluación de modelos de aprendizaje automático.
using MLJDecisionTreeInterface # Para la interfaz entre MLJ y DecisionTree.
using GraphMakie # Para la visualización de árboles de decisión.
using StatisticalMeasures # Para calcular métricas de evaluación de modelos.Status `~/.julia/environments/v1.12/Project.toml`
[cbdf2221] AlgebraOfGraphics v0.12.7
[336ed68f] CSV v0.10.16
⌅ [324d7699] CategoricalArrays v0.10.9
[a93c6f00] DataFrames v1.8.2
[7806a523] DecisionTree v0.12.4
[e9467ef8] GLMakie v0.13.9
[1ecd5474] GraphMakie v0.6.3
⌃ [add582a8] MLJ v0.20.0
⌃ [c6f25543] MLJDecisionTreeInterface v0.4.2
⌅ [08abe8d2] PrettyTables v2.4.0
⌅ [a19d573c] StatisticalMeasures v0.1.7
⌃ [f0413319] Tidier v0.7.7
Info Packages marked with ⌃ and ⌅ have new versions available. Those with ⌃ may be upgradable, but those with ⌅ are restricted by compatibility constraints from upgrading. To see why use `status --outdated`
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.
Cargar los datos del archivo
tenis.csven un data frame.Soluciónusing CSV, DataFrames df = CSV.read(download("https://aprendeconalf.es/aprendizaje-automatico-practicas-julia/datos/tenis.csv"), DataFrame)14×5 DataFrameRow 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 Sí 4 Lluvioso Moderado Alta Suave Sí 5 Lluvioso Frío Normal Suave Sí 6 Lluvioso Frío Normal Fuerte No 7 Nublado Frío Normal Fuerte Sí 8 Soleado Moderado Alta Suave No 9 Soleado Frío Normal Suave Sí 10 Lluvioso Moderado Normal Suave Sí 11 Soleado Moderado Normal Fuerte Sí 12 Nublado Moderado Alta Fuerte Sí 13 Nublado Caluroso Normal Suave Sí 14 Lluvioso Moderado Alta Fuerte No 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?
Soluciónusing 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)) figA la vista de las frecuencias de cada variable, las variable
CieloyHumedadparecen ser las que más influye en la decisión de jugar al tenis.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?
AyudaEl índice de Gini se calcula mediante la fórmula
\[ GI = 1 - \sum_{i=1}^{n} p_i^2 \]
donde \(p_i\) 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 \(1-\frac{1}{n}\) (\(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 \(1-\frac{1}{n}\) indica que las instancias están distribuidas uniformemente entre todas las clases (máxima impureza).
Soluciónfunction 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¿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 variableViento?AyudaLa 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.
\[ \Delta GI = GI_{original} - GI_{dividido} \]
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.
SoluciónCalculamos 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_humedad0.09183673469387743Calculamos 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_viento0.030612244897959162Como se puede observar, la reducción del índice de Gini al dividir el conjunto de ejemplos según la variable
Humedades mayor que la reducción del índice de Gini al dividir el conjunto con respecto a la variableViento. Por lo tanto, la variableHumedadparece 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.Construir un árbol de decisión que explique si se puede jugar al tenis en función de las variables meteorológicas.
AyudaUsar la función
DecisionTreeClassifierdel paqueteDecisionTree.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 demin_samples_splitejemplos.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 indicatrue, 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:impurityo: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.
Soluciónusing DecisionTree, CategoricalArrays # Variables predictoras. X = Matrix(select(df, Not(:Tenis))) # Variable objetivo. y = String.(df.Tenis) # Convertir las variables categóricas a enteros. X = hcat([levelcode.(categorical(X[:, j])) for j in 1:size(X, 2)]...) tree = DecisionTreeClassifier(max_depth=3) DecisionTree.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: ["No", "Sí"] root: Decision Tree Leaves: 6 Depth: 3Visualizar el árbol de decisión construido.
AyudaUsar la función
plot_treedel paqueteDecisionTree.jl.Soluciónprint_tree(tree, feature_names=names(df)[1:end-1])Feature 3: "Humedad" < 2.0 ? ├─ Feature 1: "Cielo" < 3.0 ? ├─ Feature 4: "Viento" < 2.0 ? ├─ No : 1/2 └─ Sí : 2/2 └─ No : 3/3 └─ Feature 4: "Viento" < 2.0 ? ├─ Feature 1: "Cielo" < 2.0 ? ├─ No : 1/1 └─ Sí : 2/2 └─ Sí : 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
Cargar los datos del archivo
pinguïnos.csven un data frame.Soluciónusing CSV, DataFrames df = CSV.read(download("https://aprendeconalf.es/aprendizaje-automatico-practicas-julia/datos/pingüinos.csv"), DataFrame, missingstring="NA")344×7 DataFrame319 rows omittedRow 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 Hacer un análisis de los datos perdidos en el data frame.
Solucióndescribe(df, :nmissing)7×2 DataFrameRow 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 Eliminar del data frame los casos con valores perdidos.
Solucióndropmissing!(df)333×7 DataFrame308 rows omittedRow 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 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?
SoluciónPara 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) |> drawfrec_sexo = combine(groupby(df, [:Sexo, :Especie]), nrow => :Frecuencia) data(frec_sexo) * mapping(:Sexo, :Frecuencia, stack = :Especie, color =:Especie) * visual(BarPlot) |> drawPara 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)cajas(df, :Profundidad_pico, :Especie)cajas(df, :Longitud_ala, :Especie)cajas(df, :Peso, :Especie)¿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?
Soluciónusing 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.26577182779353914Determinar 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.
SoluciónDibujamos 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) figY ahora obtenemos el valor óptimo de división del conjunto de datos según la longitud del pico.
val_optimo = valores[argmax(reducciones)]42.3Dividir aleatoriamente el dataframe en un conjunto de entrenamiento y un conjunto de test con proporciones \(3/4\) y \(1/4\) respectivamente.
AyudaSoluciónusing 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 DataFrame225 rows omittedRow 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 Construir un árbol de decisión con el conjunto de entrenamiento sin tener en cuenta la variable
Islay visualizarlo.Soluciónusing DecisionTree, CategoricalArrays # Variables predictivas. X_train = Matrix(select(df_train, Not(:Isla, :Especie))) # Variable objetivo. y_train = String.(df_train.Especie) # Construimos el árbol de decisión con profundidad máxima 3. tree = DecisionTreeClassifier(max_depth = 3) DecisionTree.fit!(tree, X_train, y_train) print_tree(tree, feature_names=names(df)[3:end])Feature 3: "Longitud_ala" < 206.5 ? ├─ Feature 1: "Longitud_pico" < 42.3 ? ├─ Adelie : 96/96 └─ Feature 1: "Longitud_pico" < 45.9 ? ├─ Adelie : 10/20 └─ Chinstrap : 37/38 └─ Feature 2: "Profundidad_pico" < 17.65 ? ├─ Gentoo : 90/90 └─ Feature 1: "Longitud_pico" < 46.55 ? ├─ Adelie : 2/2 └─ Chinstrap : 4/4Predecir la especie de los pingüinos del conjunto de test y calcular la matriz de confusión de las predicciones.
AyudaUtilizar la función
confmatdel paqueteStatisticalMeaurespara calcular la matriz de confusión de las predicciones. Esta función toma como argumentos dos vectores, uno con las predicciones del modelo y otro con las etiquetas reales, y devuelve una matriz de confusión que muestra el número de verdaderos positivos, falsos positivos, verdaderos negativos y falsos negativos.Soluciónusing StatisticalMeasures # Variables predictivas X_test = Matrix(select(df_test, Not(:Isla, :Especie))) # Variable objetivo y_test = String.(df_test.Especie) # Predecimos la especie de pingüino del conjunto de test y_pred = DecisionTree.predict(tree, X_test) # Calculamos la precisión del modelo confmat(y_pred, y_test)┌─────────────────────────────┐ │ Ground Truth │ ┌─────────┼─────────┬─────────┬─────────┤ │Predicted│ Adelie │Chinstrap│ Gentoo │ ├─────────┼─────────┼─────────┼─────────┤ │ Adelie │ 37 │ 3 │ 0 │ ├─────────┼─────────┼─────────┼─────────┤ │Chinstrap│ 1 │ 14 │ 0 │ ├─────────┼─────────┼─────────┼─────────┤ │ Gentoo │ 0 │ 0 │ 28 │ └─────────┴─────────┴─────────┴─────────┘Calcular la precisión del modelo.
AyudaLa precisión es la proporción de predicciones correctas sobre el total de predicciones.
Utilizar la función
accuracydel paqueteStatisticalMeaurespara calcular la precisión del modelo.Solución# Calculamos la precisión del modelo accuracy(y_pred, y_test)0.9518072289156626
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) |
Crear un data frame con los datos de los vinos a partir del fichero
vinos.csv.Soluciónusing CSV, DataFrames df = CSV.read(download("https://aprendeconalf.es/aprendizaje-automatico-practicas-julia/datos/vinos.csv"), DataFrame)5320×14 DataFrame5295 rows omittedRow 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 Mostrar los tipos de cada variable del data frame.
AyudaUsar la función
schemadel paqueteMLJ.Soluciónusing MLJ schema(df)┌──────────────────────┬────────────┬─────────┐ │ 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 │ └──────────────────────┴────────────┴─────────┘
Hacer un análisis de los datos perdidos en el data frame.
Solucióndescribe(df, :nmissing)14×2 DataFrameRow 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 Se considera que un vino es bueno si tiene una puntuación de calidad mayor que \(6.5\). Recodificar la variable
calidaden una variable categórica que tome el valor 1 si la calidad es mayor que \(6.5\) y 0 en caso contrario.Soluciónusing CategoricalArrays # Recodificamos la variable calidad. df.calidad = cut(df.calidad, [0, 6.5, 10], labels = [" ☹️ ", " 😊 "])5320-element CategoricalArray{String,1,UInt32}: " ☹️ " " ☹️ " " ☹️ " " ☹️ " " ☹️ " " ☹️ " " ☹️ " " ☹️ " " ☹️ " " 😊 " " ☹️ " " 😊 " " ☹️ " ⋮ " ☹️ " " ☹️ " " ☹️ " " ☹️ " " ☹️ " " ☹️ " " ☹️ " " ☹️ " " ☹️ " " ☹️ " " ☹️ " " ☹️ "Dividir el data frame en un data frame con las variables predictivas y un vector con la variable objetivo
bueno.AyudaSolucióny, 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)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.
AyudaSolución# 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 │ └──────────────────────┴──────────────────┴───────────────────────────────────┘Definir un modelo de árbol de decisión con profundidad máxima 3.
AyudaCargar el modelo
DecisionTreeClassifierdel paqueteDecisionTreecon la macros@iload.Solución# Cargamos el tipo de modelo. Tree = @iload DecisionTreeClassifier pkg = "DecisionTree" # Instanciamos el modelo con sus parámetros. arbol = Tree(max_depth =3, rng = 123)[ Info: For silent loading, specify `verbosity=0`.import MLJDecisionTreeInterface ✔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)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.
AyudaUsar la función
evaluatedel paqueteMLJpara 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 \(1-p\) en el conjunto de test.CV(nfolds = n, shuffle = true|false): Utiliza validación cruzada conniteraciones. Si se indicashuffle = true, se utiliza validación cruzada aleatoria.StratifiedCV(nfolds = n, shuffle = true|false): Utiliza validación cruzada estratificada conniteraciones. Si se indicashuffle = 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.
Soluciónevaluate(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 │ └────────────┴──────────────┴─────────────┘
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?
Soluciónevaluate(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:04Evaluating over 6 folds: 100%[=========================] Time: 0:00:02PerformanceEvaluation 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 omittedLa 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.
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?
AyudaUsar la función
Los parámetros más importantes de esta función son:TunedModeldel paqueteMLJpara ajustar los parámetros del modelo.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 connvalores.RandomSearch(resolution = n): Ajusta los parámetros del modelo utilizando una búsqueda aleatoria connvalores.
- 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.
Solución# 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: 22%[=====> ] ETA: 0:00:07Evaluating over 9 metamodels: 33%[========> ] ETA: 0:00:05Evaluating over 9 metamodels: 44%[===========> ] ETA: 0:00:03Evaluating over 9 metamodels: 56%[=============> ] ETA: 0:00:03Evaluating over 9 metamodels: 67%[================> ] ETA: 0:00:02Evaluating over 9 metamodels: 78%[===================> ] ETA: 0:00:01Evaluating over 9 metamodels: 89%[======================> ] ETA: 0:00:01Evaluating over 9 metamodels: 100%[=========================] Time: 0:00:05DecisionTreeClassifier( 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())Dibujar la curva de aprendizaje del modelo en función de la profundidad del árbol de decisión.
AyudaUsar la funciónlearning_curvedel paqueteMLJpara 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.
Solución# 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:02Evaluating over 9 metamodels: 33%[========> ] ETA: 0:00:02Evaluating over 9 metamodels: 44%[===========> ] ETA: 0:00:02Evaluating over 9 metamodels: 56%[=============> ] ETA: 0:00:02Evaluating 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:04Construir un árbol de decisión con la profundidad máxima que da mejor resultado y visualizarlo.
Solución# 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)¿Cuál es la importancia de cada variable en el modelo?
AyudaUsar la función
feature_importancesdel paqueteDecisionTreepara calcular la importancia de cada variable.Solución# 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.0Predecir la calidad de los 10 primeros vinos del conjunto de ejemplos.
AyudaUsar la función
predictdel paqueteDecisionTreepara predecir las probabilidades de pertenecer a cada clase un ejemplo o conjunto de ejemplos.Usar la función
predict_modedel paqueteDecisionTreepara predecir la clase de un ejemplo o conjunto de ejemplos.SoluciónPrimero 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}: " ☹️ " " ☹️ " " ☹️ " " ☹️ " " ☹️ " " ☹️ " " ☹️ " " ☹️ " " ☹️ " " ☹️ "