Proyecto estrictamente académico (Estadística y Probabilidades, FIGEMPA – UCE). Datos: EarthChem PetDB (CC BY 4.0), descargados el 13/10/2026. Web del proyecto: https://crisrumiguano-art.github.io/geoquimica-earthchem-ml/
Se usa la base completa (159 760 análisis). Para cada par se toman solo las filas que tienen los dos datos: no se inventa ningún valor que falte.
knitr::opts_chunk$set(echo = TRUE, message = FALSE, warning = FALSE, fig.align = "center")
if (!file.exists("data/earthchem_limpio.csv.gz")) {
dir.create("data", showWarnings = FALSE)
download.file("https://github.com/crisrumiguano-art/geoquimica-earthchem-ml/raw/main/data/earthchem_limpio.csv.gz", "data/earthchem_limpio.csv.gz", mode = "wb")
}
base <- read.csv(gzfile("data/earthchem_limpio.csv.gz", encoding = "UTF-8"), check.names = FALSE)
dim(base)## [1] 159760 75
Como en la plantilla del curso, la regresión logística predice una variable binaria (0 o 1). Pregunta: ¿una roca volcánica se formó en un margen convergente (arco volcánico, 1) o no (dorsal o intraplaca, 0)? Variables explicativas: cinco óxidos mayores. Los magmas de arco se forman con agua de la placa que se hunde: tienen más Al2O3 y K2O y menos MgO que los de dorsal.
oxidos <- c("SiO2 (wt%)", "Al2O3 (wt%)", "MgO (wt%)", "Na2O (wt%)", "K2O (wt%)")
volcanica <- grepl("^igneous:volcanic", base[["Sample Type"]])
datos <- na.omit(base[volcanica, c(oxidos, "Tectonic Group")])
datos <- datos[datos[["Tectonic Group"]] != "mixto", ]
colnames(datos) <- c("SiO2", "Al2O3", "MgO", "Na2O", "K2O", "grupo")
datos$arco <- ifelse(datos$grupo == "margen convergente", 1, 0) # 1 = margen convergente
table(datos$arco)##
## 0 1
## 24915 11239
knitr::kable(head(datos[, c("SiO2", "Al2O3", "MgO", "Na2O", "K2O", "arco")], 10), caption = "Tabla N° 1: primeras 10 filas")| SiO2 | Al2O3 | MgO | Na2O | K2O | arco | |
|---|---|---|---|---|---|---|
| 3 | 50.84 | 16.78 | 7.88 | 2.22 | 0.810 | 1 |
| 5 | 48.78 | 15.53 | 8.77 | 2.85 | 0.060 | 0 |
| 8 | 49.20 | 18.11 | 5.32 | 2.76 | 0.170 | 1 |
| 11 | 49.28 | 15.52 | 7.59 | 2.55 | 0.500 | 0 |
| 20 | 50.64 | 15.28 | 7.57 | 2.87 | 0.070 | 0 |
| 25 | 50.81 | 13.93 | 6.75 | 2.43 | 0.240 | 0 |
| 28 | 51.41 | 20.30 | 4.20 | 1.98 | 0.240 | 1 |
| 35 | 55.88 | 16.63 | 5.30 | 3.12 | 1.500 | 1 |
| 51 | 49.13 | 15.03 | 9.65 | 1.88 | 0.281 | 0 |
| 53 | 50.00 | 17.20 | 4.96 | 2.67 | 0.440 | 1 |
knitr::kable(aggregate(cbind(SiO2, Al2O3, MgO, Na2O, K2O) ~ arco, data = datos, FUN = mean), digits = 2,
caption = "Tabla N° 2: media de cada óxido en arco (1) y no arco (0)")| arco | SiO2 | Al2O3 | MgO | Na2O | K2O |
|---|---|---|---|---|---|
| 0 | 50.6 | 14.51 | 7.77 | 3.07 | 1.11 |
| 1 | 61.0 | 15.34 | 3.82 | 3.48 | 1.68 |
El 80 % de las filas se usa para ajustar el modelo y el 20 % restante para probarlo con datos que no vio.
set.seed(2026)
entrena <- sample(nrow(datos), round(0.8 * nrow(datos)))
d_ent <- datos[entrena, ]
d_prueba <- datos[-entrena, ]
c(entrenamiento = nrow(d_ent), prueba = nrow(d_prueba))## entrenamiento prueba
## 28923 7231
La regresión logística ajusta una combinación lineal z = b0 + b1·x1 + … + b5·x5 y la convierte en probabilidad con la función sigmoide, p = 1 / (1 + e^(−z)). Es una neurona con activación sigmoide: el puente hacia la Unidad 4.
modelo <- glm(arco ~ SiO2 + Al2O3 + MgO + Na2O + K2O, data = d_ent, family = "binomial")
summary(modelo)$coefficients## Estimate Std. Error z value Pr(>|z|)
## (Intercept) -22.06584719 0.328615601 -67.147899 0.000000e+00
## SiO2 0.28664457 0.004304305 66.594857 0.000000e+00
## Al2O3 0.47805944 0.009547605 50.071139 0.000000e+00
## MgO 0.01177993 0.005053370 2.331104 1.974787e-02
## Na2O -0.31888615 0.021002093 -15.183542 4.545584e-52
## K2O -0.60435840 0.021364908 -28.287433 4.933698e-176
## (Intercept) SiO2 Al2O3 MgO Na2O K2O
## 0.000 1.332 1.613 1.012 0.727 0.546
z <- predict(modelo, d_prueba) # combinación lineal
p <- 1 / (1 + exp(-z)) # probabilidad de ser arco
plot(z, d_prueba$arco + runif(length(z), -0.04, 0.04), pch = 16, cex = 0.4, col = rgb(0.22, 0.53, 0.9, 0.25), las = 1,
xlim = quantile(z, c(0.005, 0.995)), main = "Gráfica N° 1: Curva sigmoide de la regresión logística",
xlab = "z = b0 + b1·SiO2 + … + b5·K2O", ylab = "Arco (1) o no (0)")
curve(1 / (1 + exp(-x)), add = TRUE, col = "#d95926", lwd = 3)
abline(h = 0.5, lty = 3, col = "gray50")Se clasifica como arco si p ≥ 0,5. La matriz de confusión cuenta aciertos y errores en los datos de prueba.
estimado <- ifelse(p >= 0.5, 1, 0)
confusion <- table(real = d_prueba$arco, estimado = estimado)
confusion## estimado
## real 0 1
## 0 4729 249
## 1 865 1388
exactitud <- sum(diag(confusion)) / sum(confusion) * 100
sensibilidad <- confusion["1", "1"] / sum(confusion["1", ]) * 100 # arcos reconocidos
especificidad <- confusion["0", "0"] / sum(confusion["0", ]) * 100 # no arcos reconocidos
knitr::kable(data.frame(exactitud = round(exactitud, 1), sensibilidad = round(sensibilidad, 1),
especificidad = round(especificidad, 1)), caption = "Tabla N° 3: test de aprobación (%)")| exactitud | sensibilidad | especificidad |
|---|---|---|
| 84.6 | 61.6 | 95 |
Vale para rocas volcánicas con los cinco óxidos dentro del rango medido. Las clases no tienen el mismo tamaño (hay menos arcos), por eso se informan sensibilidad y especificidad además de la exactitud.
¿Qué probabilidad de ser de arco tiene una andesita con 59 % SiO2, 17 % Al2O3, 3 % MgO, 3,5 % Na2O y 1,5 % K2O? ¿Y un basalto de dorsal con 50 %, 15 %, 8 %, 2,5 % y 0,15 %?
nuevas <- data.frame(SiO2 = c(59, 50), Al2O3 = c(17, 15), MgO = c(3, 8), Na2O = c(3.5, 2.5), K2O = c(1.5, 0.15))
p_nuevas <- predict(modelo, nuevas, type = "response")
round(p_nuevas * 100, 1) # en %## 1 2
## 72.8 20.5
La regresión logística reconoce si una roca volcánica es de margen convergente en el 84.6 % de los casos de prueba (sensibilidad 61.6 %, especificidad 95 %). La andesita del ejemplo sale con 73 % de probabilidad de ser de arco y el basalto de dorsal con 20 %. La red neuronal de la Unidad 4 resuelve el mismo tipo de problema con tres clases y más exactitud, porque combina los óxidos de forma no lineal.