SmartCane/r_app/crop_messaging_utils.R
Timon d5fd4bb463 Add KPI reporting system and deployment documentation
Major Changes:
- NEW: Scripts 09 & 10 for KPI calculation and enhanced reporting
- NEW: Shell script wrappers (01-10) for easier execution
- NEW: R packages flextable and officer for enhanced Word reports
- NEW: DEPLOYMENT_README.md with complete deployment guide
- RENAMED: Numbered R scripts (02, 03, 04) for clarity
- REMOVED: Old package management scripts (using renv only)
- UPDATED: Workflow now uses scripts 09->10 instead of 05

Files Changed: 90+ files
New Packages: flextable, officer
New Scripts: 09_run_calculate_kpis.sh, 10_run_kpi_report.sh
Documentation: DEPLOYMENT_README.md, EMAIL_TO_ADMIN.txt

See DEPLOYMENT_README.md for full deployment instructions.
2025-10-14 11:49:30 +02:00

1910 lines
81 KiB
R
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# CROP_MESSAGING_UTILS.R
# ======================
# Utility functions for the SmartCane crop messaging workflow.
# These functions support crop analysis, messaging, and output generation.
#' Convert hectares to acres
#' @param hectares Numeric value in hectares
#' @return Numeric value in acres
hectares_to_acres <- function(hectares) {
return(hectares * 2.47105)
}
#' Format area with both hectares and acres
#' @param hectares Numeric value in hectares
#' @param precision Number of decimal places (default 1)
#' @return Character string with both measurements
format_area_both <- function(hectares, precision = 1) {
acres <- hectares_to_acres(hectares)
return(sprintf("%.1f ha (%.0f acres)", hectares, acres))
}
#' @param message The message to log
#' @param level The log level (default: "INFO")
#' @return NULL (used for side effects)
#'
safe_log <- function(message, level = "INFO") {
if (exists("log_message")) {
log_message(message, level)
} else {
if (level %in% c("ERROR", "WARNING")) {
warning(message)
} else {
message(message)
}
}
}
# 2. Analysis configuration
# -----------------------
# Thresholds for change detection
CI_CHANGE_INCREASE_THRESHOLD <- 0.5
CI_CHANGE_DECREASE_THRESHOLD <- -0.5
# Thresholds for field uniformity (coefficient of variation as decimal)
UNIFORMITY_THRESHOLD <- 0.15 # Below this = good uniformity, above = requires attention
EXCELLENT_UNIFORMITY_THRESHOLD <- 0.08 # Below this = excellent uniformity
POOR_UNIFORMITY_THRESHOLD <- 0.25 # Above this = poor uniformity, urgent attention needed
# Thresholds for spatial clustering (adjusted for agricultural fields)
# Agricultural fields naturally have spatial autocorrelation, so higher thresholds are needed
MORAN_THRESHOLD_HIGH <- 0.95 # Above this = very strong clustering (problematic patterns)
MORAN_THRESHOLD_MODERATE <- 0.85 # Above this = moderate clustering
MORAN_THRESHOLD_LOW <- 0.7 # Above this = normal field continuity
# Threshold for acceptable area percentage
ACCEPTABLE_AREA_THRESHOLD <- 40 # Below this percentage = management issue
#' Calculate uniformity metrics using terra statistics (optimized)
#' @param mean_val Mean CI value from terra
#' @param sd_val Standard deviation from terra
#' @param median_val Median CI value from terra
#' @param min_val Minimum CI value from terra
#' @param max_val Maximum CI value from terra
#' @param values Raw values for quantile calculations only
#' @return List with various uniformity metrics (all scaled to be comparable)
calculate_uniformity_metrics_terra <- function(mean_val, sd_val, median_val, min_val, max_val, values) {
if (is.na(mean_val) || length(values) < 2) return(list(
cv = NA, iqr_cv = NA, range_cv = NA,
mad_cv = NA, percentile_cv = NA, interpretation = "insufficient_data"
))
# 1. Coefficient of variation (from terra) - already normalized
cv <- sd_val / mean_val
# 2. IQR-based CV (IQR/median) - using R's built-in IQR function
iqr_val <- IQR(values, na.rm = TRUE)
iqr_cv <- iqr_val / median_val
# 3. Range-based CV (range/mean) - using terra min/max
range_val <- max_val - min_val
range_cv <- range_val / mean_val
# 4. MAD-based CV (MAD/median) - using R's built-in mad function
mad_val <- mad(values, constant = 1.4826, na.rm = TRUE) # scaled to match SD for normal distribution
mad_cv <- mad_val / median_val
# 5. Percentile-based CV (P90-P10)/mean - using R's built-in quantile
percentiles <- quantile(values, c(0.1, 0.9), na.rm = TRUE)
percentile_cv <- (percentiles[2] - percentiles[1]) / mean_val
# Interpretation based on CV thresholds (all metrics now comparable)
# CV < 0.15 = Very uniform, 0.15-0.30 = Moderate variation, 0.30-0.50 = High variation, >0.50 = Very high variation
interpret_uniformity <- function(metric_value) {
if (is.na(metric_value)) return("unknown")
if (metric_value < 0.15) return("very uniform")
if (metric_value < 0.30) return("moderate variation")
if (metric_value < 0.50) return("high variation")
return("very high variation")
}
return(list(
cv = cv,
iqr_cv = iqr_cv,
range_cv = range_cv,
mad_cv = mad_cv,
percentile_cv = percentile_cv,
cv_interpretation = interpret_uniformity(cv),
iqr_interpretation = interpret_uniformity(iqr_cv),
mad_interpretation = interpret_uniformity(mad_cv),
percentile_interpretation = interpret_uniformity(percentile_cv)
))
}
#' Calculate percentage within acceptable range using terra mean
#' Acceptable range = within 25% of the field mean CI value
#' This indicates what percentage of the field has "normal" performance
#' @param mean_val Mean CI value from terra
#' @param values Raw CI values
#' @param threshold_factor Factor to multiply mean by for acceptable range (default 0.25 = 25%)
#' @return Percentage of values within acceptable range
calculate_acceptable_percentage_terra <- function(mean_val, values, threshold_factor = 0.25) {
values <- values[!is.na(values) & is.finite(values)]
if (length(values) < 2 || is.na(mean_val)) return(NA)
threshold <- mean_val * threshold_factor # 25% of mean as default
within_range <- abs(values - mean_val) <= threshold
percentage <- (sum(within_range) / length(values)) * 100
return(percentage)
}
#' Calculate coefficient of variation for uniformity assessment
#' @param values Numeric vector of CI values
#' @return Coefficient of variation (CV) as decimal
calculate_cv <- function(values) {
values <- values[!is.na(values) & is.finite(values)]
if (length(values) < 2) return(NA)
cv <- sd(values) / mean(values) # Keep as decimal
return(cv)
}
#' Calculate Shannon entropy for spatial heterogeneity assessment
#' Higher entropy = more heterogeneous/variable field
#' Lower entropy = more homogeneous/uniform field
#' @param values Numeric vector of CI values
#' @param n_bins Number of bins for histogram (default 10)
#' @return Shannon entropy value
calculate_entropy <- function(values, n_bins = 10) {
values <- values[!is.na(values) & is.finite(values)]
if (length(values) < 2) return(NA)
# Create histogram bins
value_range <- range(values)
breaks <- seq(value_range[1], value_range[2], length.out = n_bins + 1)
# Count values in each bin
bin_counts <- hist(values, breaks = breaks, plot = FALSE)$counts
# Calculate probabilities (remove zero counts)
probabilities <- bin_counts[bin_counts > 0] / sum(bin_counts)
# Calculate Shannon entropy: H = -sum(p * log(p))
entropy <- -sum(probabilities * log(probabilities))
return(entropy)
}
#' Calculate percentage of field with positive vs negative change
#' @param current_values Current week CI values
#' @param previous_values Previous week CI values
#' @return List with percentage of positive and negative change areas
calculate_change_percentages <- function(current_values, previous_values) {
# Ensure same length (should be from same field boundaries)
if (length(current_values) != length(previous_values)) {
return(list(positive_pct = NA, negative_pct = NA, stable_pct = NA))
}
# Calculate pixel-wise change
change_values <- current_values - previous_values
valid_changes <- change_values[!is.na(change_values) & is.finite(change_values)]
if (length(valid_changes) < 2) {
return(list(positive_pct = NA, negative_pct = NA, stable_pct = NA))
}
# Count positive, negative, and stable areas
positive_pct <- sum(valid_changes > 0) / length(valid_changes) * 100
negative_pct <- sum(valid_changes < 0) / length(valid_changes) * 100
stable_pct <- sum(valid_changes == 0) / length(valid_changes) * 100
return(list(
positive_pct = positive_pct,
negative_pct = negative_pct,
stable_pct = stable_pct
))
}
#' Calculate spatial autocorrelation (Moran's I) for a field
#' @param ci_raster Terra raster of CI values
#' @param field_boundary Terra vector of field boundary
#' @return List with Moran's I statistic and p-value
calculate_spatial_autocorrelation <- function(ci_raster, field_boundary) {
tryCatch({
# Crop and mask raster to field boundary
field_raster <- terra::crop(ci_raster, field_boundary)
field_raster <- terra::mask(field_raster, field_boundary)
# Convert to points for spatial analysis
raster_points <- terra::as.points(field_raster, na.rm = TRUE)
# Check if we have enough points
if (length(raster_points) < 10) {
return(list(morans_i = NA, p_value = NA, interpretation = "insufficient_data"))
}
# Convert to sf for spdep
points_sf <- sf::st_as_sf(raster_points)
# Create spatial weights matrix (k-nearest neighbors)
coords <- sf::st_coordinates(points_sf)
# Use adaptive number of neighbors based on sample size
k_neighbors <- min(8, max(4, floor(nrow(coords) / 10)))
knn_nb <- spdep::knearneigh(coords, k = k_neighbors)
knn_listw <- spdep::nb2listw(spdep::knn2nb(knn_nb), style = "W", zero.policy = TRUE)
# Calculate Moran's I
ci_values <- points_sf[[1]] # First column contains CI values
moran_result <- spdep::moran.test(ci_values, knn_listw, zero.policy = TRUE)
# Interpret results
morans_i <- moran_result$estimate[1]
p_value <- moran_result$p.value
interpretation <- if (is.na(morans_i)) {
"insufficient_data"
} else if (p_value > 0.05) {
"random" # Not significant spatial pattern
} else if (morans_i > MORAN_THRESHOLD_HIGH) {
"very_strong_clustering" # Very strong clustering - may indicate management issues
} else if (morans_i > MORAN_THRESHOLD_MODERATE) {
"strong_clustering" # Strong clustering - worth monitoring
} else if (morans_i > MORAN_THRESHOLD_LOW) {
"normal_continuity" # Normal field continuity - expected for uniform fields
} else if (morans_i > 0.3) {
"weak_clustering" # Some clustering present
} else if (morans_i < -0.3) {
"dispersed" # Checkerboard pattern
} else {
"low_autocorrelation" # Low spatial autocorrelation
}
return(list(
morans_i = morans_i,
p_value = p_value,
interpretation = interpretation
))
}, error = function(e) {
warning(paste("Error calculating spatial autocorrelation:", e$message))
return(list(morans_i = NA, p_value = NA, interpretation = "error"))
})
}
#' Calculate percentage of field in extreme values using simple threshold
#' Hotspots = areas with CI > mean + 1.5*SD (high-performing areas)
#' Coldspots = areas with CI < mean - 1.5*SD (underperforming areas)
#' @param values Numeric vector of CI values
#' @param threshold_multiplier Standard deviation multiplier (default 1.5)
#' @return List with percentage of hotspots and coldspots
calculate_extreme_percentages_simple <- function(values, threshold_multiplier = 1.5) {
if (length(values) < 10) return(list(hotspot_pct = NA, coldspot_pct = NA, method = "insufficient_data"))
mean_val <- mean(values, na.rm = TRUE)
sd_val <- sd(values, na.rm = TRUE)
# Hotspots: significantly ABOVE average (good performance)
upper_threshold <- mean_val + (threshold_multiplier * sd_val)
# Coldspots: significantly BELOW average (poor performance)
lower_threshold <- mean_val - (threshold_multiplier * sd_val)
hotspot_pct <- sum(values > upper_threshold, na.rm = TRUE) / length(values) * 100
coldspot_pct <- sum(values < lower_threshold, na.rm = TRUE) / length(values) * 100
return(list(
hotspot_pct = hotspot_pct,
coldspot_pct = coldspot_pct,
method = "simple_threshold",
threshold_used = threshold_multiplier
))
}
#' Categorize CI change based on thresholds
#' @param change_value Mean change in CI between weeks
#' @return Character string: "increase", "stable", or "decrease"
categorize_change <- function(change_value) {
if (is.na(change_value)) return("unknown")
if (change_value >= CI_CHANGE_INCREASE_THRESHOLD) return("increase")
if (change_value <= CI_CHANGE_DECREASE_THRESHOLD) return("decrease")
return("stable")
}
#' Categorize field uniformity based on coefficient of variation and spatial pattern
#' @param cv_value Coefficient of variation (primary uniformity metric)
#' @param spatial_info List with spatial autocorrelation results
#' @param extreme_pct List with hotspot/coldspot percentages
#' @param acceptable_pct Percentage of field within acceptable range
#' @return Character string describing field uniformity pattern
categorize_uniformity_enhanced <- function(cv_value, spatial_info, extreme_pct, acceptable_pct = NA) {
if (is.na(cv_value)) return("unknown variation")
# Check for poor uniformity first (urgent issues)
if (cv_value > POOR_UNIFORMITY_THRESHOLD || (!is.na(acceptable_pct) && acceptable_pct < ACCEPTABLE_AREA_THRESHOLD)) {
return("poor uniformity - urgent attention needed")
}
# Check for excellent uniformity
if (cv_value <= EXCELLENT_UNIFORMITY_THRESHOLD && (!is.na(acceptable_pct) && acceptable_pct >= 45)) {
return("excellent uniformity")
}
# Check for good uniformity
if (cv_value <= UNIFORMITY_THRESHOLD) {
return("good uniformity")
}
# Field has moderate variation - determine if localized or distributed
spatial_pattern <- spatial_info$interpretation
hotspot_pct <- extreme_pct$hotspot_pct
coldspot_pct <- extreme_pct$coldspot_pct
# Determine pattern type based on CV (primary) and spatial pattern (secondary)
if (spatial_pattern %in% c("very_strong_clustering") && !is.na(hotspot_pct) && (hotspot_pct > 15 || coldspot_pct > 5)) {
# Very strong clustering with substantial extreme areas - likely problematic
if (hotspot_pct > coldspot_pct) {
return("localized high-performing areas")
} else if (coldspot_pct > hotspot_pct) {
return("localized problem areas")
} else {
return("localized hotspots and coldspots")
}
} else if (spatial_pattern %in% c("strong_clustering") && !is.na(hotspot_pct) && (hotspot_pct > 10 || coldspot_pct > 3)) {
# Strong clustering with moderate extreme areas
if (hotspot_pct > coldspot_pct) {
return("localized high-performing areas")
} else if (coldspot_pct > hotspot_pct) {
return("localized problem areas")
} else {
return("clustered variation")
}
} else {
# Normal field continuity or weak patterns - rely primarily on CV
return("moderate variation")
}
}
#' Generate enhanced message based on analysis results including spatial patterns
#' @param uniformity_category Character: enhanced uniformity category with spatial info
#' @param change_category Character: "increase", "stable", or "decrease"
#' @param extreme_pct List with hotspot/coldspot percentages
#' @param acceptable_pct Percentage of field within acceptable range
#' @param morans_i Moran's I value for additional context
#' @param growth_stage Character: growth stage (simplified for now)
#' @return List with message and worth_sending flag
generate_enhanced_message <- function(uniformity_category, change_category, extreme_pct, acceptable_pct = NA, morans_i = NA, growth_stage = "vegetation stage") {
# Enhanced message matrix based on spatial patterns
messages <- list()
# Poor uniformity scenarios (urgent)
if (uniformity_category == "poor uniformity - urgent attention needed") {
messages <- list(
"stable" = list(
message = "🚨 URGENT: Poor field uniformity detected - immediate management review required",
worth_sending = TRUE
),
"decrease" = list(
message = "🚨 CRITICAL: Poor uniformity with declining trend - emergency intervention needed",
worth_sending = TRUE
),
"increase" = list(
message = "⚠️ CAUTION: Improving but still poor uniformity - continue intensive monitoring",
worth_sending = TRUE
)
)
}
# Excellent uniformity scenarios
else if (uniformity_category == "excellent uniformity") {
messages <- list(
"stable" = list(
message = "✅ Excellent: Optimal field uniformity and stability",
worth_sending = FALSE
),
"decrease" = list(
message = "⚠️ Alert: Excellent uniformity but declining - investigate cause early",
worth_sending = TRUE
),
"increase" = list(
message = "🌟 Outstanding: Excellent uniformity with continued improvement",
worth_sending = FALSE
)
)
}
# Good uniformity scenarios
else if (uniformity_category == "good uniformity") {
# Check for very strong clustering which may indicate management issues
if (!is.na(morans_i) && morans_i > MORAN_THRESHOLD_HIGH) {
messages <- list(
"stable" = list(
message = "⚠️ Alert: Good uniformity but very strong clustering detected - check management practices",
worth_sending = TRUE
),
"decrease" = list(
message = "🚨 Alert: Good uniformity declining with clustering patterns - targeted intervention needed",
worth_sending = TRUE
),
"increase" = list(
message = "✅ Good: Improving uniformity but monitor clustering patterns",
worth_sending = FALSE
)
)
} else {
messages <- list(
"stable" = list(
message = "✅ Good: Stable field with good uniformity",
worth_sending = FALSE
),
"decrease" = list(
message = "⚠️ Alert: Good uniformity but declining trend - early intervention recommended",
worth_sending = TRUE
),
"increase" = list(
message = "✅ Great: Good uniformity with improvement trend",
worth_sending = FALSE
)
)
}
}
# Moderate variation scenarios
else if (uniformity_category == "moderate variation") {
acceptable_msg <- if (!is.na(acceptable_pct) && acceptable_pct < 45) " - low acceptable area" else ""
messages <- list(
"stable" = list(
message = paste0("⚠️ Alert: Moderate field variation detected", acceptable_msg, " - review management uniformity"),
worth_sending = TRUE
),
"decrease" = list(
message = paste0("🚨 Alert: Moderate variation with declining trend", acceptable_msg, " - intervention needed"),
worth_sending = TRUE
),
"increase" = list(
message = paste0("📈 Monitor: Improving but still moderate variation", acceptable_msg, " - continue optimization"),
worth_sending = FALSE
)
)
}
# Localized problem areas
else if (uniformity_category == "localized problem areas") {
hotspot_pct <- round(extreme_pct$hotspot_pct, 1)
coldspot_pct <- round(extreme_pct$coldspot_pct, 1)
messages <- list(
"stable" = list(
message = paste0("🚨 Alert: Problem zones detected (", coldspot_pct, "% underperforming) - targeted intervention needed"),
worth_sending = TRUE
),
"decrease" = list(
message = paste0("🚨 URGENT: Problem areas expanding with overall decline (", coldspot_pct, "% affected) - immediate action required"),
worth_sending = TRUE
),
"increase" = list(
message = paste0("⚠️ Caution: Overall improvement but ", coldspot_pct, "% problem areas remain - monitor closely"),
worth_sending = TRUE
)
)
}
# Localized high-performing areas
else if (uniformity_category == "localized high-performing areas") {
hotspot_pct <- round(extreme_pct$hotspot_pct, 1)
messages <- list(
"stable" = list(
message = paste0("💡 Opportunity: ", hotspot_pct, "% of field performing well - replicate conditions in remaining areas"),
worth_sending = FALSE
),
"decrease" = list(
message = paste0("⚠️ Alert: High-performing areas (", hotspot_pct, "%) declining - investigate cause to prevent spread"),
worth_sending = TRUE
),
"increase" = list(
message = paste0("🌟 Excellent: High-performing areas (", hotspot_pct, "%) expanding - excellent management practices"),
worth_sending = FALSE
)
)
}
# Clustered variation (general)
else if (uniformity_category == "clustered variation") {
messages <- list(
"stable" = list(
message = "⚠️ Alert: Clustered variation detected - investigate spatial management patterns",
worth_sending = TRUE
),
"decrease" = list(
message = "🚨 Alert: Clustered decline pattern - targeted investigation needed",
worth_sending = TRUE
),
"increase" = list(
message = "📈 Monitor: Clustered improvement - identify and replicate successful practices",
worth_sending = FALSE
)
)
}
# Default fallback
else {
messages <- list(
"stable" = list(message = "❓ Field analysis inconclusive - manual review recommended", worth_sending = FALSE),
"decrease" = list(message = "⚠️ Field showing decline - investigation recommended", worth_sending = TRUE),
"increase" = list(message = "📈 Field showing improvement", worth_sending = FALSE)
)
}
# Return appropriate message
if (change_category %in% names(messages)) {
return(messages[[change_category]])
} else {
return(list(
message = paste("❓ Analysis inconclusive -", uniformity_category, "with", change_category, "trend"),
worth_sending = FALSE
))
}
}
#' Load and analyze a weekly mosaic for individual fields with spatial analysis
#' @param week_file_path Path to the weekly mosaic file
#' @param field_boundaries_sf SF object with field boundaries
#' @return List with CI statistics per field including spatial metrics
analyze_weekly_mosaic <- function(week_file_path, field_boundaries_sf) {
if (!file.exists(week_file_path)) {
warning(paste("Mosaic file not found:", week_file_path))
return(NULL)
}
tryCatch({
# Load the raster and select only the CI band (5th band)
mosaic_raster <- terra::rast(week_file_path)
ci_raster <- mosaic_raster[[5]] # Select the CI band
names(ci_raster) <- "CI"
# Convert field boundaries to terra vect for extraction
field_boundaries_vect <- terra::vect(field_boundaries_sf)
# Extract CI values for each field
field_results <- list()
for (i in seq_len(nrow(field_boundaries_sf))) {
field_name <- field_boundaries_sf$field[i]
sub_field_name <- field_boundaries_sf$sub_field[i]
# Check and get field area from geojson if available
field_area_ha <- NA
if ("area_ha" %in% colnames(field_boundaries_sf)) {
field_area_ha <- field_boundaries_sf$area_ha[i]
} else if ("AREA_HA" %in% colnames(field_boundaries_sf)) {
field_area_ha <- field_boundaries_sf$AREA_HA[i]
} else if ("area" %in% colnames(field_boundaries_sf)) {
field_area_ha <- field_boundaries_sf$area[i]
} else {
# Calculate area from geometry as fallback
field_geom <- field_boundaries_sf[i,]
if (sf::st_is_longlat(field_geom)) {
# For geographic coordinates, transform to projected for area calculation
field_geom <- sf::st_transform(field_geom, 3857) # Web Mercator
}
field_area_ha <- as.numeric(sf::st_area(field_geom)) / 10000 # Convert to hectares
}
cat("Processing field:", field_name, "-", sub_field_name, "(", round(field_area_ha, 1), "ha)\n")
# Extract values for this specific field
field_vect <- field_boundaries_vect[i]
# Extract with built-in statistics from terra (PRIMARY METHOD)
terra_stats <- terra::extract(ci_raster, field_vect, fun = c("mean", "sd", "min", "max", "median"), na.rm = TRUE)
# Extract raw values for additional calculations and validation
ci_values <- terra::extract(ci_raster, field_vect, fun = NULL)
# Flatten and clean the values
field_values <- unlist(ci_values)
valid_values <- field_values[!is.na(field_values) & is.finite(field_values)]
if (length(valid_values) > 0) {
# Use TERRA as primary calculations
primary_mean <- terra_stats$mean[1]
primary_sd <- terra_stats$sd[1]
primary_cv <- primary_sd / primary_mean
primary_median <- terra_stats$median[1]
primary_min <- terra_stats$min[1]
primary_max <- terra_stats$max[1]
# Manual calculations for validation only
manual_mean <- mean(valid_values)
manual_cv <- sd(valid_values) / manual_mean
basic_stats <- list(
field = field_name,
sub_field = sub_field_name,
# PRIMARY statistics (terra-based)
mean_ci = primary_mean,
median_ci = primary_median,
sd_ci = primary_sd,
cv = primary_cv,
min_ci = primary_min,
max_ci = primary_max,
# Store raw values for change analysis
raw_values = valid_values,
# Other metrics using terra values
acceptable_pct = calculate_acceptable_percentage_terra(primary_mean, valid_values),
n_pixels = length(valid_values),
# Field area from geojson
field_area_ha = field_area_ha
)
# Calculate spatial statistics
spatial_info <- calculate_spatial_autocorrelation(ci_raster, field_vect)
extreme_pct <- calculate_extreme_percentages_simple(valid_values)
# Calculate entropy for additional uniformity measure
entropy_value <- calculate_entropy(valid_values)
# Enhanced uniformity categorization
uniformity_category <- categorize_uniformity_enhanced(
basic_stats$cv,
spatial_info,
extreme_pct,
basic_stats$acceptable_pct
)
# Combine all results
field_stats <- c(
basic_stats,
list(
spatial_autocorr = spatial_info,
extreme_percentages = extreme_pct,
entropy = entropy_value,
uniformity_category = uniformity_category
)
)
field_results[[paste0(field_name, "_", sub_field_name)]] <- field_stats
} else {
warning(paste("No valid CI values found for field:", field_name, sub_field_name))
}
}
return(field_results)
}, error = function(e) {
warning(paste("Error analyzing mosaic:", e$message))
return(NULL)
})
}
#' Run crop analysis for any estate
#' @param estate_name Character: name of the estate (e.g., "simba", "chemba")
#' @param current_week Numeric: current week number
#' @param previous_week Numeric: previous week number
#' @param year Numeric: year (default 2025)
#' @return List with analysis results
run_estate_analysis <- function(estate_name, current_week, previous_week, year = 2025) {
cat("=== CROP ANALYSIS MESSAGING SYSTEM ===\n")
cat("Analyzing:", toupper(estate_name), "estate\n")
cat("Comparing week", previous_week, "vs week", current_week, "of", year, "\n\n")
# Set project_dir globally for parameters_project.R
assign("project_dir", estate_name, envir = .GlobalEnv)
# Load project configuration
tryCatch({
source("parameters_project.R")
cat("✓ Project configuration loaded\n")
}, error = function(e) {
tryCatch({
source(here::here("r_app", "parameters_project.R"))
cat("✓ Project configuration loaded from r_app directory\n")
}, error = function(e) {
stop("Failed to load project configuration")
})
})
# Verify required variables are available
if (!exists("weekly_CI_mosaic") || !exists("field_boundaries_sf")) {
stop("Required project variables not initialized. Check project configuration.")
}
# Construct file paths for weekly mosaics
current_week_file <- sprintf("week_%02d_%d.tif", current_week, year)
previous_week_file <- sprintf("week_%02d_%d.tif", previous_week, year)
current_week_path <- file.path(weekly_CI_mosaic, current_week_file)
previous_week_path <- file.path(weekly_CI_mosaic, previous_week_file)
cat("Looking for files:\n")
cat("- Current week:", current_week_path, "\n")
cat("- Previous week:", previous_week_path, "\n\n")
# Check if files exist and handle missing data scenarios
current_exists <- file.exists(current_week_path)
previous_exists <- file.exists(previous_week_path)
if (!current_exists) {
cat("❌ Current week mosaic not found. No analysis possible.\n")
return(NULL)
}
# Analyze both weeks for all fields
cat("Analyzing weekly mosaics per field...\n")
current_field_stats <- analyze_weekly_mosaic(current_week_path, field_boundaries_sf)
if (!previous_exists) {
cat("⚠️ Previous week mosaic not found (likely due to clouds). Performing spatial-only analysis.\n")
previous_field_stats <- NULL
} else {
previous_field_stats <- analyze_weekly_mosaic(previous_week_path, field_boundaries_sf)
}
if (is.null(current_field_stats)) {
stop("Could not analyze current weekly mosaic")
}
# Generate field results
field_results <- generate_field_results(current_field_stats, previous_field_stats, current_week, previous_week)
return(list(
estate_name = estate_name,
current_week = current_week,
previous_week = previous_week,
year = year,
field_results = field_results,
current_field_stats = current_field_stats,
previous_field_stats = previous_field_stats
))
}
#' Generate analysis results for all fields
#' @param current_field_stats Analysis results for current week
#' @param previous_field_stats Analysis results for previous week
#' @param current_week Current week number
#' @param previous_week Previous week number
#' @return List with field results
generate_field_results <- function(current_field_stats, previous_field_stats, current_week, previous_week) {
field_results <- list()
# Get common field names between both weeks (or all current fields if previous is missing)
if (!is.null(previous_field_stats)) {
common_fields <- intersect(names(current_field_stats), names(previous_field_stats))
} else {
common_fields <- names(current_field_stats)
}
for (field_id in common_fields) {
current_field <- current_field_stats[[field_id]]
previous_field <- if (!is.null(previous_field_stats)) previous_field_stats[[field_id]] else NULL
# Calculate change metrics for this field (only if previous data exists)
if (!is.null(previous_field)) {
ci_change <- current_field$mean_ci - previous_field$mean_ci
change_category <- categorize_change(ci_change)
# Calculate spatial change percentages
change_percentages <- calculate_change_percentages(
current_field$raw_values,
previous_field$raw_values
)
} else {
# No previous data - spatial analysis only
ci_change <- NA
change_category <- "spatial_only"
change_percentages <- list(positive_pct = NA, negative_pct = NA, stable_pct = NA)
}
# Use enhanced uniformity category from current week analysis
uniformity_category <- current_field$uniformity_category
# Generate enhanced message for this field
message_result <- generate_enhanced_message(
uniformity_category,
change_category,
current_field$extreme_percentages,
current_field$acceptable_pct,
current_field$spatial_autocorr$morans_i
)
# Store results
field_results[[field_id]] <- list(
current_stats = current_field,
previous_stats = previous_field,
ci_change = ci_change,
change_category = change_category,
change_percentages = change_percentages,
uniformity_category = uniformity_category,
message_result = message_result
)
}
return(field_results)
}
#' Format analysis results for WhatsApp/Word copy-paste
#' @param analysis_results Results from run_estate_analysis
#' @return Character string with formatted text
format_for_whatsapp <- function(analysis_results) {
field_results <- analysis_results$field_results
estate_name <- toupper(analysis_results$estate_name)
current_week <- analysis_results$current_week
previous_week <- analysis_results$previous_week
output <- c()
output <- c(output, paste("🌾", estate_name, "CROP ANALYSIS"))
output <- c(output, paste("📅 Week", current_week, "vs Week", previous_week))
output <- c(output, "")
# Summary statistics
alert_count <- sum(sapply(field_results, function(x) x$message_result$worth_sending))
total_fields <- length(field_results)
# Calculate total area and area statistics
total_hectares <- sum(sapply(field_results, function(x) x$current_stats$field_area_ha), na.rm = TRUE)
output <- c(output, "📊 SUMMARY:")
output <- c(output, paste("• Estate:", estate_name))
output <- c(output, paste("• Fields analyzed:", total_fields))
output <- c(output, paste("• Total area:", format_area_both(total_hectares)))
output <- c(output, paste("• Alerts needed:", alert_count))
output <- c(output, "")
# Field-by-field alerts only
if (alert_count > 0) {
output <- c(output, "🚨 PRIORITY FIELDS:")
for (field_id in names(field_results)) {
field_info <- field_results[[field_id]]
if (field_info$message_result$worth_sending) {
field_name <- paste(field_info$current_stats$field, field_info$current_stats$sub_field, sep="-")
area <- field_info$current_stats$field_area_ha
message <- field_info$message_result$message
output <- c(output, paste("•", field_name, paste0("(", format_area_both(area), "):"), message))
}
}
} else {
output <- c(output, "✅ No urgent alerts - all fields stable")
}
# Quick farm summary
output <- c(output, "")
output <- c(output, "📈 QUICK STATS:")
# Calculate improving vs declining areas (only if temporal data available)
has_temporal_data <- any(sapply(field_results, function(x) !is.na(x$change_percentages$positive_pct)))
if (has_temporal_data) {
total_improving <- sum(sapply(field_results, function(x) {
if (!is.na(x$change_percentages$positive_pct)) {
(x$change_percentages$positive_pct / 100) * x$current_stats$field_area_ha
} else 0
}), na.rm = TRUE)
total_declining <- sum(sapply(field_results, function(x) {
if (!is.na(x$change_percentages$negative_pct)) {
(x$change_percentages$negative_pct / 100) * x$current_stats$field_area_ha
} else 0
}), na.rm = TRUE)
total_stable <- sum(sapply(field_results, function(x) {
if (!is.na(x$change_percentages$stable_pct)) {
(x$change_percentages$stable_pct / 100) * x$current_stats$field_area_ha
} else 0
}), na.rm = TRUE)
improving_pct <- (total_improving / total_hectares) * 100
declining_pct <- (total_declining / total_hectares) * 100
stable_pct <- (total_stable / total_hectares) * 100
output <- c(output, paste("• Improving areas:", format_area_both(total_improving), paste0("(", round(improving_pct, 1), "%)")))
output <- c(output, paste("• Declining areas:", format_area_both(total_declining), paste0("(", round(declining_pct, 1), "%)")))
output <- c(output, paste("• Stable areas:", format_area_both(total_stable), paste0("(", round(stable_pct, 1), "%)")))
# Overall trend
if (improving_pct > declining_pct) {
trend_diff <- round(improving_pct - declining_pct, 1)
output <- c(output, paste("• Trend: ✅ POSITIVE (+", trend_diff, "%)"))
} else if (declining_pct > improving_pct) {
trend_diff <- round(declining_pct - improving_pct, 1)
output <- c(output, paste("• Trend: ⚠️ NEGATIVE (-", trend_diff, "%)"))
} else {
output <- c(output, "• Trend: BALANCED")
}
} else {
output <- c(output, "• Analysis: Spatial patterns only (previous week data unavailable)")
}
# Add farm-wide analysis summary
output <- c(output, "")
output <- c(output, "=== FARM-WIDE ANALYSIS SUMMARY ===")
output <- c(output, "")
# Field uniformity statistics
excellent_fields <- sum(sapply(field_results, function(x) x$current_stats$cv <= 0.08))
good_fields <- sum(sapply(field_results, function(x) x$current_stats$cv > 0.08 & x$current_stats$cv <= 0.15))
moderate_fields <- sum(sapply(field_results, function(x) x$current_stats$cv > 0.15 & x$current_stats$cv <= 0.30))
poor_fields <- sum(sapply(field_results, function(x) x$current_stats$cv > 0.30))
output <- c(output, "FIELD UNIFORMITY SUMMARY:")
output <- c(output, "│ Uniformity Level │ Count │ Percent │")
output <- c(output, sprintf("│ Excellent (CV≤0.08) │ %5d │ %6.1f%% │", excellent_fields, (excellent_fields/total_fields)*100))
output <- c(output, sprintf("│ Good (CV 0.08-0.15) │ %5d │ %6.1f%% │", good_fields, (good_fields/total_fields)*100))
output <- c(output, sprintf("│ Moderate (CV 0.15-0.30) │ %5d │ %6.1f%% │", moderate_fields, (moderate_fields/total_fields)*100))
output <- c(output, sprintf("│ Poor (CV>0.30) │ %5d │ %6.1f%% │", poor_fields, (poor_fields/total_fields)*100))
output <- c(output, sprintf("│ Total fields │ %5d │ %6.1f%% │", total_fields, 100.0))
output <- c(output, "")
# Farm-wide area change summary
output <- c(output, "FARM-WIDE AREA CHANGE SUMMARY:")
output <- c(output, "│ Change Type │ Area (ha/acres) │ Percent │")
if (has_temporal_data) {
output <- c(output, sprintf("│ Improving areas │ %s │ %6.1f%% │", format_area_both(total_improving), improving_pct))
output <- c(output, sprintf("│ Stable areas │ %s │ %6.1f%% │", format_area_both(total_stable), stable_pct))
output <- c(output, sprintf("│ Declining areas │ %s │ %6.1f%% │", format_area_both(total_declining), declining_pct))
output <- c(output, sprintf("│ Total area │ %s │ %6.1f%% │", format_area_both(total_hectares), 100.0))
} else {
output <- c(output, "│ Improving areas │ N/A │ N/A │")
output <- c(output, "│ Stable areas │ N/A │ N/A │")
output <- c(output, "│ Declining areas │ N/A │ N/A │")
output <- c(output, sprintf("│ Total area │ %s │ %6.1f%% │", format_area_both(total_hectares), 100.0))
}
output <- c(output, "")
# Key insights
output <- c(output, "KEY INSIGHTS:")
good_uniformity_pct <- ((excellent_fields + good_fields) / total_fields) * 100
excellent_uniformity_pct <- (excellent_fields / total_fields) * 100
output <- c(output, sprintf("• %d%% of fields have good uniformity (CV ≤ 0.15)", round(good_uniformity_pct)))
output <- c(output, sprintf("• %d%% of fields have excellent uniformity (CV ≤ 0.08)", round(excellent_uniformity_pct)))
if (has_temporal_data) {
output <- c(output, sprintf("• %s (%.1f%%) of farm area is improving week-over-week", format_area_both(total_improving), improving_pct))
output <- c(output, sprintf("• %s (%.1f%%) of farm area is stable week-over-week", format_area_both(total_stable), stable_pct))
output <- c(output, sprintf("• %s (%.1f%%) of farm area is declining week-over-week", format_area_both(total_declining), declining_pct))
output <- c(output, sprintf("• Total farm area analyzed: %s", format_area_both(total_hectares)))
if (improving_pct > declining_pct) {
trend_diff <- round(improving_pct - declining_pct, 1)
output <- c(output, sprintf("• Overall trend: POSITIVE (%.1f%% more area improving than declining)", trend_diff))
} else if (declining_pct > improving_pct) {
trend_diff <- round(declining_pct - improving_pct, 1)
output <- c(output, sprintf("• Overall trend: NEGATIVE (%.1f%% more area declining than improving)", trend_diff))
} else {
output <- c(output, "• Overall trend: BALANCED (equal improvement and decline)")
}
# Add note about 0% decline potentially being due to missing data
if (declining_pct == 0) {
output <- c(output, "")
output <- c(output, "⚠️ IMPORTANT NOTE: 0% decline does NOT necessarily mean all crops are healthy.")
output <- c(output, "• This may be due to missing satellite data from the previous week (cloud cover)")
output <- c(output, "• Areas with clouds (CI=0) cannot be analyzed for decline")
output <- c(output, "• True decline levels may be higher than reported")
}
} else {
output <- c(output, "• Analysis: Spatial patterns only (previous week data unavailable)")
output <- c(output, "• Total farm area analyzed: %.1f hectares", total_hectares)
output <- c(output, "• Note: Due to clouds in previous week (CI=0), no decline measurements available")
output <- c(output, "• This does NOT mean fields didn't decline - only that no comparison data exists")
}
# Add KPI Dashboard Tables
output <- c(output, "")
output <- c(output, "=== FARM KEY PERFORMANCE INDICATORS ===")
output <- c(output, "")
# Table 1: Field Performance Distribution & Risk Assessment
output <- c(output, "FIELD PERFORMANCE INDICATORS")
output <- c(output, "________________________________________")
output <- c(output, "UNIFORMITY DISTRIBUTION: RISK ASSESSMENT:")
output <- c(output, "CV Category Count Percent Risk Level Count Percent")
# Calculate risk levels based on CV + Moran's I combination
risk_low <- 0
risk_moderate <- 0
risk_high <- 0
risk_very_high <- 0
for (field_id in names(field_results)) {
field_info <- field_results[[field_id]]
cv <- field_info$current_stats$cv
morans_i <- field_info$current_stats$spatial_autocorr$morans_i
# Risk logic: Low CV + Low clustering = Low risk, High CV + High clustering = High risk
if (!is.na(cv) && !is.na(morans_i)) {
if (cv <= 0.10 && morans_i <= 0.8) {
risk_low <- risk_low + 1
} else if (cv <= 0.20 && morans_i <= 0.9) {
risk_moderate <- risk_moderate + 1
} else if (cv <= 0.30 || morans_i <= 0.95) {
risk_high <- risk_high + 1
} else {
risk_very_high <- risk_very_high + 1
}
} else {
risk_moderate <- risk_moderate + 1 # Default for missing data
}
}
output <- c(output, sprintf("Excellent (CV≤0.08) %d %5.1f%% Low (CV≤0.10) %d %5.1f%%",
excellent_fields, (excellent_fields/total_fields)*100, risk_low, (risk_low/total_fields)*100))
output <- c(output, sprintf("Good (CV 0.08-0.15) %d %5.1f%% Moderate (0.10-0.20) %d %5.1f%%",
good_fields, (good_fields/total_fields)*100, risk_moderate, (risk_moderate/total_fields)*100))
output <- c(output, sprintf("Moderate (0.15-0.30) %d %5.1f%% High (0.20-0.30) %d %5.1f%%",
moderate_fields, (moderate_fields/total_fields)*100, risk_high, (risk_high/total_fields)*100))
output <- c(output, sprintf("Poor (CV>0.30) %d %5.1f%% Very High (>0.30) %d %5.1f%%",
poor_fields, (poor_fields/total_fields)*100, risk_very_high, (risk_very_high/total_fields)*100))
output <- c(output, sprintf("Total fields %d 100.0%% Total fields %d 100.0%%",
total_fields, total_fields))
output <- c(output, "")
# Performance quartiles and CI change patterns
if (has_temporal_data) {
# Calculate performance quartiles based on combination of current CI and change
field_performance <- sapply(field_results, function(x) {
current_ci <- x$current_stats$mean_ci
ci_change <- x$ci_change
# Combine current performance with improvement trend
performance_score <- current_ci + (ci_change * 0.5) # Weight change as 50% of current
return(performance_score)
})
sorted_performance <- sort(field_performance, decreasing = TRUE)
q75 <- quantile(sorted_performance, 0.75, na.rm = TRUE)
q25 <- quantile(sorted_performance, 0.25, na.rm = TRUE)
top_quartile <- sum(field_performance >= q75, na.rm = TRUE)
bottom_quartile <- sum(field_performance <= q25, na.rm = TRUE)
middle_quartile <- total_fields - top_quartile - bottom_quartile
avg_ci_top <- mean(sapply(field_results[field_performance >= q75], function(x) x$current_stats$mean_ci), na.rm = TRUE)
avg_ci_mid <- mean(sapply(field_results[field_performance > q25 & field_performance < q75], function(x) x$current_stats$mean_ci), na.rm = TRUE)
avg_ci_bot <- mean(sapply(field_results[field_performance <= q25], function(x) x$current_stats$mean_ci), na.rm = TRUE)
output <- c(output, "PERFORMANCE QUARTILES: CI CHANGE PATTERNS:")
output <- c(output, "Quartile Count Avg CI Change Type Hectares Percent")
output <- c(output, sprintf("Top 25%% %d %4.1f Improving areas %5.1f ha %5.1f%%",
top_quartile, avg_ci_top, total_improving, improving_pct))
output <- c(output, sprintf("Average (25-75%%) %d %4.1f Stable areas %5.1f ha %5.1f%%",
middle_quartile, avg_ci_mid, total_stable, stable_pct))
output <- c(output, sprintf("Bottom 25%% %d %4.1f Declining areas %5.1f ha %5.1f%%",
bottom_quartile, avg_ci_bot, total_declining, declining_pct))
output <- c(output, sprintf("Total fields %d %4.1f Total area %5.1f ha 100.0%%",
total_fields, mean(sapply(field_results, function(x) x$current_stats$mean_ci), na.rm = TRUE), total_hectares))
}
output <- c(output, "")
# Table 2: Anomaly Detection & Management Alerts
output <- c(output, "ANOMALY DETECTION & MANAGEMENT PRIORITIES")
output <- c(output, "________________________________________")
# Weed detection (CI increase > 1.5)
weed_fields <- 0
weed_area <- 0
harvest_fields <- 0
harvest_area <- 0
fallow_fields <- 0
fallow_area <- 0
high_hotspot_fields <- 0
high_hotspot_area <- 0
if (has_temporal_data) {
for (field_id in names(field_results)) {
field_info <- field_results[[field_id]]
ci_change <- field_info$ci_change
current_ci <- field_info$current_stats$mean_ci
area <- field_info$current_stats$field_area_ha
hotspots <- field_info$current_stats$extreme_percentages$hotspot_pct
# Weed detection: CI increase > 1.5
if (!is.na(ci_change) && ci_change > 1.5) {
weed_fields <- weed_fields + 1
weed_area <- weed_area + area
}
# Harvesting/theft detection: CI decrease > 1.5
if (!is.na(ci_change) && ci_change < -1.5) {
harvest_fields <- harvest_fields + 1
harvest_area <- harvest_area + area
}
# Fallow detection: CI < 2.0
if (!is.na(current_ci) && current_ci < 2.0) {
fallow_fields <- fallow_fields + 1
fallow_area <- fallow_area + area
}
# High hotspot detection: > 5%
if (!is.na(hotspots) && hotspots > 5.0) {
high_hotspot_fields <- high_hotspot_fields + 1
high_hotspot_area <- high_hotspot_area + area
}
}
}
output <- c(output, "WEED PRESENCE INDICATORS: HARVESTING/THEFT INDICATORS:")
output <- c(output, "High CI Increase (>1.5): High CI Decrease (>1.5):")
output <- c(output, "Fields to check Count Area Fields to check Count Area")
output <- c(output, sprintf("Potential weed areas %d %4.1f ha Potential harvesting %d %4.1f ha",
weed_fields, weed_area, harvest_fields, harvest_area))
output <- c(output, sprintf("Total monitored fields %d %5.1f ha Total monitored fields%d %5.1f ha",
total_fields, total_hectares, total_fields, total_hectares))
output <- c(output, "")
output <- c(output, "FALLOW FIELD DETECTION: HOTSPOT ANALYSIS:")
output <- c(output, "Low CI Fields (<2.0): Spatial Clustering:")
output <- c(output, "Fields to check Count Area High hotspot fields (>5%) Count Area")
output <- c(output, sprintf("Potential fallow %d %4.1f ha Spatial issues detected %d %4.1f ha",
fallow_fields, fallow_area, high_hotspot_fields, high_hotspot_area))
output <- c(output, sprintf("Total catchment fields %d %5.1f ha Total analyzed fields %d %5.1f ha",
total_fields, total_hectares, total_fields, total_hectares))
output <- c(output, "")
# Table 3: Priority Action Items & Field Rankings
output <- c(output, "IMMEDIATE ACTION PRIORITIES")
output <- c(output, "________________________________________")
# Find urgent and monitoring fields
urgent_fields <- sapply(field_results, function(x) x$message_result$worth_sending && grepl("URGENT", x$message_result$message))
monitoring_fields <- sapply(field_results, function(x) x$message_result$worth_sending && !grepl("URGENT", x$message_result$message))
output <- c(output, "URGENT INTERVENTIONS: MONITORING REQUIRED:")
output <- c(output, "Field Name Issue Type Area Field Name Issue Type Area")
urgent_count <- 0
monitoring_count <- 0
for (field_id in names(field_results)) {
if (urgent_fields[field_id]) {
field_info <- field_results[[field_id]]
field_name <- paste(field_info$current_stats$field, field_info$current_stats$sub_field, sep="-")
if (nchar(field_name) > 15) field_name <- substr(field_name, 1, 15)
area <- field_info$current_stats$field_area_ha
if (urgent_count == 0) {
output <- c(output, sprintf("%-15s Poor uniformity %4.1f ha %-15s %-13s %4.1f ha",
field_name, area, "", "", 0.0))
}
urgent_count <- urgent_count + 1
}
if (monitoring_fields[field_id]) {
field_info <- field_results[[field_id]]
field_name <- paste(field_info$current_stats$field, field_info$current_stats$sub_field, sep="-")
if (nchar(field_name) > 15) field_name <- substr(field_name, 1, 15)
area <- field_info$current_stats$field_area_ha
if (monitoring_count == 0) {
# Update the previous line to include monitoring field
last_line <- output[length(output)]
if (grepl("Poor uniformity", last_line) && grepl("0.0 ha$", last_line)) {
output[length(output)] <- sprintf("%-15s Poor uniformity %4.1f ha %-15s %-13s %4.1f ha",
sub(" .*", "", last_line),
as.numeric(sub(".*Poor uniformity ([0-9.]+) ha.*", "\\1", last_line)),
field_name, "Moderate var.", area)
}
}
monitoring_count <- monitoring_count + 1
}
}
if (urgent_count == 0 && monitoring_count == 0) {
output <- c(output, "No urgent interventions - - No monitoring required - -")
}
output <- c(output, "")
# Field performance ranking
if (has_temporal_data) {
output <- c(output, "FIELD PERFORMANCE RANKING: WEEKLY PRIORITIES:")
output <- c(output, "Rank Field Name CI Status Priority Level Fields Action Required")
# Sort fields by performance score
field_names <- names(field_performance)
sorted_indices <- order(field_performance, decreasing = TRUE)
priority_immediate <- sum(urgent_fields)
priority_weekly <- sum(monitoring_fields)
priority_routine <- total_fields - priority_immediate - priority_weekly
for (i in 1:min(3, length(sorted_indices))) {
field_id <- field_names[sorted_indices[i]]
field_info <- field_results[[field_id]]
field_name <- paste(field_info$current_stats$field, field_info$current_stats$sub_field, sep="-")
if (nchar(field_name) > 12) field_name <- substr(field_name, 1, 12)
ci <- field_info$current_stats$mean_ci
status <- if (field_info$current_stats$cv <= 0.08) "Excellent" else if (field_info$current_stats$cv <= 0.15) "Good" else "Caution"
if (i == 1) {
output <- c(output, sprintf("%d %-12s %4.1f %-9s Immediate %d Field inspection",
i, field_name, ci, status, priority_immediate))
} else if (i == 2) {
output <- c(output, sprintf("%d %-12s %4.1f %-9s This week %d Continue monitoring",
i, field_name, ci, status, priority_weekly))
} else {
output <- c(output, sprintf("%d %-12s %4.1f %-9s Monitor %d Routine management",
i, field_name, ci, status, priority_routine))
}
}
output <- c(output, sprintf("... Total fields %d", total_fields))
}
return(paste(output, collapse = "\n"))
}
#' Format analysis results as CSV data
#' @param analysis_results Results from run_estate_analysis
#' @return Data frame ready for write.csv
format_as_csv <- function(analysis_results) {
field_results <- analysis_results$field_results
estate_name <- analysis_results$estate_name
current_week <- analysis_results$current_week
previous_week <- analysis_results$previous_week
csv_data <- data.frame()
for (field_id in names(field_results)) {
field_info <- field_results[[field_id]]
row_data <- data.frame(
estate = estate_name,
field = field_info$current_stats$field,
sub_field = field_info$current_stats$sub_field,
area_ha = round(field_info$current_stats$field_area_ha, 2),
current_week = current_week,
previous_week = previous_week,
current_week_ci = round(field_info$current_stats$mean_ci, 3),
previous_week_ci = if (!is.null(field_info$previous_stats)) round(field_info$previous_stats$mean_ci, 3) else NA,
ci_change = round(field_info$ci_change, 3),
change_category = field_info$change_category,
cv = round(field_info$current_stats$cv, 3),
uniformity_category = field_info$uniformity_category,
acceptable_pct = round(field_info$current_stats$acceptable_pct, 1),
hotspot_pct = round(field_info$current_stats$extreme_percentages$hotspot_pct, 1),
coldspot_pct = round(field_info$current_stats$extreme_percentages$coldspot_pct, 1),
morans_i = round(field_info$current_stats$spatial_autocorr$morans_i, 3),
alert_needed = field_info$message_result$worth_sending,
message = field_info$message_result$message,
stringsAsFactors = FALSE
)
csv_data <- rbind(csv_data, row_data)
}
return(csv_data)
}
#' Format analysis results as markdown table
#' @param analysis_results Results from run_estate_analysis
#' @return Character string with markdown table
format_as_markdown_table <- function(analysis_results) {
field_results <- analysis_results$field_results
estate_name <- toupper(analysis_results$estate_name)
current_week <- analysis_results$current_week
previous_week <- analysis_results$previous_week
output <- c()
output <- c(output, paste("# Crop Analysis Summary -", estate_name, "Estate"))
output <- c(output, paste("**Analysis Period:** Week", previous_week, "vs Week", current_week))
output <- c(output, "")
output <- c(output, "| Field | Area (ha) | Current CI | Change | Uniformity | Alert | Message |")
output <- c(output, "|-------|-----------|------------|--------|------------|-------|---------|")
for (field_id in names(field_results)) {
field_info <- field_results[[field_id]]
field_name <- paste(field_info$current_stats$field, field_info$current_stats$sub_field, sep="-")
area <- round(field_info$current_stats$field_area_ha, 1)
current_ci <- round(field_info$current_stats$mean_ci, 3)
change <- field_info$change_category
uniformity <- field_info$uniformity_category
alert <- if(field_info$message_result$worth_sending) "🚨 YES" else "✅ NO"
message <- field_info$message_result$message
row <- paste("|", field_name, "|", area, "|", current_ci, "|", change, "|", uniformity, "|", alert, "|", message, "|")
output <- c(output, row)
}
return(paste(output, collapse = "\n"))
}
#' Create Word document with analysis results
#' @param analysis_results Results from run_estate_analysis
#' @param output_dir Directory to save the Word document
#' @return Path to the created Word document
create_word_document <- function(analysis_results, output_dir) {
estate_name <- toupper(analysis_results$estate_name)
current_week <- analysis_results$current_week
previous_week <- analysis_results$previous_week
# Create a new Word document
doc <- officer::read_docx()
# Add title
doc <- officer::body_add_par(doc, paste(estate_name, "Crop Analysis Report"), style = "heading 1")
# Add summary
field_results <- analysis_results$field_results
alert_count <- sum(sapply(field_results, function(x) x$message_result$worth_sending))
total_fields <- length(field_results)
total_hectares <- sum(sapply(field_results, function(x) x$current_stats$field_area_ha), na.rm = TRUE)
doc <- officer::body_add_par(doc, "Summary", style = "heading 2")
doc <- officer::body_add_par(doc, paste("• Fields analyzed:", total_fields))
doc <- officer::body_add_par(doc, paste("• Total area:", format_area_both(total_hectares)))
doc <- officer::body_add_par(doc, paste("• Alerts needed:", alert_count))
doc <- officer::body_add_par(doc, "")
# Field-by-field alerts only
if (alert_count > 0) {
doc <- officer::body_add_par(doc, "Priority Fields", style = "heading 2")
for (field_id in names(field_results)) {
field_info <- field_results[[field_id]]
if (field_info$message_result$worth_sending) {
field_name <- paste(field_info$current_stats$field, field_info$current_stats$sub_field, sep="-")
area <- round(field_info$current_stats$field_area_ha, 1)
message <- field_info$message_result$message
doc <- officer::body_add_par(doc, paste("•", field_name, paste0("(", format_area_both(area), "):"), message))
}
}
doc <- officer::body_add_par(doc, "")
} else {
doc <- officer::body_add_par(doc, "✅ No urgent alerts - all fields stable")
doc <- officer::body_add_par(doc, "")
}
# Quick farm summary
doc <- officer::body_add_par(doc, "Quick Stats", style = "heading 2")
# Calculate improving vs declining areas (only if temporal data available)
has_temporal_data <- any(sapply(field_results, function(x) !is.na(x$change_percentages$positive_pct)))
if (has_temporal_data) {
total_improving <- sum(sapply(field_results, function(x) {
if (!is.na(x$change_percentages$positive_pct)) {
(x$change_percentages$positive_pct / 100) * x$current_stats$field_area_ha
} else 0
}), na.rm = TRUE)
total_declining <- sum(sapply(field_results, function(x) {
if (!is.na(x$change_percentages$negative_pct)) {
(x$change_percentages$negative_pct / 100) * x$current_stats$field_area_ha
} else 0
}), na.rm = TRUE)
total_stable <- sum(sapply(field_results, function(x) {
if (!is.na(x$change_percentages$stable_pct)) {
(x$change_percentages$stable_pct / 100) * x$current_stats$field_area_ha
} else 0
}), na.rm = TRUE)
improving_pct <- (total_improving / total_hectares) * 100
declining_pct <- (total_declining / total_hectares) * 100
stable_pct <- (total_stable / total_hectares) * 100
doc <- officer::body_add_par(doc, paste("• Improving areas:", format_area_both(total_improving), paste0("(", round(improving_pct, 1), "%)")))
doc <- officer::body_add_par(doc, paste("• Stable areas:", format_area_both(total_stable), paste0("(", round(stable_pct, 1), "%)")))
doc <- officer::body_add_par(doc, paste("• Declining areas:", format_area_both(total_declining), paste0("(", round(declining_pct, 1), "%)")))
# Overall trend
if (improving_pct > declining_pct) {
trend_diff <- round(improving_pct - declining_pct, 1)
doc <- officer::body_add_par(doc, paste("• Trend: POSITIVE (+", trend_diff, "%)"))
} else if (declining_pct > improving_pct) {
trend_diff <- round(declining_pct - improving_pct, 1)
doc <- officer::body_add_par(doc, paste("• Trend: NEGATIVE (-", trend_diff, "%)"))
} else {
doc <- officer::body_add_par(doc, "• Trend: BALANCED")
}
} else {
doc <- officer::body_add_par(doc, "• Analysis: Spatial patterns only (previous week data unavailable)")
}
doc <- officer::body_add_par(doc, "")
# Add farm-wide analysis summary
doc <- officer::body_add_par(doc, "Farm-Wide Analysis Summary", style = "heading 2")
doc <- officer::body_add_par(doc, "")
# Field uniformity statistics
excellent_fields <- sum(sapply(field_results, function(x) x$current_stats$cv <= 0.08))
good_fields <- sum(sapply(field_results, function(x) x$current_stats$cv > 0.08 & x$current_stats$cv <= 0.15))
moderate_fields <- sum(sapply(field_results, function(x) x$current_stats$cv > 0.15 & x$current_stats$cv <= 0.30))
poor_fields <- sum(sapply(field_results, function(x) x$current_stats$cv > 0.30))
# Create uniformity table
uniformity_data <- data.frame(
"Uniformity Level" = c("Excellent (CV≤0.08)", "Good (CV 0.08-0.15)", "Moderate (CV 0.15-0.30)", "Poor (CV>0.30)", "Total fields"),
"Fields" = c(excellent_fields, good_fields, moderate_fields, poor_fields, total_fields),
"Percent" = c(
round((excellent_fields/total_fields)*100, 1),
round((good_fields/total_fields)*100, 1),
round((moderate_fields/total_fields)*100, 1),
round((poor_fields/total_fields)*100, 1),
100.0
),
stringsAsFactors = FALSE
)
uniformity_ft <- flextable::flextable(uniformity_data)
uniformity_ft <- flextable::autofit(uniformity_ft)
uniformity_ft <- flextable::set_header_labels(uniformity_ft,
"Uniformity.Level" = "Uniformity Level"
)
doc <- officer::body_add_par(doc, "Field Uniformity Summary", style = "heading 3")
doc <- flextable::body_add_flextable(doc, uniformity_ft)
doc <- officer::body_add_par(doc, "")
# Farm-wide area change summary
doc <- officer::body_add_par(doc, "Farm-Wide Area Change Summary", style = "heading 3")
if (has_temporal_data) {
change_data <- data.frame(
"Change Type" = c("Improving areas", "Stable areas", "Declining areas", "Total area"),
"Area" = c(format_area_both(total_improving), format_area_both(total_stable), format_area_both(total_declining), format_area_both(total_hectares)),
"Percent" = c(round(improving_pct, 1), round(stable_pct, 1), round(declining_pct, 1), 100.0),
stringsAsFactors = FALSE
)
} else {
change_data <- data.frame(
"Change Type" = c("Improving areas", "Stable areas", "Declining areas", "Total area"),
"Area" = c("N/A", "N/A", "N/A", format_area_both(total_hectares)),
"Percent" = c("N/A", "N/A", "N/A", 100.0),
stringsAsFactors = FALSE
)
}
change_ft <- flextable::flextable(change_data)
change_ft <- flextable::autofit(change_ft)
change_ft <- flextable::set_header_labels(change_ft,
"Change.Type" = "Change Type",
"Area" = "Area (ha/acres)"
)
doc <- flextable::body_add_flextable(doc, change_ft)
doc <- officer::body_add_par(doc, "")
# Create and add detailed results tables using flextable (split into multiple tables for better formatting)
csv_data <- format_as_csv(analysis_results)
# Split data into multiple tables for better readability
doc <- officer::body_add_par(doc, "Detailed Results", style = "heading 2")
# Table 2: Current Week Analysis
current_data <- csv_data[, c("field", "current_week_ci", "cv", "uniformity_category", "acceptable_pct")]
current_data$current_week_ci <- round(current_data$current_week_ci, 3)
current_data$cv <- round(current_data$cv, 3)
current_data$acceptable_pct <- round(current_data$acceptable_pct, 1)
current_ft <- flextable::flextable(current_data)
current_ft <- flextable::autofit(current_ft)
current_ft <- flextable::set_header_labels(current_ft,
"field" = "Field",
# "sub_field" = "Sub-field",
"current_week_ci" = "Current CI",
"cv" = "CV",
"uniformity_category" = "Uniformity",
"acceptable_pct" = "Acceptable %"
)
current_ft <- flextable::theme_vanilla(current_ft)
current_ft <- flextable::fontsize(current_ft, size = 9)
current_ft <- flextable::width(current_ft, width = 1.0) # Set column width
doc <- officer::body_add_par(doc, "Current Week Analysis", style = "heading 3")
doc <- flextable::body_add_flextable(doc, current_ft)
doc <- officer::body_add_par(doc, "")
# Table 3: Change Analysis (only if temporal data available)
if (has_temporal_data && any(!is.na(csv_data$ci_change))) {
change_data <- csv_data[, c("field", "previous_week_ci", "ci_change", "change_category")]
change_data <- change_data[!is.na(change_data$ci_change), ] # Remove rows with NA change
change_data$previous_week_ci <- round(change_data$previous_week_ci, 3)
change_data$ci_change <- round(change_data$ci_change, 3)
change_ft <- flextable::flextable(change_data)
change_ft <- flextable::autofit(change_ft)
change_ft <- flextable::set_header_labels(change_ft,
"field" = "Field",
# "sub_field" = "Sub-field",
"previous_week_ci" = "Previous CI",
"ci_change" = "CI Change",
"change_category" = "Change Type"
)
change_ft <- flextable::theme_vanilla(change_ft)
change_ft <- flextable::fontsize(change_ft, size = 9)
change_ft <- flextable::width(change_ft, width = 1.0) # Set column width
doc <- officer::body_add_par(doc, "Week-over-Week Change Analysis", style = "heading 3")
doc <- flextable::body_add_flextable(doc, change_ft)
doc <- officer::body_add_par(doc, "")
}
# Table 4: Spatial Analysis Results (split into two tables for better fit)
spatial_data <- csv_data[, c("field", "hotspot_pct", "coldspot_pct", "morans_i", "alert_needed")]
spatial_data$hotspot_pct <- round(spatial_data$hotspot_pct, 1)
spatial_data$coldspot_pct <- round(spatial_data$coldspot_pct, 1)
spatial_data$morans_i <- round(spatial_data$morans_i, 3)
spatial_data$alert_needed <- ifelse(spatial_data$alert_needed, "YES", "NO")
spatial_ft <- flextable::flextable(spatial_data)
spatial_ft <- flextable::autofit(spatial_ft)
spatial_ft <- flextable::set_header_labels(spatial_ft,
"field" = "Field",
# "sub_field" = "Sub-field",
"hotspot_pct" = "Hotspots %",
"coldspot_pct" = "Coldspots %",
"morans_i" = "Moran's I",
"alert_needed" = "Alert"
)
spatial_ft <- flextable::theme_vanilla(spatial_ft)
spatial_ft <- flextable::fontsize(spatial_ft, size = 9)
spatial_ft <- flextable::width(spatial_ft, width = 0.8) # Set column width
doc <- officer::body_add_par(doc, "Spatial Analysis Results", style = "heading 3")
doc <- flextable::body_add_flextable(doc, spatial_ft)
doc <- officer::body_add_par(doc, "")
# Table 5: Alert Messages (separate table for long messages)
message_data <- csv_data[, c("field","message")]
message_data$message <- substr(message_data$message, 1, 80) # Truncate long messages for table fit
message_ft <- flextable::flextable(message_data)
message_ft <- flextable::autofit(message_ft)
message_ft <- flextable::set_header_labels(message_ft,
"field" = "Field",
# "sub_field" = "Sub-field",
"message" = "Alert Message"
)
message_ft <- flextable::theme_vanilla(message_ft)
message_ft <- flextable::fontsize(message_ft, size = 8) # Smaller font for messages
message_ft <- flextable::width(message_ft, width = 2.0) # Wider column for messages
doc <- officer::body_add_par(doc, "Alert Messages", style = "heading 3")
doc <- flextable::body_add_flextable(doc, message_ft)
# Add interpretation guide for all columns
doc <- officer::body_add_par(doc, "")
doc <- officer::body_add_par(doc, "Column Interpretation Guide", style = "heading 3")
doc <- officer::body_add_par(doc, "")
# Table 1 interpretation
doc <- officer::body_add_par(doc, "Field Information Table:", style = "Normal")
doc <- officer::body_add_par(doc, "• Field/Sub-field: Field identifiers and names")
doc <- officer::body_add_par(doc, "• Area (ha): Field size in hectares")
doc <- officer::body_add_par(doc, "• Current/Previous Week: Weeks being compared")
doc <- officer::body_add_par(doc, "")
# Table 2 interpretation
doc <- officer::body_add_par(doc, "Current Week Analysis Table:", style = "Normal")
doc <- officer::body_add_par(doc, "• Current CI: Crop Index (0-10 scale, higher = healthier crop)")
doc <- officer::body_add_par(doc, "• CV: Coefficient of Variation (lower = more uniform field)")
doc <- officer::body_add_par(doc, "• Uniformity: Field uniformity rating (Excellent/Good/Moderate/Poor)")
doc <- officer::body_add_par(doc, "• Acceptable %: % of field within ±25% of average CI (higher = more uniform)")
doc <- officer::body_add_par(doc, "")
# Table 3 interpretation (only if temporal data available)
if (has_temporal_data && any(!is.na(csv_data$ci_change))) {
doc <- officer::body_add_par(doc, "Week-over-Week Change Analysis Table:", style = "Normal")
doc <- officer::body_add_par(doc, "• Previous CI: Crop Index from previous week")
doc <- officer::body_add_par(doc, "• CI Change: Week-over-week change in CI values")
doc <- officer::body_add_par(doc, "• Change Type: >+0.5 = Improving, -0.5 to +0.5 = Stable, <-0.5 = Declining")
doc <- officer::body_add_par(doc, "")
}
# Table 4 interpretation
doc <- officer::body_add_par(doc, "Spatial Analysis Results Table:", style = "Normal")
doc <- officer::body_add_par(doc, "• Hotspots %: % of field significantly above average (> mean + 1.5×SD)")
doc <- officer::body_add_par(doc, "• Coldspots %: % of field significantly below average (< mean - 1.5×SD)")
doc <- officer::body_add_par(doc, "• Moran's I: Spatial autocorrelation (-1 to +1, higher = more clustered)")
doc <- officer::body_add_par(doc, "• Alert: YES/NO indicating if field needs management attention")
doc <- officer::body_add_par(doc, "")
# Table 5 interpretation
doc <- officer::body_add_par(doc, "Alert Messages Table:", style = "Normal")
doc <- officer::body_add_par(doc, "• Message: Specific recommendations or warnings for each field")
doc <- officer::body_add_par(doc, "")
# Overall interpretation guide
doc <- officer::body_add_par(doc, "Performance Thresholds:", style = "heading 3")
doc <- officer::body_add_par(doc, "Acceptable %: >45% = Excellent uniformity, 35-45% = Good, <35% = Needs attention")
doc <- officer::body_add_par(doc, "CV: <0.08 = Excellent, 0.08-0.15 = Good, 0.15-0.30 = Moderate, >0.30 = Poor")
doc <- officer::body_add_par(doc, "Moran's I: >0.7 = Strong clustering, 0.3-0.7 = Normal field patterns, <0.3 = Random")
doc <- officer::body_add_par(doc, "Hotspots/Coldspots: >10% = Significant spatial issues, 3-10% = Monitor, <3% = Normal")
# Add KPI Dashboard to Word Document
doc <- officer::body_add_par(doc, "")
doc <- officer::body_add_par(doc, "Farm Key Performance Indicators", style = "heading 2")
doc <- officer::body_add_par(doc, "")
# Table 1: Field Performance Distribution & Risk Assessment
doc <- officer::body_add_par(doc, "Field Performance Indicators", style = "heading 3")
# Calculate risk levels based on CV + Moran's I combination
risk_low <- 0
risk_moderate <- 0
risk_high <- 0
risk_very_high <- 0
for (field_id in names(field_results)) {
field_info <- field_results[[field_id]]
cv <- field_info$current_stats$cv
morans_i <- field_info$current_stats$spatial_autocorr$morans_i
# Risk logic: Low CV + Low clustering = Low risk, High CV + High clustering = High risk
if (!is.na(cv) && !is.na(morans_i)) {
if (cv <= 0.10 && morans_i <= 0.8) {
risk_low <- risk_low + 1
} else if (cv <= 0.20 && morans_i <= 0.9) {
risk_moderate <- risk_moderate + 1
} else if (cv <= 0.30 || morans_i <= 0.95) {
risk_high <- risk_high + 1
} else {
risk_very_high <- risk_very_high + 1
}
} else {
risk_moderate <- risk_moderate + 1 # Default for missing data
}
}
# Uniformity Distribution Table
uniformity_kpi_data <- data.frame(
"CV Category" = c("Excellent (CV≤0.08)", "Good (CV 0.08-0.15)", "Moderate (0.15-0.30)", "Poor (CV>0.30)", "Total fields"),
"Count" = c(excellent_fields, good_fields, moderate_fields, poor_fields, total_fields),
"Percent" = c(round((excellent_fields/total_fields)*100, 1), round((good_fields/total_fields)*100, 1),
round((moderate_fields/total_fields)*100, 1), round((poor_fields/total_fields)*100, 1), 100.0),
stringsAsFactors = FALSE
)
# Risk Assessment Table
risk_data <- data.frame(
"Risk Level" = c("Low (CV≤0.10)", "Moderate (0.10-0.20)", "High (0.20-0.30)", "Very High (>0.30)", "Total fields"),
"Count" = c(risk_low, risk_moderate, risk_high, risk_very_high, total_fields),
"Percent" = c(round((risk_low/total_fields)*100, 1), round((risk_moderate/total_fields)*100, 1),
round((risk_high/total_fields)*100, 1), round((risk_very_high/total_fields)*100, 1), 100.0),
stringsAsFactors = FALSE
)
uniformity_kpi_ft <- flextable::flextable(uniformity_kpi_data)
uniformity_kpi_ft <- flextable::autofit(uniformity_kpi_ft)
risk_ft <- flextable::flextable(risk_data)
risk_ft <- flextable::autofit(risk_ft)
doc <- officer::body_add_par(doc, "Uniformity Distribution:")
doc <- flextable::body_add_flextable(doc, uniformity_kpi_ft)
doc <- officer::body_add_par(doc, "")
doc <- officer::body_add_par(doc, "Risk Assessment:")
doc <- flextable::body_add_flextable(doc, risk_ft)
doc <- officer::body_add_par(doc, "")
# Performance Quartiles (if temporal data available)
if (has_temporal_data) {
# Calculate performance quartiles based on combination of current CI and change
field_performance <- sapply(field_results, function(x) {
current_ci <- x$current_stats$mean_ci
ci_change <- x$ci_change
# Combine current performance with improvement trend
performance_score <- current_ci + (ci_change * 0.5) # Weight change as 50% of current
return(performance_score)
})
sorted_performance <- sort(field_performance, decreasing = TRUE)
q75 <- quantile(sorted_performance, 0.75, na.rm = TRUE)
q25 <- quantile(sorted_performance, 0.25, na.rm = TRUE)
top_quartile <- sum(field_performance >= q75, na.rm = TRUE)
bottom_quartile <- sum(field_performance <= q25, na.rm = TRUE)
middle_quartile <- total_fields - top_quartile - bottom_quartile
avg_ci_top <- mean(sapply(field_results[field_performance >= q75], function(x) x$current_stats$mean_ci), na.rm = TRUE)
avg_ci_mid <- mean(sapply(field_results[field_performance > q25 & field_performance < q75], function(x) x$current_stats$mean_ci), na.rm = TRUE)
avg_ci_bot <- mean(sapply(field_results[field_performance <= q25], function(x) x$current_stats$mean_ci), na.rm = TRUE)
quartile_data <- data.frame(
"Quartile" = c("Top 25%", "Average (25-75%)", "Bottom 25%", "Total fields"),
"Count" = c(top_quartile, middle_quartile, bottom_quartile, total_fields),
"Avg CI" = c(round(avg_ci_top, 1), round(avg_ci_mid, 1), round(avg_ci_bot, 1),
round(mean(sapply(field_results, function(x) x$current_stats$mean_ci), na.rm = TRUE), 1)),
stringsAsFactors = FALSE
)
quartile_ft <- flextable::flextable(quartile_data)
quartile_ft <- flextable::autofit(quartile_ft)
doc <- officer::body_add_par(doc, "Performance Quartiles:")
doc <- flextable::body_add_flextable(doc, quartile_ft)
doc <- officer::body_add_par(doc, "")
}
# Table 2: Anomaly Detection
doc <- officer::body_add_par(doc, "Anomaly Detection & Management Priorities", style = "heading 3")
# Calculate anomalies
weed_fields <- 0
weed_area <- 0
harvest_fields <- 0
harvest_area <- 0
fallow_fields <- 0
fallow_area <- 0
high_hotspot_fields <- 0
high_hotspot_area <- 0
if (has_temporal_data) {
for (field_id in names(field_results)) {
field_info <- field_results[[field_id]]
ci_change <- field_info$ci_change
current_ci <- field_info$current_stats$mean_ci
area <- field_info$current_stats$field_area_ha
hotspots <- field_info$current_stats$extreme_percentages$hotspot_pct
# Weed detection: CI increase > 1.5
if (!is.na(ci_change) && ci_change > 1.5) {
weed_fields <- weed_fields + 1
weed_area <- weed_area + area
}
# Harvesting/theft detection: CI decrease > 1.5
if (!is.na(ci_change) && ci_change < -1.5) {
harvest_fields <- harvest_fields + 1
harvest_area <- harvest_area + area
}
# Fallow detection: CI < 2.0
if (!is.na(current_ci) && current_ci < 2.0) {
fallow_fields <- fallow_fields + 1
fallow_area <- fallow_area + area
}
# High hotspot detection: > 5%
if (!is.na(hotspots) && hotspots > 5.0) {
high_hotspot_fields <- high_hotspot_fields + 1
high_hotspot_area <- high_hotspot_area + area
}
}
}
anomaly_data <- data.frame(
"Detection Type" = c("Potential weed areas (CI increase >1.5)", "Potential harvesting (CI decrease >1.5)",
"Potential fallow fields (CI <2.0)", "High hotspot fields (>5%)"),
"Fields to Check" = c(weed_fields, harvest_fields, fallow_fields, high_hotspot_fields),
"Area (ha)" = c(round(weed_area, 1), round(harvest_area, 1), round(fallow_area, 1), round(high_hotspot_area, 1)),
stringsAsFactors = FALSE
)
anomaly_ft <- flextable::flextable(anomaly_data)
anomaly_ft <- flextable::autofit(anomaly_ft)
doc <- flextable::body_add_flextable(doc, anomaly_ft)
doc <- officer::body_add_par(doc, "")
# Table 3: Priority Actions
doc <- officer::body_add_par(doc, "Immediate Action Priorities", style = "heading 3")
# Find urgent and monitoring fields
urgent_fields <- sapply(field_results, function(x) x$message_result$worth_sending && grepl("URGENT", x$message_result$message))
monitoring_fields <- sapply(field_results, function(x) x$message_result$worth_sending && !grepl("URGENT", x$message_result$message))
urgent_data <- data.frame()
monitoring_data <- data.frame()
for (field_id in names(field_results)) {
if (urgent_fields[field_id]) {
field_info <- field_results[[field_id]]
field_name <- paste(field_info$current_stats$field, field_info$current_stats$sub_field, sep="-")
area <- field_info$current_stats$field_area_ha
urgent_data <- rbind(urgent_data, data.frame(
"Field Name" = field_name,
"Issue Type" = "Poor uniformity",
"Area (ha)" = round(area, 1),
stringsAsFactors = FALSE
))
}
if (monitoring_fields[field_id]) {
field_info <- field_results[[field_id]]
field_name <- paste(field_info$current_stats$field, field_info$current_stats$sub_field, sep="-")
area <- field_info$current_stats$field_area_ha
monitoring_data <- rbind(monitoring_data, data.frame(
"Field Name" = field_name,
"Issue Type" = "Moderate variation",
"Area (ha)" = round(area, 1),
stringsAsFactors = FALSE
))
}
}
if (nrow(urgent_data) > 0) {
urgent_ft <- flextable::flextable(urgent_data)
urgent_ft <- flextable::autofit(urgent_ft)
doc <- officer::body_add_par(doc, "Urgent Interventions:")
doc <- flextable::body_add_flextable(doc, urgent_ft)
} else {
doc <- officer::body_add_par(doc, "Urgent Interventions: None required")
}
doc <- officer::body_add_par(doc, "")
if (nrow(monitoring_data) > 0) {
monitoring_ft <- flextable::flextable(monitoring_data)
monitoring_ft <- flextable::autofit(monitoring_ft)
doc <- officer::body_add_par(doc, "Monitoring Required:")
doc <- flextable::body_add_flextable(doc, monitoring_ft)
} else {
doc <- officer::body_add_par(doc, "Monitoring Required: None required")
}
doc <- officer::body_add_par(doc, "")
# Add interpretation guide for Table 5
doc <- officer::body_add_par(doc, "")
doc <- officer::body_add_par(doc, "Column Guide - Alert Messages:", style = "heading 3")
doc <- officer::body_add_par(doc, "• Alert Message: Specific recommendations based on field analysis")
doc <- officer::body_add_par(doc, "• 🚨 URGENT: Immediate management action required")
doc <- officer::body_add_par(doc, "• ⚠️ ALERT: Early intervention recommended")
doc <- officer::body_add_par(doc, "• ✅ POSITIVE: Good performance, continue current practices")
doc <- officer::body_add_par(doc, "• 📈 OPPORTUNITY: Potential for improvement identified")
# Save the document
timestamp <- format(Sys.time(), "%Y%m%d_%H%M")
filename <- paste0("crop_analysis_", estate_name, "_w", current_week, "vs", previous_week, "_", timestamp, ".docx")
filepath <- file.path(output_dir, filename)
print(doc, target = filepath)
return(filepath)
}
#' Save analysis outputs in multiple formats
#' @param analysis_results Results from run_estate_analysis
#' @param output_dir Directory to save files (optional)
#' @return List with file paths created
save_analysis_outputs <- function(analysis_results, output_dir = NULL) {
estate_name <- analysis_results$estate_name
current_week <- analysis_results$current_week
previous_week <- analysis_results$previous_week
# Create output directory if not specified
if (is.null(output_dir)) {
output_dir <- file.path("output", estate_name)
}
if (!dir.exists(output_dir)) dir.create(output_dir, recursive = TRUE)
timestamp <- format(Sys.time(), "%Y%m%d_%H%M")
base_filename <- paste0("crop_analysis_", estate_name, "_w", current_week, "vs", previous_week, "_", timestamp)
# Generate output formats
whatsapp_text <- format_for_whatsapp(analysis_results)
# Save files
whatsapp_file <- file.path(output_dir, paste0(base_filename, "_whatsapp.txt"))
writeLines(whatsapp_text, whatsapp_file)
# Create Word document
docx_file <- create_word_document(analysis_results, output_dir)
# Display summary
cat("\n=== OUTPUT FILES CREATED ===\n")
cat("📱 WhatsApp format:", whatsapp_file, "\n")
cat("<EFBFBD> Word document:", docx_file, "\n")
# Display WhatsApp format in console for immediate copy
cat("\n=== WHATSAPP/WORD READY FORMAT ===\n")
cat("(Copy text below directly to WhatsApp or Word)\n")
cat(rep("=", 50), "\n")
cat(whatsapp_text)
cat("\n", rep("=", 50), "\n")
return(list(
whatsapp_file = whatsapp_file,
docx_file = docx_file
))
}