---
title: "Smart Warehouse Inventory Simulation"
subtitle: "ACCA7022 Data Manipulation – Task 1 R code and output"
author: "Sobia Tabassam (Student Number: 2222632)"
date: "23rd October 2026"
output: word_document
---


```{r setup, include=FALSE}
# Show the code and its output in the knitted document
knitr::opts_chunk$set(echo = TRUE)
```

Purpose: Purpose: Generates simulated warehouse data for 28 days, five zones and six product categories. Uses loops to calculate zone totals, category processing-time averages, daily-zone averages, category-level anomalies, monthly-baseline production anomalies and peak production days.


# Task A – Generate warehouse data

```{r task-a}
# Set a seed so the random numbers can be reproduced
set.seed(123)

# Define constants
BaseUnits <- 100
TimePerUnit <- 0.85

# Create an empty dataframe
warehouse_data <- data.frame(
  Day = integer(0),
  Zone = integer(0),
  Category = integer(0),
  UnitsProcessed = integer(0),
  ProcessingTime = numeric(0)
)

# Keep track of the row number
row_number <- 1

# Generate data using nested loops
for (day in 1:28) {
  
  for (zone in 1:5) {
    
    for (category in 1:6) {
      
      # Generate random integer between -12 and +12
      RandomVariation <- sample(-12:12, 1)
      
      # Calculate units processed
      units <- BaseUnits +
        (zone * 8) +
        (category * 5) +
        RandomVariation
      
      # Calculate processing time
      processing_time <- units * TimePerUnit
      
      # Add the values to the dataframe
      warehouse_data[row_number, ] <- c(
        day,
        zone,
        category,
        units,
        processing_time
      )
      
      # Move to the next row
      row_number <- row_number + 1
    }
  }
}

# Display first 10 rows
head(warehouse_data, 10)

# Display number of rows and columns
nrow(warehouse_data)
ncol(warehouse_data)
```

Nested loops were used to generate the warehouse_data dataframe containing 840 rows and five columns. Each row represents a unique combination of day, warehouse zone and product category. UnitsProcessed was calculated using the required formula with random variation, while ProcessingTime was calculated using 0.85 minutes per unit. The use of set.seed(123) makes the simulated results reproducible.

# Task B – Total units processed per zone

```{r task-b}
# Create the results dataframe
zone_totals <- data.frame(
  Zone = 1:5,
  TotalUnits = numeric(5)
)

# Loop through each zone
for (zone in 1:5) {
  
  # Start total at zero
  zone_total <- 0
  
  # Check every row in warehouse_data
  for (i in 1:nrow(warehouse_data)) {
    
    # If the row belongs to the current zone
    if (warehouse_data$Zone[i] == zone) {
      
      # Add its units to the total
      zone_total <- zone_total +
        warehouse_data$UnitsProcessed[i]
    }
  }
  
  # Store the final total
  zone_totals$TotalUnits[zone] <- zone_total
}

# Display results
zone_totals
```

Loops were used to calculate the total units processed by each warehouse zone. The totals increased from 21,059 units in Zone 1 to 26,463 units in Zone 5. This increasing pattern is consistent with the simulation formula, where each increase in zone number contributes an additional eight units before random variation is applied.

# Task C – Average processing time per category

```{r task-c}
# Create results dataframe
category_average_time <- data.frame(
  Category = 1:6,
  AverageProcessingTime = numeric(6)
)

# Loop through each category
for (category in 1:6) {
  
  # Reset total and count
  total_time <- 0
  record_count <- 0
  
  # Check every warehouse record
  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
    }
  }
  
  # Calculate average
  average_time <- total_time / record_count
  
  # Store rounded result
  category_average_time$AverageProcessingTime[category] <-
    round(average_time, 2)
}

# Display results
category_average_time
```

Loops were used to calculate both the total processing time and number of matching records for each product category. The average processing time increased from 109.36 minutes for Category 1 to 131.65 minutes for Category 6. This pattern is consistent with the simulation formula because higher category numbers contribute more units to UnitsProcessed, which consequently increases processing time.

# Task D – Identify stock processing anomalies

## Set up the results dataframes

```{r task-d-setup}
# Create dataframe for all 140 daily-zone averages
# 28 days x 5 zones = 140 combinations
daily_zone_summary <- data.frame(
  Day = integer(140),
  Zone = integer(140),
  DailyZoneAverage = numeric(140)
)

# Create the required dataframe with six columns
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))
)

# Counter for storing daily-zone averages
row_number <- 1
```

## Steps 1 and 2 – Calculate daily-zone averages and identify anomalies

```{r task-d-loops}
# Process all 28 days
for (day in 1:28) {
  
  # Process Zones 1 to 5 for each day
  for (zone in 1:5) {
    
    # Reset total and counter for each day-zone
    daily_zone_total <- 0
    daily_zone_count <- 0
    
    # Calculate total units across all six categories
    for (i in 1:nrow(warehouse_data)) {
      
      if (warehouse_data$Day[i] == day &&
          warehouse_data$Zone[i] == zone) {
        
        daily_zone_total <- daily_zone_total +
          warehouse_data$UnitsProcessed[i]
        
        daily_zone_count <- daily_zone_count + 1
      } 
    }
    
    # Confirm that all six categories were included
    if (daily_zone_count != 6) {
      stop("Error: Expected six categories per day-zone.")
    }
    
    # Calculate average for this specific day and zone
    daily_zone_average <-
      daily_zone_total / daily_zone_count
    
    # Store the daily-zone average
    daily_zone_summary$Day[row_number] <- day
    daily_zone_summary$Zone[row_number] <- zone
    
    daily_zone_summary$DailyZoneAverage[row_number] <-
      round(daily_zone_average, 2)
    
    # Move to next daily-zone summary row
    row_number <- row_number + 1
    
    # Check each category within this day and zone
    for (i in 1:nrow(warehouse_data)) {
      
      if (warehouse_data$Day[i] == day &&
          warehouse_data$Zone[i] == zone) {
        
        units <- warehouse_data$UnitsProcessed[i]
        
        # Calculate the upper and lower thresholds
        upper_threshold <- daily_zone_average * 1.15
        lower_threshold <- daily_zone_average * 0.85
        
        # Mark TRUE if more than 15% above or below average
        is_anomaly <-
          units > upper_threshold ||
          units < lower_threshold
        
        # Store the corresponding daily-zone average
        processing_anomalies$DailyZoneAverage[i] <-
          round(daily_zone_average, 2)
        
        # Store TRUE or FALSE
        processing_anomalies$Anomaly[i] <- is_anomaly
      }
    }
  }
}
```

## Results 1 – All 140 daily-zone averages

```{r task-d-results-1}
cat("\n========================================\n")
cat("TASK D - DAILY-ZONE AVERAGES\n")
cat("========================================\n")

# Display all 140 averages
print(daily_zone_summary)

# Confirm total number of averages
cat("\nNumber of daily-zone averages:\n")
print(nrow(daily_zone_summary))
```

## Results 2 – First 10 TRUE anomalies

```{r task-d-results-2}
# Select records where Anomaly is TRUE
true_anomalies <- processing_anomalies[
  processing_anomalies$Anomaly == TRUE,
  ,
  drop = FALSE
]

# Create a separate dataframe containing the first 10
first_10_anomalies <- head(true_anomalies, 10)

# Reset row numbers to 1, 2, 3 ... 10
rownames(first_10_anomalies) <- NULL

cat("\n========================================\n")
cat("TASK D - FIRST 10 TRUE ANOMALIES\n")
cat("========================================\n")

# Display the first 10 TRUE anomalies in Console
print(first_10_anomalies)

# Open the first 10 TRUE anomalies in Data Viewer
# (only when working interactively; View() cannot run while knitting)
if (interactive()) View(first_10_anomalies)
```

## Results 3 – Verification checks

```{r task-d-results-3}
cat("\n========================================\n")
cat("TASK D - VERIFICATION RESULTS\n")
cat("========================================\n")

# Verify total number of daily-zone combinations
cat("\nTotal daily-zone averages:\n")
print(nrow(daily_zone_summary))

# Verify total number of warehouse observations
cat("\nTotal warehouse observations:\n")
print(nrow(processing_anomalies))

# Verify number of TRUE anomalies
cat("\nTotal TRUE anomalies:\n")
print(nrow(true_anomalies))

# Verify number of FALSE observations
cat("\nTotal FALSE observations:\n")
print(sum(!processing_anomalies$Anomaly))

# Verify dimensions of processing_anomalies
cat("\nDimensions of processing_anomalies:\n")
print(dim(processing_anomalies))

# Verify the six required column names
cat("\nColumn names:\n")
print(names(processing_anomalies))

# Verify six categories for every day-zone combination
cat("\nRecords per day and zone:\n")
print(table(processing_anomalies$Day,
            processing_anomalies$Zone))
```

## Final validation

```{r task-d-validation}
# Confirm that all 140 day-zone averages were calculated
stopifnot(nrow(daily_zone_summary) == 140)

# Confirm that all 840 warehouse records were checked
stopifnot(nrow(processing_anomalies) == 840)

# Confirm that the required six columns exist
stopifnot(ncol(processing_anomalies) == 6)

# Confirm that each day-zone contains six categories
stopifnot(
  all(table(processing_anomalies$Day,
            processing_anomalies$Zone) == 6)
)

# Confirm that no anomaly flags are missing
stopifnot(!any(is.na(processing_anomalies$Anomaly)))

cat("\nTASK D VALIDATION COMPLETED SUCCESSFULLY.\n")
```

## Additional Task D Analysis – Monthly Average Daily Production

```{r task-d-monthly-analysis}
# =====================================================
# ADDITIONAL TASK D ANALYSIS
# MONTHLY AVERAGE DAILY PRODUCTION
# Using loops, counters and accumulators
# =====================================================

# Step 1: Create a dataframe for daily zone totals
daily_zone_totals <- data.frame(
  Day = integer(140),
  Zone = integer(140),
  DailyTotal = numeric(140)
)

result_row <- 1

# Step 2: Calculate daily totals across six categories
for (day in 1:28) {
  for (zone in 1:5) {

    daily_total <- 0

    for (i in 1:nrow(warehouse_data)) {
      if (warehouse_data$Day[i] == day &&
          warehouse_data$Zone[i] == zone) {

        daily_total <- daily_total +
          warehouse_data$UnitsProcessed[i]
      }
    }

    daily_zone_totals$Day[result_row] <- day
    daily_zone_totals$Zone[result_row] <- zone
    daily_zone_totals$DailyTotal[result_row] <- daily_total

    result_row <- result_row + 1
  }
}

# Step 3: Calculate each zone's average daily
# production across the 28 days
monthly_daily_average <- data.frame(
  Zone = 1:5,
  MonthlyDailyAverage = numeric(5)
)

for (zone in 1:5) {

  monthly_total <- 0
  day_count <- 0

  for (i in 1:nrow(daily_zone_totals)) {
    if (daily_zone_totals$Zone[i] == zone) {

      monthly_total <- monthly_total +
        daily_zone_totals$DailyTotal[i]

      day_count <- day_count + 1
    }
  }

  monthly_daily_average$MonthlyDailyAverage[zone] <-
    monthly_total / day_count
}

# Step 4: Create dataframe for monthly anomaly results
monthly_anomalies <- data.frame(
  Day = daily_zone_totals$Day,
  Zone = daily_zone_totals$Zone,
  DailyTotal = daily_zone_totals$DailyTotal,
  MonthlyDailyAverage = numeric(140),
  LowerThreshold = numeric(140),
  UpperThreshold = numeric(140),
  Anomaly = logical(140)
)

# Step 5: Compare each daily total with the
# corresponding zone's monthly daily average
for (i in 1:nrow(monthly_anomalies)) {

  zone <- monthly_anomalies$Zone[i]

  average <- monthly_daily_average$MonthlyDailyAverage[zone]

  lower <- average * 0.85
  upper <- average * 1.15

  monthly_anomalies$MonthlyDailyAverage[i] <- average
  monthly_anomalies$LowerThreshold[i] <- lower
  monthly_anomalies$UpperThreshold[i] <- upper

  if (monthly_anomalies$DailyTotal[i] < lower ||
      monthly_anomalies$DailyTotal[i] > upper) {

    monthly_anomalies$Anomaly[i] <- TRUE

  } else {

    monthly_anomalies$Anomaly[i] <- FALSE
  }
}

# Step 6: Display results
print(monthly_daily_average)

# Display first 10 daily-zone records
print(head(monthly_anomalies, 10))

# Display records flagged as anomalies
print(monthly_anomalies[
  monthly_anomalies$Anomaly,
])

# Display anomaly counts
print(table(monthly_anomalies$Anomaly))

# Display all 140 monthly anomaly results
print(monthly_anomalies, row.names = FALSE)
# Step 7: Validation
stopifnot(nrow(daily_zone_totals) == 140)
stopifnot(nrow(monthly_daily_average) == 5)
stopifnot(nrow(monthly_anomalies) == 140)

# Confirm that every record has an anomaly result
stopifnot(!any(is.na(monthly_anomalies$Anomaly)))

```
Nested loops were used to calculate the average units processed across all six product categories for each day and zone, producing 140 daily-zone averages. Each of the 840 category records was compared against its daily-zone average using a ±15% threshold, identifying 41 anomalies (4.9%).

An additional analysis calculated the total daily production for each zone and its average across the 28-day period. The same ±15% threshold was applied to 140 daily-zone totals, but no anomalies were detected. This shows that although individual categories experienced unusual variations, overall daily production remained within the monthly thresholds. Both analyses were completed using loops, counters and conditional statements without prohibited aggregation functions.


# Task E – Most productive day for each zone

```{r task-e}
# Create results dataframe
peak_zone_days <- data.frame(
  Zone = 1:5,
  Day = integer(5),
  TotalUnits = numeric(5)
)

# Loop through each zone
for (zone in 1:5) {
  
  # Starting values
  highest_total <- -Inf
  highest_day <- 0
  
  # Check each day
  for (day in 1:28) {
    
    daily_total <- 0
    
    # Calculate total for this zone and day
    for (i in 1:nrow(warehouse_data)) {
      
      if (warehouse_data$Zone[i] == zone &&
          warehouse_data$Day[i] == day) {
        
        daily_total <- daily_total +
          warehouse_data$UnitsProcessed[i]
      }
    }
    
    # Check whether this is the highest total so far
    if (daily_total > highest_total) {
      
      highest_total <- daily_total
      highest_day <- day
    }
  }
  
  # Store result
  peak_zone_days$Day[zone] <- highest_day
  peak_zone_days$TotalUnits[zone] <- highest_total
}

# Display results
peak_zone_days
```

Nested loops were used to calculate the daily total for each zone and to compare it with the highest total found so far, which starts at -Inf so that Day 1 always sets the first benchmark. The peak days were Day 16 for Zone 1 (786 units), Day 6 for Zone 2 (838), Day 8 for Zone 3 (895), Day 3 for Zone 4 (948) and Day 19 for Zone 5 (972). The margins over the runner-up days were small (only 3 units in Zone 5), so these peaks reflect random variation rather than a genuine operational pattern.


# Summary

The Smart Warehouse Inventory Simulation demonstrates how loops, dataframes and conditional statements can generate and analyse structured data in R. The simulation produced 840 records across 28 days, five zones and six product categories. Further loops calculated zone totals, category processing-time averages, daily-zone averages and peak production days.

Task D used two approaches to anomaly detection. The original category-level analysis identified 41 anomalies (4.9%) across 840 records, with 38 occurring in Categories 1 and 6. However, the additional monthly daily-average analysis identified no anomalies across 140 daily-zone totals. This indicates that individual categories experienced variations while overall daily production remained within the ±15% monthly thresholds.

The results reflect the simulation formula, with higher zone and category numbers generally producing greater output and processing times. Overall, the simulation demonstrates how loops, counters, accumulators and conditional logic can support warehouse performance analysis without using prohibited high-level aggregation functions.


# Supporting evidence for Task 2 (Testing and Validation)

The sections below are not part of Tasks A–E. They provide the executable evidence for the testing and validation discussed in Section C of the report.

## Verification of Table 2 using loops (report Appendix A8)

```{r a6-verification}
# Create counters for anomalies above and below the
# daily-zone average for each of the 6 categories
category_high <- numeric(6)
category_low <- numeric(6)

# Create a counter for anomalies in each of the 5 zones
zone_flags <- numeric(5)

# Start the overall anomaly count at zero
total_anomalies <- 0

# Loop through every row of processing_anomalies
for (i in 1:nrow(processing_anomalies)) {
  
  # Only count rows that were flagged as anomalies
  if (processing_anomalies$Anomaly[i] == TRUE) {
    
    # Add one to the overall anomaly count
    total_anomalies <- total_anomalies + 1
    
    # Store the category and zone of this record
    category <- processing_anomalies$Category[i]
    zone <- processing_anomalies$Zone[i]
    
    # Add one to the anomaly count for this zone
    zone_flags[zone] <- zone_flags[zone] + 1
    
    # Check whether the record is above the daily-zone average
    if (processing_anomalies$UnitsProcessed[i] >
        processing_anomalies$DailyZoneAverage[i]) {
      
      # Count it as a HIGH anomaly for this category
      category_high[category] <-
        category_high[category] + 1
      
    } else {
      
      # Otherwise count it as a LOW anomaly for this category
      category_low[category] <-
        category_low[category] + 1
    }
  }
}

# Display verification results
total_anomalies
category_high
category_low
zone_flags
```

This loop recounts the anomalies independently to verify Table 2. It confirms 41 anomalies in total: 21 were above the daily-zone average (2 in Category 5 and 19 in Category 6) and 20 were below it (19 in Category 1 and 1 in Category 2). Together, Categories 1 and 6 account for 38 of the 41 anomalies, which shows that the ±15% rule mainly picks up the built-in category effect in the formula rather than genuinely unusual activity. Anomalies also fall as the zone number rises (15, 12, 9, 3, 2). In higher zones the daily-zone average is larger, so the ±15% band is wider in units, and the fixed gap between categories is less likely to cross it. These counts match Table 2 exactly.

## Verification of Table 3 using loops (report Appendix A9)

```{r a7-verification}
# Loop through each zone
for (zone in 1:5) {
  
  # Starting values: -Inf makes sure Day 1 becomes
  # the first highest total
  highest_total <- -Inf
  second_highest <- -Inf
  highest_day <- 0
  second_day <- 0
  
  # Running total for the zone (used to calculate the mean)
  zone_total <- 0
  
  # Loop through each day
  for (day in 1:28) {
    
    # Start the daily total at zero
    daily_total <- 0
    
    # Add up all units for this zone on this day
    for (i in 1:nrow(warehouse_data)) {
      
      if (warehouse_data$Zone[i] == zone &&
          warehouse_data$Day[i] == day) {
        
        daily_total <- daily_total +
          warehouse_data$UnitsProcessed[i]
      }
    }
    
    # Add the daily total to the zone total
    zone_total <- zone_total + daily_total
    
    # If this day is the new highest, move the old
    # highest down to second place
    if (daily_total > highest_total) {
      
      second_highest <- highest_total
      second_day <- highest_day
      
      highest_total <- daily_total
      highest_day <- day
      
    # Otherwise check whether it beats the second highest
    } else if (daily_total > second_highest) {
      
      second_highest <- daily_total
      second_day <- day
    }
  }
  
  # Calculate the mean daily total for this zone
  mean_daily_total <- zone_total / 28
  
  # Calculate how far the peak day is above the mean (%)
  peak_vs_mean <-
    ((highest_total - mean_daily_total) /
       mean_daily_total) * 100
  
  # Display the results for this zone
  cat(
    "Zone:", zone,
    "Peak Day:", highest_day,
    "Peak Total:", highest_total,
    "Runner-up Day:", second_day,
    "Runner-up Total:", second_highest,
    "Mean:", round(mean_daily_total, 1),
    "Peak vs Mean:", round(peak_vs_mean, 1), "%\n"
  )
}
```


This loop verifies Table 3 by recalculating the peak day for each zone and also recording the runner-up day and the mean daily total. The peak days (16, 6, 8, 3 and 19) and totals (786, 838, 895, 948 and 972) match Task E exactly. Each peak is only 2.8% to 5.7% above the zone’s mean daily total, and the gap to the runner-up is small: 14, 6, 11, 22 and 3 units for Zones 1 to 5. This shows the peak days are driven by random variation rather than a genuine operational pattern, so they should not be used on their own for staffing or planning decisions.


## Small test case with known outcomes

Same loop logic as Tasks A-E, run on a tiny dataset (2 days x 2 zones x 2 categories) with RandomVariation = 0, so every expected result can be calculated by hand.

```{r small-test}
# Create an empty dataframe for the small test dataset
test_data <- data.frame(Day = integer(0), Zone = integer(0),
                        Category = integer(0), UnitsProcessed = integer(0),
                        ProcessingTime = numeric(0))

# Keep track of the row number
row_number <- 1

# Generate 8 records (2 days x 2 zones x 2 categories) using the same
# formula as Task A, but with no random variation so results are predictable
for (day in 1:2) {
  for (zone in 1:2) {
    for (category in 1:2) {
      units <- 100 + (zone * 8) + (category * 5) + 0   # no random variation
      test_data[row_number, ] <- c(day, zone, category, units, units * 0.85)
      row_number <- row_number + 1
    }
  }
}

# Inject a known outlier: Day 2, Zone 1, Category 2 set to 160 units
test_data$UnitsProcessed[6] <- 160
test_data$ProcessingTime[6] <- 160 * 0.85
test_data

# Task B logic: zone totals (expected Zone 1 = 504, Zone 2 = 494)
for (zone in 1:2) {
  zone_total <- 0
  for (i in 1:nrow(test_data)) {
    if (test_data$Zone[i] == zone) zone_total <- zone_total + test_data$UnitsProcessed[i]
  }
  cat("Zone", zone, "total:", zone_total, "\n")
}

# Task C logic: category averages (expected Cat 1 = 99.45; Cat 2 = 112.625 by hand, which R rounds to 112.62)
for (category in 1:2) {
  total_time <- 0; record_count <- 0
  for (i in 1:nrow(test_data)) {
    if (test_data$Category[i] == category) {
      total_time <- total_time + test_data$ProcessingTime[i]
      record_count <- record_count + 1
    }
  }
  cat("Category", category, "average time:", round(total_time / record_count, 2), "\n")
}

# Task D logic: anomalies (expected only Day 2, Zone 1: both rows flagged,
# because the average is 136.5, so 160 > 156.98 and 113 < 116.03)
for (day in 1:2) {
  for (zone in 1:2) {
    total <- 0; count <- 0
    for (i in 1:nrow(test_data)) {
      if (test_data$Day[i] == day && test_data$Zone[i] == zone) {
        total <- total + test_data$UnitsProcessed[i]; count <- count + 1
      }
    }
    avg <- total / count
    for (i in 1:nrow(test_data)) {
      if (test_data$Day[i] == day && test_data$Zone[i] == zone) {
        u <- test_data$UnitsProcessed[i]
        cat("Day", day, "Zone", zone, "Cat", test_data$Category[i],
            "units", u, "avg", avg, "anomaly:", u > avg * 1.15 || u < avg * 0.85, "\n")
      }
    }
  }
}

# Task E logic: peak day (expected Zone 1 = Day 2 (273);
# Zone 2 = Day 1 because both days total 247 and the strict > keeps the first)
for (zone in 1:2) {
  highest_total <- -Inf; highest_day <- 0
  for (day in 1:2) {
    daily_total <- 0
    for (i in 1:nrow(test_data)) {
      if (test_data$Zone[i] == zone && test_data$Day[i] == day)
        daily_total <- daily_total + test_data$UnitsProcessed[i]
    }
    if (daily_total > highest_total) { highest_total <- daily_total; highest_day <- day }
  }
  cat("Zone", zone, "peak day:", highest_day, "total:", highest_total, "\n")
}
```

### Small test case – summary of findings

A controlled dataset of 8 records was created using 2 days, 2 zones and 2 product categories.

Random variation was removed to allow manual verification.

Task B: Zone totals were correctly calculated as 504 units for Zone 1 and 494 units for Zone 2.

Task C: Average processing times were 99.45 minutes for Category 1 and 112.62 minutes for Category 2.

Task D: Two anomalies were correctly identified on Day 2, Zone 1, where the daily-zone average was 136.5. Both 113 and 160 units fell outside the +/-15% limits.

Task E: Zone 1 peaked on Day 2 with 273 units. Zone 2 peaked on Day 1 with 247 units. The earlier day was selected when totals were tied.

Overall, the test results matched the expected values, supporting the correctness of the loop calculations, anomaly detection and peak-day selection.

Limitation: Further tests using different random seeds and exact +/-15% boundary values are recommended.


## Small test case – supplementary Task D (monthly baseline)

Same loop logic as the supplementary monthly-baseline analysis, run on a tiny dataset (2 days x 2 zones x 2 categories) with no random variation. Two values are injected so that the expected results, including TRUE anomalies, can be calculated by hand.

```{r small-test-supplementary}
# Build 8 records (2 days x 2 zones x 2 categories) with no
# random variation, using the same formula as Task A
test_data2 <- data.frame(Day = integer(0), Zone = integer(0),
                         Category = integer(0), UnitsProcessed = integer(0))
row_number <- 1
for (day in 1:2) {
  for (zone in 1:2) {
    for (category in 1:2) {
      units <- 100 + (zone * 8) + (category * 5)
      test_data2[row_number, ] <- c(day, zone, category, units)
      row_number <- row_number + 1
    }
  }
}

# Inject known values (expected results worked out by hand):
# Zone 1: Day 1 = 113 + 118 = 231, Day 2 = 113 + 160 = 273
#         average = 252, limits 214.2 to 289.8 -> both FALSE
# Zone 2: Day 1 = 121 + 126 = 247, Day 2 = 224 + 126 = 350
#         average = 298.5, limits 253.725 to 343.275 -> both TRUE
test_data2$UnitsProcessed[6] <- 160   # Day 2, Zone 1, Category 2
test_data2$UnitsProcessed[7] <- 224   # Day 2, Zone 2, Category 1

# Step 1: daily totals for each day and zone (expected 231, 247, 273, 350)
test_totals <- data.frame(Day = integer(4), Zone = integer(4),
                          DailyTotal = numeric(4))
result_row <- 1
for (day in 1:2) {
  for (zone in 1:2) {
    daily_total <- 0
    for (i in 1:nrow(test_data2)) {
      if (test_data2$Day[i] == day && test_data2$Zone[i] == zone) {
        daily_total <- daily_total + test_data2$UnitsProcessed[i]
      }
    }
    test_totals$Day[result_row] <- day
    test_totals$Zone[result_row] <- zone
    test_totals$DailyTotal[result_row] <- daily_total
    result_row <- result_row + 1
  }
}

# Step 2: each zone's average daily total (expected 252 and 298.5)
test_average <- numeric(2)
for (zone in 1:2) {
  zone_sum <- 0
  day_count <- 0
  for (i in 1:nrow(test_totals)) {
    if (test_totals$Zone[i] == zone) {
      zone_sum <- zone_sum + test_totals$DailyTotal[i]
      day_count <- day_count + 1
    }
  }
  test_average[zone] <- zone_sum / day_count
}

# Step 3: flag totals outside +/-15% of the zone average
# (expected Zone 1 FALSE, FALSE; Zone 2 TRUE, TRUE)
for (i in 1:nrow(test_totals)) {
  zone <- test_totals$Zone[i]
  lower <- test_average[zone] * 0.85
  upper <- test_average[zone] * 1.15
  flag <- test_totals$DailyTotal[i] < lower ||
          test_totals$DailyTotal[i] > upper
  cat("Day", test_totals$Day[i], "Zone", zone,
      "total", test_totals$DailyTotal[i],
      "average", test_average[zone],
      "limits", round(lower, 2), "-", round(upper, 2),
      "anomaly:", flag, "\n")
}
# Step 4: Validate expected results

# Check number of records
stopifnot(nrow(test_data2) == 8)
stopifnot(nrow(test_totals) == 4)

# Check daily totals
expected_totals <- c(231, 247, 273, 350)

stopifnot(all(
  test_totals$DailyTotal == expected_totals
))

# Check monthly daily averages
expected_averages <- c(252, 298.5)

stopifnot(isTRUE(all.equal(
  test_average, expected_averages
)))

# Check expected anomaly classifications
expected_flags <- c(FALSE, TRUE, FALSE, TRUE)
actual_flags <- logical(4)

for (i in 1:nrow(test_totals)) {
  zone <- test_totals$Zone[i]

  lower <- test_average[zone] * 0.85
  upper <- test_average[zone] * 1.15

  if (test_totals$DailyTotal[i] < lower ||
      test_totals$DailyTotal[i] > upper) {
    actual_flags[i] <- TRUE
  } else {
    actual_flags[i] <- FALSE
  }
}

stopifnot(identical(actual_flags, expected_flags))
stopifnot(!any(is.na(actual_flags)))

cat("\nAll supplementary Task D small tests PASSED.\n")
```

### Supplementary small test – summary of findings

Zone 1 daily totals of 231 and 273 averaged 252, so both stayed within the limits of 214.2 to 289.8 and were correctly marked FALSE. Zone 2 daily totals of 247 and 350 averaged 298.5, so both fell outside the limits of 253.725 to 343.275 (displayed by R as 253.72 and 343.28) and were correctly marked TRUE. The results match the hand calculations, confirming that the supplementary loops calculate daily totals, zone averages and anomaly flags correctly, including the TRUE branch that the main data never triggers. The stopifnot() checks then compared the record counts, daily totals, zone averages and anomaly flags with the expected values automatically, and R printed “All supplementary Task D small tests PASSED”, so every check succeeded.


