Refactor KPI reporting and status alert logic; streamline directory structure for KPI reports and enhance field analysis summaries with additional metrics and alerts.
This commit is contained in:
parent
f1821dab59
commit
b2d4093601
|
|
@ -145,33 +145,68 @@ categorize_cv_trend_long_term <- function(cv_slope) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#' Determine status alert based on harvest probability and crop health
|
#' Determine status alert for CANE_SUPPLY client (harvest/milling workflow)
|
||||||
|
#'
|
||||||
|
#' Alerts focus on: harvest readiness, crop health monitoring, exception detection
|
||||||
|
#' Uses WEEKLY trends (Four_week_trend) not daily noise for consistency
|
||||||
|
#' Designed for harvest/milling clients who manage expectation, not daily operations
|
||||||
|
#'
|
||||||
#' Priority order:
|
#' Priority order:
|
||||||
#' 1. harvest_ready (imminent + mature ≥12 months)
|
#' 1. harvest_ready → Schedule harvest operations
|
||||||
#' 2. decline_stress (drop ≥CI_CHANGE_DECLINE_THRESHOLD but still >1.5)
|
#' 2. harvested_bare → Record completion / detect unexpected harvest
|
||||||
#' 3. harvested_bare (Mean CI < 1.5)
|
#' 3. stress_detected → Monitor crop health (consistent decline)
|
||||||
#' @param imminent_prob Numeric harvest probability
|
#' 4. germination_delayed → Early warning for young fields
|
||||||
#' @param age_week Numeric age in weeks
|
#' 5. growth_on_track → Positive signal (no action needed)
|
||||||
#' @param weekly_ci_change Numeric weekly CI change
|
#' 6. NA → Normal growth (no alert)
|
||||||
#' @param mean_ci Numeric mean CI value
|
#'
|
||||||
|
#' @param imminent_prob Numeric harvest probability (0-1)
|
||||||
|
#' @param age_week Numeric age in weeks since planting/harvest
|
||||||
|
#' @param mean_ci Numeric mean Chlorophyll Index
|
||||||
|
#' @param four_week_trend Numeric 4-week trend in CI (slope of growth)
|
||||||
|
#' @param weekly_ci_change Numeric week-over-week CI change
|
||||||
|
#' @param cv Numeric coefficient of variation (field uniformity)
|
||||||
#' @return Character status alert code or NA
|
#' @return Character status alert code or NA
|
||||||
calculate_status_alert <- function(imminent_prob, age_week, weekly_ci_change, mean_ci) {
|
calculate_status_alert <- function(imminent_prob, age_week, mean_ci,
|
||||||
# Priority 1: Ready for harvest-check
|
four_week_trend, weekly_ci_change, cv) {
|
||||||
|
|
||||||
|
# Priority 1: HARVEST READY - highest business priority
|
||||||
|
# Field is mature (≥12 months) AND harvest model predicts imminent harvest
|
||||||
if (!is.na(imminent_prob) && imminent_prob > 0.5 && !is.na(age_week) && age_week >= 52) {
|
if (!is.na(imminent_prob) && imminent_prob > 0.5 && !is.na(age_week) && age_week >= 52) {
|
||||||
return("harvest_ready")
|
return("harvest_ready")
|
||||||
}
|
}
|
||||||
|
|
||||||
# Priority 2: Strong decline (using configurable threshold)
|
# Priority 2: HARVESTED/BARE - indicator of completion (or unexpected harvest)
|
||||||
if (!is.na(weekly_ci_change) && weekly_ci_change <= CI_CHANGE_DECLINE_THRESHOLD && !is.na(mean_ci) && mean_ci > 1.5) {
|
# Mean CI dropped below vegetative threshold
|
||||||
return("decline_stress")
|
|
||||||
}
|
|
||||||
|
|
||||||
# Priority 3: Harvested/bare
|
|
||||||
if (!is.na(mean_ci) && mean_ci < 1.5) {
|
if (!is.na(mean_ci) && mean_ci < 1.5) {
|
||||||
return("harvested_bare")
|
return("harvested_bare")
|
||||||
}
|
}
|
||||||
|
|
||||||
# Fallback: no alert
|
# Priority 3: STRESS DETECTED - consistent health decline (weekly trend)
|
||||||
|
# Uses Four_week_trend (smooth trend) NOT daily fluctuation to avoid noise
|
||||||
|
# Crop declining but not yet bare → opportunity to investigate
|
||||||
|
if (!is.na(four_week_trend) && four_week_trend < -0.3 &&
|
||||||
|
!is.na(mean_ci) && mean_ci > 1.5) {
|
||||||
|
return("stress_detected")
|
||||||
|
}
|
||||||
|
|
||||||
|
# Priority 4: GERMINATION DELAYED - early warning for young fields
|
||||||
|
# Age 4-8 weeks is typical germination window; low CI = slow start
|
||||||
|
if (!is.na(age_week) && age_week >= 4 && age_week <= 8 &&
|
||||||
|
!is.na(mean_ci) && mean_ci < 1.5) {
|
||||||
|
return("germination_delayed")
|
||||||
|
}
|
||||||
|
|
||||||
|
# Priority 5: GROWTH ON TRACK - positive operational status
|
||||||
|
# Field is healthy, uniform, and growing steadily (no action needed)
|
||||||
|
# Conditions: good uniformity (CV < 0.15) AND stable/positive weekly trend
|
||||||
|
if (!is.na(cv) && cv < 0.15 &&
|
||||||
|
!is.na(four_week_trend) && four_week_trend >= -0.2 &&
|
||||||
|
!is.na(weekly_ci_change) && weekly_ci_change >= -0.2) {
|
||||||
|
return("growth_on_track")
|
||||||
|
}
|
||||||
|
|
||||||
|
# Default: NORMAL GROWTH (no specific alert)
|
||||||
|
# Field is growing but may have minor variability; continues normal monitoring
|
||||||
NA_character_
|
NA_character_
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -290,14 +325,16 @@ calculate_all_field_kpis <- function(current_stats,
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
# Column 14: Status_Alert (multi-priority logic)
|
# Column 14: Status_Alert (multi-priority logic for harvest/milling workflow)
|
||||||
Status_Alert = {
|
Status_Alert = {
|
||||||
sapply(seq_len(nrow(current_stats)), function(idx) {
|
sapply(seq_len(nrow(current_stats)), function(idx) {
|
||||||
calculate_status_alert(
|
calculate_status_alert(
|
||||||
Imminent_prob[idx],
|
imminent_prob = Imminent_prob[idx],
|
||||||
Age_week[idx],
|
age_week = Age_week[idx],
|
||||||
Weekly_ci_change[idx],
|
mean_ci = Mean_CI[idx],
|
||||||
Mean_CI[idx]
|
four_week_trend = Four_week_trend[idx],
|
||||||
|
weekly_ci_change = Weekly_ci_change[idx],
|
||||||
|
cv = CV[idx]
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -111,7 +111,7 @@ safe_log(paste("weekly_CI_mosaic path:", weekly_CI_mosaic))
|
||||||
# NO workspace-wide fallback that might load wrong project
|
# NO workspace-wide fallback that might load wrong project
|
||||||
|
|
||||||
# Build expected KPI file path strictly from project_dir
|
# Build expected KPI file path strictly from project_dir
|
||||||
kpi_data_dir <- file.path(paths$reports_dir, "kpis") # Should be: laravel_app/storage/app/{project}/reports/kpis
|
kpi_data_dir <- paths$kpi_reports_dir # file.path(paths$reports_dir, "kpis") # Should be: laravel_app/storage/app/{project}/reports/kpis
|
||||||
|
|
||||||
# Calculate week from report_date
|
# Calculate week from report_date
|
||||||
current_week <- as.numeric(format(as.Date(report_date), "%V"))
|
current_week <- as.numeric(format(as.Date(report_date), "%V"))
|
||||||
|
|
|
||||||
|
|
@ -144,12 +144,12 @@ week_suffix <- paste0("week", sprintf("%02d", current_week), "_", current_year)
|
||||||
|
|
||||||
# Candidate filenames we expect (exact and common variants)
|
# Candidate filenames we expect (exact and common variants)
|
||||||
expected_summary_names <- c(
|
expected_summary_names <- c(
|
||||||
paste0(project_dir, "_kpi_summary_tables_", week_suffix, ".rds"),
|
paste0(project_dir, "_field_analysis_", week_suffix, ".rds"),
|
||||||
paste0(project_dir, "_kpi_summary_tables_", date_suffix, ".rds"),
|
paste0(project_dir, "_field_analysis_", date_suffix, ".rds"),
|
||||||
paste0(project_dir, "_kpi_summary_tables.rds"),
|
paste0(project_dir, "_field_analysis.rds"),
|
||||||
"kpi_summary_tables.rds",
|
"field_analysis.rds",
|
||||||
paste0("kpi_summary_tables_", week_suffix, ".rds"),
|
paste0("field_analysis_", week_suffix, ".rds"),
|
||||||
paste0("kpi_summary_tables_", date_suffix, ".rds")
|
paste0("field_analysis_", date_suffix, ".rds")
|
||||||
)
|
)
|
||||||
|
|
||||||
expected_field_details_names <- c(
|
expected_field_details_names <- c(
|
||||||
|
|
@ -169,13 +169,26 @@ try_load_from_dir <- function(dir, candidates) {
|
||||||
return(NULL)
|
return(NULL)
|
||||||
}
|
}
|
||||||
|
|
||||||
# Try primary directory first
|
# Try primary directory first (field_level/)
|
||||||
summary_file <- try_load_from_dir(kpi_data_dir, expected_summary_names)
|
summary_file <- try_load_from_dir(kpi_data_dir, expected_summary_names)
|
||||||
field_details_file <- try_load_from_dir(kpi_data_dir, expected_field_details_names)
|
field_details_file <- try_load_from_dir(kpi_data_dir, expected_field_details_names)
|
||||||
|
|
||||||
# If not found, perform a workspace-wide search (slower) limited to laravel_app storage
|
# If not found, try parent directory (kpis/) where RDS is often saved by Script 80
|
||||||
if (is.null(summary_file) || is.null(field_details_file)) {
|
if (is.null(summary_file) || is.null(field_details_file)) {
|
||||||
safe_log(paste("KPI files not found in", kpi_data_dir, "—searching workspace for RDS files"))
|
parent_dir <- dirname(kpi_data_dir) # One level up: reports/kpis/
|
||||||
|
safe_log(paste("KPI files not found in", kpi_data_dir, "—trying parent directory:", parent_dir))
|
||||||
|
|
||||||
|
if (is.null(summary_file)) {
|
||||||
|
summary_file <- try_load_from_dir(parent_dir, expected_summary_names)
|
||||||
|
}
|
||||||
|
if (is.null(field_details_file)) {
|
||||||
|
field_details_file <- try_load_from_dir(parent_dir, expected_field_details_names)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# If still not found, perform a workspace-wide search (slower) limited to laravel_app storage
|
||||||
|
if (is.null(summary_file) || is.null(field_details_file)) {
|
||||||
|
safe_log(paste("KPI files not found in", kpi_data_dir, "or parent directory—searching workspace for RDS files"))
|
||||||
# List rds files under laravel_app/storage/app recursively
|
# List rds files under laravel_app/storage/app recursively
|
||||||
files <- list.files(path = file.path("laravel_app", "storage", "app"), pattern = "\\.rds$", recursive = TRUE, full.names = TRUE)
|
files <- list.files(path = file.path("laravel_app", "storage", "app"), pattern = "\\.rds$", recursive = TRUE, full.names = TRUE)
|
||||||
# Try to match by expected names
|
# Try to match by expected names
|
||||||
|
|
@ -487,7 +500,7 @@ if (exists("summary_data") && !is.null(summary_data) && "field_analysis" %in% na
|
||||||
|
|
||||||
## 1.1 Overview of cane supply area, showing zones with number of acres being harvest ready
|
## 1.1 Overview of cane supply area, showing zones with number of acres being harvest ready
|
||||||
|
|
||||||
```{r overview_map, fig.width=8, fig.height=7, fig.align="center", echo=FALSE, message=FALSE, warning=FALSE}
|
```{r overview_map, fig.width=7, fig.height=6, fig.align="center", echo=FALSE, message=FALSE, warning=FALSE}
|
||||||
# Create a hexbin overview map with ggplot
|
# Create a hexbin overview map with ggplot
|
||||||
tryCatch({
|
tryCatch({
|
||||||
# Use per-field field_analysis data from RDS (already loaded in load_kpi_data chunk)
|
# Use per-field field_analysis data from RDS (already loaded in load_kpi_data chunk)
|
||||||
|
|
@ -647,8 +660,8 @@ tryCatch({
|
||||||
## 1.2 Key Performance Indicators
|
## 1.2 Key Performance Indicators
|
||||||
|
|
||||||
```{r combined_kpi_table, echo=FALSE, results='asis'}
|
```{r combined_kpi_table, echo=FALSE, results='asis'}
|
||||||
# Create summary KPI table from field_analysis_summary data
|
# Create consolidated KPI table from field_analysis data
|
||||||
# This shows: Phases, Triggers, Area Change, and Total Farm acreage
|
# Shows: Phases, Triggers, Area Change, Cloud Influence, and Total Farm
|
||||||
|
|
||||||
if (exists("summary_data") && !is.null(summary_data) && "field_analysis" %in% names(summary_data)) {
|
if (exists("summary_data") && !is.null(summary_data) && "field_analysis" %in% names(summary_data)) {
|
||||||
# Load field analysis data
|
# Load field analysis data
|
||||||
|
|
@ -659,24 +672,46 @@ if (exists("summary_data") && !is.null(summary_data) && "field_analysis" %in% na
|
||||||
!is.data.frame(summary_data$field_analysis_summary)) {
|
!is.data.frame(summary_data$field_analysis_summary)) {
|
||||||
|
|
||||||
# Create summary by aggregating by Status_Alert and Phase categories
|
# Create summary by aggregating by Status_Alert and Phase categories
|
||||||
# This groups fields by their phase and status to show distribution
|
|
||||||
phase_summary <- field_analysis_df %>%
|
phase_summary <- field_analysis_df %>%
|
||||||
filter(!is.na(Phase)) %>%
|
filter(!is.na(Phase)) %>%
|
||||||
group_by(Phase) %>%
|
group_by(Phase) %>%
|
||||||
summarise(Acreage = sum(Acreage, na.rm = TRUE), .groups = "drop") %>%
|
summarise(
|
||||||
|
Acreage = sum(Acreage, na.rm = TRUE),
|
||||||
|
Field_count = n_distinct(Field_id),
|
||||||
|
.groups = "drop"
|
||||||
|
) %>%
|
||||||
mutate(Category = Phase) %>%
|
mutate(Category = Phase) %>%
|
||||||
select(Category, Acreage)
|
select(Category, Acreage, Field_count)
|
||||||
|
|
||||||
# Try to create Status trigger summary - use Status_Alert if available, otherwise use empty
|
# Create Status trigger summary - includes both active alerts and "No active triggers"
|
||||||
trigger_summary <- tryCatch({
|
trigger_summary <- tryCatch({
|
||||||
field_analysis_df %>%
|
# Active alerts (fields with non-NA Status_Alert)
|
||||||
|
active_alerts <- field_analysis_df %>%
|
||||||
filter(!is.na(Status_Alert), Status_Alert != "") %>%
|
filter(!is.na(Status_Alert), Status_Alert != "") %>%
|
||||||
group_by(Status_Alert) %>%
|
group_by(Status_Alert) %>%
|
||||||
summarise(Acreage = sum(Acreage, na.rm = TRUE), .groups = "drop") %>%
|
summarise(
|
||||||
|
Acreage = sum(Acreage, na.rm = TRUE),
|
||||||
|
Field_count = n_distinct(Field_id),
|
||||||
|
.groups = "drop"
|
||||||
|
) %>%
|
||||||
mutate(Category = Status_Alert) %>%
|
mutate(Category = Status_Alert) %>%
|
||||||
select(Category, Acreage)
|
select(Category, Acreage, Field_count)
|
||||||
|
|
||||||
|
# No active triggers (fields with NA Status_Alert)
|
||||||
|
no_alerts <- field_analysis_df %>%
|
||||||
|
filter(is.na(Status_Alert) | Status_Alert == "") %>%
|
||||||
|
summarise(
|
||||||
|
Acreage = sum(Acreage, na.rm = TRUE),
|
||||||
|
Field_count = n_distinct(Field_id),
|
||||||
|
.groups = "drop"
|
||||||
|
) %>%
|
||||||
|
mutate(Category = "No active triggers") %>%
|
||||||
|
select(Category, Acreage, Field_count)
|
||||||
|
|
||||||
|
# Combine active alerts and no-alert fields
|
||||||
|
bind_rows(active_alerts, no_alerts)
|
||||||
}, error = function(e) {
|
}, error = function(e) {
|
||||||
data.frame(Category = character(), Acreage = numeric())
|
data.frame(Category = character(), Acreage = numeric(), Field_count = numeric())
|
||||||
})
|
})
|
||||||
|
|
||||||
# Combine into summary
|
# Combine into summary
|
||||||
|
|
@ -689,25 +724,38 @@ if (exists("summary_data") && !is.null(summary_data) && "field_analysis" %in% na
|
||||||
|
|
||||||
# Phase names and trigger names to extract from summary
|
# Phase names and trigger names to extract from summary
|
||||||
phase_names <- c("Germination", "Tillering", "Grand Growth", "Maturation", "Unknown Phase")
|
phase_names <- c("Germination", "Tillering", "Grand Growth", "Maturation", "Unknown Phase")
|
||||||
trigger_names <- c("Harvest Ready", "Strong Recovery", "Growth On Track", "Stress Detected",
|
# Trigger names now include both active alerts AND "No active triggers" (calculated dynamically above)
|
||||||
"Germination Complete", "Germination Started", "No Active Trigger",
|
trigger_names <- c("harvest_ready", "harvested_bare", "stress_detected",
|
||||||
"Ready for harvest-check", "Strong decline in crop health", "Harvested/bare")
|
"germination_delayed", "growth_on_track", "No active triggers")
|
||||||
|
|
||||||
# Extract phase distribution - match on category names directly
|
# Extract phase distribution - match on category names directly
|
||||||
if (!is.null(field_analysis_summary) && nrow(field_analysis_summary) > 0) {
|
if (!is.null(field_analysis_summary) && nrow(field_analysis_summary) > 0) {
|
||||||
|
# Phase rows with field count
|
||||||
phase_rows <- field_analysis_summary %>%
|
phase_rows <- field_analysis_summary %>%
|
||||||
filter(Category %in% phase_names) %>%
|
filter(Category %in% phase_names) %>%
|
||||||
select(Category, Acreage) %>%
|
select(Category, Acreage, Field_count) %>%
|
||||||
mutate(KPI_Group = "PHASE DISTRIBUTION", .before = 1)
|
mutate(KPI_Group = "PHASE DISTRIBUTION", .before = 1)
|
||||||
|
|
||||||
# Extract status triggers - match on category names directly
|
# Trigger rows with field count
|
||||||
trigger_rows <- field_analysis_summary %>%
|
trigger_rows <- field_analysis_summary %>%
|
||||||
filter(Category %in% trigger_names) %>%
|
filter(Category %in% trigger_names) %>%
|
||||||
select(Category, Acreage) %>%
|
select(Category, Acreage, Field_count) %>%
|
||||||
mutate(KPI_Group = "STATUS TRIGGERS", .before = 1)
|
mutate(KPI_Group = "OPERATIONAL ALERTS", .before = 1)
|
||||||
|
|
||||||
|
# If no triggers found, add a placeholder row
|
||||||
|
if (nrow(trigger_rows) == 0) {
|
||||||
|
trigger_rows <- data.frame(
|
||||||
|
KPI_Group = "OPERATIONAL ALERTS",
|
||||||
|
Category = "No active triggers",
|
||||||
|
Acreage = 0,
|
||||||
|
Field_count = 0,
|
||||||
|
stringsAsFactors = FALSE
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
# Calculate area change from field_analysis data
|
# Calculate area change from field_analysis data
|
||||||
total_acreage <- sum(field_analysis_df$Acreage, na.rm = TRUE)
|
total_acreage <- sum(field_analysis_df$Acreage, na.rm = TRUE)
|
||||||
|
total_fields <- n_distinct(field_analysis_df$Field_id)
|
||||||
|
|
||||||
# Parse Weekly_ci_change to determine improvement/decline
|
# Parse Weekly_ci_change to determine improvement/decline
|
||||||
parse_ci_change <- function(change_str) {
|
parse_ci_change <- function(change_str) {
|
||||||
|
|
@ -721,10 +769,20 @@ if (exists("summary_data") && !is.null(summary_data) && "field_analysis" %in% na
|
||||||
|
|
||||||
field_analysis_df$ci_change_numeric <- sapply(field_analysis_df$Weekly_ci_change, parse_ci_change)
|
field_analysis_df$ci_change_numeric <- sapply(field_analysis_df$Weekly_ci_change, parse_ci_change)
|
||||||
|
|
||||||
improving_acreage <- sum(field_analysis_df$Acreage[field_analysis_df$ci_change_numeric > 0.2], na.rm = TRUE)
|
# Area change rows with field count
|
||||||
declining_acreage <- sum(field_analysis_df$Acreage[field_analysis_df$ci_change_numeric < -0.2], na.rm = TRUE)
|
improving_df <- field_analysis_df %>%
|
||||||
stable_acreage <- sum(field_analysis_df$Acreage[field_analysis_df$ci_change_numeric >= -0.2 &
|
filter(ci_change_numeric > 0.2)
|
||||||
field_analysis_df$ci_change_numeric <= 0.2], na.rm = TRUE)
|
stable_df <- field_analysis_df %>%
|
||||||
|
filter(ci_change_numeric >= -0.2 & ci_change_numeric <= 0.2)
|
||||||
|
declining_df <- field_analysis_df %>%
|
||||||
|
filter(ci_change_numeric < -0.2)
|
||||||
|
|
||||||
|
improving_acreage <- sum(improving_df$Acreage, na.rm = TRUE)
|
||||||
|
improving_field_count <- n_distinct(improving_df$Field_id)
|
||||||
|
stable_acreage <- sum(stable_df$Acreage, na.rm = TRUE)
|
||||||
|
stable_field_count <- n_distinct(stable_df$Field_id)
|
||||||
|
declining_acreage <- sum(declining_df$Acreage, na.rm = TRUE)
|
||||||
|
declining_field_count <- n_distinct(declining_df$Field_id)
|
||||||
|
|
||||||
improving_pct <- ifelse(total_acreage > 0, round(improving_acreage / total_acreage * 100, 1), 0)
|
improving_pct <- ifelse(total_acreage > 0, round(improving_acreage / total_acreage * 100, 1), 0)
|
||||||
declining_pct <- ifelse(total_acreage > 0, round(declining_acreage / total_acreage * 100, 1), 0)
|
declining_pct <- ifelse(total_acreage > 0, round(declining_acreage / total_acreage * 100, 1), 0)
|
||||||
|
|
@ -741,24 +799,54 @@ if (exists("summary_data") && !is.null(summary_data) && "field_analysis" %in% na
|
||||||
KPI_Group = "AREA CHANGE",
|
KPI_Group = "AREA CHANGE",
|
||||||
Category = c("Improving", "Stable", "Declining"),
|
Category = c("Improving", "Stable", "Declining"),
|
||||||
Acreage = c(round(improving_acreage, 2), round(stable_acreage, 2), round(declining_acreage, 2)),
|
Acreage = c(round(improving_acreage, 2), round(stable_acreage, 2), round(declining_acreage, 2)),
|
||||||
|
Field_count = c(improving_field_count, stable_field_count, declining_field_count),
|
||||||
Percent = c(paste0(improving_pct, "%"), paste0(stable_pct, "%"), paste0(declining_pct, "%")),
|
Percent = c(paste0(improving_pct, "%"), paste0(stable_pct, "%"), paste0(declining_pct, "%")),
|
||||||
stringsAsFactors = FALSE
|
stringsAsFactors = FALSE
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Cloud influence rows with field count - aggregate by Cloud_category
|
||||||
|
cloud_rows <- tryCatch({
|
||||||
|
field_analysis_df %>%
|
||||||
|
filter(!is.na(Cloud_category)) %>%
|
||||||
|
group_by(Cloud_category) %>%
|
||||||
|
summarise(
|
||||||
|
Acreage = sum(Acreage, na.rm = TRUE),
|
||||||
|
Field_count = n_distinct(Field_id),
|
||||||
|
.groups = "drop"
|
||||||
|
) %>%
|
||||||
|
mutate(
|
||||||
|
KPI_Group = "CLOUD INFLUENCE",
|
||||||
|
Category = Cloud_category,
|
||||||
|
Percent = paste0(round(Acreage / total_acreage * 100, 1), "%"),
|
||||||
|
Acreage = round(Acreage, 2)
|
||||||
|
) %>%
|
||||||
|
select(KPI_Group, Category, Acreage, Field_count, Percent)
|
||||||
|
}, error = function(e) {
|
||||||
|
data.frame(
|
||||||
|
KPI_Group = character(),
|
||||||
|
Category = character(),
|
||||||
|
Acreage = numeric(),
|
||||||
|
Field_count = numeric(),
|
||||||
|
Percent = character()
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
# Total farm row
|
# Total farm row
|
||||||
total_row <- data.frame(
|
total_row <- data.frame(
|
||||||
KPI_Group = "TOTAL FARM",
|
KPI_Group = "TOTAL FARM",
|
||||||
Category = "Total Acreage",
|
Category = "Total Acreage",
|
||||||
Acreage = round(total_acreage, 2),
|
Acreage = round(total_acreage, 2),
|
||||||
|
Field_count = total_fields,
|
||||||
Percent = "100%",
|
Percent = "100%",
|
||||||
stringsAsFactors = FALSE
|
stringsAsFactors = FALSE
|
||||||
)
|
)
|
||||||
|
|
||||||
# Combine all rows with percentages for all
|
# Combine all rows
|
||||||
combined_df <- bind_rows(
|
combined_df <- bind_rows(
|
||||||
phase_pcts,
|
phase_pcts,
|
||||||
trigger_pcts,
|
trigger_pcts,
|
||||||
area_change_rows,
|
area_change_rows,
|
||||||
|
cloud_rows,
|
||||||
total_row
|
total_row
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -769,7 +857,7 @@ if (exists("summary_data") && !is.null(summary_data) && "field_analysis" %in% na
|
||||||
KPI_display = if_else(row_number() == 1, KPI_Group, "")
|
KPI_display = if_else(row_number() == 1, KPI_Group, "")
|
||||||
) %>%
|
) %>%
|
||||||
ungroup() %>%
|
ungroup() %>%
|
||||||
select(KPI_display, Category, Acreage, Percent)
|
select(KPI_display, Category, Acreage, Percent, Field_count)
|
||||||
|
|
||||||
# Render as flextable with merged cells
|
# Render as flextable with merged cells
|
||||||
ft <- flextable(combined_df) %>%
|
ft <- flextable(combined_df) %>%
|
||||||
|
|
@ -777,7 +865,8 @@ if (exists("summary_data") && !is.null(summary_data) && "field_analysis" %in% na
|
||||||
KPI_display = "KPI Category",
|
KPI_display = "KPI Category",
|
||||||
Category = "Item",
|
Category = "Item",
|
||||||
Acreage = "Acreage",
|
Acreage = "Acreage",
|
||||||
Percent = "Percent"
|
Percent = "Percentage of total fields",
|
||||||
|
Field_count = "# Fields"
|
||||||
) %>%
|
) %>%
|
||||||
merge_v(j = "KPI_display") %>%
|
merge_v(j = "KPI_display") %>%
|
||||||
autofit()
|
autofit()
|
||||||
|
|
@ -787,8 +876,9 @@ if (exists("summary_data") && !is.null(summary_data) && "field_analysis" %in% na
|
||||||
phase_count <- nrow(phase_rows)
|
phase_count <- nrow(phase_rows)
|
||||||
trigger_count <- nrow(trigger_rows)
|
trigger_count <- nrow(trigger_rows)
|
||||||
area_count <- nrow(area_change_rows)
|
area_count <- nrow(area_change_rows)
|
||||||
|
cloud_count <- nrow(cloud_rows)
|
||||||
|
|
||||||
# Add lines after phases, triggers, and area change groups (before totals)
|
# Add lines after phases, triggers, area change, and cloud groups (before totals)
|
||||||
if (phase_count > 0) {
|
if (phase_count > 0) {
|
||||||
ft <- ft %>% hline(i = phase_count, border = officer::fp_border(width = 1))
|
ft <- ft %>% hline(i = phase_count, border = officer::fp_border(width = 1))
|
||||||
}
|
}
|
||||||
|
|
@ -798,6 +888,9 @@ if (exists("summary_data") && !is.null(summary_data) && "field_analysis" %in% na
|
||||||
if (area_count > 0) {
|
if (area_count > 0) {
|
||||||
ft <- ft %>% hline(i = phase_count + trigger_count + area_count, border = officer::fp_border(width = 1))
|
ft <- ft %>% hline(i = phase_count + trigger_count + area_count, border = officer::fp_border(width = 1))
|
||||||
}
|
}
|
||||||
|
if (cloud_count > 0) {
|
||||||
|
ft <- ft %>% hline(i = phase_count + trigger_count + area_count + cloud_count, border = officer::fp_border(width = 1))
|
||||||
|
}
|
||||||
|
|
||||||
ft
|
ft
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -808,40 +901,6 @@ if (exists("summary_data") && !is.null(summary_data) && "field_analysis" %in% na
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
## Cloud Coverage Summary
|
|
||||||
|
|
||||||
```{r cloud_coverage_summary, echo=FALSE}
|
|
||||||
# Display cloud coverage summary aggregated by category
|
|
||||||
# Cloud coverage data is included in the field_analysis RDS from Script 80
|
|
||||||
if (exists("summary_data") && !is.null(summary_data) && "field_analysis" %in% names(summary_data)) {
|
|
||||||
field_analysis_df <- summary_data$field_analysis
|
|
||||||
|
|
||||||
# Aggregate cloud coverage by category
|
|
||||||
cloud_summary <- field_analysis_df %>%
|
|
||||||
filter(!is.na(Cloud_category)) %>%
|
|
||||||
group_by(Cloud_category) %>%
|
|
||||||
summarise(
|
|
||||||
"Number of Fields" = n(),
|
|
||||||
"Total Acreage" = round(sum(Acreage, na.rm = TRUE), 1),
|
|
||||||
.groups = "drop"
|
|
||||||
) %>%
|
|
||||||
rename("Cloud Category" = Cloud_category) %>%
|
|
||||||
arrange(`Cloud Category`)
|
|
||||||
if (nrow(cloud_summary) > 0) {
|
|
||||||
# Create flextable
|
|
||||||
ft <- flextable(cloud_summary) %>%
|
|
||||||
autofit() %>%
|
|
||||||
theme_vanilla()
|
|
||||||
|
|
||||||
ft
|
|
||||||
} else {
|
|
||||||
cat("Cloud coverage data not available for summary.\n")
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
cat("Field analysis data not available for cloud coverage summary.\n")
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
```{r data, message=TRUE, warning=TRUE, include=FALSE}
|
```{r data, message=TRUE, warning=TRUE, include=FALSE}
|
||||||
# All data comes from the field analysis performed in 09_field_analysis_weekly.R
|
# All data comes from the field analysis performed in 09_field_analysis_weekly.R
|
||||||
# The report renders KPI tables and field summaries from that data
|
# The report renders KPI tables and field summaries from that data
|
||||||
|
|
@ -1005,15 +1064,42 @@ Both algorithms are not always in sync, and can have contradictory results. Wide
|
||||||
## Report Metadata
|
## Report Metadata
|
||||||
|
|
||||||
```{r report_metadata, echo=FALSE}
|
```{r report_metadata, echo=FALSE}
|
||||||
|
# Calculate total area from field analysis data
|
||||||
|
total_area_acres <- 0
|
||||||
|
if (exists("summary_data") && !is.null(summary_data) && "field_analysis" %in% names(summary_data)) {
|
||||||
|
total_area_acres <- sum(summary_data$field_analysis$Acreage, na.rm = TRUE)
|
||||||
|
}
|
||||||
|
|
||||||
|
# Calculate total fields
|
||||||
|
total_fields_count <- 0
|
||||||
|
if (exists("AllPivots0")) {
|
||||||
|
total_fields_count <- nrow(AllPivots0 %>% filter(!is.na(field)) %>% group_by(field) %>% summarise())
|
||||||
|
} else if (exists("summary_data") && !is.null(summary_data) && "field_analysis" %in% names(summary_data)) {
|
||||||
|
total_fields_count <- n_distinct(summary_data$field_analysis$Field_id)
|
||||||
|
}
|
||||||
|
|
||||||
metadata_info <- data.frame(
|
metadata_info <- data.frame(
|
||||||
Metric = c("Report Generated", "Data Source", "Analysis Period", "Total Fields", "Next Update"),
|
Metric = c(
|
||||||
|
"Report Generated",
|
||||||
|
"Data Source",
|
||||||
|
"Analysis Period",
|
||||||
|
"Total Fields [number]",
|
||||||
|
"Total Area [acres]",
|
||||||
|
"Next Update",
|
||||||
|
"Service provided",
|
||||||
|
"Starting date service"
|
||||||
|
),
|
||||||
Value = c(
|
Value = c(
|
||||||
format(Sys.time(), "%Y-%m-%d %H:%M:%S"),
|
format(Sys.time(), "%Y-%m-%d %H:%M:%S"),
|
||||||
paste("Project", toupper(project_dir)),
|
paste("Project", toupper(project_dir)),
|
||||||
paste("Week", current_week, "of", year),
|
paste("Week", current_week, "of", year),
|
||||||
ifelse(exists("AllPivots0"), nrow(AllPivots0 %>% filter(!is.na(field)) %>% group_by(field) %>% summarise()), "Unknown"),
|
ifelse(total_fields_count > 0, total_fields_count, "Unknown"),
|
||||||
"Next Wednesday"
|
ifelse(total_area_acres > 0, round(total_area_acres, 0), "Unknown"),
|
||||||
)
|
"Next Wednesday",
|
||||||
|
"Cane Supply Office - Weekly",
|
||||||
|
"23 dec 2025"
|
||||||
|
),
|
||||||
|
stringsAsFactors = FALSE
|
||||||
)
|
)
|
||||||
|
|
||||||
ft <- flextable(metadata_info) %>%
|
ft <- flextable(metadata_info) %>%
|
||||||
|
|
|
||||||
|
|
@ -173,9 +173,10 @@ setup_project_directories <- function(project_dir, data_source = "merged_tif") {
|
||||||
|
|
||||||
# TIER 6: KPI & REPORTING (Scripts 80/90/91 output)
|
# TIER 6: KPI & REPORTING (Scripts 80/90/91 output)
|
||||||
reports_dir <- here(laravel_storage_dir, "reports")
|
reports_dir <- here(laravel_storage_dir, "reports")
|
||||||
kpi_reports_dir <- here(reports_dir, "kpis", "field_level")
|
kpi_reports_dir <- here(reports_dir, "kpis")
|
||||||
kpi_field_stats_dir <- here(reports_dir, "kpis", "field_stats")
|
#kpi_reports_dir <- here(reports_dir, "kpis", "field_level")
|
||||||
kpi_field_analysis_dir <- here(reports_dir, "kpis", "field_analysis")
|
#kpi_field_stats_dir <- here(reports_dir, "kpis", "field_stats")
|
||||||
|
#kpi_field_analysis_dir <- here(reports_dir, "kpis", "field_analysis")
|
||||||
|
|
||||||
# TIER 7: SUPPORT (various scripts)
|
# TIER 7: SUPPORT (various scripts)
|
||||||
vrt_dir <- here(data_dir, "vrt") # Virtual Raster files created during CI extraction
|
vrt_dir <- here(data_dir, "vrt") # Virtual Raster files created during CI extraction
|
||||||
|
|
@ -188,7 +189,7 @@ setup_project_directories <- function(project_dir, data_source = "merged_tif") {
|
||||||
extracted_ci_base_dir, daily_ci_vals_dir, cumulative_ci_vals_dir, ci_for_python_dir,
|
extracted_ci_base_dir, daily_ci_vals_dir, cumulative_ci_vals_dir, ci_for_python_dir,
|
||||||
growth_model_interpolated_dir,
|
growth_model_interpolated_dir,
|
||||||
weekly_mosaic_dir,
|
weekly_mosaic_dir,
|
||||||
reports_dir, kpi_reports_dir, kpi_field_stats_dir, kpi_field_analysis_dir,
|
reports_dir, kpi_reports_dir, #kpi_field_stats_dir, kpi_field_analysis_dir,
|
||||||
data_dir, vrt_dir, harvest_dir, log_dir
|
data_dir, vrt_dir, harvest_dir, log_dir
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -227,8 +228,8 @@ setup_project_directories <- function(project_dir, data_source = "merged_tif") {
|
||||||
# TIER 6: KPI & reporting
|
# TIER 6: KPI & reporting
|
||||||
reports_dir = reports_dir,
|
reports_dir = reports_dir,
|
||||||
kpi_reports_dir = kpi_reports_dir,
|
kpi_reports_dir = kpi_reports_dir,
|
||||||
kpi_field_stats_dir = kpi_field_stats_dir,
|
#kpi_field_stats_dir = kpi_field_stats_dir,
|
||||||
kpi_field_analysis_dir = kpi_field_analysis_dir,
|
#kpi_field_analysis_dir = kpi_field_analysis_dir,
|
||||||
|
|
||||||
# TIER 7: Support
|
# TIER 7: Support
|
||||||
data_dir = data_dir,
|
data_dir = data_dir,
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue