library(readxl)
library(dplyr)
library(gt)
library(plotly)
library(DT)
datos <- read_excel("dataset_mundial_petro.xlsx")
cat("Número de registros (filas crudas):", nrow(datos), "\n")
## Número de registros (filas crudas): 49212
cat("Número de variables disponibles:", ncol(datos), "\n")
## Número de variables disponibles: 32
El archivo trae múltiples filas por yacimiento (una
fila distinta por cada registro de reservas/producción asociado a un
mismo campo), por eso más adelante se agrupa por Unit ID
antes de construir la tabla del modelo.
Y = PRODUCTION_START_YEAR (efecto: año en que el yacimiento empieza a producir)
Antes se usaban solo dos variables independientes
(Discovery year y FID Year). Se decidió
incorporar dos variables adicionales porque resultó más conveniente para
tener un modelo más completo, con lógica causal y no solo eligiendo las
de mejor correlación:
Discovery year):
año en que se descubrió el yacimiento.FID Year): año de la
Decisión Final de Inversión.Status year): año
del último estado operativo registrado del yacimiento (activo, cerrado,
en construcción, etc.).Quantity (converted)):
volumen de reservas/producción reportado para el yacimiento.Un yacimiento se descubre (X1), luego recibe aprobación de inversión (X2), y a lo largo de su vida operativa acumula estados y volúmenes reportados (X3, X4) antes o junto con el arranque de producción (Y). Por eso X1, X2, X3 y X4 se tratan como variables explicativas y Y como el efecto.
FID Year y Production start year llegan
como texto porque incluyen valores como “2025 (expected)”; se extrae el
año numérico de esos casos con una función auxiliar.
extraer_anio <- function(v) as.numeric(sub("^(\\d{4}).*", "\\1", as.character(v)))
datos <- datos %>%
mutate(
X1_raw = as.numeric(`Discovery year`),
X2_raw = extraer_anio(`FID Year`),
X3_raw = as.numeric(`Status year`),
X4_raw = as.numeric(`Quantity (converted)`),
Y_raw = extraer_anio(`Production start year`)
)
Antes de ajustar cualquier modelo, se define el dominio de cada variable, ya que el modelo solo se interpreta dentro de estos rangos, nunca fuera de ellos.
resumen_dominios <- data.frame(
Variable = c("X1 (DISCOVERY_YEAR)", "X2 (FID_YEAR)", "X3 (STATUS_YEAR)",
"X4 (QUANTITY)", "Y (PRODUCTION_START_YEAR)"),
Dominio_teorico = c(
"Enteros positivos (Z+): un año calendario no puede ser negativo, fraccionario ni anterior al origen de la industria petrolera",
"Enteros positivos (Z+): puede incluir años próximos porque hay proyectos con FID ya aprobado a futuro",
"Enteros positivos (Z+): fecha de la última actualización de estado del yacimiento",
"Reales no negativos (R >= 0): un volumen de reservas o producción físico no puede ser negativo",
"Enteros positivos (Z+): año calendario de arranque de producción; no existen registros de años negativos"
),
Rango_observado = c(
paste0("[", min(datos$X1_raw, na.rm = TRUE), ", ", max(datos$X1_raw, na.rm = TRUE), "]"),
paste0("[", min(datos$X2_raw, na.rm = TRUE), ", ", max(datos$X2_raw, na.rm = TRUE), "]"),
paste0("[", min(datos$X3_raw, na.rm = TRUE), ", ", max(datos$X3_raw, na.rm = TRUE), "]"),
paste0("[0, ", round(max(datos$X4_raw, na.rm = TRUE), 0), "]"),
paste0("[", min(datos$Y_raw, na.rm = TRUE), ", ", max(datos$Y_raw, na.rm = TRUE), "]")
)
) %>%
gt() %>%
tab_header(title = md("**Dominio teórico y rango observado de cada variable**"))
resumen_dominios
| Dominio teórico y rango observado de cada variable | ||
| Variable | Dominio_teorico | Rango_observado |
|---|---|---|
| X1 (DISCOVERY_YEAR) | Enteros positivos (Z+): un año calendario no puede ser negativo, fraccionario ni anterior al origen de la industria petrolera | [1869, 2023] |
| X2 (FID_YEAR) | Enteros positivos (Z+): puede incluir años próximos porque hay proyectos con FID ya aprobado a futuro | [1975, 2029] |
| X3 (STATUS_YEAR) | Enteros positivos (Z+): fecha de la última actualización de estado del yacimiento | [1989, 2024] |
| X4 (QUANTITY) | Reales no negativos (R >= 0): un volumen de reservas o producción físico no puede ser negativo | [0, 1] |
| Y (PRODUCTION_START_YEAR) | Enteros positivos (Z+): año calendario de arranque de producción; no existen registros de años negativos | [1896, 2040] |
Sobre Y en particular: como Y representa años, su dominio se restringe a los enteros positivos dentro del rango observado. El modelo solo se usa dentro de ese rango, y ahí Y siempre se mantiene positivo.
En la versión anterior, algunas variables tenían muchos más datos
disponibles que otras (por ejemplo, FID Year solo tenía
unos pocos cientos de valores frente a miles del resto), lo que generaba
tablas desbalanceadas. Esto ocurre porque el archivo trae varias filas
por yacimiento (una por cada registro de reservas). Para corregirlo:
Unit ID (identificador único de
yacimiento) y se toma un solo valor representativo de cada variable por
yacimiento.agregado <- datos %>%
group_by(`Unit ID`) %>%
summarise(
DISCOVERY_YEAR = first(X1_raw),
FID_YEAR = first(X2_raw),
STATUS_YEAR = first(X3_raw),
QUANTITY = suppressWarnings(max(X4_raw, na.rm = TRUE)),
PRODUCTION_START_YEAR = first(Y_raw),
.groups = "drop"
) %>%
mutate(QUANTITY = ifelse(is.infinite(QUANTITY), NA, QUANTITY))
tabla_original <- agregado %>% select(-`Unit ID`)
cat("Yacimientos únicos disponibles:", nrow(tabla_original), "\n")
## Yacimientos únicos disponibles: 8334
tabla <- tabla_original %>%
filter(!is.na(DISCOVERY_YEAR), !is.na(FID_YEAR), !is.na(STATUS_YEAR),
!is.na(QUANTITY), !is.na(PRODUCTION_START_YEAR)) %>%
filter(DISCOVERY_YEAR > 0, FID_YEAR > 0, STATUS_YEAR > 0,
QUANTITY >= 0, PRODUCTION_START_YEAR > 0)
cat("Yacimientos con las 5 variables completas y válidas:", nrow(tabla), "\n")
## Yacimientos con las 5 variables completas y válidas: 248
# Verificación de balance: todas las columnas deben tener el mismo número de datos
sapply(tabla, function(col) sum(!is.na(col)))
## DISCOVERY_YEAR FID_YEAR STATUS_YEAR
## 248 248 248
## QUANTITY PRODUCTION_START_YEAR
## 248 248
datatable(tabla,
options = list(pageLength = 10),
colnames = c("Año de Descubrimiento (X1)", "Año de FID (X2)",
"Año del Último Estado (X3)", "Cantidad de Reservas/Producción (X4)",
"Año de Inicio de Producción (Y)"))
Con 4 variables independientes ya no se puede ver todo en un solo gráfico 3D. Primero se muestra cada X por separado contra Y (más fácil de leer que una matriz de dispersión completa), y luego un 3D con las dos variables de mayor peso (X1, X2) frente a Y.
QUANTITY tiene valores muy dispares (desde 0 hasta
varios millones), así que solo para verla en el gráfico se usa una
escala logarítmica; el modelo y los datos originales no se
modifican.
tabla <- tabla %>% mutate(QUANTITY_LOG = log10(QUANTITY + 1))
g1 <- plot_ly(tabla, x = ~DISCOVERY_YEAR, y = ~PRODUCTION_START_YEAR,
type = "scatter", mode = "markers",
marker = list(size = 5, color = "#2E86C1", opacity = 0.6)) %>%
layout(xaxis = list(title = "X1 Discovery"), yaxis = list(title = "Y"))
g2 <- plot_ly(tabla, x = ~FID_YEAR, y = ~PRODUCTION_START_YEAR,
type = "scatter", mode = "markers",
marker = list(size = 5, color = "#2E86C1", opacity = 0.6)) %>%
layout(xaxis = list(title = "X2 FID"), yaxis = list(title = "Y"))
g3 <- plot_ly(tabla, x = ~STATUS_YEAR, y = ~PRODUCTION_START_YEAR,
type = "scatter", mode = "markers",
marker = list(size = 5, color = "#2E86C1", opacity = 0.6)) %>%
layout(xaxis = list(title = "X3 Status"), yaxis = list(title = "Y"))
g4 <- plot_ly(tabla, x = ~QUANTITY_LOG, y = ~PRODUCTION_START_YEAR,
type = "scatter", mode = "markers",
marker = list(size = 5, color = "#2E86C1", opacity = 0.6)) %>%
layout(xaxis = list(title = "X4 Quantity (log10)"), yaxis = list(title = "Y"))
subplot(g1, g2, g3, g4, nrows = 2, shareX = FALSE, shareY = TRUE,
titleX = TRUE, titleY = TRUE, margin = 0.06) %>%
layout(showlegend = FALSE)
plot_ly(tabla, x = ~DISCOVERY_YEAR, y = ~FID_YEAR, z = ~PRODUCTION_START_YEAR,
type = "scatter3d", mode = "markers",
marker = list(size = 4, color = "#2E86C1")) %>%
layout(scene = list(
xaxis = list(title = "X1 DISCOVERY_YEAR"),
yaxis = list(title = "X2 FID_YEAR"),
zaxis = list(title = "Y PRODUCTION_START_YEAR")
))
Los puntos crecen de forma conjunta con X2 principalmente, y X1, X3 y X4 aportan variación adicional sin romper la tendencia general. Se prueba con regresión lineal múltiple de 4 variables:
\[y = b_0 + b_1 x_1 + b_2 x_2 + b_3 x_3 + b_4 x_4\]
modelo <- lm(PRODUCTION_START_YEAR ~ DISCOVERY_YEAR + FID_YEAR + STATUS_YEAR + QUANTITY,
data = tabla)
summary(modelo)
##
## Call:
## lm(formula = PRODUCTION_START_YEAR ~ DISCOVERY_YEAR + FID_YEAR +
## STATUS_YEAR + QUANTITY, data = tabla)
##
## Residuals:
## Min 1Q Median 3Q Max
## -25.4225 -1.2296 -0.0468 1.2834 7.6489
##
## Coefficients:
## Estimate Std. Error t value Pr(>|t|)
## (Intercept) -804.13349 308.54181 -2.606 0.00972 **
## DISCOVERY_YEAR -0.01017 0.01351 -0.753 0.45208
## FID_YEAR 0.99450 0.01839 54.079 < 2e-16 ***
## STATUS_YEAR 0.41479 0.15682 2.645 0.00870 **
## QUANTITY -0.31762 1.39034 -0.228 0.81949
## ---
## Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
##
## Residual standard error: 2.899 on 243 degrees of freedom
## Multiple R-squared: 0.9584, Adjusted R-squared: 0.9577
## F-statistic: 1400 on 4 and 243 DF, p-value: < 2.2e-16
b0 <- coef(modelo)[1]
b1 <- coef(modelo)[2]
b2 <- coef(modelo)[3]
b3 <- coef(modelo)[4]
b4 <- coef(modelo)[5]
data.frame(
Parámetro = c("b0 (intercepto)", "b1 (pendiente X1)", "b2 (pendiente X2)",
"b3 (pendiente X3)", "b4 (pendiente X4)"),
Valor = c(round(b0, 4), round(b1, 4), round(b2, 4), round(b3, 4), round(b4, 6))
) %>%
gt() %>%
tab_header(title = md("**Parámetros del Modelo**"))
| Parámetros del Modelo | |
| Parámetro | Valor |
|---|---|
| b0 (intercepto) | -804.133500 |
| b1 (pendiente X1) | -0.010200 |
| b2 (pendiente X2) | 0.994500 |
| b3 (pendiente X3) | 0.414800 |
| b4 (pendiente X4) | -0.317618 |
Sobre el signo de b1: X1 (DISCOVERY_YEAR) sale con pendiente negativa; el peso explicativo principal lo lleva X2. Esto no es un problema porque, como se definió en la sección 4, el modelo solo se usa dentro del rango de X1 (1938 a 2022), y en ese rango Y se mantiene siempre positivo.
Con 4 variables no existe un único “plano” 3D que represente el modelo completo. Se muestran dos evidencias complementarias:
(a) Corte parcial: el plano ajustado para X1 y X2, dejando X3 y X4 fijas en su valor promedio (para poder graficar en 3D).
x3_medio <- mean(tabla$STATUS_YEAR)
x4_medio <- mean(tabla$QUANTITY)
x1_seq <- seq(min(tabla$DISCOVERY_YEAR), max(tabla$DISCOVERY_YEAR), length.out = 25)
x2_seq <- seq(min(tabla$FID_YEAR), max(tabla$FID_YEAR), length.out = 25)
malla <- outer(x1_seq, x2_seq, function(a, b) b0 + b1 * a + b2 * b + b3 * x3_medio + b4 * x4_medio)
plot_ly() %>%
add_markers(data = tabla, x = ~DISCOVERY_YEAR, y = ~FID_YEAR, z = ~PRODUCTION_START_YEAR,
marker = list(size = 4, color = "#2E86C1"), name = "Datos reales") %>%
add_surface(x = x1_seq, y = x2_seq, z = t(malla), opacity = 0.6,
showscale = FALSE, name = "Corte del modelo (X3, X4 en su media)") %>%
layout(scene = list(
xaxis = list(title = "X1 DISCOVERY_YEAR"),
yaxis = list(title = "X2 FID_YEAR"),
zaxis = list(title = "Y PRODUCTION_START_YEAR")
))
(b) Predicho vs. observado: la forma estándar de evaluar el ajuste cuando hay más de 2 variables independientes.
tabla$Y_predicho <- predict(modelo)
plot_ly(tabla, x = ~PRODUCTION_START_YEAR, y = ~Y_predicho,
type = "scatter", mode = "markers",
marker = list(size = 6, color = "#2E86C1")) %>%
add_trace(x = ~PRODUCTION_START_YEAR, y = ~PRODUCTION_START_YEAR,
mode = "lines", line = list(color = "red", dash = "dash"),
name = "Ajuste perfecto") %>%
layout(title = "Y observado vs. Y predicho",
xaxis = list(title = "Y observado"),
yaxis = list(title = "Y predicho"))
Mientras más cerca estén los puntos de la línea roja, mejor es el ajuste del modelo.
pearson_x1 <- cor(tabla$DISCOVERY_YEAR, tabla$PRODUCTION_START_YEAR)
pearson_x2 <- cor(tabla$FID_YEAR, tabla$PRODUCTION_START_YEAR)
pearson_x3 <- cor(tabla$STATUS_YEAR, tabla$PRODUCTION_START_YEAR)
pearson_x4 <- cor(tabla$QUANTITY, tabla$PRODUCTION_START_YEAR)
f_pvalue <- pf(summary(modelo)$fstatistic[1],
summary(modelo)$fstatistic[2],
summary(modelo)$fstatistic[3], lower.tail = FALSE)
r2 <- summary(modelo)$r.squared
data.frame(
Test = c("Pearson X1 (DISCOVERY_YEAR)", "Pearson X2 (FID_YEAR)",
"Pearson X3 (STATUS_YEAR)", "Pearson X4 (QUANTITY)", "F-test modelo"),
Valor = c(round(pearson_x1, 4), round(pearson_x2, 4), round(pearson_x3, 4),
round(pearson_x4, 4), format(f_pvalue, scientific = TRUE, digits = 4)),
Resultado = c(ifelse(abs(pearson_x1) > 0.7, "Supera 0.7", "No supera 0.7"),
ifelse(abs(pearson_x2) > 0.7, "Supera 0.7", "No supera 0.7"),
ifelse(abs(pearson_x3) > 0.7, "Supera 0.7", "No supera 0.7"),
ifelse(abs(pearson_x4) > 0.7, "Supera 0.7", "No supera 0.7"),
ifelse(f_pvalue < 0.05, "Significativo", "No significativo"))
) %>%
gt() %>%
tab_header(title = md("**Resumen del Test**"))
| Resumen del Test | ||
| Test | Valor | Resultado |
|---|---|---|
| Pearson X1 (DISCOVERY_YEAR) | 0.6021 | No supera 0.7 |
| Pearson X2 (FID_YEAR) | 0.9783 | Supera 0.7 |
| Pearson X3 (STATUS_YEAR) | 0.4043 | No supera 0.7 |
| Pearson X4 (QUANTITY) | 0.2207 | No supera 0.7 |
| F-test modelo | 1.946e-166 | Significativo |
cat("R² del modelo:", round(r2, 4), "\n")
## R² del modelo: 0.9584
Nota honesta: de las 4 variables, solo X2 (FID_YEAR) supera 0.7 de correlación con Y por sí sola; X1, X3 y X4 no llegan a ese nivel individualmente. Aun así, el modelo conjunto explica 95.8% de la variabilidad de Y (R² = 0.9584), y el F-test confirma que el modelo en conjunto es significativo.
escenarios <- data.frame(
Escenario = c("Típico (medianas)", "Descubrimiento antiguo", "Reservas grandes",
"FID reciente", "Estado más antiguo"),
DISCOVERY_YEAR = c(median(tabla$DISCOVERY_YEAR), min(tabla$DISCOVERY_YEAR),
median(tabla$DISCOVERY_YEAR), median(tabla$DISCOVERY_YEAR),
median(tabla$DISCOVERY_YEAR)),
FID_YEAR = c(median(tabla$FID_YEAR), median(tabla$FID_YEAR),
median(tabla$FID_YEAR), max(tabla$FID_YEAR),
median(tabla$FID_YEAR)),
STATUS_YEAR = c(median(tabla$STATUS_YEAR), median(tabla$STATUS_YEAR),
median(tabla$STATUS_YEAR), median(tabla$STATUS_YEAR),
min(tabla$STATUS_YEAR)),
QUANTITY = c(median(tabla$QUANTITY), median(tabla$QUANTITY),
max(tabla$QUANTITY), median(tabla$QUANTITY),
median(tabla$QUANTITY))
)
escenarios$PRODUCTION_START_YEAR_ESTIMADA <- predict(modelo, newdata = escenarios)
escenarios %>%
mutate(PRODUCTION_START_YEAR_ESTIMADA = round(PRODUCTION_START_YEAR_ESTIMADA, 2)) %>%
gt() %>%
tab_header(title = md("**Estimaciones del Modelo**"))
| Estimaciones del Modelo | |||||
| Escenario | DISCOVERY_YEAR | FID_YEAR | STATUS_YEAR | QUANTITY | PRODUCTION_START_YEAR_ESTIMADA |
|---|---|---|---|---|---|
| Típico (medianas) | 2004 | 2018.5 | 2022 | 1 | 2021.28 |
| Descubrimiento antiguo | 1938 | 2018.5 | 2022 | 1 | 2021.95 |
| Reservas grandes | 2004 | 2018.5 | 2022 | 1 | 2021.28 |
| FID reciente | 2004 | 2029.0 | 2022 | 1 | 2031.72 |
| Estado más antiguo | 2004 | 2018.5 | 2018 | 1 | 2019.62 |
Con DISCOVERY_YEAR (X1), FID_YEAR (X2), STATUS_YEAR (X3) y QUANTITY (X4) se obtiene el modelo de regresión lineal múltiple:
\[y = -804.13 + -0.0102x_1 + 0.9945x_2 + 0.4148x_3 + -0.317618x_4\]
Interpretación de cada pendiente (manteniendo las demás variables constantes):
El modelo explica 95.8% de la variabilidad de Y (R² = 0.9584). El F-test resulta significativo (p = 1.95e-166), lo que confirma que, en conjunto, las 4 variables sí aportan información útil para explicar Y — aunque, tomadas de forma individual, X1, X3 y X4 no superen 0.7 de correlación con Y.
El dominio de Y se definió como los enteros positivos dentro del rango observado (1975–2034), y el modelo se interpreta únicamente dentro de ese dominio.