Este documento tiene como objetivo aplicar un flujo completo de análisis a un conjunto de líneas de arroz, combinando datos fenotípicos y genómicos. Se implementan métodos como análisis descriptivo, clustering no supervisado (K-means), PCA y clasificación supervisada. Finalmente, se evalúa el desempeño de los modelos.
library(tidyverse)
library(glue)
library(caret)
library(FactoMineR)
library(factoextra)
library(rpart)
library(rpart.plot)
library(DT)
library(corrplot)
library(knitr)
# Lectura de datos
feno <- read.csv("quantitative_traits.csv")
geno <- read.csv("12k_ld_imputed.csv")
datos <- merge(feno, geno, by = "ID") %>% distinct() %>% drop_na()
feno <- datos[, 1:13] # ID + 12 rasgos
n_lineas <- nrow(feno) # número de líneas evaluadas
n_variables_feno <- ncol(feno) - 1 # número de rasgos fenotípicos
n_variables_gen <- ncol(geno) - 1 # SNPs
variables <- names(feno)[-1] # nombres sin el ID
# Mostrar resultados
knitr::asis_output(
glue("
### Resumen general de los datos
- Número de líneas evaluadas: {n_lineas}
- Número de variables fenotípicas: {n_variables_feno}
Variables fenotípicas disponibles:
{paste0(' - ', variables, collapse = '\n')}
- Número de variables genéticas (SNPs): {n_variables_gen}
")
)
Se generó una tabla con medidas de tendencia central y rangos para cada rasgo
tabla <- feno %>%
group_by(ID) %>%
summarise(across(where(is.numeric), list(media = mean, min = min, max = max), .names = "{.col}_{.fn}"))
datatable(tabla, options = list(scrollX = TRUE))
# Guardar archivo CSV para descarga
write.csv(tabla, "tabla_resumen.csv", row.names = FALSE)
Los histogramas permitieron observar la distribución de las variables
fenotípicas. Algunas mostraron asimetrías y rangos amplios,
especialmente en variables como Heading.date y
Grain.weight
feno %>% select(-ID) %>%
pivot_longer(cols = everything(), names_to = "variable", values_to = "valor") %>%
ggplot(aes(x = valor)) +
geom_histogram(bins = 30, fill = "#69b3a2", color = "white") +
facet_wrap(~variable, scales = "free") +
theme_minimal()
Se calcularon y visualizaron correlaciones entre las variables fenotípicas. Se encontraron asociaciones
mat_cor <- cor(feno %>% select(-ID))
corrplot(mat_cor, method = "color", type = "upper", tl.col = "black", title = "Correlación entre rasgos", mar=c(0,0,1,0))
El análisis de componentes principales permitió visualizar la variabilidad entre líneas. Las dos primeras componentes explicaron gran parte de la varianza fenotípica
pca <- PCA(feno %>% select(-ID), graph = FALSE)
fviz_pca_ind(pca, geom = "point", title = "PCA - Individuos")
fviz_pca_var(pca, col.var = "contrib", gradient.cols = c("blue", "orange", "red"))
El método K-means permitió identificar 3 grupos distintos de líneas fenotípicas basados en sus coordenadas PCA. Estos grupos pueden reflejar diferentes perfiles agronómicos
datos_escalados <- scale(feno %>% select(-ID))
pca_coord <- as.data.frame(PCA(datos_escalados, graph = FALSE)$ind$coord[, 1:2])
set.seed(123)
kmeans_model <- kmeans(pca_coord, centers = 3, nstart = 25)
fviz_pca_ind(PCA(datos_escalados, graph = FALSE),
geom.ind = "point",
col.ind = as.factor(kmeans_model$cluster),
palette = "Set2",
addEllipses = TRUE,
legend.title = "Cluster")
Se clasificaron líneas con base en si su peso de grano (‘Grain.weight’)
pca_coords <- as.data.frame(pca$ind$coord[, 1:2])
pca_coords$grupo <- ifelse(feno$Grain.weight > median(feno$Grain.weight), "Alto", "Bajo") %>%
factor(levels = c("Bajo", "Alto"))
set.seed(123)
modelo1 <- train(grupo ~ ., data = pca_coords, method = "rpart")
pred1 <- predict(modelo1, pca_coords)
conf1 <- confusionMatrix(pred1, pca_coords$grupo)
data.frame(
Modelo = "Árbol - PCA fenotípico",
Accuracy = round(conf1$overall["Accuracy"], 3),
Sensibilidad = round(conf1$byClass["Sensitivity"], 3),
Especificidad = round(conf1$byClass["Specificity"], 3)
) %>% knitr::kable()
| Modelo | Accuracy | Sensibilidad | Especificidad | |
|---|---|---|---|---|
| Accuracy | Árbol - PCA fenotípico | 0.802 | 0.805 | 0.799 |
rpart.plot(modelo1$finalModel, main = "Árbol de clasificación - Grain.weight")
# Modelo logístico con interacción entre las componentes PCA
modelo_inter <- glm(grupo ~ Dim.1 * Dim.2, data = pca_coords, family = "binomial")
# Predicción
pred_probs <- predict(modelo_inter, type = "response")
pred_class <- ifelse(pred_probs > 0.5, "Alto", "Bajo") %>% factor(levels = c("Bajo", "Alto"))
# Evaluación del modelo
conf_inter <- confusionMatrix(pred_class, pca_coords$grupo)
data.frame(
Modelo = "Logística - PCA fenotípico Grain.weight",
Accuracy = round(conf_inter$overall["Accuracy"], 3),
Sensibilidad = round(conf_inter$byClass["Sensitivity"], 3),
Especificidad = round(conf_inter$byClass["Specificity"], 3)
) %>% knitr::kable()
| Modelo | Accuracy | Sensibilidad | Especificidad | |
|---|---|---|---|---|
| Accuracy | Logística - PCA fenotípico Grain.weight | 0.814 | 0.859 | 0.755 |
Se clasificaron líneas en alto/bajo según Culm.length
usando árboles de decisión
pca_coords2 <- as.data.frame(pca$ind$coord[, 1:2])
pca_coords2$grupo <- ifelse(feno$Culm.length > median(feno$Culm.length), "Alto", "Bajo")
pca_coords2$grupo <- as.factor(pca_coords2$grupo)
set.seed(123)
modelo2 <- train(grupo ~ ., data = pca_coords2, method = "rpart")
pred2 <- predict(modelo2, pca_coords2)
conf2 <- confusionMatrix(pred2, pca_coords2$grupo)
data.frame(
Modelo = "Árbol - PCA Culm.length",
Accuracy = round(conf2$overall["Accuracy"], 3),
Sensibilidad = round(conf2$byClass["Sensitivity"], 3),
Especificidad = round(conf2$byClass["Specificity"], 3)
) %>% knitr::kable()
| Modelo | Accuracy | Sensibilidad | Especificidad | |
|---|---|---|---|---|
| Accuracy | Árbol - PCA Culm.length | 0.827 | 0.801 | 0.853 |
rpart.plot(modelo2$finalModel, main = "Árbol de clasificación - Culm.length")
# Modelo logístico para Culm.length
modelo_inter2 <- glm(grupo ~ Dim.1 * Dim.2, data = pca_coords2, family = "binomial")
# Predicción
pred_probs2 <- predict(modelo_inter2, type = "response")
pred_class2 <- ifelse(pred_probs2 > 0.5, "Alto", "Bajo") %>% factor(levels = c("Bajo", "Alto"))
# Evaluación del modelo
conf_inter2 <- confusionMatrix(pred_class2, pca_coords2$grupo)
data.frame(
Modelo = "Logística - PCA Culm.length",
Accuracy = round(conf_inter2$overall["Accuracy"], 3),
Sensibilidad = round(conf_inter2$byClass["Sensitivity"], 3),
Especificidad = round(conf_inter2$byClass["Specificity"], 3)
) %>% knitr::kable()
| Modelo | Accuracy | Sensibilidad | Especificidad | |
|---|---|---|---|---|
| Accuracy | Logística - PCA Culm.length | 0.169 | 0.164 | 0.175 |
Se entrenó un modelo de Random Forest utilizando todas las variables fenotípicas disponibles, excluyendo el ID y la variable objetivo (Grain.weight). El objetivo fue clasificar las líneas como de peso de grano “Alto” o “Bajo”, según la mediana. A continuación, se muestran los resultados del modelo, incluyendo la matriz de confusión y la importancia de las variables.
library(randomForest)
datos_rf <- feno %>%
mutate(grupo = ifelse(Grain.weight > median(Grain.weight), "Alto", "Bajo")) %>%
select(-ID, -Grain.weight)
datos_rf$grupo <- as.factor(datos_rf$grupo)
set.seed(123)
split <- sample(2, nrow(datos_rf), replace = TRUE, prob = c(0.7, 0.3))
train_rf <- datos_rf[split == 1, ]
test_rf <- datos_rf[split == 2, ]
# Entrenar Random Forest
modelo_rf <- randomForest(grupo ~ ., data = train_rf, importance = TRUE)
pred_rf <- predict(modelo_rf, test_rf)
conf_rf_feno <- confusionMatrix(pred_rf, test_rf$grupo)
data.frame(
Modelo = "Random Forest - Fenotípico",
Accuracy = round(conf_rf_feno$overall["Accuracy"], 3),
Sensibilidad = round(conf_rf_feno$byClass["Sensitivity"], 3),
Especificidad = round(conf_rf_feno$byClass["Specificity"], 3)
) %>% knitr::kable()
| Modelo | Accuracy | Sensibilidad | Especificidad | |
|---|---|---|---|---|
| Accuracy | Random Forest - Fenotípico | 0.758 | 0.663 | 0.835 |
plot(modelo_rf, main = "Error del modelo - Random Forest fenotípico")
varImpPlot(modelo_rf, main = "Importancia de variables fenotípicas")
El PCA con datos de SNPs (12k) permitió visualizar relaciones
genéticas entre líneas. Se identificaron agrupamientos genéticos y se
usaron los datos para entrenar un modelo Random Forest, clasificando
líneas por Grain.weight
Se realizó un análisis de componentes principales sobre los SNPs para explorar la estructura genética de las líneas.
pca_gen <- PCA(geno[,-1], graph = FALSE)
fviz_pca_ind(pca_gen, geom = "point", title = "PCA con datos genotípicos")
snp_data <- geno %>%
select(-ID) %>%
select(where(is.numeric)) %>%
select(where(~ var(.) > 0))
grupo <- factor(ifelse(datos$Grain.weight > median(datos$Grain.weight, na.rm = TRUE), "Alto", "Bajo"))
ids_validos <- geno$ID %in% datos$ID
snp_data <- snp_data[ids_validos, ]
grupo <- grupo[match(geno$ID[ids_validos], datos$ID)]
datos_rf_solo_snps <- data.frame(ID = geno$ID[ids_validos], grupo, snp_data)
# Dividir y entrenar modelo
set.seed(123)
train_index <- createDataPartition(datos_rf_solo_snps$grupo, p = 0.7, list = FALSE)
train_rf <- datos_rf_solo_snps[train_index, ]
test_rf <- datos_rf_solo_snps[-train_index, ]
modelo_rf <- randomForest(grupo ~ ., data = train_rf %>% select(-ID), importance = TRUE)
pred_rf <- predict(modelo_rf, newdata = test_rf %>% select(-ID, -grupo))
conf_rf_solo_snps <- confusionMatrix(pred_rf, test_rf$grupo)
data.frame(
Modelo = "Random Forest - Solo SNPs",
Accuracy = round(conf_rf_solo_snps$overall["Accuracy"], 3),
Sensibilidad = round(conf_rf_solo_snps$byClass["Sensitivity"], 3),
Especificidad = round(conf_rf_solo_snps$byClass["Specificity"], 3)
) %>% knitr::kable()
| Modelo | Accuracy | Sensibilidad | Especificidad | |
|---|---|---|---|---|
| Accuracy | Random Forest - Solo SNPs | 0.673 | 0.479 | 0.818 |
plot(modelo_rf, main = "Error del modelo - Random Forest (solo SNPs)")
varImpPlot(modelo_rf, n.var = 15, main = "Top 15 SNPs más importantes")
Se utilizaron las dos primeras componentes principales obtenidas del PCA genotípico como predictores para clasificar las líneas según su peso de grano (Grain.weight), usando modelos de árbol de decisión y regresión logística.
ids_geno <- geno$ID
sub_feno <- datos %>%
filter(ID %in% ids_geno, !is.na(Grain.weight))
sub_feno <- sub_feno[match(ids_geno, sub_feno$ID), ]
sub_feno <- sub_feno[!is.na(sub_feno$Grain.weight), ]
pca_coords_raw <- as.data.frame(pca_gen$ind$coord[, 1:2])
pca_coords_raw <- pca_coords_raw[match(sub_feno$ID, ids_geno), ]
colnames(pca_coords_raw) <- c("Dim.1", "Dim.2")
stopifnot(nrow(sub_feno) == nrow(pca_coords_raw))
pca_coords_gen <- pca_coords_raw
pca_coords_gen$grupo <- factor(ifelse(sub_feno$Grain.weight > median(sub_feno$Grain.weight), "Alto", "Bajo"))
set.seed(123)
modelo_gen_tree <- train(grupo ~ ., data = pca_coords_gen, method = "rpart")
pred_gen_tree <- predict(modelo_gen_tree, pca_coords_gen)
conf_gen_tree <- confusionMatrix(pred_gen_tree, pca_coords_gen$grupo)
data.frame(
Modelo = "Árbol - PCA genotípico",
Accuracy = round(conf_gen_tree$overall["Accuracy"], 3),
Sensibilidad = round(conf_gen_tree$byClass["Sensitivity"], 3),
Especificidad = round(conf_gen_tree$byClass["Specificity"], 3)
) %>% knitr::kable()
| Modelo | Accuracy | Sensibilidad | Especificidad | |
|---|---|---|---|---|
| Accuracy | Árbol - PCA genotípico | 0.617 | 0.335 | 0.829 |
rpart.plot(modelo_gen_tree$finalModel, main = "Árbol de clasificación - PCA genotípico")
modelo_gen_log <- glm(grupo ~ Dim.1 * Dim.2, data = pca_coords_gen, family = "binomial")
pred_probs_gen <- predict(modelo_gen_log, type = "response")
pred_class_gen <- ifelse(pred_probs_gen > 0.5, "Alto", "Bajo") %>% factor(levels = c("Bajo", "Alto"))
conf_gen_log <- confusionMatrix(pred_class_gen, pca_coords_gen$grupo)
data.frame(
Modelo = "Logística - PCA genotípico",
Accuracy = round(conf_gen_log$overall["Accuracy"], 3),
Sensibilidad = round(conf_gen_log$byClass["Sensitivity"], 3),
Especificidad = round(conf_gen_log$byClass["Specificity"], 3)
) %>% knitr::kable()
| Modelo | Accuracy | Sensibilidad | Especificidad | |
|---|---|---|---|---|
| Accuracy | Logística - PCA genotípico | 0.396 | 0.649 | 0.206 |
En este modelo se utilizaron directamente todos los SNPs (tras eliminar aquellos con varianza cero), para predecir el peso de grano (Grain.weight) en categorías de Alto y Bajo.
datos_rf_geno <- datos %>%
mutate(grupo = factor(ifelse(Grain.weight > median(Grain.weight), "Alto", "Bajo"))) %>%
select(-Grain.weight)
datos_num_geno <- datos_rf_geno %>% select(where(is.numeric)) %>% select(where(~ var(.) > 0))
datos_rf_clean_geno <- bind_cols(datos_rf_geno %>% select(ID, grupo), datos_num_geno)
set.seed(123)
train_index_geno <- createDataPartition(datos_rf_clean_geno$grupo, p = 0.7, list = FALSE)
train_geno <- datos_rf_clean_geno[train_index_geno, ]
test_geno <- datos_rf_clean_geno[-train_index_geno, ]
modelo_rf_geno <- randomForest(grupo ~ ., data = train_geno %>% select(-ID), importance = TRUE)
pred_rf_geno <- predict(modelo_rf_geno, newdata = test_geno %>% select(-ID, -grupo))
conf_rf_geno <- confusionMatrix(pred_rf_geno, test_geno$grupo)
data.frame(
Modelo = "Random Forest - Genotípico",
Accuracy = round(conf_rf_geno$overall["Accuracy"], 3),
Sensibilidad = round(conf_rf_geno$byClass["Sensitivity"], 3),
Especificidad = round(conf_rf_geno$byClass["Specificity"], 3)
) %>% knitr::kable()
| Modelo | Accuracy | Sensibilidad | Especificidad | |
|---|---|---|---|---|
| Accuracy | Random Forest - Genotípico | 0.662 | 0.392 | 0.865 |
varImpPlot(modelo_rf, n.var = 15, main = "Top 15 SNPs más importantes - Random Forest")
Aquí se comparan modelos para dos variables objetivo diferentes (Grain.weight y Culm.length) usando árboles de decisión.
comparacion_pca <- data.frame(
Modelo = c("Árbol - PCA Grain.weight","Árbol - PCA Culm.length",
"Logística - PCA Grain.weight","Logística - PCA Culm.length"),
Accuracy = c(conf1$overall["Accuracy"], conf_inter$overall["Accuracy"],
conf2$overall["Accuracy"], conf_inter2$overall["Accuracy"]),
Sensibilidad = c(conf1$byClass["Sensitivity"], conf_inter$byClass["Sensitivity"],
conf2$byClass["Sensitivity"], conf_inter2$byClass["Sensitivity"]),
Especificidad = c(conf1$byClass["Specificity"], conf_inter$byClass["Specificity"],
conf2$byClass["Specificity"], conf_inter2$byClass["Specificity"])
)
knitr::kable(comparacion_pca, digits = 3)
| Modelo | Accuracy | Sensibilidad | Especificidad |
|---|---|---|---|
| Árbol - PCA Grain.weight | 0.802 | 0.805 | 0.799 |
| Árbol - PCA Culm.length | 0.814 | 0.859 | 0.755 |
| Logística - PCA Grain.weight | 0.827 | 0.801 | 0.853 |
| Logística - PCA Culm.length | 0.169 | 0.164 | 0.175 |
Se comparan árboles de decisión y regresiones logísticas entrenados con las dos primeras componentes principales de los datos fenotípicos y genotípicos.
comparacion_pca <- data.frame(
Modelo = c("Árbol - PCA fenotípico","Árbol - PCA genotípico",
"Logística - PCA fenotípico","Logística - PCA genotípico"),
Accuracy = c(conf1$overall["Accuracy"], conf_inter$overall["Accuracy"],
conf_gen_tree$overall["Accuracy"], conf_gen_log$overall["Accuracy"]),
Sensibilidad = c(conf1$byClass["Sensitivity"], conf_inter$byClass["Sensitivity"],
conf_gen_tree$byClass["Sensitivity"], conf_gen_log$byClass["Sensitivity"]),
Especificidad = c(conf1$byClass["Specificity"], conf_inter$byClass["Specificity"],
conf_gen_tree$byClass["Specificity"], conf_gen_log$byClass["Specificity"])
)
knitr::kable(comparacion_pca, digits = 3)
| Modelo | Accuracy | Sensibilidad | Especificidad |
|---|---|---|---|
| Árbol - PCA fenotípico | 0.802 | 0.805 | 0.799 |
| Árbol - PCA genotípico | 0.814 | 0.859 | 0.755 |
| Logística - PCA fenotípico | 0.617 | 0.335 | 0.829 |
| Logística - PCA genotípico | 0.396 | 0.649 | 0.206 |
Aquí comparamos los modelos Random Forest entrenados con: Todas las variables fenotípicas y todos los SNPs (genotípicos)
comparacion_rf <- data.frame(
Modelo = c("Random Forest - Fenotípico", "Random Forest - Solo SNPs", "Random Forest - SNPs completos"),
Accuracy = c(conf_rf_feno$overall["Accuracy"], conf_rf_solo_snps$overall["Accuracy"], conf_rf_geno$overall["Accuracy"]),
Sensibilidad = c(conf_rf_feno$byClass["Sensitivity"], conf_rf_solo_snps$byClass["Sensitivity"], conf_rf_geno$byClass["Sensitivity"]),
Especificidad = c(conf_rf_feno$byClass["Specificity"], conf_rf_solo_snps$byClass["Specificity"], conf_rf_geno$byClass["Specificity"])
)
knitr::kable(comparacion_rf, digits = 3)
| Modelo | Accuracy | Sensibilidad | Especificidad |
|---|---|---|---|
| Random Forest - Fenotípico | 0.758 | 0.663 | 0.835 |
| Random Forest - Solo SNPs | 0.673 | 0.479 | 0.818 |
| Random Forest - SNPs completos | 0.662 | 0.392 | 0.865 |