1 CARGAR LIBRERÍAS

library(tidyverse)
library(randomForest)
library(caret)
library(ranger)
library(Metrics)
library(ggplot2)
library(corrplot)
library(skimr)
library(knitr)
library(kableExtra)
library(rpart)
library(rpart.plot)
library(DT)
library(viridis)

2 CARGAR DATASET

datos_originales <- read_csv2("Oil__Gas____Other_Regulated_Wells__Beginning_1860.csv")

dims <- dim(datos_originales)
cat("Dimensiones del dataset original:", dims[1], "filas y", dims[2], "columnas.\n")
## Dimensiones del dataset original: 47407 filas y 55 columnas.
datatable(
  head(datos_originales, 50),
  caption = htmltools::tags$caption(
    style = 'caption-side: top; text-align: center; font-weight: bold; color: #1b365d; font-size: 15px;',
    'Tabla 1: Visualización interactiva y estilizada de las primeras observaciones del dataset'
  ),
  options = list(
    pageLength = 5,
    autoWidth = TRUE,
    scrollX = TRUE,
    dom = 'Bfrtip',
    language = list(url = '//cdn.datatables.net/plug-ins/1.10.25/i18n/Spanish.json')
  ),
  class = 'cell-border stripe hover compact'
)

3 PROCESAMIENTO DE DATOS Y SELECCIÓN DE VARIABLES

variables_requeridas <- c(
  "Well Type",             
  "True Vertical Depth, ft",
  "Proposed Depth",
  "Elevation",
  "Surface Longitude",
  "Surface Latitude",
  "County",
  "Region",
  "Original Well Type",
  "Objective Formation",
  "Producing Formation",
  "Slant",
  "Spacing Acres"
)

datos_proc <- datos_originales %>%
  select(any_of(variables_requeridas))

names(datos_proc) <- c(
  "Well_Type",
  "True_Vertical_Depth",
  "Proposed_Depth",
  "Elevation",
  "Surface_Longitude",
  "Surface_Latitude",
  "County",
  "Region",
  "Original_Well_Type",
  "Objective_Formation",
  "Producing_Formation",
  "Slant",
  "Spacing_Acres"
)

datos_proc <- datos_proc %>% distinct()

numeric_vars <- c("True_Vertical_Depth", "Proposed_Depth", "Elevation", "Surface_Longitude", "Surface_Latitude", "Spacing_Acres")
for(v in numeric_vars) {
  if(v %in% names(datos_proc)) {
    datos_proc[[v]] <- as.numeric(as.character(datos_proc[[v]]))
  }
}

cat_vars <- c("County", "Region", "Original_Well_Type", "Objective_Formation", "Producing_Formation", "Slant")
for(v in cat_vars) {
  if(v %in% names(datos_proc)) {
    datos_proc[[v]] <- addNA(as.factor(datos_proc[[v]]))
    levels(datos_proc[[v]])[is.na(levels(datos_proc[[v]]))] <- "Desconocido"
  }
}

datos_limpios <- datos_proc %>% 
  filter(!is.na(Well_Type))

for(v in numeric_vars) {
  if(v %in% names(datos_limpios) && sum(is.na(datos_limpios[[v]])) > 0) {
    datos_limpios[[v]][is.na(datos_limpios[[v]])] <- median(datos_limpios[[v]], na.rm = TRUE)
  }
}

datos_limpios$Well_Type <- make.names(datos_limpios$Well_Type)
datos_limpios$Well_Type <- as.factor(datos_limpios$Well_Type)

cat("Dimensiones finales del dataset limpio:", nrow(datos_limpios), "filas y", ncol(datos_limpios), "columnas.\n")
## Dimensiones finales del dataset limpio: 46370 filas y 11 columnas.

4 DIVISIÓN DE DATOS

set.seed(123)

if(nrow(datos_limpios) > 10) {
  train_index <- createDataPartition(datos_limpios$Well_Type, p = 0.70, list = FALSE)
  train_set <- datos_limpios[train_index, ]
  test_set <- datos_limpios[-train_index, ]
} else {
  train_index <- sample(1:nrow(datos_limpios), size = 0.7 * nrow(datos_limpios))
  train_set <- datos_limpios[train_index, ]
  test_set <- datos_limpios[-train_index, ]
}

partition_summary <- data.frame(
  Conjunto = c("Entrenamiento (Train)", "Prueba (Test)", "Total"),
  Observaciones = c(nrow(train_set), nrow(test_set), nrow(datos_limpios)),
  Porcentaje = c("70%", "30%", "100%")
)

partition_summary %>%
  tabla_formato("Tabla 2: Resumen de la división estratificada del dataset")
Tabla 2: Resumen de la división estratificada del dataset
Conjunto Observaciones Porcentaje
Entrenamiento (Train) 32469 70%
Prueba (Test) 13901 30%
Total 46370 100%

5 CONSTRUCCIÓN DEL MODELO

set.seed(123)

p_preds <- ncol(train_set) - 1
mtry_opt <- floor(sqrt(p_preds)) 

rf_model_ranger <- ranger(
  formula = Well_Type ~ .,
  data = train_set,
  num.trees = 500,
  mtry = mtry_opt,
  importance = "impurity",
  probability = FALSE,
  num.threads = 0, 
  seed = 123
)

6 ENTRENAMIENTO DEL MODELO

6.1 Entrenamiento del Random Forest

print(rf_model_ranger)
## Ranger result
## 
## Call:
##  ranger(formula = Well_Type ~ ., data = train_set, num.trees = 500,      mtry = mtry_opt, importance = "impurity", probability = FALSE,      num.threads = 0, seed = 123) 
## 
## Type:                             Classification 
## Number of trees:                  500 
## Sample size:                      32469 
## Number of independent variables:  10 
## Mtry:                             3 
## Target node size:                 1 
## Variable importance mode:         impurity 
## Splitrule:                        gini 
## OOB prediction error:             21.04 %

6.2 Tasa de Error Out-of-Bag (OOB)

cat("Tasa de Error OOB (Classification Error):", round(rf_model_ranger$prediction.error * 100, 4), "%\n")
## Tasa de Error OOB (Classification Error): 21.0447 %

6.3 Importancia de las variables

importance_df <- data.frame(
  Variable = names(rf_model_ranger$variable.importance),
  IncNodePurity = as.numeric(rf_model_ranger$variable.importance)
) %>% arrange(desc(IncNodePurity))

head(importance_df, 10) %>%
  tabla_formato("Tabla 3: Top 10 Variables más influyentes en la Clasificación")
Tabla 3: Top 10 Variables más influyentes en la Clasificación
Variable IncNodePurity
Original_Well_Type 4171.09860
Elevation 4022.57214
True_Vertical_Depth 3543.71893
Proposed_Depth 2840.38178
County 1005.31169
Region 994.03113
Surface_Latitude 542.00678
Producing_Formation 154.55103
Objective_Formation 59.32067
Surface_Longitude 0.00000

7 PREDICCIONES

predictions <- predict(rf_model_ranger, data = test_set, num.threads = 0)$predictions

results_df <- data.frame(
  Valor_Real = test_set$Well_Type,
  Valor_Predicho = predictions
)

cat("Primeras predicciones generadas exitosamente para el conjunto de prueba.\n")
## Primeras predicciones generadas exitosamente para el conjunto de prueba.

8 EVALUACIÓN DEL MODELO

conf_matrix <- confusionMatrix(as.factor(results_df$Valor_Predicho), as.factor(results_df$Valor_Real))

accuracy_val <- conf_matrix$overall['Accuracy']
kappa_val <- conf_matrix$overall['Kappa']

8.1 Exactitud Global (Accuracy) e Índice Kappa

cat("Accuracy Global:", round(accuracy_val * 100, 2), "%\n")
## Accuracy Global: 78.75 %
cat("Índice Kappa de Cohen:", kappa_val, "\n")
## Índice Kappa de Cohen: 0.7124553

8.2 Matriz de Confusión (Mapa de Calor)

cm_table <- as.data.frame(conf_matrix$table)
colnames(cm_table) <- c("Predichos", "Referencia", "Frecuencia")

ggplot(cm_table, aes(x = Referencia, y = Predichos, fill = Frecuencia)) +
  geom_tile(color = "white") +
  scale_fill_viridis_c(option = "viridis", direction = -1) +
  geom_text(aes(label = Frecuencia), color = "white", size = 4, fontface = "bold") +
  theme_minimal() +
  labs(
    title = "Matriz de Confusión - Random Forest",
    subtitle = "Comparación entre la categoría real y la categoría estimada",
    x = "Categoría Registrada en Campo (Referencia)",
    y = "Categoría Estimada por el Modelo",
    fill = "Frecuencia"
  ) +
  theme(
    axis.text.x = element_text(angle = 45, hjust = 1, face = "bold"),
    axis.text.y = element_text(face = "bold"),
    plot.title = element_text(face = "bold", size = 14),
    panel.grid.major = element_blank(),
    legend.position = "right"
  )

9 VISUALIZACIÓN DE RESULTADOS Y DIAGNÓSTICO

9.1 Distribución de la Variable Objetivo (Desbalance de Clases)

ggplot(datos_limpios, aes(x = Well_Type, fill = Well_Type)) +
  geom_bar(color = "black", alpha = 0.7) +
  scale_fill_viridis_d(option = "viridis") +
  theme_minimal() +
  labs(title = "Distribución del Tipo de Pozo", x = "Tipo de Pozo", y = "Frecuencia") +
  theme(axis.text.x = element_text(angle = 45, hjust = 1), legend.position = "none")

9.2 Importancia de Variables Predictoras

ggplot(head(importance_df, 10), aes(x = reorder(Variable, IncNodePurity), y = IncNodePurity)) +
  geom_point(color = "#440154", size = 3) +
  geom_segment(aes(x = Variable, xend = Variable, y = 0, yend = IncNodePurity), color = "#440154", size = 1) +
  coord_flip() +
  theme_minimal() +
  labs(title = "Top 10 Variables de Importancia (Clasificación)", x = "Variables", y = "Importancia (Gini Impurity)")

10 CONCLUSIONES

El modelo de Random Forest demostró ser una herramienta muy confiable para clasificar el tipo de pozo petrolero, destacando que la profundidad, las características de la formación y la ubicación geográfica son los factores que más determinan el resultado. Gracias a esto, el modelo logró un rendimiento general alto al acertar consistentemente con los datos reales de campo. No obstante, se identificó un desbalance en los registros históricos de algunas categorías menos frecuentes, lo que sugiere que equilibrar los datos en futuros trabajos podría optimizar aún más las predicciones.


Autor: Jennifer Cordones | Machine Learning de Clasificación (Random Forest) — Oil, Gas & Other Regulated Wells - NY State