# =============================================================================
# HEADER
# -----------------------------------------------------------------------------
# Student name:   Suhaana Kapali
# Student number: 2538421
# Module code:    ACCA7022
# Module:         Advanced Statistical Programming
# Assessment:     Data Manipulation (Task 1: R Code) - Smart Warehouse
#                 Inventory Simulation
# Script name:    warehouse_manipulation.r
# Date:           20/10/2026

# PURPOSE
#   1. Generate a simulated warehouse dataset (28 days x 5 zones x 6 categories)
#      using nested loops.
#   2. Summarise it with loops only, as required by the brief:
#        B. total units per zone
#        C. average processing time per product category
#        D. anomaly detection (+/- 15% of the daily zone average)
#        E. most productive day for each zone
#   3. Run loop-based validation checks on the results.
#


# SECTION 0: SETUP - random seed and constants

# Fix the random number generator so the "random" variation is the same every
# time the script runs. This makes results reproducible and checkable.
set.seed(7022)

# Constants taken directly from the assessment specification. Storing them in
# named variables (rather than typing 100, 0.85, 28... repeatedly) makes the
# code readable and means each value only has to be changed in one place.
base_units     <- 100    # BaseUnits in the UnitsProcessed formula
time_per_unit  <- 0.85   # TimePerUnit: minutes needed per unit
num_days       <- 28     # days of warehouse operation
num_zones      <- 5      # warehouse zones
num_categories <- 6      # product categories



# TASK A: GENERATE WAREHOUSE DATA (nested loops)


# One row is needed for every Day x Zone x Category combination:
# 28 * 5 * 6 = 840 rows.
total_rows <- num_days * num_zones * num_categories

# Create the dataframe at its full size BEFORE the loops ("pre-allocation").
# Growing a dataframe with rbind() inside a loop copies it every iteration,
# which is slow; filling a pre-sized dataframe avoids this.
# The column types are chosen to match the data:
#   integer for IDs, numeric for units and time.
warehouse_data <- data.frame(
  Day            = integer(total_rows),
  Zone           = integer(total_rows),
  Category       = integer(total_rows),
  UnitsProcessed = numeric(total_rows),
  ProcessingTime = numeric(total_rows)
)

# row_index records which row of warehouse_data is being filled next.
# It starts at 1 and increases by 1 for every record created.
row_index <- 1

# Three nested loops: every day, then every zone in that day, then every
# category in that zone. The innermost block therefore runs exactly 840 times,
# once per unique combination.
for (day in 1:num_days) {
  for (zone in 1:num_zones) {
    for (category in 1:num_categories) {
      
      # RandomVariation: one random integer from -12 to +12 inclusive.
      random_variation <- sample(-12:12, 1)
      
      # UnitsProcessed = BaseUnits + (Zone * 8) + (Category * 5) + RandomVariation
      units <- base_units + (zone * 8) + (category * 5) + random_variation
      
      # ProcessingTime = UnitsProcessed * TimePerUnit
      proc_time <- units * time_per_unit
      
      # Write the five values into the current row.
      warehouse_data$Day[row_index]            <- day
      warehouse_data$Zone[row_index]           <- zone
      warehouse_data$Category[row_index]       <- category
      warehouse_data$UnitsProcessed[row_index] <- units
      warehouse_data$ProcessingTime[row_index] <- proc_time
      
      # Move on to the next empty row.
      row_index <- row_index + 1
    }
  }
}

# Required output: first 10 rows, then the number of rows and columns.
cat("===== TASK A: Warehouse data =====\n")
print(head(warehouse_data, 10))
cat("Number of rows:   ", nrow(warehouse_data), "\n")
cat("Number of columns:", ncol(warehouse_data), "\n\n")


# =============================================================================
# TASK B: TOTAL UNITS PROCESSED PER ZONE
# =============================================================================
# Method: for each zone, reset a running total to 0, scan every record, and add
# the units of records that belong to that zone.

# Result dataframe pre-allocated with one row per zone.
zone_totals <- data.frame(
  Zone       = 1:num_zones,
  TotalUnits = numeric(num_zones)
)

for (zone in 1:num_zones) {
  
  # Reset the accumulator for each zone. Without this reset, each zone's total
  # would wrongly include the totals of the zones before it.
  running_total <- 0
  
  # Inner loop visits every one of the 840 records.
  for (i in 1:nrow(warehouse_data)) {
    
    # Only add units if the record belongs to the zone currently being summed.
    if (warehouse_data$Zone[i] == zone) {
      running_total <- running_total + warehouse_data$UnitsProcessed[i]
    }
  }
  
  # Store the finished total in the result dataframe.
  zone_totals$TotalUnits[zone] <- running_total
}

cat("===== TASK B: Total units per zone =====\n")
print(zone_totals)
cat("\n")


# =============================================================================
# TASK C: AVERAGE PROCESSING TIME PER PRODUCT CATEGORY
# =============================================================================
# Method: loops calculate BOTH the total processing time and the number of
# matching records (as required); the average is total / count.

category_average_time <- data.frame(
  Category              = 1:num_categories,
  AverageProcessingTime = numeric(num_categories)
)

for (category in 1:num_categories) {
  
  # Two accumulators, both reset for each category.
  total_time   <- 0   # sum of ProcessingTime for this category
  record_count <- 0   # how many records belong to this category
  
  for (i in 1:nrow(warehouse_data)) {
    if (warehouse_data$Category[i] == category) {
      total_time   <- total_time + warehouse_data$ProcessingTime[i]
      record_count <- record_count + 1
    }
  }
  
  # Average = total / count. Counting the records (instead of assuming 140)
  # keeps the code correct even if the dataset size changes.
  # round() to 2 decimal places for tidy display.
  category_average_time$AverageProcessingTime[category] <-
    round(total_time / record_count, 2)
}

cat("===== TASK C: Average processing time per category =====\n")
print(category_average_time)
cat("\n")



# TASK D: IDENTIFY STOCK PROCESSING ANOMALIES
# =============================================================================
# A record is an anomaly if its UnitsProcessed is more than 15% above OR more
# than 15% below the average for its Day/Zone pair.
# This needs two stages: (1) work out every Day/Zone average,
# (2) compare each record with the average for its own Day/Zone.

# ---- Step D1: Day/Zone average lookup table ----------------------------------
# A matrix is used because each average is identified by exactly two indices
# (day, zone), so it can be retrieved directly as daily_zone_avg[day, zone].
daily_zone_avg <- matrix(0, nrow = num_days, ncol = num_zones)

for (day in 1:num_days) {
  for (zone in 1:num_zones) {
    
    sum_units <- 0   # total units for this Day/Zone (across the 6 categories)
    count     <- 0   # number of records found for this Day/Zone
    
    for (i in 1:nrow(warehouse_data)) {
      # && means both conditions must be true: same day AND same zone.
      if (warehouse_data$Day[i] == day && warehouse_data$Zone[i] == zone) {
        sum_units <- sum_units + warehouse_data$UnitsProcessed[i]
        count     <- count + 1
      }
    }
    
    daily_zone_avg[day, zone] <- sum_units / count
  }
}

# ---- Step D2: create the processing_anomalies dataframe ----------------------
# Copies the identifying columns from warehouse_data and pre-allocates the two
# new columns (DailyZoneAverage numeric, Anomaly logical).
processing_anomalies <- data.frame(
  Day              = warehouse_data$Day,
  Zone             = warehouse_data$Zone,
  Category         = warehouse_data$Category,
  UnitsProcessed   = warehouse_data$UnitsProcessed,
  DailyZoneAverage = numeric(nrow(warehouse_data)),
  Anomaly          = logical(nrow(warehouse_data))
)

# ---- Step D3: test every record against its Day/Zone average -----------------
for (i in 1:nrow(processing_anomalies)) {
  
  # Look up the average for this record's Day and Zone.
  this_avg <- daily_zone_avg[processing_anomalies$Day[i],
                             processing_anomalies$Zone[i]]
  
  # Store it rounded to 2 decimals for display. The test below uses the
  # UNROUNDED value, so rounding cannot change which records are flagged.
  processing_anomalies$DailyZoneAverage[i] <- round(this_avg, 2)
  
  # || means "or": flag if more than 15% above OR more than 15% below.
  # Strict inequalities are used, exactly as written in the specification.
  if (processing_anomalies$UnitsProcessed[i] > this_avg * 1.15 ||
      processing_anomalies$UnitsProcessed[i] < this_avg * 0.85) {
    processing_anomalies$Anomaly[i] <- TRUE
  } else {
    processing_anomalies$Anomaly[i] <- FALSE
  }
}

# Required output: first 10 rows where Anomaly is TRUE (logical subsetting
# keeps only the TRUE rows; head() then takes the first 10 of those).
cat("===== TASK D: First 10 anomalies =====\n")
print(head(processing_anomalies[processing_anomalies$Anomaly == TRUE, ], 10))

# Count the anomalies with a loop (keeps to the "loops only" requirement).
anomaly_count <- 0
for (i in 1:nrow(processing_anomalies)) {
  if (processing_anomalies$Anomaly[i] == TRUE) {
    anomaly_count <- anomaly_count + 1
  }
}
cat("Total anomalies found:", anomaly_count, "of",
    nrow(processing_anomalies), "records\n\n")


# =============================================================================
# TASK E: MOST PRODUCTIVE DAY FOR EACH ZONE
# =============================================================================
# Method: for every zone, work out each day's total units, and keep track of
# the largest total seen so far ("running maximum") together with its day.

peak_zone_days <- data.frame(
  Zone       = 1:num_zones,
  Day        = integer(num_zones),
  TotalUnits = numeric(num_zones)
)

for (zone in 1:num_zones) {
  
  best_day   <- 0   # day with the highest total found so far
  best_total <- 0   # that highest total (starts at 0; all real totals exceed it)
  
  for (day in 1:num_days) {
    
    # Total units for this zone on this day (sum of its 6 category records).
    day_total <- 0
    for (i in 1:nrow(warehouse_data)) {
      if (warehouse_data$Zone[i] == zone && warehouse_data$Day[i] == day) {
        day_total <- day_total + warehouse_data$UnitsProcessed[i]
      }
    }
    
    # Keep this day only if it beats the best so far. Using > (not >=) means
    # that if two days tie, the EARLIER day is kept.
    if (day_total > best_total) {
      best_total <- day_total
      best_day   <- day
    }
  }
  
  peak_zone_days$Day[zone]        <- best_day
  peak_zone_days$TotalUnits[zone] <- best_total
}

cat("===== TASK E: Peak day for each zone =====\n")
print(peak_zone_days)
cat("\n")



# VALIDATION CHECKS (loop-based self-tests; evidence for the testing section)

# Each check counts problems found; 0 problems = PASS.

cat("\n===== VALIDATION CHECKS =====\n")

# CHECK 1: each Day/Zone/Category combination appears exactly once.
# Rows are created in Day -> Zone -> Category order (30 records per day, 6 per
# zone), so the expected values for row i can be worked out and compared.
order_errors <- 0

for (i in 1:nrow(warehouse_data)) {
  
  expected_day      <- (i - 1) %/% (num_zones * num_categories) + 1
  expected_zone     <- ((i - 1) %/% num_categories) %% num_zones + 1
  expected_category <- (i - 1) %% num_categories + 1
  
  if (warehouse_data$Day[i] != expected_day ||
      warehouse_data$Zone[i] != expected_zone ||
      warehouse_data$Category[i] != expected_category) {
    order_errors <- order_errors + 1
  }
}

cat("Check 1 - Dataset combinations:",
    ifelse(order_errors == 0, "PASS", "FAIL"), "\n")


# CHECK 2: the random variation implied by each row is between -12 and +12.
variation_errors <- 0

for (i in 1:nrow(warehouse_data)) {
  
  implied_variation <- warehouse_data$UnitsProcessed[i] -
    base_units -
    (warehouse_data$Zone[i] * 8) -
    (warehouse_data$Category[i] * 5)
  
  if (implied_variation < -12 || implied_variation > 12) {
    variation_errors <- variation_errors + 1
  }
}

cat("Check 2 - Random variation:",
    ifelse(variation_errors == 0, "PASS", "FAIL"), "\n")


# CHECK 3: ProcessingTime = UnitsProcessed * TimePerUnit in every row
# (small tolerance allowed for floating-point arithmetic).
time_errors <- 0

for (i in 1:nrow(warehouse_data)) {
  
  difference <- warehouse_data$ProcessingTime[i] -
    warehouse_data$UnitsProcessed[i] * time_per_unit
  
  if (difference > 0.000001 || difference < -0.000001) {
    time_errors <- time_errors + 1
  }
}

cat("Check 3 - Processing time:",
    ifelse(time_errors == 0, "PASS", "FAIL"), "\n")


# CHECK 4: the five zone totals add up to the grand total of all units,
# which is calculated separately with its own loop.
grand_total <- 0

for (i in 1:nrow(warehouse_data)) {
  grand_total <- grand_total + warehouse_data$UnitsProcessed[i]
}

zone_total_sum <- 0

for (zone in 1:num_zones) {
  zone_total_sum <- zone_total_sum + zone_totals$TotalUnits[zone]
}

cat("Check 4 - Zone totals:",
    ifelse(grand_total == zone_total_sum, "PASS", "FAIL"), "\n")


# CHECK 5: recalculate the Day 1, Zone 2 average and compare it with the
# value stored in the Task D lookup matrix.
manual_total <- 0
manual_count <- 0

for (i in 1:nrow(warehouse_data)) {
  
  if (warehouse_data$Day[i] == 1 &&
      warehouse_data$Zone[i] == 2) {
    
    manual_total <- manual_total + warehouse_data$UnitsProcessed[i]
    manual_count <- manual_count + 1
  }
}

manual_average <- manual_total / manual_count
stored_average <- daily_zone_avg[1, 2]

cat("Check 5 - Daily zone average:",
    ifelse(abs(manual_average - stored_average) < 0.000001,
           "PASS", "FAIL"), "\n")

cat("Manual average:", manual_average, "\n")
cat("Stored average:", stored_average, "\n")

# Print the six underlying records so the average can also be confirmed by
# hand (add the six UnitsProcessed values and divide by 6).
cat("\nDay 1, Zone 2 records used for the manual check:\n")
print(warehouse_data[warehouse_data$Day == 1 & warehouse_data$Zone == 2, ])

cat("\n===== VALIDATION COMPLETE =====\n")

# =============================== END OF SCRIPT ===============================
