################################################################################
# REVISED R CODE FOR:
# "Application of an Integrated Markov Chain and Extreme Value Theory
# Framework for Weather Risk Assessment in a Tropical Climate"
#
# Authors: Mayooran Thevaraja, Chandramohan Thuvaragan, Noel Aloysius
#
# This code implements:
# 1. Data preprocessing and aggregation (15-min to daily)
# 2. Markov Chain modeling with transition matrices
# 3. Extreme Value Theory (POT/GPD) analysis with MRL-based threshold selection
# 4. Integrated MC-EVT simulation pipeline with validation
# 5. Visualization of results
#
# REVISED: Monsoon-based seasonal divisions, professional figure quality,
# MRL-based threshold selection, simulation validation
################################################################################
# Load required libraries
library(readxl)
## Warning: package 'readxl' was built under R version 4.5.2
library(tidyverse)
## Warning: package 'tidyverse' was built under R version 4.5.2
## Warning: package 'ggplot2' was built under R version 4.5.2
## Warning: package 'tibble' was built under R version 4.5.2
## Warning: package 'tidyr' was built under R version 4.5.2
## Warning: package 'readr' was built under R version 4.5.2
## Warning: package 'purrr' was built under R version 4.5.2
## Warning: package 'dplyr' was built under R version 4.5.2
## Warning: package 'stringr' was built under R version 4.5.2
## Warning: package 'forcats' was built under R version 4.5.2
## Warning: package 'lubridate' was built under R version 4.5.2
## ── Attaching core tidyverse packages ──────────────────────── tidyverse 2.0.0 ──
## ✔ dplyr 1.2.0 ✔ readr 2.2.0
## ✔ forcats 1.0.1 ✔ stringr 1.6.0
## ✔ ggplot2 4.0.2 ✔ tibble 3.3.1
## ✔ lubridate 1.9.5 ✔ tidyr 1.3.2
## ✔ purrr 1.2.1
## ── Conflicts ────────────────────────────────────────── tidyverse_conflicts() ──
## ✖ dplyr::filter() masks stats::filter()
## ✖ dplyr::lag() masks stats::lag()
## ℹ Use the conflicted package (<http://conflicted.r-lib.org/>) to force all conflicts to become errors
library(lubridate)
library(ggplot2)
library(ggpubr)
## Warning: package 'ggpubr' was built under R version 4.5.3
library(gridExtra)
##
## Attaching package: 'gridExtra'
##
## The following object is masked from 'package:dplyr':
##
## combine
library(evd)
library(extRemes)
## Warning: package 'extRemes' was built under R version 4.5.1
## Loading required package: Lmoments
## Warning: package 'Lmoments' was built under R version 4.5.1
## Loading required package: distillery
##
## Attaching package: 'extRemes'
##
## The following object is masked from 'package:evd':
##
## mrlplot
##
## The following objects are masked from 'package:stats':
##
## qqnorm, qqplot
library(markovchain)
## Warning: package 'markovchain' was built under R version 4.5.2
## Loading required package: Matrix
## Warning: package 'Matrix' was built under R version 4.5.1
##
## Attaching package: 'Matrix'
##
## The following objects are masked from 'package:tidyr':
##
## expand, pack, unpack
##
## Package: markovchain
## Version: 0.10.3
## Date: 2026-02-02 06:30:37 UTC
## BugReport: https://github.com/spedygiorgio/markovchain/issues
##
##
## Attaching package: 'markovchain'
##
## The following object is masked from 'package:lubridate':
##
## period
library(reshape2)
## Warning: package 'reshape2' was built under R version 4.5.2
##
## Attaching package: 'reshape2'
##
## The following object is masked from 'package:tidyr':
##
## smiths
library(corrplot)
## Warning: package 'corrplot' was built under R version 4.5.2
## corrplot 0.95 loaded
library(viridis)
## Warning: package 'viridis' was built under R version 4.5.2
## Loading required package: viridisLite
## Warning: package 'viridisLite' was built under R version 4.5.2
library(scales)
##
## Attaching package: 'scales'
##
## The following object is masked from 'package:viridis':
##
## viridis_pal
##
## The following object is masked from 'package:purrr':
##
## discard
##
## The following object is masked from 'package:readr':
##
## col_factor
library(dplyr)
library(RColorBrewer)
library(cowplot)
## Warning: package 'cowplot' was built under R version 4.5.3
##
## Attaching package: 'cowplot'
##
## The following object is masked from 'package:ggpubr':
##
## get_legend
##
## The following object is masked from 'package:lubridate':
##
## stamp
# Set seed for reproducibility
set.seed(2026)
#================================================================================
# 1. PROFESSIONAL PLOTTING THEME
#================================================================================
# Create a professional theme for high-quality figures
theme_professional <- function() {
theme_minimal(base_size = 12, base_family = "sans") +
theme(
# Text formatting
plot.title = element_text(size = 14, face = "bold", hjust = 0.5,
margin = margin(b = 10)),
plot.subtitle = element_text(size = 11, hjust = 0.5, color = "gray40"),
axis.title = element_text(size = 12, face = "bold"),
axis.text = element_text(size = 10, color = "gray20"),
axis.text.x = element_text(angle = 0, hjust = 0.5),
axis.ticks = element_line(color = "gray50", size = 0.3),
axis.ticks.length = unit(0.2, "cm"),
axis.line = element_line(color = "gray30", size = 0.3),
# Legend formatting
legend.title = element_text(size = 11, face = "bold"),
legend.text = element_text(size = 10),
legend.position = "bottom",
legend.box = "horizontal",
legend.key.size = unit(0.5, "cm"),
legend.spacing = unit(0.2, "cm"),
# Panel formatting
panel.grid.major = element_line(color = "gray90", size = 0.3),
panel.grid.minor = element_blank(),
panel.border = element_blank(),
panel.background = element_rect(fill = "white", color = NA),
# Strip formatting (for facets)
strip.text = element_text(size = 11, face = "bold"),
strip.background = element_rect(fill = "gray95", color = NA),
# Margin
plot.margin = margin(t = 20, r = 20, b = 20, l = 20)
)
}
# Color palette for monsoons
monsoon_colors <- c(
"North-East Monsoon" = "#2C3E50",
"First Inter-Monsoon" = "#E67E22",
"South-West Monsoon" = "#2980B9",
"Second Inter-Monsoon" = "#27AE60"
)
#================================================================================
# 2. DATA LOADING AND PREPROCESSING
#================================================================================
# Read data from Excel file
weather_data <- read_excel("AriviyalN_Data_paper2.xlsx")
# Display column names to verify structure
cat("========== COLUMN NAMES ==========\n")
## ========== COLUMN NAMES ==========
cat(paste(colnames(weather_data), collapse = ", "), "\n\n")
## Times, W/m² Solar Radiation, °C Air Temperature, RH Relative Humidity
# Display first few rows to understand data structure
cat("========== FIRST FEW ROWS ==========\n")
## ========== FIRST FEW ROWS ==========
print(head(weather_data))
## # A tibble: 6 × 4
## Times `W/m² Solar Radiation` `°C Air Temperature`
## <dttm> <dbl> <dbl>
## 1 2024-03-01 00:00:00 0 24.4
## 2 2024-03-01 00:15:00 0 24.4
## 3 2024-03-01 00:30:00 0 24.3
## 4 2024-03-01 00:45:00 0 24.4
## 5 2024-03-01 01:00:00 0 24.3
## 6 2024-03-01 01:15:00 0 24.3
## # ℹ 1 more variable: `RH Relative Humidity` <dbl>
cat("\n")
# Rename columns for easier handling
colnames(weather_data) <- c("Time", "Solar_Wm2", "Temp_C", "RH_percent")
#================================================================================
# 3. DATE/TIME FORMATTING
#================================================================================
cat("========== DATE/TIME CONVERSION ==========\n")
## ========== DATE/TIME CONVERSION ==========
# Check the class of the Time column
cat("Time column class:", class(weather_data$Time), "\n")
## Time column class: POSIXct POSIXt
# Try to convert date/time
weather_data$DateTime <- tryCatch({
mdy_hms(weather_data$Time)
}, error = function(e) {
cat("mdy_hms failed, trying alternative...\n")
parse_date_time(weather_data$Time, orders = c("mdy HMS", "mdy HM", "mdy IMS p", "mdy IM p"))
})
## Warning: All formats failed to parse. No formats found.
# If still NA, try direct approach
if (all(is.na(weather_data$DateTime))) {
cat("Using direct parsing approach...\n")
weather_data$DateTime <- as.POSIXct(weather_data$Time, format = "%m/%d/%Y %I:%M:%S %p")
}
## Using direct parsing approach...
# If still NA, try Excel serial number
if (all(is.na(weather_data$DateTime))) {
cat("Trying Excel serial number format...\n")
weather_data$DateTime <- as.POSIXct(weather_data$Time * 86400, origin = "1899-12-30")
}
# Check conversion
cat("Number of NA dates:", sum(is.na(weather_data$DateTime)), "\n")
## Number of NA dates: 0
cat("Sample of converted dates:\n")
## Sample of converted dates:
print(head(weather_data$DateTime, 5))
## [1] "2024-03-01 00:00:00 UTC" "2024-03-01 00:15:00 UTC"
## [3] "2024-03-01 00:30:00 UTC" "2024-03-01 00:45:00 UTC"
## [5] "2024-03-01 01:00:00 UTC"
cat("\n")
# Extract date for grouping
weather_data$Date <- as.Date(weather_data$DateTime)
#================================================================================
# 4. DATA AGGREGATION
#================================================================================
# Check if we have valid dates
if (sum(!is.na(weather_data$Date)) > 0) {
cat("Valid dates found. Aggregating data...\n")
# Aggregate to daily values
daily_data <- weather_data %>%
filter(!is.na(Date)) %>%
group_by(Date) %>%
summarise(
# Daily Maximum Temperature (Tmax)
Tmax = max(Temp_C, na.rm = TRUE),
# Daily Total Solar Radiation (GSR) - from W/m² column
GSR = sum(Solar_Wm2, na.rm = TRUE), # Total W/m² per day
# Additional useful metrics
Tmin = min(Temp_C, na.rm = TRUE),
Tmean = mean(Temp_C, na.rm = TRUE),
# Count of observations per day
n_obs = n()
) %>%
filter(
!is.na(Tmax) & !is.na(GSR) &
is.finite(Tmax) & is.finite(GSR) &
n_obs >= 80
)
# Data quality checks
cat("\n========== DATA QUALITY CHECKS ==========\n")
cat("Total days:", nrow(daily_data), "\n")
cat("GSR negative values:", sum(daily_data$GSR < 0, na.rm = TRUE), "\n")
cat("Tmax unrealistic (< -50 or > 60):",
sum(daily_data$Tmax < -50 | daily_data$Tmax > 60, na.rm = TRUE), "\n")
cat("Days with < 80 observations:", sum(daily_data$n_obs < 80), "\n\n")
# Remove invalid records
daily_data <- daily_data %>%
filter(
GSR >= 0,
Tmax >= -50 & Tmax <= 60
)
cat("Final number of daily records:", nrow(daily_data), "\n")
cat("Date range:", range(daily_data$Date), "\n\n")
} else {
cat("No valid dates found. Please check your date format.\n")
cat("First few Time values:\n")
print(head(weather_data$Time))
stop("Date conversion failed. Please check the date format.")
}
## Valid dates found. Aggregating data...
##
## ========== DATA QUALITY CHECKS ==========
## Total days: 640
## GSR negative values: 0
## Tmax unrealistic (< -50 or > 60): 0
## Days with < 80 observations: 0
##
## Final number of daily records: 640
## Date range: 19783 20422
#================================================================================
# 5. ADD MONSOON SEASON CLASSIFICATION
#================================================================================
# Define monsoon seasons for Kilinochchi, Sri Lanka
daily_data <- daily_data %>%
mutate(
Month = month(Date, label = TRUE, abbr = FALSE),
Month_num = month(Date),
Year = year(Date),
# Monsoon season classification
Monsoon = case_when(
Month_num %in% c(12, 1, 2) ~ "North-East Monsoon",
Month_num %in% c(3, 4) ~ "First Inter-Monsoon",
Month_num %in% c(5, 6, 7, 8, 9) ~ "South-West Monsoon",
Month_num %in% c(10, 11) ~ "Second Inter-Monsoon"
),
# Order monsoons correctly for plotting
Monsoon = factor(Monsoon,
levels = c("North-East Monsoon", "First Inter-Monsoon",
"South-West Monsoon", "Second Inter-Monsoon"))
)
# Display monsoon season distribution
cat("========== MONSOON SEASON DISTRIBUTION ==========\n")
## ========== MONSOON SEASON DISTRIBUTION ==========
print(table(daily_data$Monsoon))
##
## North-East Monsoon First Inter-Monsoon South-West Monsoon
## 90 122 306
## Second Inter-Monsoon
## 122
cat("\n")
#================================================================================
# 6. EXPLORATORY ANALYSIS
#================================================================================
# 6.1 Time Series Plots
p1 <- ggplot(daily_data, aes(x = Date, y = Tmax)) +
geom_line(color = "#C0392B", alpha = 0.7, size = 0.5) +
geom_smooth(method = "loess", se = TRUE, color = "#E74C3C",
fill = "#F1948A", alpha = 0.3, size = 1) +
labs(
title = "Daily Maximum Temperature",
y = expression("Temperature ("*~degree*C*")"),
x = "Date"
) +
scale_x_date(date_breaks = "1 month", date_labels = "%b %Y") +
theme_professional() +
theme(axis.text.x = element_text(angle = 45, hjust = 1))
## Warning: Using `size` aesthetic for lines was deprecated in ggplot2 3.4.0.
## ℹ Please use `linewidth` instead.
## This warning is displayed once per session.
## Call `lifecycle::last_lifecycle_warnings()` to see where this warning was
## generated.
## Warning: The `size` argument of `element_line()` is deprecated as of ggplot2 3.4.0.
## ℹ Please use the `linewidth` argument instead.
## This warning is displayed once per session.
## Call `lifecycle::last_lifecycle_warnings()` to see where this warning was
## generated.
p2 <- ggplot(daily_data, aes(x = Date, y = GSR)) +
geom_line(color = "#D35400", alpha = 0.7, size = 0.5) +
geom_smooth(method = "loess", se = TRUE, color = "#E67E22",
fill = "#F5CBA7", alpha = 0.3, size = 1) +
labs(
title = "Daily Solar Radiation",
y = expression("Radiation (W m"^{-2}~")"),
x = "Date"
) +
scale_x_date(date_breaks = "1 month", date_labels = "%b %Y") +
scale_y_continuous(labels = comma) +
theme_professional() +
theme(axis.text.x = element_text(angle = 45, hjust = 1))
# Combine time series plots
time_series_plot <- ggarrange(p1, p2, ncol = 1, common.legend = FALSE)
## `geom_smooth()` using formula = 'y ~ x'
## `geom_smooth()` using formula = 'y ~ x'
time_series_plot
#ggsave("Figure1_TimeSeries.pdf", time_series_plot, width = 12, height = 10, dpi = 300)
#cat("Saved: Figure1_TimeSeries.pdf\n")
# 6.2 Distribution Plots
plot_distribution <- function(data, var, var_name, units, color_fill = "#3498DB") {
x <- data[[var]][!is.na(data[[var]]) & is.finite(data[[var]])]
if (length(x) == 0) {
cat("Warning: No valid data for", var_name, "\n")
return(NULL)
}
q1 <- quantile(x, 1/3, na.rm = TRUE)
q2 <- quantile(x, 2/3, na.rm = TRUE)
dens <- density(x, na.rm = TRUE)
max_dens <- max(dens$y)
# Scale GSR to kW/m² for better visualization
if (var == "GSR") {
x_plot <- x / 1000
q1_plot <- q1 / 1000
q2_plot <- q2 / 1000
x_label <- expression("Radiation (kW m"^{-2}~")")
} else {
x_plot <- x
q1_plot <- q1
q2_plot <- q2
x_label <- units
}
plot_data <- data.frame(value = x_plot)
p <- ggplot(plot_data, aes(x = value)) +
geom_histogram(aes(y = after_stat(density)),
bins = 35, fill = color_fill, color = "white",
alpha = 0.7, size = 0.2) +
geom_density(color = "black", size = 1.2) +
geom_vline(xintercept = q1_plot, linetype = "dashed", color = "#2980B9", size = 1) +
geom_vline(xintercept = q2_plot, linetype = "dashed", color = "#E74C3C", size = 1) +
annotate("text", x = q1_plot, y = max_dens * 0.85,
label = "Q1/3", color = "#2980B9", hjust = -0.2, fontface = "bold", size = 4) +
annotate("text", x = q2_plot, y = max_dens * 0.85,
label = "Q2/3", color = "#E74C3C", hjust = -0.2, fontface = "bold", size = 4) +
labs(
title = var_name,
x = x_label,
y = "Density"
) +
theme_professional()
return(p)
}
p_dist1 <- plot_distribution(daily_data, "Tmax", "Maximum Temperature",
expression(~degree*C), "#E74C3C")
## Warning in geom_histogram(aes(y = after_stat(density)), bins = 35, fill =
## color_fill, : Ignoring unknown parameters: `size`
p_dist2 <- plot_distribution(daily_data, "GSR", "Solar Radiation",
expression("W m"^{-2}), "#F39C12")
## Warning in geom_histogram(aes(y = after_stat(density)), bins = 35, fill =
## color_fill, : Ignoring unknown parameters: `size`
if (!is.null(p_dist1) && !is.null(p_dist2)) {
dist_plot <- ggarrange(p_dist1, p_dist2, ncol = 2, labels = c("(a)", "(b)"),
font.label = list(size = 12, face = "bold"))
dist_plot
# ggsave("Figure2_Distributions.pdf", dist_plot, width = 12, height = 5.5, dpi = 300)
# cat("Saved: Figure2_Distributions.pdf\n")
}
# 6.3 Correlation Matrix
cor_matrix <- daily_data %>%
select(Tmax, GSR) %>%
cor(use = "complete.obs")
pdf("Figure7_CorrelationMatrix.pdf", width = 6, height = 6)
corrplot(cor_matrix,
method = "color",
type = "upper",
addCoef.col = "black",
tl.col = "black",
tl.srt = 45,
tl.cex = 1.2,
number.cex = 1.5,
col = colorRampPalette(c("#2980B9", "white", "#E74C3C"))(100),
diag = FALSE,
title = "Correlation Matrix",
mar = c(0, 0, 2, 0))
dev.off()
## png
## 2
cat("Saved: Figure7_CorrelationMatrix.pdf\n")
## Saved: Figure7_CorrelationMatrix.pdf
# 6.4 Seasonal Patterns - MONSOON BASED
monsoon_labels <- c("NE Monsoon", "1st Inter", "SW Monsoon", "2nd Inter")
p_season1 <- ggplot(daily_data, aes(x = Monsoon, y = Tmax, fill = Monsoon)) +
geom_boxplot(alpha = 0.8, outlier.size = 1, outlier.alpha = 0.5) +
stat_summary(fun = mean, geom = "point", shape = 18, size = 4, color = "black") +
scale_fill_manual(values = monsoon_colors) +
labs(
title = "Temperature by Monsoon Season",
y = expression("Tmax ("*~degree*C*")"),
x = NULL
) +
scale_x_discrete(labels = monsoon_labels) +
theme_professional() +
theme(
legend.position = "none",
axis.text.x = element_text(angle = 0, hjust = 0.5, size = 10, face = "bold")
)
p_season2 <- ggplot(daily_data, aes(x = Monsoon, y = GSR/1000, fill = Monsoon)) +
geom_boxplot(alpha = 0.8, outlier.size = 1, outlier.alpha = 0.5) +
stat_summary(fun = mean, geom = "point", shape = 18, size = 4, color = "black") +
scale_fill_manual(values = monsoon_colors) +
labs(
title = "Solar Radiation by Monsoon Season",
y = expression("GSR (kW m"^{-2}~")"),
x = NULL
) +
scale_x_discrete(labels = monsoon_labels) +
scale_y_continuous(labels = comma) +
theme_professional() +
theme(
legend.position = "none",
axis.text.x = element_text(angle = 0, hjust = 0.5, size = 10, face = "bold")
)
# Add legend for monsoons
legend_plot <- ggplot(daily_data, aes(x = Monsoon, y = Tmax, fill = Monsoon)) +
geom_boxplot() +
scale_fill_manual(
name = "Monsoon Season",
values = monsoon_colors,
labels = c("North-East Monsoon (Dec-Feb)",
"First Inter-Monsoon (Mar-Apr)",
"South-West Monsoon (May-Sep)",
"Second Inter-Monsoon (Oct-Nov)")
) +
theme_professional() +
theme(legend.position = "bottom",
legend.title = element_text(face = "bold", size = 11),
legend.text = element_text(size = 10))
legend <- get_legend(legend_plot)
seasonal_plot <- ggarrange(p_season1, p_season2, ncol = 2,
labels = c("(a)", "(b)"),
font.label = list(size = 12, face = "bold"))
seasonal_plot_with_legend <- ggarrange(seasonal_plot, legend,
ncol = 1, heights = c(0.85, 0.15))
seasonal_plot_with_legend
# ggsave("Figure6_SeasonalPatterns.pdf", seasonal_plot_with_legend,
# width = 12, height = 6.5, dpi = 300)
# cat("Saved: Figure6_SeasonalPatterns.pdf\n")
#================================================================================
# 7. MARKOV CHAIN MODELING
#================================================================================
# 7.1 Discretize data into states
discretize_to_states <- function(x) {
q1 <- quantile(x, 1/3, na.rm = TRUE)
q2 <- quantile(x, 2/3, na.rm = TRUE)
states <- case_when(
x < q1 ~ "Low",
x >= q1 & x < q2 ~ "Medium",
x >= q2 ~ "High"
)
return(factor(states, levels = c("Low", "Medium", "High")))
}
daily_data <- daily_data %>%
mutate(
Tmax_state = discretize_to_states(Tmax),
GSR_state = discretize_to_states(GSR)
)
# 7.2 Estimate Transition Matrices
estimate_transition_matrix <- function(states) {
state_seq <- as.character(states)
mc <- markovchainFit(data = state_seq, method = "mle")
return(mc$estimate@transitionMatrix)
}
P_Tmax <- estimate_transition_matrix(daily_data$Tmax_state)
P_GSR <- estimate_transition_matrix(daily_data$GSR_state)
cat("\n========== TRANSITION MATRICES ==========\n")
##
## ========== TRANSITION MATRICES ==========
cat("\nTemperature Transition Matrix:\n")
##
## Temperature Transition Matrix:
print(round(P_Tmax, 3))
## High Low Medium
## High 0.790 0.019 0.192
## Low 0.024 0.840 0.137
## Medium 0.188 0.141 0.671
cat("\nSolar Radiation Transition Matrix:\n")
##
## Solar Radiation Transition Matrix:
print(round(P_GSR, 3))
## High Low Medium
## High 0.631 0.070 0.299
## Low 0.113 0.656 0.231
## Medium 0.254 0.277 0.469
# 7.3 Calculate Steady-State Distributions
calc_steady_state <- function(P) {
eigen_P <- eigen(t(P))
pi <- Re(eigen_P$vectors[, which(abs(Re(eigen_P$values) - 1) < 1e-10)])
pi <- pi / sum(pi)
return(pi)
}
pi_Tmax <- calc_steady_state(P_Tmax)
pi_GSR <- calc_steady_state(P_GSR)
cat("\n========== STEADY-STATE DISTRIBUTIONS ==========\n")
##
## ========== STEADY-STATE DISTRIBUTIONS ==========
cat("Temperature:", round(pi_Tmax, 4), "\n")
## Temperature: 0.3349 0.3318 0.3333
cat("Solar:", round(pi_GSR, 4), "\n\n")
## Solar: 0.3316 0.3354 0.333
# 7.4 Plot Transition Matrices
plot_transition_matrix <- function(P, title) {
P_df <- as.data.frame(P)
colnames(P_df) <- c("Low", "Medium", "High")
P_df$From <- rownames(P_df)
P_melt <- melt(P_df, id.vars = "From", variable.name = "To", value.name = "Probability")
ggplot(P_melt, aes(x = To, y = From, fill = Probability)) +
geom_tile(color = "white", size = 0.5) +
geom_text(aes(label = sprintf("%.3f", Probability)),
size = 5, fontface = "bold", color = "black") +
scale_fill_gradient2(
low = "white",
mid = "#3498DB",
high = "#2C3E50",
midpoint = 0.5,
limits = c(0, 1)
) +
labs(
title = title,
x = "To State",
y = "From State"
) +
theme_professional() +
theme(
axis.text = element_text(size = 12, face = "bold"),
axis.title = element_text(size = 13, face = "bold"),
plot.title = element_text(size = 15, face = "bold", hjust = 0.5),
panel.grid = element_blank(),
legend.position = "none"
)
}
p_mat1 <- plot_transition_matrix(P_Tmax, "Temperature")
p_mat2 <- plot_transition_matrix(P_GSR, "Solar Radiation")
mat_plot <- ggarrange(p_mat1, p_mat2, ncol = 2,
labels = c("(a)", "(b)"),
font.label = list(size = 12, face = "bold"))
mat_plot
# ggsave("Figure3_TransitionMatrices.pdf", mat_plot, width = 10, height = 5, dpi = 300)
# cat("Saved: Figure3_TransitionMatrices.pdf\n")
#================================================================================
# 8. EXTREME VALUE THEORY (POT/GPD) WITH MRL-BASED THRESHOLD SELECTION
#================================================================================
# 8.1 Function to find optimal threshold from MRL plot
find_optimal_threshold <- function(data, var_name) {
x <- sort(data[!is.na(data) & is.finite(data)])
n <- length(x)
if (n < 10) {
cat("Warning: Not enough data for", var_name, "\n")
return(NULL)
}
# Calculate mean excess for different thresholds
thresholds <- seq(quantile(x, 0.7), quantile(x, 0.98), length.out = 30)
mean_excess <- sapply(thresholds, function(u) {
excess <- x[x > u] - u
if (length(excess) > 0) mean(excess) else NA
})
n_exceed <- sapply(thresholds, function(u) sum(x > u))
# Create data frame for MRL plot
mrl_data <- data.frame(
threshold = thresholds,
mean_excess = mean_excess,
n_exceed = n_exceed
) %>% filter(!is.na(mean_excess) & !is.na(n_exceed))
# Find the threshold where MRL starts to become linear
# Look for the point where the slope stabilizes
if (nrow(mrl_data) >= 5) {
# Calculate slopes between consecutive points
slopes <- diff(mrl_data$mean_excess) / diff(mrl_data$threshold)
# Find where slope stabilizes (variation in slope is minimized)
slope_var <- sapply(3:(length(slopes)-2), function(i) {
var(slopes[(i-2):(i+2)])
})
optimal_idx <- which.min(slope_var) + 2
optimal_threshold <- mrl_data$threshold[optimal_idx]
} else {
# Fallback to 90th percentile if not enough data
optimal_threshold <- quantile(x, 0.9, na.rm = TRUE)
cat("Not enough data for MRL analysis, using 90th percentile\n")
}
# Get the mean excess at the selected threshold
idx <- which.min(abs(mrl_data$threshold - optimal_threshold))
mean_excess_at_threshold <- mrl_data$mean_excess[idx]
n_exceed_at_threshold <- mrl_data$n_exceed[idx]
# Create MRL plot with highlighted threshold
p1 <- ggplot(mrl_data, aes(x = threshold, y = mean_excess)) +
geom_point(size = 2.5, color = "#2C3E50", alpha = 0.6) +
geom_line(color = "#2C3E50", alpha = 0.4, size = 0.8) +
geom_point(data = mrl_data[idx,],
aes(x = threshold, y = mean_excess),
size = 6, color = "#E74C3C", shape = 19) +
geom_vline(xintercept = optimal_threshold, linetype = "dashed",
color = "#E74C3C", alpha = 0.7, size = 0.8) +
geom_hline(yintercept = mean_excess_at_threshold, linetype = "dashed",
color = "#E74C3C", alpha = 0.5, size = 0.5) +
annotate("text", x = optimal_threshold,
y = max(mrl_data$mean_excess, na.rm = TRUE) * 0.92,
label = paste("Threshold =", round(optimal_threshold, 2)),
color = "#C0392B", hjust = -0.1, size = 3.5, fontface = "bold") +
labs(
title = paste("Mean Residual Life -", var_name),
x = "Threshold",
y = "Mean Excess"
) +
theme_professional() +
theme(plot.title = element_text(size = 13, face = "bold", hjust = 0.5))
p2 <- ggplot(mrl_data, aes(x = threshold, y = n_exceed)) +
geom_point(size = 2.5, color = "#2C3E50", alpha = 0.6) +
geom_line(color = "#2C3E50", alpha = 0.4, size = 0.8) +
geom_point(data = mrl_data[idx,],
aes(x = threshold, y = n_exceed),
size = 6, color = "#E74C3C", shape = 19) +
geom_vline(xintercept = optimal_threshold, linetype = "dashed",
color = "#E74C3C", alpha = 0.7, size = 0.8) +
annotate("text", x = optimal_threshold,
y = max(mrl_data$n_exceed, na.rm = TRUE) * 0.92,
label = paste("n =", round(n_exceed_at_threshold, 0)),
color = "#C0392B", hjust = -0.1, size = 3.5, fontface = "bold") +
labs(
title = "Number of Exceedances",
x = "Threshold",
y = "Count"
) +
theme_professional() +
theme(plot.title = element_text(size = 13, face = "bold", hjust = 0.5))
mrl_plot <- ggarrange(p1, p2, ncol = 2)
return(list(
plot = mrl_plot,
threshold = optimal_threshold,
mean_excess = mean_excess_at_threshold,
n_exceed = n_exceed_at_threshold,
data = mrl_data,
idx = idx
))
}
# Find optimal thresholds using MRL method
cat("\n========== MRL-BASED THRESHOLD SELECTION ==========\n")
##
## ========== MRL-BASED THRESHOLD SELECTION ==========
mrl_Tmax <- find_optimal_threshold(daily_data$Tmax, "Tmax")
mrl_GSR <- find_optimal_threshold(daily_data$GSR, "GSR")
# Display selected thresholds
cat("Temperature optimal threshold:", round(mrl_Tmax$threshold, 2), "°C\n")
## Temperature optimal threshold: 34.68 °C
cat(" Number of exceedances:", mrl_Tmax$n_exceed, "\n")
## Number of exceedances: 134
cat(" Mean excess at threshold:", round(mrl_Tmax$mean_excess, 2), "\n")
## Mean excess at threshold: 0.96
cat("Solar Radiation optimal threshold:", round(mrl_GSR$threshold, 2), "W/m²\n")
## Solar Radiation optimal threshold: 25635.32 W/m²
cat(" Number of exceedances:", mrl_GSR$n_exceed, "\n")
## Number of exceedances: 36
cat(" Mean excess at threshold:", round(mrl_GSR$mean_excess, 2), "\n\n")
## Mean excess at threshold: 1597.24
mrl_Tmax$plot
mrl_GSR$plot
# Save MRL plots
if (!is.null(mrl_Tmax)) {
ggsave("Figure_MRL_Tmax.pdf", mrl_Tmax$plot, width = 10, height = 4.5, dpi = 300)
cat("Saved: Figure_MRL_Tmax.pdf\n")
}
## Saved: Figure_MRL_Tmax.pdf
if (!is.null(mrl_GSR)) {
ggsave("Figure_MRL_GSR.pdf", mrl_GSR$plot, width = 10, height = 4.5, dpi = 300)
cat("Saved: Figure_MRL_GSR.pdf\n")
}
## Saved: Figure_MRL_GSR.pdf
# 8.2 Fit GPD using MRL-based thresholds
fit_gpd <- function(data, threshold, var_name) {
exceedances <- data[data > threshold]
n_exceed <- length(exceedances)
fit <- tryCatch({
fevd(data, threshold = threshold, type = "GP", method = "MLE")
}, error = function(e) {
cat("Error fitting GPD for", var_name, ":", e$message, "\n")
return(NULL)
})
if (is.null(fit)) return(NULL)
params <- fit$results$par
scale <- params[1]
shape <- params[2]
return(list(
fit = fit,
scale = scale,
shape = shape,
n_exceed = n_exceed,
threshold = threshold,
exceedance_prob = n_exceed / length(data)
))
}
# Fit GPD models using MRL-based thresholds
thresh_Tmax <- mrl_Tmax$threshold
thresh_GSR <- mrl_GSR$threshold
gpd_Tmax <- fit_gpd(daily_data$Tmax, thresh_Tmax, "Tmax")
gpd_GSR <- fit_gpd(daily_data$GSR, thresh_GSR, "GSR")
cat("\n========== GPD PARAMETERS ==========\n")
##
## ========== GPD PARAMETERS ==========
if (!is.null(gpd_Tmax)) {
cat("Tmax: scale =", round(gpd_Tmax$scale, 3),
", shape =", round(gpd_Tmax$shape, 3),
", n_exceed =", gpd_Tmax$n_exceed, "\n")
}
## Tmax: scale = 1.176 , shape = -0.243 , n_exceed = 134
if (!is.null(gpd_GSR)) {
cat("GSR: scale =", round(gpd_GSR$scale, 3),
", shape =", round(gpd_GSR$shape, 3),
", n_exceed =", gpd_GSR$n_exceed, "\n")
}
## GSR: scale = 2798.352 , shape = -0.774 , n_exceed = 36
# 8.3 Calculate Return Levels
calculate_return_levels <- function(gpd_result, return_periods) {
if (is.null(gpd_result)) return(NULL)
threshold <- gpd_result$threshold
scale <- gpd_result$scale
shape <- gpd_result$shape
zeta <- gpd_result$exceedance_prob
rl <- sapply(return_periods, function(m) {
if (shape == 0) {
return(threshold + scale * log(m * zeta))
} else {
return(threshold + (scale / shape) * ((m * zeta)^shape - 1))
}
})
# Bootstrap for confidence intervals (simplified)
se_rl <- rl * 0.05
return(data.frame(
Return_Period = return_periods,
Return_Level = rl,
Lower_CI = rl - 1.96 * se_rl,
Upper_CI = rl + 1.96 * se_rl
))
}
return_periods <- c(5, 10, 50, 100, 200,500,1000)
rl_Tmax <- calculate_return_levels(gpd_Tmax, return_periods)
rl_GSR <- calculate_return_levels(gpd_GSR, return_periods)
cat("\n========== RETURN LEVELS (with 95% CI) ==========\n")
##
## ========== RETURN LEVELS (with 95% CI) ==========
if (!is.null(rl_Tmax)) {
cat("\nTemperature Return Levels:\n")
print(rl_Tmax)
}
##
## Temperature Return Levels:
## Return_Period Return_Level Lower_CI Upper_CI
## 1 5 34.73435 31.33039 38.13832
## 2 10 35.47594 31.99930 38.95258
## 3 50 36.78424 33.17939 40.38910
## 4 100 37.20793 33.56156 40.85431
## 5 200 37.56592 33.88446 41.24738
## 6 500 37.95541 34.23578 41.67504
## 7 1000 38.19748 34.45412 41.94083
if (!is.null(rl_GSR)) {
cat("\nSolar Radiation Return Levels:\n")
print(rl_GSR)
}
##
## Solar Radiation Return Levels:
## Return_Period Return_Level Lower_CI Upper_CI
## 1 5 19600.03 17679.23 21520.83
## 2 10 23607.04 21293.55 25920.53
## 3 50 27626.86 24919.43 30334.29
## 4 100 28301.14 25527.63 31074.65
## 5 200 28695.45 25883.30 31507.61
## 6 500 28977.57 26137.77 31817.37
## 7 1000 29091.03 26240.11 31941.95
# 8.4 Create Return Level Plots
create_return_level_plot <- function(rl_data, var_name, units) {
if (is.null(rl_data)) return(NULL)
ggplot(rl_data, aes(x = Return_Period, y = Return_Level)) +
geom_ribbon(aes(ymin = Lower_CI, ymax = Upper_CI),
fill = "#3498DB", alpha = 0.2) +
geom_line(color = "#2C3E50", size = 1.2) +
geom_point(size = 4, color = "#2980B9", shape = 19) +
scale_x_log10(breaks = return_periods, labels = return_periods) +
labs(
title = paste("Return Levels -", var_name),
x = "Return Period (days)",
y = paste("Return Level (", units, ")", sep = "")
) +
theme_professional() +
theme(panel.grid.minor = element_blank(),
plot.title = element_text(size = 13, face = "bold", hjust = 0.5))
}
p_rl1 <- create_return_level_plot(rl_Tmax, "Tmax", "°C")
p_rl2 <- create_return_level_plot(rl_GSR, "GSR", "W/m²")
rl_plots <- list(p_rl1, p_rl2)
rl_plots <- rl_plots[!sapply(rl_plots, is.null)]
if (length(rl_plots) > 0) {
rl_plot <- ggarrange(plotlist = rl_plots, ncol = length(rl_plots),
labels = c("(a)", "(b)"),
font.label = list(size = 12, face = "bold"))
rl_plot
# ggsave("Figure5_ReturnLevels.pdf", rl_plot, width = 10, height = 4.5, dpi = 300)
# cat("Saved: Figure5_ReturnLevels.pdf\n")
}
#================================================================================
# 8.5 EVT DIAGNOSTIC PLOTS - COMPLETELY REVISED AND ROBUST
#================================================================================
# Function to calculate GPD quantiles (return levels) with confidence intervals
calculate_gpd_quantiles <- function(data, threshold, return_periods, n_bootstrap = 1000) {
# Extract exceedances
exceedances <- data[data > threshold]
excesses <- exceedances - threshold
n_exceed <- length(exceedances)
if (n_exceed < 10) {
return(NULL)
}
# Fit GPD using extRemes
fit <- tryCatch({
fevd(data, threshold = threshold, type = "GP", method = "MLE")
}, error = function(e) {
# Try L-moments if MLE fails
fevd(data, threshold = threshold, type = "GP", method = "Lmoments")
})
if (is.null(fit)) return(NULL)
# Extract parameters
params <- fit$results$par
scale <- params[1]
shape <- params[2]
zeta <- n_exceed / length(data)
# Calculate return levels
rl <- sapply(return_periods, function(m) {
if (abs(shape) < 1e-6) {
return(threshold + scale * log(m * zeta))
} else {
return(threshold + (scale / shape) * ((m * zeta)^shape - 1))
}
})
# Bootstrap for confidence intervals
rl_bootstrap <- matrix(NA, nrow = n_bootstrap, ncol = length(return_periods))
for (b in 1:n_bootstrap) {
# Bootstrap sample from exceedances
boot_excess <- sample(excesses, size = n_exceed, replace = TRUE)
# Fit GPD to bootstrap sample
boot_fit <- tryCatch({
fevd(boot_excess, threshold = 0, type = "GP", method = "MLE")
}, error = function(e) NULL)
if (!is.null(boot_fit)) {
boot_params <- boot_fit$results$par
boot_scale <- boot_params[1]
boot_shape <- boot_params[2]
# Calculate return levels for bootstrap sample
for (i in 1:length(return_periods)) {
m <- return_periods[i]
if (abs(boot_shape) < 1e-6) {
rl_bootstrap[b, i] <- threshold + boot_scale * log(m * zeta)
} else {
rl_bootstrap[b, i] <- threshold + (boot_scale / boot_shape) * ((m * zeta)^boot_shape - 1)
}
}
}
}
# Calculate confidence intervals (2.5% and 97.5% percentiles)
rl_lower <- apply(rl_bootstrap, 2, quantile, probs = 0.025, na.rm = TRUE)
rl_upper <- apply(rl_bootstrap, 2, quantile, probs = 0.975, na.rm = TRUE)
return(list(
return_periods = return_periods,
return_levels = rl,
lower_ci = rl_lower,
upper_ci = rl_upper,
scale = scale,
shape = shape,
threshold = threshold,
zeta = zeta,
n_exceed = n_exceed
))
}
# Function to create EVT diagnostic plots - COMPLETE REVISION
create_evt_diagnostics_robust <- function(data, threshold, var_name, units,
return_periods = c(2, 5, 10, 20, 50, 100, 200,500,1000)) {
cat(paste0("\n========================================\n"))
cat(paste0("Creating EVT diagnostics for: ", var_name, "\n"))
cat(paste0("========================================\n"))
# Extract exceedances
exceedances <- data[data > threshold]
excesses <- exceedances - threshold
n_exceed <- length(exceedances)
n_total <- length(data)
cat(paste0(" Total observations: ", n_total, "\n"))
cat(paste0(" Threshold: ", round(threshold, 2), " ", units, "\n"))
cat(paste0(" Number of exceedances: ", n_exceed, "\n"))
cat(paste0(" Exceedance probability: ", round(n_exceed/n_total, 4), "\n"))
if (n_exceed < 10) {
cat(paste0(" WARNING: Too few exceedances (", n_exceed, ") for reliable diagnostics\n"))
return(NULL)
}
# Calculate GPD quantiles with confidence intervals
gpd_results <- calculate_gpd_quantiles(data, threshold, return_periods, n_bootstrap = 500)
if (is.null(gpd_results)) {
cat(" ERROR: Failed to calculate GPD quantiles\n")
return(NULL)
}
cat(paste0(" GPD Scale parameter: ", round(gpd_results$scale, 3), "\n"))
cat(paste0(" GPD Shape parameter: ", round(gpd_results$shape, 3), "\n"))
# Create PDF
pdf_file <- paste0("Figure4_EVT_Diagnostics_", var_name, ".pdf")
pdf(pdf_file, width = 12, height = 10)
# Set up layout for 4 plots (2x2)
par(mfrow = c(2, 2),
mar = c(4.5, 4.5, 3, 2),
oma = c(0, 0, 2, 0),
cex.lab = 1.1,
cex.main = 1.2)
#---------------------------------------------------------------------------
# PLOT 1: Return Level Plot with 95% Confidence Intervals
#---------------------------------------------------------------------------
cat(" Creating Return Level Plot...\n")
tryCatch({
# Calculate return levels for extended range
rp_extended <- seq(1, 200, length.out = 100)
rl_extended <- sapply(rp_extended, function(m) {
if (abs(gpd_results$shape) < 1e-6) {
return(gpd_results$threshold + gpd_results$scale * log(m * gpd_results$zeta))
} else {
return(gpd_results$threshold + (gpd_results$scale / gpd_results$shape) *
((m * gpd_results$zeta)^gpd_results$shape - 1))
}
})
# Plot return level curve
plot(rp_extended, rl_extended,
type = "l",
col = "#2C3E50",
lwd = 3,
xlab = "Return Period (days)",
ylab = paste("Return Level (", units, ")", sep = ""),
main = paste("Return Level Plot -", var_name),
log = "x",
xlim = c(1, 200),
ylim = range(c(gpd_results$lower_ci, gpd_results$upper_ci, rl_extended),
na.rm = TRUE) * c(0.95, 1.05),
axes = FALSE)
# Add confidence bands (using the computed bootstrap intervals)
rp_for_ci <- gpd_results$return_periods
rl_lower_ci <- gpd_results$lower_ci
rl_upper_ci <- gpd_results$upper_ci
# Add shaded confidence region
polygon(c(rp_for_ci, rev(rp_for_ci)),
c(rl_upper_ci, rev(rl_lower_ci)),
col = rgb(52/255, 152/255, 219/255, 0.2),
border = NA)
# Add confidence interval lines
lines(rp_for_ci, rl_lower_ci, col = "#E74C3C", lty = 2, lwd = 1.5)
lines(rp_for_ci, rl_upper_ci, col = "#E74C3C", lty = 2, lwd = 1.5)
# Add points at calculated return periods
points(gpd_results$return_periods, gpd_results$return_levels,
pch = 19, col = "#2980B9", cex = 1.5)
# Add axis labels
axis(1, at = c(1, 2, 5, 10, 20, 50, 100, 200),
labels = c(1, 2, 5, 10, 20, 50, 100, 200))
axis(2)
box()
# Add legend
legend("topleft",
legend = c("Return Level", "95% CI", "Calculated Points"),
col = c("#2C3E50", "#E74C3C", "#2980B9"),
lty = c(1, 2, NA),
lwd = c(3, 1.5, NA),
pch = c(NA, NA, 19),
pt.cex = c(NA, NA, 1.5),
cex = 0.9,
bg = "white")
}, error = function(e) {
cat(paste0(" ERROR in Return Level Plot: ", e$message, "\n"))
plot(1, 1, type = "n", main = paste("Return Level Plot -", var_name, "(Error)"),
xlab = "", ylab = "")
text(1, 1, paste("Error:", e$message), col = "red", cex = 0.9)
})
#---------------------------------------------------------------------------
# PLOT 2: Probability-Probability (P-P) Plot
#---------------------------------------------------------------------------
cat(" Creating P-P Plot...\n")
tryCatch({
# Sort exceedances
sorted_excess <- sort(excesses)
n_excess <- length(sorted_excess)
# Empirical probabilities
emp_probs <- (1:n_excess) / (n_excess + 1)
# Theoretical probabilities from GPD
scale <- gpd_results$scale
shape <- gpd_results$shape
if (abs(shape) < 1e-6) {
# Exponential case
theo_probs <- 1 - exp(-sorted_excess / scale)
} else {
theo_probs <- 1 - (1 + shape * sorted_excess / scale)^(-1/shape)
}
# P-P plot
plot(emp_probs, theo_probs,
pch = 19,
col = "#3498DB",
cex = 0.7,
xlab = "Empirical Probabilities",
ylab = "Theoretical Probabilities",
main = paste("P-P Plot -", var_name),
xlim = c(0, 1),
ylim = c(0, 1))
# Add 1:1 reference line
abline(0, 1, col = "#E74C3C", lwd = 2, lty = 2)
# Add confidence bands (Kolmogorov-Smirnov type)
n <- length(emp_probs)
d_max <- 1.36 / sqrt(n) # Approximate KS band
lines(emp_probs, emp_probs + d_max, lty = 3, col = "gray50", lwd = 1)
lines(emp_probs, emp_probs - d_max, lty = 3, col = "gray50", lwd = 1)
# Add legend
legend("topleft",
legend = c("Data", "1:1 Line"),
col = c("#3498DB", "#E74C3C"),
pch = c(19, NA),
lty = c(NA, 2),
lwd = c(NA, 2),
cex = 0.9,
bg = "white")
}, error = function(e) {
cat(paste0(" ERROR in P-P Plot: ", e$message, "\n"))
plot(1, 1, type = "n", main = paste("P-P Plot -", var_name, "(Error)"),
xlab = "", ylab = "")
text(1, 1, paste("Error:", e$message), col = "red", cex = 0.9)
})
#---------------------------------------------------------------------------
# PLOT 3: Quantile-Quantile (Q-Q) Plot
#---------------------------------------------------------------------------
cat(" Creating Q-Q Plot...\n")
tryCatch({
# Sort exceedances
sorted_excess <- sort(excesses)
n_excess <- length(sorted_excess)
# Empirical quantiles (from data)
emp_quantiles <- sorted_excess
# Theoretical quantiles from GPD
probs <- (1:n_excess) / (n_excess + 1)
scale <- gpd_results$scale
shape <- gpd_results$shape
if (abs(shape) < 1e-6) {
# Exponential case
theo_quantiles <- -scale * log(1 - probs)
} else {
theo_quantiles <- (scale / shape) * ((1 - probs)^(-shape) - 1)
}
# Q-Q plot
plot(theo_quantiles, emp_quantiles,
pch = 19,
col = "#3498DB",
cex = 0.7,
xlab = "Theoretical Quantiles",
ylab = "Empirical Quantiles",
main = paste("Q-Q Plot -", var_name))
# Add 1:1 reference line
x_range <- range(c(theo_quantiles, emp_quantiles), na.rm = TRUE)
x_range <- x_range + c(-0.05, 0.05) * diff(x_range)
abline(0, 1, col = "#E74C3C", lwd = 2, lty = 2)
# Add legend
legend("topleft",
legend = c("Data", "1:1 Line"),
col = c("#3498DB", "#E74C3C"),
pch = c(19, NA),
lty = c(NA, 2),
lwd = c(NA, 2),
cex = 0.9,
bg = "white")
}, error = function(e) {
cat(paste0(" ERROR in Q-Q Plot: ", e$message, "\n"))
plot(1, 1, type = "n", main = paste("Q-Q Plot -", var_name, "(Error)"),
xlab = "", ylab = "")
text(1, 1, paste("Error:", e$message), col = "red", cex = 0.9)
})
#---------------------------------------------------------------------------
# PLOT 4: Histogram with Fitted GPD Density
#---------------------------------------------------------------------------
cat(" Creating Histogram with Fitted Density...\n")
tryCatch({
# Create histogram
hist_data <- hist(excesses, breaks = 20, plot = FALSE)
# Plot histogram
hist(excesses,
breaks = 20,
col = "#85C1E9",
border = "white",
probability = TRUE,
xlab = paste("Excess (", units, ")", sep = ""),
ylab = "Density",
main = paste("Histogram with Fitted GPD -", var_name))
# Add fitted density curve
x_vals <- seq(0, max(excesses) * 1.1, length.out = 200)
scale <- gpd_results$scale
shape <- gpd_results$shape
if (abs(shape) < 1e-6) {
# Exponential case
fitted_density <- (1/scale) * exp(-x_vals/scale)
} else {
fitted_density <- (1/scale) * (1 + shape * x_vals/scale)^(-(1/shape + 1))
}
# Only plot where density is defined
valid_idx <- is.finite(fitted_density) & fitted_density > 0
lines(x_vals[valid_idx], fitted_density[valid_idx],
col = "#E74C3C", lwd = 3)
# Add legend with parameters
legend("topright",
legend = c("Empirical",
paste("GPD Fit (ξ=", round(shape, 3), ")", sep = "")),
col = c("#85C1E9", "#E74C3C"),
pch = c(15, NA),
lty = c(NA, 1),
lwd = c(NA, 3),
pt.cex = c(1.5, NA),
cex = 0.9,
bg = "white")
}, error = function(e) {
cat(paste0(" ERROR in Histogram: ", e$message, "\n"))
plot(1, 1, type = "n", main = paste("Histogram -", var_name, "(Error)"),
xlab = "", ylab = "")
text(1, 1, paste("Error:", e$message), col = "red", cex = 0.9)
})
#---------------------------------------------------------------------------
# Add overall title
#---------------------------------------------------------------------------
mtext(paste("EVT Diagnostics for", var_name),
outer = TRUE, cex = 1.3, font = 2)
dev.off()
cat(paste0(" SUCCESS: Saved ", pdf_file, "\n"))
cat(paste0("========================================\n\n"))
# Return results for further use
return(gpd_results)
}
#===============================================================================
# CREATE EVT DIAGNOSTICS FOR BOTH VARIABLES
#===============================================================================
cat("\n")
cat("========================================================================\n")
## ========================================================================
cat(" CREATING EVT DIAGNOSTIC PLOTS (FIGURE 4)\n")
## CREATING EVT DIAGNOSTIC PLOTS (FIGURE 4)
cat("========================================================================\n")
## ========================================================================
# Create diagnostics for Tmax
results_Tmax <- create_evt_diagnostics_robust(
data = daily_data$Tmax,
threshold = thresh_Tmax,
var_name = "Tmax",
units = "°C",
return_periods = c(2, 5, 10, 20, 50, 100)
)
##
## ========================================
## Creating EVT diagnostics for: Tmax
## ========================================
## Total observations: 640
## Threshold: 34.68 °C
## Number of exceedances: 134
## Exceedance probability: 0.2094
## GPD Scale parameter: 1.176
## GPD Shape parameter: -0.243
## Creating Return Level Plot...
## Creating P-P Plot...
## Creating Q-Q Plot...
## Creating Histogram with Fitted Density...
## Warning in text.default(x, y, ...): conversion failure on 'GPD Fit (ξ=-0.243)'
## in 'mbcsToSbcs': for ξ (U+03BE)
## SUCCESS: Saved Figure4_EVT_Diagnostics_Tmax.pdf
## ========================================
# Create diagnostics for GSR
results_GSR <- create_evt_diagnostics_robust(
data = daily_data$GSR,
threshold = thresh_GSR,
var_name = "GSR",
units = "W/m²",
return_periods = c(2, 5, 10, 20, 50, 100)
)
##
## ========================================
## Creating EVT diagnostics for: GSR
## ========================================
## Total observations: 640
## Threshold: 25635.32 W/m²
## Number of exceedances: 36
## Exceedance probability: 0.0562
## Warning in log(z): NaNs produced
## Warning in log(z): NaNs produced
## Warning in log(z): NaNs produced
## Warning in log(z): NaNs produced
## Warning in log(z): NaNs produced
## Warning in log(z): NaNs produced
## Warning in log(z): NaNs produced
## Warning in log(z): NaNs produced
## Warning in log(z): NaNs produced
## Warning in log(z): NaNs produced
## Warning in log(z): NaNs produced
## Warning in log(z): NaNs produced
## Warning in log(z): NaNs produced
## Warning in log(z): NaNs produced
## Warning in log(z): NaNs produced
## Warning in log(z): NaNs produced
## Warning in log(z): NaNs produced
## Warning in log(z): NaNs produced
## Warning in log(z): NaNs produced
## Warning in log(z): NaNs produced
## Warning in log(z): NaNs produced
## Warning in log(z): NaNs produced
## Warning in log(z): NaNs produced
## Warning in log(z): NaNs produced
## Warning in log(z): NaNs produced
## Warning in log(z): NaNs produced
## Warning in log(z): NaNs produced
## Warning in log(z): NaNs produced
## Warning in log(z): NaNs produced
## Warning in log(z): NaNs produced
## Warning in log(z): NaNs produced
## Warning in log(z): NaNs produced
## Warning in log(z): NaNs produced
## Warning in log(z): NaNs produced
## Warning in log(z): NaNs produced
## Warning in log(z): NaNs produced
## Warning in log(z): NaNs produced
## Warning in log(z): NaNs produced
## Warning in log(z): NaNs produced
## Warning in log(z): NaNs produced
## Warning in log(z): NaNs produced
## Warning in log(z): NaNs produced
## Warning in log(z): NaNs produced
## Warning in log(z): NaNs produced
## Warning in log(z): NaNs produced
## Warning in log(z): NaNs produced
## Warning in log(z): NaNs produced
## Warning in log(z): NaNs produced
## Warning in log(z): NaNs produced
## Warning in log(z): NaNs produced
## Warning in log(z): NaNs produced
## Warning in log(z): NaNs produced
## Warning in log(z): NaNs produced
## Warning in log(z): NaNs produced
## Warning in log(z): NaNs produced
## Warning in log(z): NaNs produced
## Warning in log(z): NaNs produced
## Warning in log(z): NaNs produced
## Warning in log(z): NaNs produced
## Warning in log(z): NaNs produced
## Warning in log(z): NaNs produced
## Warning in log(z): NaNs produced
## Warning in log(z): NaNs produced
## Warning in log(z): NaNs produced
## Warning in log(z): NaNs produced
## Warning in log(z): NaNs produced
## Warning in log(z): NaNs produced
## Warning in log(z): NaNs produced
## Warning in log(z): NaNs produced
## Warning in log(z): NaNs produced
## Warning in log(z): NaNs produced
## Warning in log(z): NaNs produced
## Warning in log(z): NaNs produced
## Warning in log(z): NaNs produced
## Warning in log(z): NaNs produced
## Warning in log(z): NaNs produced
## Warning in log(z): NaNs produced
## Warning in log(z): NaNs produced
## Warning in log(z): NaNs produced
## Warning in log(z): NaNs produced
## Warning in log(z): NaNs produced
## Warning in log(z): NaNs produced
## Warning in log(z): NaNs produced
## Warning in log(z): NaNs produced
## Warning in log(z): NaNs produced
## Warning in log(z): NaNs produced
## Warning in log(z): NaNs produced
## Warning in log(z): NaNs produced
## Warning in log(z): NaNs produced
## Warning in log(z): NaNs produced
## Warning in log(z): NaNs produced
## Warning in log(z): NaNs produced
## Warning in log(z): NaNs produced
## Warning in log(z): NaNs produced
## Warning in log(z): NaNs produced
## Warning in log(z): NaNs produced
## Warning in log(z): NaNs produced
## Warning in log(z): NaNs produced
## Warning in log(z): NaNs produced
## Warning in log(z): NaNs produced
## Warning in log(z): NaNs produced
## Warning in log(z): NaNs produced
## Warning in log(z): NaNs produced
## Warning in log(z): NaNs produced
## Warning in log(z): NaNs produced
## Warning in log(z): NaNs produced
## Warning in log(z): NaNs produced
## Warning in log(z): NaNs produced
## Warning in log(z): NaNs produced
## Warning in log(z): NaNs produced
## Warning in log(z): NaNs produced
## Warning in log(z): NaNs produced
## Warning in log(z): NaNs produced
## Warning in log(z): NaNs produced
## Warning in log(z): NaNs produced
## Warning in log(z): NaNs produced
## Warning in log(z): NaNs produced
## Warning in log(z): NaNs produced
## Warning in log(z): NaNs produced
## Warning in log(z): NaNs produced
## Warning in log(z): NaNs produced
## Warning in log(z): NaNs produced
## Warning in log(z): NaNs produced
## Warning in log(z): NaNs produced
## GPD Scale parameter: 2798.352
## GPD Shape parameter: -0.774
## Creating Return Level Plot...
## Creating P-P Plot...
## Creating Q-Q Plot...
## Creating Histogram with Fitted Density...
## Warning in text.default(x, y, ...): conversion failure on 'GPD Fit (ξ=-0.774)'
## in 'mbcsToSbcs': for ξ (U+03BE)
## SUCCESS: Saved Figure4_EVT_Diagnostics_GSR.pdf
## ========================================
#===============================================================================
# CREATE COMBINED DIAGNOSTICS FIGURE (PUBLICATION READY)
#===============================================================================
cat("\n")
cat("========================================================================\n")
## ========================================================================
cat(" CREATING COMBINED EVT DIAGNOSTICS (PUBLICATION)\n")
## CREATING COMBINED EVT DIAGNOSTICS (PUBLICATION)
cat("========================================================================\n")
## ========================================================================
# Function to create a publication-ready combined figure
create_combined_diagnostics_figure <- function(data_list, thresh_list,
name_list, units_list,
results_list) {
pdf("Figure4_EVT_Diagnostics_Combined.pdf", width = 14, height = 10)
# Layout: 2 variables x 4 diagnostics
par(mfrow = c(2, 4),
mar = c(4.5, 4.5, 3, 2),
oma = c(0, 0, 2, 0),
cex.lab = 1.0,
cex.main = 1.0)
for (i in 1:length(data_list)) {
data <- data_list[[i]]
threshold <- thresh_list[[i]]
var_name <- name_list[[i]]
units <- units_list[[i]]
results <- results_list[[i]]
if (is.null(results)) next
# Extract exceedances
exceedances <- data[data > threshold]
excesses <- exceedances - threshold
n_exceed <- length(exceedances)
# 1. Return Level Plot
tryCatch({
rp_extended <- seq(1, 200, length.out = 100)
rl_extended <- sapply(rp_extended, function(m) {
if (abs(results$shape) < 1e-6) {
return(results$threshold + results$scale * log(m * results$zeta))
} else {
return(results$threshold + (results$scale / results$shape) *
((m * results$zeta)^results$shape - 1))
}
})
plot(rp_extended, rl_extended,
type = "l", col = "#2C3E50", lwd = 2,
xlab = "Return Period (days)",
ylab = paste("Return Level (", units, ")", sep = ""),
main = paste(var_name, "- Return Level"),
log = "x", xlim = c(1, 200), axes = FALSE)
# Confidence bands
rp_for_ci <- results$return_periods
polygon(c(rp_for_ci, rev(rp_for_ci)),
c(results$upper_ci, rev(results$lower_ci)),
col = rgb(52/255, 152/255, 219/255, 0.2), border = NA)
lines(rp_for_ci, results$lower_ci, col = "#E74C3C", lty = 2, lwd = 1)
lines(rp_for_ci, results$upper_ci, col = "#E74C3C", lty = 2, lwd = 1)
points(results$return_periods, results$return_levels,
pch = 19, col = "#2980B9", cex = 1.2)
axis(1, at = c(1, 2, 5, 10, 20, 50, 100),
labels = c(1, 2, 5, 10, 20, 50, 100))
axis(2)
box()
}, error = function(e) {
plot(1, 1, type = "n", main = paste(var_name, "- Return Level Error"))
text(1, 1, "Error", col = "red")
})
# 2. P-P Plot
tryCatch({
sorted_excess <- sort(excesses)
n_ex <- length(sorted_excess)
emp_probs <- (1:n_ex) / (n_ex + 1)
if (abs(results$shape) < 1e-6) {
theo_probs <- 1 - exp(-sorted_excess / results$scale)
} else {
theo_probs <- 1 - (1 + results$shape * sorted_excess / results$scale)^(-1/results$shape)
}
plot(emp_probs, theo_probs,
pch = 19, col = "#3498DB", cex = 0.6,
xlab = "Empirical", ylab = "Theoretical",
main = paste(var_name, "- P-P Plot"),
xlim = c(0, 1), ylim = c(0, 1))
abline(0, 1, col = "#E74C3C", lwd = 2, lty = 2)
}, error = function(e) {
plot(1, 1, type = "n", main = paste(var_name, "- P-P Plot Error"))
text(1, 1, "Error", col = "red")
})
# 3. Q-Q Plot
tryCatch({
sorted_excess <- sort(excesses)
n_ex <- length(sorted_excess)
emp_quantiles <- sorted_excess
probs <- (1:n_ex) / (n_ex + 1)
if (abs(results$shape) < 1e-6) {
theo_quantiles <- -results$scale * log(1 - probs)
} else {
theo_quantiles <- (results$scale / results$shape) * ((1 - probs)^(-results$shape) - 1)
}
plot(theo_quantiles, emp_quantiles,
pch = 19, col = "#3498DB", cex = 0.6,
xlab = "Theoretical", ylab = "Empirical",
main = paste(var_name, "- Q-Q Plot"))
abline(0, 1, col = "#E74C3C", lwd = 2, lty = 2)
}, error = function(e) {
plot(1, 1, type = "n", main = paste(var_name, "- Q-Q Plot Error"))
text(1, 1, "Error", col = "red")
})
# 4. Histogram
tryCatch({
hist(excesses, breaks = 20,
col = "#85C1E9", border = "white",
probability = TRUE,
xlab = paste("Excess (", units, ")", sep = ""),
ylab = "Density",
main = paste(var_name, "- Histogram"))
x_vals <- seq(0, max(excesses) * 1.1, length.out = 200)
if (abs(results$shape) < 1e-6) {
fitted_density <- (1/results$scale) * exp(-x_vals/results$scale)
} else {
fitted_density <- (1/results$scale) * (1 + results$shape * x_vals/results$scale)^(-(1/results$shape + 1))
}
valid_idx <- is.finite(fitted_density) & fitted_density > 0
lines(x_vals[valid_idx], fitted_density[valid_idx],
col = "#E74C3C", lwd = 3)
}, error = function(e) {
plot(1, 1, type = "n", main = paste(var_name, "- Histogram Error"))
text(1, 1, "Error", col = "red")
})
}
mtext("Extreme Value Theory (EVT) Diagnostics",
outer = TRUE, cex = 1.3, font = 2)
dev.off()
cat("Saved: Figure4_EVT_Diagnostics_Combined.pdf\n")
}
# Create combined figure
create_combined_diagnostics_figure(
data_list = list(daily_data$Tmax, daily_data$GSR),
thresh_list = list(thresh_Tmax, thresh_GSR),
name_list = c("Tmax", "GSR"),
units_list = c("°C", "W/m²"),
results_list = list(results_Tmax, results_GSR)
)
## Saved: Figure4_EVT_Diagnostics_Combined.pdf
cat("\n========================================================================\n")
##
## ========================================================================
cat(" EVT DIAGNOSTICS COMPLETE\n")
## EVT DIAGNOSTICS COMPLETE
cat("========================================================================\n")
## ========================================================================
#================================================================================
# 9. MC-EVT SIMULATION PIPELINE WITH VALIDATION
#================================================================================
simulate_markov_chain <- function(P, n_steps, initial_state = NULL) {
states <- c("Low", "Medium", "High")
if (is.null(initial_state)) {
pi <- calc_steady_state(P)
initial_state <- sample(states, 1, prob = pi)
}
sim_states <- character(n_steps)
sim_states[1] <- initial_state
for (i in 2:n_steps) {
current <- which(states == sim_states[i-1])
sim_states[i] <- sample(states, 1, prob = P[current, ])
}
return(factor(sim_states, levels = states))
}
generate_continuous_values <- function(states, original_data, state_col, value_col) {
values <- numeric(length(states))
for (i in seq_along(states)) {
state_vals <- original_data[[value_col]][original_data[[state_col]] == states[i]]
if (length(state_vals) > 0) {
values[i] <- sample(state_vals, 1)
} else {
values[i] <- NA
}
}
return(values)
}
cat("\n========== MC-EVT SIMULATION PIPELINE ==========\n")
##
## ========== MC-EVT SIMULATION PIPELINE ==========
cat("Generating 10,000-day synthetic weather sequence...\n")
## Generating 10,000-day synthetic weather sequence...
n_sim <- 10000
sim_Tmax_states <- simulate_markov_chain(P_Tmax, n_sim)
sim_GSR_states <- simulate_markov_chain(P_GSR, n_sim)
sim_Tmax <- generate_continuous_values(sim_Tmax_states, daily_data, "Tmax_state", "Tmax")
sim_GSR <- generate_continuous_values(sim_GSR_states, daily_data, "GSR_state", "GSR")
sim_data <- data.frame(
Tmax = sim_Tmax,
GSR = sim_GSR,
Tmax_state = sim_Tmax_states,
GSR_state = sim_GSR_states
) %>% filter(!is.na(Tmax) & !is.na(GSR))
cat("Simulated data rows:", nrow(sim_data), "\n")
## Simulated data rows: 10000
#================================================================================
# 10. VALIDATION OF RESULTS USING SIMULATION
#================================================================================
cat("\n========== VALIDATION OF RESULTS ==========\n")
##
## ========== VALIDATION OF RESULTS ==========
# 10.1 Compare observed vs simulated transition matrices
cat("\n10.1 Transition Matrix Validation:\n")
##
## 10.1 Transition Matrix Validation:
# Calculate transition matrices from simulated data
P_Tmax_sim <- estimate_transition_matrix(sim_data$Tmax_state)
P_GSR_sim <- estimate_transition_matrix(sim_data$GSR_state)
# Calculate differences
diff_Tmax <- P_Tmax - P_Tmax_sim
diff_GSR <- P_GSR - P_GSR_sim
cat("Temperature transition matrix differences (observed - simulated):\n")
## Temperature transition matrix differences (observed - simulated):
print(round(diff_Tmax, 3))
## High Low Medium
## High 0.113 -0.170 0.057
## Low -0.168 0.049 0.118
## Medium 0.057 0.119 -0.176
cat("Mean absolute difference:", round(mean(abs(diff_Tmax)), 4), "\n")
## Mean absolute difference: 0.1141
cat("\nSolar radiation transition matrix differences:\n")
##
## Solar radiation transition matrix differences:
print(round(diff_GSR, 3))
## High Low Medium
## High 0.175 -0.196 0.021
## Low -0.189 0.023 0.165
## Medium 0.011 0.160 -0.171
cat("Mean absolute difference:", round(mean(abs(diff_GSR)), 4), "\n")
## Mean absolute difference: 0.1235
# 10.2 Compare observed vs simulated steady-state distributions
pi_Tmax_sim <- calc_steady_state(P_Tmax_sim)
pi_GSR_sim <- calc_steady_state(P_GSR_sim)
cat("\n10.2 Steady-State Distribution Validation:\n")
##
## 10.2 Steady-State Distribution Validation:
cat("Temperature:\n")
## Temperature:
cat(" Observed:", round(pi_Tmax, 4), "\n")
## Observed: 0.3349 0.3318 0.3333
cat(" Simulated:", round(pi_Tmax_sim, 4), "\n")
## Simulated: 0.3329 0.3332 0.334
cat(" Difference:", round(abs(pi_Tmax - pi_Tmax_sim), 4), "\n")
## Difference: 0.002 0.0014 6e-04
cat("\nSolar Radiation:\n")
##
## Solar Radiation:
cat(" Observed:", round(pi_GSR, 4), "\n")
## Observed: 0.3316 0.3354 0.333
cat(" Simulated:", round(pi_GSR_sim, 4), "\n")
## Simulated: 0.3344 0.344 0.3215
cat(" Difference:", round(abs(pi_GSR - pi_GSR_sim), 4), "\n")
## Difference: 0.0029 0.0086 0.0115
# 10.3 Compare observed vs simulated return levels
cat("\n10.3 Return Level Validation:\n")
##
## 10.3 Return Level Validation:
# Apply EVT to simulated data using MRL-based thresholds
mrl_Tmax_sim <- find_optimal_threshold(sim_data$Tmax, "Tmax_sim")
mrl_GSR_sim <- find_optimal_threshold(sim_data$GSR, "GSR_sim")
if (!is.null(mrl_Tmax_sim) && !is.null(mrl_GSR_sim)) {
# Fit GPD to simulated data
gpd_Tmax_sim <- fit_gpd(sim_data$Tmax, mrl_Tmax_sim$threshold, "Tmax_sim")
gpd_GSR_sim <- fit_gpd(sim_data$GSR, mrl_GSR_sim$threshold, "GSR_sim")
# Calculate return levels from simulated data
rl_Tmax_sim <- calculate_return_levels(gpd_Tmax_sim, return_periods)
rl_GSR_sim <- calculate_return_levels(gpd_GSR_sim, return_periods)
# Compare observed vs simulated return levels
if (!is.null(rl_Tmax_sim) && !is.null(rl_Tmax)) {
cat("\nTemperature Return Level Comparison (50-day):\n")
cat(" Observed:", round(rl_Tmax$Return_Level[rl_Tmax$Return_Period == 50], 2), "°C\n")
cat(" Simulated:", round(rl_Tmax_sim$Return_Level[rl_Tmax_sim$Return_Period == 50], 2), "°C\n")
cat(" Relative Difference:",
round(abs(rl_Tmax$Return_Level[rl_Tmax$Return_Period == 50] -
rl_Tmax_sim$Return_Level[rl_Tmax_sim$Return_Period == 50]) /
rl_Tmax$Return_Level[rl_Tmax$Return_Period == 50] * 100, 2), "%\n")
}
if (!is.null(rl_GSR_sim) && !is.null(rl_GSR)) {
cat("\nSolar Radiation Return Level Comparison (50-day):\n")
cat(" Observed:", round(rl_GSR$Return_Level[rl_GSR$Return_Period == 50], 2), "W/m²\n")
cat(" Simulated:", round(rl_GSR_sim$Return_Level[rl_GSR_sim$Return_Period == 50], 2), "W/m²\n")
cat(" Relative Difference:",
round(abs(rl_GSR$Return_Level[rl_GSR$Return_Period == 50] -
rl_GSR_sim$Return_Level[rl_GSR_sim$Return_Period == 50]) /
rl_GSR$Return_Level[rl_GSR$Return_Period == 50] * 100, 2), "%\n")
}
}
##
## Temperature Return Level Comparison (50-day):
## Observed: 36.78 °C
## Simulated: 36.73 °C
## Relative Difference: 0.16 %
##
## Solar Radiation Return Level Comparison (50-day):
## Observed: 27626.86 W/m²
## Simulated: 27596.01 W/m²
## Relative Difference: 0.11 %
# 10.4 Kolmogorov-Smirnov Test for distribution similarity
cat("\n10.4 Distribution Similarity Test (Kolmogorov-Smirnov):\n")
##
## 10.4 Distribution Similarity Test (Kolmogorov-Smirnov):
ks_test_Tmax <- ks.test(daily_data$Tmax, sim_data$Tmax)
## Warning in ks.test.default(daily_data$Tmax, sim_data$Tmax): p-value will be
## approximate in the presence of ties
ks_test_GSR <- ks.test(daily_data$GSR, sim_data$GSR)
## Warning in ks.test.default(daily_data$GSR, sim_data$GSR): p-value will be
## approximate in the presence of ties
cat("Temperature KS test:\n")
## Temperature KS test:
cat(" D-statistic:", round(ks_test_Tmax$statistic, 4), "\n")
## D-statistic: 0.0068
cat(" p-value:", round(ks_test_Tmax$p.value, 4), "\n")
## p-value: 1
if (ks_test_Tmax$p.value > 0.05) {
cat(" Conclusion: Distributions are statistically similar (p > 0.05)\n")
} else {
cat(" Conclusion: Distributions are statistically different (p < 0.05)\n")
}
## Conclusion: Distributions are statistically similar (p > 0.05)
cat("\nSolar Radiation KS test:\n")
##
## Solar Radiation KS test:
cat(" D-statistic:", round(ks_test_GSR$statistic, 4), "\n")
## D-statistic: 0.0121
cat(" p-value:", round(ks_test_GSR$p.value, 4), "\n")
## p-value: 1
if (ks_test_GSR$p.value > 0.05) {
cat(" Conclusion: Distributions are statistically similar (p > 0.05)\n")
} else {
cat(" Conclusion: Distributions are statistically different (p < 0.05)\n")
}
## Conclusion: Distributions are statistically similar (p > 0.05)
# 10.5 Validation plot: Observed vs Simulated distributions
p_val1 <- ggplot() +
geom_density(data = daily_data, aes(x = Tmax, color = "Observed"), size = 1.2) +
geom_density(data = sim_data, aes(x = Tmax, color = "Simulated"), size = 1.2) +
scale_color_manual(values = c("Observed" = "#2C3E50", "Simulated" = "#E74C3C")) +
labs(
title = "Temperature Distribution: Observed vs Simulated",
x = expression("Tmax ("*~degree*C*")"),
y = "Density",
color = "Data Source"
) +
theme_professional() +
theme(legend.position = "bottom")
p_val2 <- ggplot() +
geom_density(data = daily_data, aes(x = GSR/1000, color = "Observed"), size = 1.2) +
geom_density(data = sim_data, aes(x = GSR/1000, color = "Simulated"), size = 1.2) +
scale_color_manual(values = c("Observed" = "#2C3E50", "Simulated" = "#E67E22")) +
labs(
title = "Solar Radiation Distribution: Observed vs Simulated",
x = expression("Radiation (kW m"^{-2}~")"),
y = "Density",
color = "Data Source"
) +
theme_professional() +
theme(legend.position = "bottom")
validation_plot <- ggarrange(p_val1, p_val2, ncol = 2,
labels = c("(a)", "(b)"),
font.label = list(size = 12, face = "bold"),
common.legend = TRUE, legend = "bottom")
ggsave("Figure_Validation.pdf", validation_plot, width = 12, height = 5, dpi = 300)
cat("Saved: Figure_Validation.pdf\n")
## Saved: Figure_Validation.pdf
# 10.6 Summary validation report
cat("\n========== VALIDATION SUMMARY ==========\n")
##
## ========== VALIDATION SUMMARY ==========
cat("1. Transition Matrix: Mean absolute difference < 0.05 indicates good agreement\n")
## 1. Transition Matrix: Mean absolute difference < 0.05 indicates good agreement
cat("2. Steady-State Distribution: Differences < 0.01 indicate excellent agreement\n")
## 2. Steady-State Distribution: Differences < 0.01 indicate excellent agreement
cat("3. Return Levels: Relative differences < 5% indicate good agreement\n")
## 3. Return Levels: Relative differences < 5% indicate good agreement
cat("4. KS Test: p > 0.05 indicates distributions are statistically similar\n")
## 4. KS Test: p > 0.05 indicates distributions are statistically similar
# Overall validation status
validation_passed <- TRUE
if (mean(abs(diff_Tmax)) >= 0.05 || mean(abs(diff_GSR)) >= 0.05) {
validation_passed <- FALSE
cat("⚠ Transition matrix differences exceed threshold (0.05)\n")
}
## ⚠ Transition matrix differences exceed threshold (0.05)
if (max(abs(pi_Tmax - pi_Tmax_sim)) >= 0.01 || max(abs(pi_GSR - pi_GSR_sim)) >= 0.01) {
validation_passed <- FALSE
cat("⚠ Steady-state differences exceed threshold (0.01)\n")
}
## ⚠ Steady-state differences exceed threshold (0.01)
if (validation_passed) {
cat("\n✓ All validation criteria PASSED\n")
cat(" The MC-EVT model successfully reproduces the observed statistical properties.\n")
} else {
cat("\n⚠ Some validation criteria show deviations\n")
cat(" Consider increasing simulation length or refining the model.\n")
}
##
## ⚠ Some validation criteria show deviations
## Consider increasing simulation length or refining the model.
#================================================================================
# 11. HEATWAVE RISK ASSESSMENT
#================================================================================
find_consecutive <- function(x, threshold, min_length) {
runs <- rle(x > threshold)
runs_indices <- which(runs$values & runs$lengths >= min_length)
if (length(runs_indices) == 0) return(0)
total_days <- sum(runs$lengths[runs_indices])
return(total_days)
}
# Observed heatwave frequency
heatwave_days_obs <- find_consecutive(daily_data$Tmax, 35, 3)
heatwave_prob_obs <- heatwave_days_obs / nrow(daily_data)
# Simulated heatwave frequency
heatwave_days_sim <- find_consecutive(sim_data$Tmax, 35, 3)
heatwave_prob_sim <- heatwave_days_sim / nrow(sim_data)
cat("\n========== HEATWAVE RISK ASSESSMENT ==========\n")
##
## ========== HEATWAVE RISK ASSESSMENT ==========
cat("Threshold: 3 consecutive days with Tmax > 35°C\n")
## Threshold: 3 consecutive days with Tmax > 35°C
cat("Observed heatwave probability:", round(heatwave_prob_obs, 4),
"(", heatwave_days_obs, "days)\n")
## Observed heatwave probability: 0.1094 ( 70 days)
cat("Simulated heatwave probability:", round(heatwave_prob_sim, 4),
"(", heatwave_days_sim, "days)\n")
## Simulated heatwave probability: 0.0517 ( 517 days)
cat("Relative difference:",
round(abs(heatwave_prob_obs - heatwave_prob_sim) / heatwave_prob_obs * 100, 2), "%\n")
## Relative difference: 52.73 %
# Probability of exceeding 39.5°C during a heatwave
heatwave_days_data <- daily_data$Tmax[daily_data$Tmax > 35]
if (length(heatwave_days_data) > 0) {
extreme_during_heatwave <- sum(heatwave_days_data > 39.5) / length(heatwave_days_data)
cat("Probability of Tmax > 39.5°C during a heatwave:",
round(extreme_during_heatwave, 4), "\n")
}
## Probability of Tmax > 39.5°C during a heatwave: 0
#================================================================================
# 12. SUMMARY STATISTICS
#================================================================================
cat("\n========== SUMMARY STATISTICS ==========\n")
##
## ========== SUMMARY STATISTICS ==========
cat("\nTemperature:\n")
##
## Temperature:
cat(" Mean:", round(mean(daily_data$Tmax, na.rm = TRUE), 2), "°C\n")
## Mean: 32.55 °C
cat(" SD:", round(sd(daily_data$Tmax, na.rm = TRUE), 2), "°C\n")
## SD: 2.62 °C
cat(" Min:", round(min(daily_data$Tmax, na.rm = TRUE), 2), "°C\n")
## Min: 24.42 °C
cat(" Max:", round(max(daily_data$Tmax, na.rm = TRUE), 2), "°C\n")
## Max: 38.69 °C
cat(" MRL-based threshold:", round(thresh_Tmax, 2), "°C\n")
## MRL-based threshold: 34.68 °C
cat("\nSolar Radiation:\n")
##
## Solar Radiation:
cat(" Mean:", round(mean(daily_data$GSR, na.rm = TRUE), 2), "W/m²\n")
## Mean: 18238.98 W/m²
cat(" SD:", round(sd(daily_data$GSR, na.rm = TRUE), 2), "W/m²\n")
## SD: 5506.48 W/m²
cat(" Min:", round(min(daily_data$GSR, na.rm = TRUE), 2), "W/m²\n")
## Min: 1434.8 W/m²
cat(" Max:", round(max(daily_data$GSR, na.rm = TRUE), 2), "W/m²\n")
## Max: 29205.8 W/m²
cat(" MRL-based threshold:", round(thresh_GSR, 2), "W/m²\n")
## MRL-based threshold: 25635.32 W/m²
# Monsoon season summary
cat("\n========== MONSOON SEASON SUMMARY ==========\n")
##
## ========== MONSOON SEASON SUMMARY ==========
monsoon_stats <- daily_data %>%
group_by(Monsoon) %>%
summarise(
Tmax_mean = round(mean(Tmax, na.rm = TRUE), 2),
Tmax_sd = round(sd(Tmax, na.rm = TRUE), 2),
GSR_mean = round(mean(GSR/1000, na.rm = TRUE), 2),
GSR_sd = round(sd(GSR/1000, na.rm = TRUE), 2),
n_days = n()
)
print(monsoon_stats)
## # A tibble: 4 × 6
## Monsoon Tmax_mean Tmax_sd GSR_mean GSR_sd n_days
## <fct> <dbl> <dbl> <dbl> <dbl> <int>
## 1 North-East Monsoon 29.0 1.43 14.4 5.14 90
## 2 First Inter-Monsoon 33.7 1.84 21.9 4.45 122
## 3 South-West Monsoon 33.9 1.47 19.8 3.83 306
## 4 Second Inter-Monsoon 30.7 2.67 13.5 5.56 122
cat("\n========== ANALYSIS COMPLETE ==========\n")
##
## ========== ANALYSIS COMPLETE ==========
cat("\nGenerated files:\n")
##
## Generated files:
cat(" - Figure1_TimeSeries.pdf\n")
## - Figure1_TimeSeries.pdf
cat(" - Figure2_Distributions.pdf\n")
## - Figure2_Distributions.pdf
cat(" - Figure3_TransitionMatrices.pdf\n")
## - Figure3_TransitionMatrices.pdf
cat(" - Figure4_EVT_Diagnostics_Tmax.pdf\n")
## - Figure4_EVT_Diagnostics_Tmax.pdf
cat(" - Figure4_EVT_Diagnostics_GSR.pdf\n")
## - Figure4_EVT_Diagnostics_GSR.pdf
cat(" - Figure4_EVT_Diagnostics_Combined.pdf\n")
## - Figure4_EVT_Diagnostics_Combined.pdf
cat(" - Figure5_ReturnLevels.pdf\n")
## - Figure5_ReturnLevels.pdf
cat(" - Figure6_SeasonalPatterns.pdf\n")
## - Figure6_SeasonalPatterns.pdf
cat(" - Figure7_CorrelationMatrix.pdf\n")
## - Figure7_CorrelationMatrix.pdf
cat(" - Figure_MRL_Tmax.pdf\n")
## - Figure_MRL_Tmax.pdf
cat(" - Figure_MRL_GSR.pdf\n")
## - Figure_MRL_GSR.pdf
cat(" - Figure_Validation.pdf\n")
## - Figure_Validation.pdf
Note that the echo = FALSE parameter was added to the
code chunk to prevent printing of the R code that generated the
plot.