fixed the KPIs calculation for agronomic utils, fixed the path for saving the excel and rds, updated the main file

This commit is contained in:
DimitraVeropoulou 2026-02-12 15:43:58 +01:00
parent 13015f6ec0
commit 750db99a41
2 changed files with 195 additions and 316 deletions

View file

@ -370,7 +370,7 @@ main <- function() {
current_year <- as.numeric(format(end_date, "%G"))
# Call with correct signature
kpi_results <- calculate_all_kpis(
kpi_results <- calculate_all_field_analysis_agronomic_support(
field_boundaries_sf = field_boundaries_sf,
current_week = current_week,
current_year = current_year,

View file

@ -1,8 +1,8 @@
# 80_UTILS_AGRONOMIC_SUPPORT.R
# ============================================================================
# SPECIFIC KPI UTILITIES (SCRIPT 80 - CLIENT TYPE: agronomic_support)
# AURA-SPECIFIC KPI UTILITIES (SCRIPT 80 - CLIENT TYPE: agronomic_support)
#
# Contains all 6 KPI calculation functions and helpers:
# Contains all 6 AURA KPI calculation functions and helpers:
# - Field uniformity KPI (CV-based, spatial autocorrelation)
# - Area change KPI (week-over-week CI changes)
# - TCH forecasted KPI (tonnage projections from harvest data)
@ -12,7 +12,7 @@
# - KPI reporting (summary tables, field details, text interpretation)
# - KPI export (Excel, RDS, data export)
#
# Orchestrator: calculate_all_kpis()
# Orchestrator: calculate_all_field_analysis_agronomic_support()
# Dependencies: 00_common_utils.R (safe_log), sourced from common
# Used by: 80_calculate_kpis.R (when client_type == "agronomic_support")
# ============================================================================
@ -24,6 +24,8 @@ library(tidyr)
library(readxl)
library(writexl)
library(spdep)
library(caret)
library(CAST)
# ============================================================================
# SHARED HELPER FUNCTIONS (NOW IN 80_UTILS_COMMON.R)
@ -65,7 +67,7 @@ prepare_predictions <- function(harvest_model, field_data, scenario = "optimisti
}
# ============================================================================
# KPI CALCULATION FUNCTIONS (6 KPIS)
# AURA KPI CALCULATION FUNCTIONS (6 KPIS)
# ============================================================================
#' KPI 1: Calculate field uniformity based on CV and spatial autocorrelation
@ -75,36 +77,52 @@ prepare_predictions <- function(harvest_model, field_data, scenario = "optimisti
#'
#' @param ci_pixels_by_field List of CI pixel arrays for each field
#' @param field_boundaries_sf SF object with field geometries
#' @param ci_raster Raster object with CI values (for spatial autocorrelation)
#' @param ci_band Raster band with CI values
#'
#' @return Data frame with field_idx, cv_value, morans_i, uniformity_score, interpretation
calculate_field_uniformity_kpi <- function(ci_pixels_by_field, field_boundaries_sf, ci_raster = NULL) {
results_list <- list()
calculate_field_uniformity_kpi <- function(ci_pixels_by_field, field_boundaries_sf, ci_band = NULL) {
result <- data.frame(
field_idx = integer(),
cv_value = numeric(),
morans_i = numeric(),
uniformity_score = numeric(),
interpretation = character(),
stringsAsFactors = FALSE
)
for (field_idx in seq_len(nrow(field_boundaries_sf))) {
ci_pixels <- ci_pixels_by_field[[field_idx]]
if (is.null(ci_pixels) || length(ci_pixels) == 0) {
results_list[[length(results_list) + 1]] <- list(
result <- rbind(result, data.frame(
field_idx = field_idx,
cv_value = NA_real_,
morans_i = NA_real_,
uniformity_score = NA_real_,
interpretation = "No data"
)
interpretation = "No data",
stringsAsFactors = FALSE
))
next
}
cv_val <- calculate_cv(ci_pixels)
morans_i <- NA_real_
if (!is.null(ci_raster)) {
morans_result <- calculate_spatial_autocorrelation(ci_raster, field_boundaries_sf[field_idx, ])
if (is.list(morans_result)) {
morans_i <- morans_result$morans_i
} else {
morans_i <- morans_result
}
if (!is.null(ci_band) && inherits(ci_band, "SpatRaster")) {
tryCatch({
# Get single field geometry
single_field <- field_boundaries_sf[field_idx, ]
morans_result <- calculate_spatial_autocorrelation(ci_band, single_field)
if (is.list(morans_result)) {
morans_i <- morans_result$morans_i
} else {
morans_i <- morans_result
}
}, error = function(e) {
message(paste(" Warning: Spatial autocorrelation failed for field", field_idx, ":", e$message))
morans_i <<- NA_real_
})
}
# Normalize CV (0-1 scale, invert so lower CV = higher score)
@ -135,18 +153,15 @@ calculate_field_uniformity_kpi <- function(ci_pixels_by_field, field_boundaries_
interpretation <- "Very poor uniformity"
}
results_list[[length(results_list) + 1]] <- list(
result <- rbind(result, data.frame(
field_idx = field_idx,
cv_value = cv_val,
morans_i = morans_i,
uniformity_score = round(uniformity_score, 3),
interpretation = interpretation
)
}
# Convert accumulated list to data frame in a single operation
result <- do.call(rbind, lapply(results_list, as.data.frame))
interpretation = interpretation,
stringsAsFactors = FALSE
))
}
return(result)
}
@ -214,12 +229,19 @@ calculate_tch_forecasted_kpi <- function(field_statistics, harvesting_data = NUL
next
}
if (is.na(result$mean_ci[i])) {
result$tch_forecasted[i] <- NA_real_
result$tch_lower_bound[i] <- NA_real_
result$tch_upper_bound[i] <- NA_real_
result$confidence[i] <- "No data"
}
ci_val <- result$mean_ci[i]
# Simple linear model
tch_est <- 50 + (ci_val * 10)
# Confidence interval based on CI range
tch_lower <- tch_est * 0.85
tch_upper <- tch_est * 1.15
result$tch_forecasted[i] <- round(tch_est, 2)
result$tch_lower_bound[i] <- round(tch_lower, 2)
result$tch_upper_bound[i] <- round(tch_upper, 2)
result$confidence[i] <- "Medium"
}
return(result)
@ -338,190 +360,107 @@ calculate_weed_presence_kpi <- function(ci_pixels_by_field) {
return(result)
}
# #' Calculate Gap Filling Score KPI (placeholder)
# #' @param ci_raster Current week CI raster
# #' @param field_boundaries Field boundaries
# #' @return Data frame with field-level gap filling scores
# calculate_gap_filling_kpi <- function(ci_raster, field_boundaries) {
# # Handle both sf and SpatVector inputs
# if (!inherits(field_boundaries, "SpatVector")) {
# field_boundaries_vect <- terra::vect(field_boundaries)
# } else {
# field_boundaries_vect <- field_boundaries
# }
#' Calculate Gap Filling Score KPI (placeholder)
#' @param ci_raster Current week CI raster
#' @param field_boundaries Field boundaries
#' @return List with summary data frame and field-level results data frame
calculate_gap_filling_kpi <- function(ci_raster, field_boundaries) {
# Handle both sf and SpatVector inputs
if (!inherits(field_boundaries, "SpatVector")) {
field_boundaries_vect <- terra::vect(field_boundaries)
} else {
field_boundaries_vect <- field_boundaries
}
# results_list <- list()
field_results <- data.frame()
# # Ensure field_boundaries_vect is valid and matches field_boundaries dimensions
# n_fields_vect <- length(field_boundaries_vect)
# n_fields_sf <- nrow(field_boundaries)
# if (n_fields_sf != n_fields_vect) {
# warning(paste("Field boundary mismatch: nrow(field_boundaries)=", n_fields_sf, "vs length(field_boundaries_vect)=", n_fields_vect, ". Using actual SpatVector length."))
# }
for (i in seq_len(nrow(field_boundaries))) {
field_name <- if ("field" %in% names(field_boundaries)) field_boundaries$field[i] else NA_character_
sub_field_name <- if ("sub_field" %in% names(field_boundaries)) field_boundaries$sub_field[i] else NA_character_
field_vect <- field_boundaries_vect[i]
# for (i in seq_len(n_fields_vect)) {
# field_vect <- field_boundaries_vect[i]
# Extract CI values using helper function
ci_values <- extract_ci_values(ci_raster, field_vect)
valid_values <- ci_values[!is.na(ci_values) & is.finite(ci_values)]
# # Extract CI values using helper function
# ci_values <- extract_ci_values(ci_raster, field_vect)
# valid_values <- ci_values[!is.na(ci_values) & is.finite(ci_values)]
if (length(valid_values) > 1) {
# Gap score using 2σ below median to detect outliers
median_ci <- median(valid_values)
sd_ci <- sd(valid_values)
outlier_threshold <- median_ci - (2 * sd_ci)
low_ci_pixels <- sum(valid_values < outlier_threshold)
total_pixels <- length(valid_values)
gap_score <- round((low_ci_pixels / total_pixels) * 100, 2)
# if (length(valid_values) > 1) {
# # Calculate % of valid (non-NA) values = gap filling success
# total_pixels <- length(ci_values)
# valid_pixels <- length(valid_values)
# gap_filling_success <- (valid_pixels / total_pixels) * 100
# na_percent <- ((total_pixels - valid_pixels) / total_pixels) * 100
# Classify gap severity
gap_level <- dplyr::case_when(
gap_score < 10 ~ "Minimal",
gap_score < 25 ~ "Moderate",
TRUE ~ "Significant"
)
# results_list[[length(results_list) + 1]] <- list(
# field_idx = i,
# gap_filling_success = round(gap_filling_success, 2),
# na_percent_pre_interpolation = round(na_percent, 2),
# mean_ci = round(mean(valid_values), 2)
# )
# } else {
# # Not enough valid data
# results_list[[length(results_list) + 1]] <- list(
# field_idx = i,
# gap_filling_success = NA_real_,
# na_percent_pre_interpolation = NA_real_,
# mean_ci = NA_real_
# )
# }
# }
field_results <- rbind(field_results, data.frame(
field = field_name,
sub_field = sub_field_name,
gap_level = gap_level,
gap_score = gap_score,
mean_ci = mean(valid_values),
outlier_threshold = outlier_threshold
))
} else {
# Not enough valid data, fill with NA row
field_results <- rbind(field_results, data.frame(
field = field_name,
sub_field = sub_field_name,
gap_level = NA_character_,
gap_score = NA_real_,
mean_ci = NA_real_,
outlier_threshold = NA_real_
))
}
}
# Summarize results
gap_summary <- field_results %>%
dplyr::group_by(gap_level) %>%
dplyr::summarise(field_count = n(), .groups = 'drop') %>%
dplyr::mutate(percent = round((field_count / sum(field_count)) * 100, 1))
# Convert accumulated list to data frame in a single operation
field_results <- do.call(rbind, lapply(results_list, as.data.frame))
return(field_results)
return(list(summary = gap_summary, field_results = field_results))
}
# ============================================================================
# KPI ORCHESTRATOR AND REPORTING
# ============================================================================
#' Create summary tables for all 6 KPIs (AGGREGATED farm-level summaries)
#' Create summary tables for all 6 KPIs
#'
#' @param all_kpis List containing results from all 6 KPI functions (per-field data)
#' @param all_kpis List containing results from all 6 KPI functions
#'
#' @return List of summary data frames ready for reporting (farm-level aggregates)
#' @return List of summary data frames ready for reporting
create_summary_tables <- function(all_kpis) {
# ==========================================
# 1. UNIFORMITY SUMMARY (count by interpretation)
# ==========================================
uniformity_summary <- all_kpis$uniformity %>%
group_by(interpretation) %>%
summarise(
field_count = n(),
avg_cv = mean(cv_value, na.rm = TRUE),
avg_morans_i = mean(morans_i, na.rm = TRUE),
.groups = 'drop'
) %>%
rename(
Status = interpretation,
`Field Count` = field_count,
`Avg CV` = avg_cv,
`Avg Moran's I` = avg_morans_i
)
# ==========================================
# 2. AREA CHANGE SUMMARY (improving/stable/declining counts)
# ==========================================
area_change_summary <- all_kpis$area_change %>%
group_by(interpretation) %>%
summarise(
field_count = n(),
avg_ci_change = mean(mean_ci_pct_change, na.rm = TRUE),
.groups = 'drop'
) %>%
rename(
Status = interpretation,
`Field Count` = field_count,
`Avg CI Change %` = avg_ci_change
)
# ==========================================
# 3. TCH FORECAST SUMMARY (yield statistics)
# ==========================================
tch_summary <- all_kpis$tch_forecasted %>%
summarise(
avg_tch = mean(tch_forecasted, na.rm = TRUE),
min_tch = min(tch_forecasted, na.rm = TRUE),
max_tch = max(tch_forecasted, na.rm = TRUE),
avg_ci = mean(mean_ci, na.rm = TRUE),
fields_with_data = sum(!is.na(tch_forecasted))
) %>%
rename(
`Avg Forecast (t/ha)` = avg_tch,
`Min (t/ha)` = min_tch,
`Max (t/ha)` = max_tch,
`Avg CI` = avg_ci,
`Fields` = fields_with_data
)
# ==========================================
# 4. GROWTH DECLINE SUMMARY (trend interpretation)
# ==========================================
growth_summary <- all_kpis$growth_decline %>%
group_by(trend_interpretation) %>%
summarise(
field_count = n(),
avg_trend = mean(four_week_trend, na.rm = TRUE),
.groups = 'drop'
) %>%
rename(
Trend = trend_interpretation,
`Field Count` = field_count,
`Avg 4-Week Trend` = avg_trend
)
# ==========================================
# 5. WEED PRESSURE SUMMARY (risk level counts)
# ==========================================
weed_summary <- all_kpis$weed_presence %>%
group_by(weed_pressure_risk) %>%
summarise(
field_count = n(),
avg_fragmentation = mean(fragmentation_index, na.rm = TRUE),
.groups = 'drop'
) %>%
rename(
`Risk Level` = weed_pressure_risk,
`Field Count` = field_count,
`Avg Fragmentation` = avg_fragmentation
)
# ==========================================
# 6. GAP FILLING SUMMARY
# ==========================================
gap_summary <- if (!is.null(all_kpis$gap_filling) && is.data.frame(all_kpis$gap_filling) && nrow(all_kpis$gap_filling) > 0) {
all_kpis$gap_filling %>%
summarise(
avg_gap_filling = mean(gap_filling_success, na.rm = TRUE),
avg_na_percent = mean(na_percent_pre_interpolation, na.rm = TRUE),
fields_with_data = n()
) %>%
rename(
`Avg Gap Filling Success %` = avg_gap_filling,
`Avg NA % Pre-Interpolation` = avg_na_percent,
`Fields Analyzed` = fields_with_data
)
} else {
data.frame(`Avg Gap Filling Success %` = NA_real_, `Avg NA % Pre-Interpolation` = NA_real_, `Fields Analyzed` = 0, check.names = FALSE)
}
# Return as list (each element is a farm-level summary table)
kpi_summary <- list(
uniformity = uniformity_summary,
area_change = area_change_summary,
tch_forecast = tch_summary,
growth_decline = growth_summary,
weed_pressure = weed_summary,
gap_filling = gap_summary
uniformity = all_kpis$uniformity %>%
select(field_idx, cv_value, morans_i, uniformity_score, interpretation),
area_change = all_kpis$area_change %>%
select(field_idx, mean_ci_pct_change, interpretation),
tch_forecast = all_kpis$tch_forecasted %>%
select(field_idx, mean_ci, tch_forecasted, tch_lower_bound, tch_upper_bound, confidence),
growth_decline = all_kpis$growth_decline %>%
select(field_idx, four_week_trend, trend_interpretation, decline_severity),
weed_pressure = all_kpis$weed_presence %>%
select(field_idx, fragmentation_index, weed_pressure_risk),
gap_filling = if (!is.null(all_kpis$gap_filling)) {
all_kpis$gap_filling %>%
select(field_idx, gap_score, gap_level, mean_ci)
} else {
NULL
}
)
return(kpi_summary)
}
@ -531,7 +470,7 @@ create_summary_tables <- function(all_kpis) {
#' @param all_kpis List with all KPI results
#' @param field_boundaries_sf SF object with field boundaries
#'
#' @return Data frame with one row per field, all KPI columns (renamed for reporting compatibility)
#' @return Data frame with one row per field, all KPI columns
create_field_detail_table <- function(field_df, all_kpis, field_boundaries_sf) {
result <- field_df %>%
left_join(
@ -543,7 +482,7 @@ create_field_detail_table <- function(field_df, all_kpis, field_boundaries_sf) {
by = c("field_idx")
) %>%
left_join(
all_kpis$tch_forecasted %>% select(field_idx, tch_forecasted, mean_ci),
all_kpis$tch_forecasted %>% select(field_idx, tch_forecasted),
by = c("field_idx")
) %>%
left_join(
@ -553,26 +492,7 @@ create_field_detail_table <- function(field_df, all_kpis, field_boundaries_sf) {
left_join(
all_kpis$weed_presence %>% select(field_idx, weed_pressure_risk),
by = c("field_idx")
) %>%
# Rename columns to match reporting script expectations
rename(
Field = field_name,
`Growth Uniformity` = uniformity_interpretation,
`Yield Forecast (t/ha)` = tch_forecasted,
`Decline Risk` = decline_severity,
`Weed Risk` = weed_pressure_risk,
`CI Change %` = mean_ci_pct_change,
`Mean CI` = mean_ci,
`CV Value` = cv_value
) %>%
# Add placeholder columns expected by reporting script (will be populated from other sources)
mutate(
`Field Size (ha)` = NA_real_,
`Gap Score` = NA_real_
) %>%
select(field_idx, Field, `Field Size (ha)`, `Growth Uniformity`, `Yield Forecast (t/ha)`,
`Gap Score`, `Decline Risk`, `Weed Risk`, `CI Change %`, `Mean CI`, `CV Value`)
)
return(result)
}
@ -583,7 +503,7 @@ create_field_detail_table <- function(field_df, all_kpis, field_boundaries_sf) {
#' @return Character string with formatted KPI summary text
create_field_kpi_text <- function(all_kpis) {
text_parts <- c(
"## KPI ANALYSIS SUMMARY\n",
"## AURA KPI ANALYSIS SUMMARY\n",
"### Field Uniformity\n",
paste(all_kpis$uniformity$interpretation, collapse = "; "), "\n",
"### Growth Trends\n",
@ -597,69 +517,21 @@ create_field_kpi_text <- function(all_kpis) {
#' Export detailed KPI data to Excel/RDS
#'
#' @param all_kpis List with all KPI results (per-field data)
#' @param kpi_summary List with summary tables (farm-level aggregates)
#' @param project_dir Project name (for filename)
#' @param all_kpis List with all KPI results
#' @param kpi_summary List with summary tables
#' @param output_dir Directory for output files
#' @param week Week number
#' @param year Year
#' @param field_boundaries_sf SF object with field boundaries (optional, for field_details_table)
#'
#' @return List of output file paths
export_kpi_data <- function(all_kpis, kpi_summary, project_dir, output_dir, week, year, field_boundaries_sf = NULL) {
export_kpi_data <- function(all_kpis, kpi_summary, output_dir, week, year, project_dir) {
# Ensure output directory exists
if (!dir.exists(output_dir)) {
dir.create(output_dir, recursive = TRUE)
}
# Create unified field details table if field_boundaries_sf is provided
field_details_table <- NULL
if (!is.null(field_boundaries_sf)) {
tryCatch({
# Create a basic field_df from the boundaries
# Robust field name extraction with multiple fallbacks
field_name <- NA_character_
# Check for 'name' column in the data.frame
if ("name" %in% names(field_boundaries_sf)) {
field_name <- field_boundaries_sf$name
} else if ("properties" %in% names(field_boundaries_sf)) {
# Extract from properties column (may be a list-column)
props <- field_boundaries_sf$properties
if (is.list(props) && length(props) > 0 && "name" %in% names(props[[1]])) {
field_name <- sapply(props, function(x) ifelse(is.null(x$name), NA_character_, x$name))
} else if (!is.list(props)) {
# Try direct access if properties is a simple column
field_name <- props
}
}
# Ensure field_name is a character vector of appropriate length
if (length(field_name) != nrow(field_boundaries_sf)) {
field_name <- rep(NA_character_, nrow(field_boundaries_sf))
}
# Replace only NA elements with fallback names, keeping valid names intact
na_indices <- which(is.na(field_name))
if (length(na_indices) > 0) {
field_name[na_indices] <- paste0("Field_", na_indices)
}
field_df <- data.frame(
field_idx = 1:nrow(field_boundaries_sf),
field_name = field_name,
stringsAsFactors = FALSE
)
field_details_table <- create_field_detail_table(field_df, all_kpis, field_boundaries_sf)
message(paste("✓ Field details table created with", nrow(field_details_table), "fields"))
}, error = function(e) {
message(paste("WARNING: Could not create field_details_table:", e$message))
})
}
# Export all KPI tables to a single Excel file - use project_dir"
excel_file <- file.path(output_dir, paste0(project_dir, "_kpi_summary_tables_week", sprintf("%02d_%d", week, year), ".xlsx"))
# Export all KPI tables to a single Excel file
excel_file <- paste0(project_dir, "_field_analysis_week", sprintf("%02d_%d", week, year), ".xlsx")
excel_path <- file.path(output_dir, excel_file)
sheets <- list(
"Uniformity" = as.data.frame(kpi_summary$uniformity),
@ -670,40 +542,38 @@ export_kpi_data <- function(all_kpis, kpi_summary, project_dir, output_dir, week
"Gap_Filling" = as.data.frame(kpi_summary$gap_filling)
)
write_xlsx(sheets, excel_file)
message(paste("KPI data exported to:", excel_file))
write_xlsx(sheets, excel_path)
message(paste("AURA KPI data exported to:", excel_path))
# Export to RDS for programmatic access (CRITICAL: Both per-field AND summary tables)
# The reporting script expects: summary_tables (list of 6 summary tables)
# We also provide: all_kpis (per-field data) and field_details (unified field view)
rds_file <- file.path(output_dir, paste0(project_dir, "_kpi_summary_tables_week", sprintf("%02d_%d", week, year), ".rds"))
# Create the export structure that reporting scripts expect
export_data <- list(
summary_tables = kpi_summary, # Farm-level aggregates (6 KPI summaries)
all_kpis = all_kpis, # Per-field data (6 KPI per-field tables)
field_details = field_details_table # Unified field-level detail table
# Also export to RDS for programmatic access
rds_file <- paste0(project_dir, "_field_analysis_week", sprintf("%02d_%d", week, year), ".rds")
rds_path <- file.path(output_dir, rds_file)
# Save complete structure including metadata
kpi_export_data <- list(
kpis = all_kpis,
summary_tables = kpi_summary,
metadata = list(
week = week,
year = year,
project = project_dir,
created_at = Sys.time()
)
)
saveRDS(export_data, rds_file)
message(paste("✓ KPI RDS exported to:", rds_file))
message(" Structure: list($summary_tables, $all_kpis, $field_details)")
saveRDS(kpi_export_data, rds_path)
message(paste("✓ AURA KPI RDS exported to:", rds_path))
# Return including field_details for orchestrator to capture
return(list(
excel = excel_file,
rds = rds_file,
field_details = field_details_table
))
return(list(excel = excel_path, rds = rds_path))
}
# ============================================================================
# ORCHESTRATOR FUNCTION
# ============================================================================
#' Calculate all 6 KPIs
#' Calculate all 6 AURA KPIs
#'
#' Main entry point for KPI calculation.
#' Main entry point for AURA KPI calculation.
#' This function orchestrates the 6 KPI calculations and returns all results.
#'
#' @param field_boundaries_sf SF object with field geometries
@ -714,7 +584,6 @@ export_kpi_data <- function(all_kpis, kpi_summary, project_dir, output_dir, week
#' @param ci_rds_path Path to combined CI RDS file
#' @param harvesting_data Data frame with harvest data (optional)
#' @param output_dir Directory for KPI exports
#' @param project_dir Project name (for filename in exports)
#'
#' @return List with results from all 6 KPI functions
#'
@ -722,11 +591,11 @@ export_kpi_data <- function(all_kpis, kpi_summary, project_dir, output_dir, week
#' This function:
#' 1. Loads current week mosaic and extracts field statistics
#' 2. (Optionally) loads previous week mosaic for comparison metrics
#' 3. Calculates all 6 KPIs
#' 3. Calculates all 6 AURA KPIs
#' 4. Creates summary tables
#' 5. Exports results to Excel/RDS
#'
calculate_all_kpis <- function(
calculate_all_field_analysis_agronomic_support <- function(
field_boundaries_sf,
current_week,
current_year,
@ -738,7 +607,7 @@ calculate_all_kpis <- function(
project_dir = NULL
) {
message("\n============ KPI CALCULATION (6 KPIs) ============")
message("\n============ AURA KPI CALCULATION (6 KPIs) ============")
# Load current week mosaic
message("Loading current week mosaic...")
@ -751,7 +620,12 @@ calculate_all_kpis <- function(
# Extract field statistics
message("Extracting field statistics from current mosaic...")
current_stats <- extract_field_statistics_from_ci(current_mosaic, field_boundaries_sf)
ci_pixels_by_field <- extract_ci_values(current_mosaic, field_boundaries_sf)
#Extract CI pixels for each field individually
ci_pixels_by_field <- list()
for (i in seq_len(nrow(field_boundaries_sf))) {
field_vect <- terra::vect(field_boundaries_sf[i, ])
ci_pixels_by_field[[i]] <- extract_ci_values(current_mosaic, field_vect)
}
# Load previous week mosaic (if available)
previous_stats <- NULL
@ -787,14 +661,19 @@ calculate_all_kpis <- function(
message("Calculating KPI 4: Growth Decline...")
growth_decline_kpi <- calculate_growth_decline_kpi(
ci_pixels_by_field # Would need historical data for real trend
list(ci_pixels_by_field) # Would need historical data for real trend
)
message("Calculating KPI 5: Weed Presence...")
weed_kpi <- calculate_weed_presence_kpi(ci_pixels_by_field)
message("Calculating KPI 6: Gap Filling...")
gap_filling_kpi <- calculate_gap_filling_kpi(current_mosaic, field_boundaries_sf)
gap_filling_result <- calculate_gap_filling_kpi(current_mosaic, field_boundaries_sf)
# Add field_idx to gap filling results
gap_filling_kpi <- gap_filling_result$field_results %>%
mutate(field_idx = row_number()) %>%
select(field_idx, gap_score, gap_level, mean_ci, outlier_threshold)
# Compile results
all_kpis <- list(
@ -807,21 +686,21 @@ calculate_all_kpis <- function(
)
# Create summary tables
message("\nCreating summary tables...")
kpi_summary <- create_summary_tables(all_kpis)
# Export - pass project_dir for proper filename and field_boundaries_sf for field details table
if (is.null(project_dir)) {
project_dir <- "AURA" # Fallback if not provided
}
export_result <- export_kpi_data(all_kpis, kpi_summary, project_dir, output_dir, current_week, current_year, field_boundaries_sf)
# Export
message("\nExporting KPI data...")
export_paths <- export_kpi_data(all_kpis, kpi_summary, output_dir, current_week, current_year, project_dir)
message(paste("\n✓", project_dir, "KPI calculation complete. Week", current_week, current_year, "\n"))
message(paste("\n✓ AURA KPI calculation complete. Week", current_week, current_year))
# Return combined structure (for integration with 80_calculate_kpis.R)
# Capture field_details from export_result to propagate it out
return(list(
all_kpis = all_kpis,
kpis = all_kpis,
summary_tables = kpi_summary,
field_details = export_result$field_details # Propagate field_details from export_kpi_data
))
metadata = list(
week = current_week,
year = current_year,
project = project_dir,
export_paths = export_paths) ))
}