---
title: "Task 1: Smart Warehouse Inventory Simulation"
author: "Darpanbhai Raiyani (Student Number: 2534009)"
output:
  html_document:
    toc: true
    toc_float: true
    number_sections: false
    df_print: default
---

```{r setup, include=FALSE}
# ==============================================================================
# File: warehouse_manipulation_f.Rmd
# Name: Darpanbhai Raiyani Shantilal
# ==============================================================================
knitr::opts_chunk$set(echo = TRUE, message = FALSE, warning = FALSE)
```

# Document Header

| Item                  | Details                                        |
|-----------------------|------------------------------------------------|
| **Name**              | RAIYANI DARPANBHAI SHANTILAL                   |
| **Student Number**    | 2534009                                        |
| **Module Code**       | ACCA7022 (Advanced Statistical Programming)    |
| **Assessment Title**  | Coursework 1 – Smart Warehouse Inventory Simulation (Task 1: R Code) |
| **Script Name**       | warehouse_manipulation.r                       |

## Overview

This document simulates the daily operation of a smart warehouse. Every day
(28 days), each of the 5 warehouse zones processes units from each of the 6
product categories. That gives 28 x 5 x 6 = **840 records**.

The work is split into five parts (A to E):

* **A** – Generate the `warehouse_data` dataframe using nested loops.
* **B** – Total units processed per zone (`zone_totals`).
* **C** – Average processing time per product category (`category_average_time`).
* **D** – Detect unusual (anomalous) records (`processing_anomalies`).
* **E** – Find the most productive day for each zone (`peak_zone_days`).

# Part A: Generate Warehouse Data

## What this part does

1. Defines the fixed values (constants) given in the task.
2. Creates an empty dataframe with the correct columns and 840 rows.
3. Uses **three nested loops** (day -> zone -> category) to fill in one row for
   every combination.
4. Calculates `UnitsProcessed` and `ProcessingTime` for each row.
5. Displays the first 10 rows and the number of rows and columns.

## Step A1: Set up constants and the random seed

```{r part-a-setup}
# set.seed() makes the random variation repeatable. The sample method is set
# explicitly so that modern R versions use the same discrete sampling method.
set.seed(123, sample.kind = "Rejection")

# ---- Fixed values taken from the task description ----
num_days       <- 28     # days of warehouse operation (1 to 28)
num_zones      <- 5      # warehouse zones (1 to 5)
num_categories <- 6      # product categories (1 to 6)

base_units     <- 100    # BaseUnits in the UnitsProcessed formula
time_per_unit  <- 0.85   # TimePerUnit in the ProcessingTime formula (minutes)

# Total rows needed = one row for every Day x Zone x Category combination
total_rows <- num_days * num_zones * num_categories   # 28 * 5 * 6 = 840
```

## Step A2: Create an empty dataframe with the correct columns

```{r part-a-create}
# We create the dataframe at its full size (840 rows) first, filled with
# placeholder zeros. The loops below then replace the zeros with real values.
# Pre-sizing the dataframe is simpler and faster than adding one row at a time.
warehouse_data <- data.frame(
  Day            = numeric(total_rows),   # day of operation (1 to 28)
  Zone           = numeric(total_rows),   # warehouse zone ID (1 to 5)
  Category       = numeric(total_rows),   # product category ID (1 to 6)
  UnitsProcessed = numeric(total_rows),   # units processed
  ProcessingTime = numeric(total_rows)    # processing time in minutes
)
```

## Step A3: Populate the dataframe using nested loops

```{r part-a-loops}
# row_index tracks which row of the dataframe we are currently filling.
# It starts at 1 and goes up by 1 each time a row is completed.
row_index <- 1

# OUTER LOOP: go through each day, 1 to 28
for (day in 1:num_days) {

  # MIDDLE LOOP: for the current day, go through each zone, 1 to 5
  for (zone in 1:num_zones) {

    # INNER LOOP: for the current day and zone, go through each category, 1 to 6
    for (category in 1:num_categories) {

      # Random integer between -12 and +12 (both ends included).
      # sample() picks 1 value at random from the whole numbers -12, -11, ..., 12.
      random_variation <- sample(-12:12, 1)

      # Formula from the task:
      units_processed <- base_units + (zone * 8) + (category * 5) + random_variation

      # Formula from the task:
      processing_time <- units_processed * time_per_unit

      # Store all five values in the current row of the dataframe
      warehouse_data$Day[row_index]            <- day
      warehouse_data$Zone[row_index]           <- zone
      warehouse_data$Category[row_index]       <- category
      warehouse_data$UnitsProcessed[row_index] <- units_processed
      warehouse_data$ProcessingTime[row_index] <- processing_time

      # Move on to the next empty row
      row_index <- row_index + 1
    }
  }
}
```

## Step A4: Display the results

```{r part-a-display}
# Show the first 10 rows of the dataframe
cat("First 10 rows of warehouse_data:\n")
print(head(warehouse_data, 10))

# Show the size of the dataframe (rows and columns)
cat("\nTotal number of rows   :", nrow(warehouse_data), "\n")
cat("Total number of columns:", ncol(warehouse_data), "\n")
```

**Check:** the dataframe should have **840 rows** (28 x 5 x 6) and **5 columns**.

---

# Part B: Total Units Processed per Zone

## What this part does

For each zone (1 to 5), add up `UnitsProcessed` across **all days and all
categories**. We do this with a loop over zones, and an inner loop that reads
every row of `warehouse_data` and adds the units to the running total whenever
the row belongs to the current zone.

```{r part-b}
# Create the results dataframe: one row per zone, totals start at 0
zone_totals <- data.frame(
  Zone       = 1:num_zones,
  TotalUnits = numeric(num_zones)
)

# OUTER LOOP: take each zone in turn
for (z in 1:num_zones) {

  # Running total for this zone, reset to 0 at the start of every zone
  zone_sum <- 0

  # INNER LOOP: look at every row in warehouse_data
  for (i in 1:nrow(warehouse_data)) {

    # Only add the units if this row belongs to the zone we are working on
    if (warehouse_data$Zone[i] == z) {
      zone_sum <- zone_sum + warehouse_data$UnitsProcessed[i]
    }
  }

  # Save the finished total for this zone in the results dataframe
  zone_totals$TotalUnits[z] <- zone_sum
}

# Display the completed dataframe
cat("zone_totals:\n")
print(zone_totals)
```

**Reading the result:** the formula adds `Zone * 8` units, so higher-numbered
zones should have higher totals. The table above should show totals rising from
Zone 1 to Zone 5.

---

# Part C: Average Processing Time per Product Category

## What this part does

For each category (1 to 6), we need the average `ProcessingTime` across all days
and zones. An average is **total / count**, so the loop calculates both:

* the **total processing time** for the category, and
* the **number of matching records** for the category.

Then the average is total divided by count.

```{r part-c}
# Create the results dataframe: one row per category
category_average_time <- data.frame(
  Category              = 1:num_categories,
  AverageProcessingTime = numeric(num_categories)
)

# OUTER LOOP: take each category in turn
for (cat_id in 1:num_categories) {

  # Reset both counters at the start of every category
  total_time   <- 0   # running total of ProcessingTime for this category
  record_count <- 0   # number of records found for this category

  # INNER LOOP: look at every row in warehouse_data
  for (i in 1:nrow(warehouse_data)) {

    # Only use rows that belong to the current category
    if (warehouse_data$Category[i] == cat_id) {
      total_time   <- total_time + warehouse_data$ProcessingTime[i]  # add the time
      record_count <- record_count + 1                               # count the record
    }
  }

  # Average = total / count. round() keeps it to 2 decimal places.
  category_average_time$AverageProcessingTime[cat_id] <- round(total_time / record_count, 2)
}

# Display the completed dataframe
cat("category_average_time:\n")
print(category_average_time)
```

**Reading the result:** each category should have 140 records (28 days x 5
zones). The formula adds `Category * 5` units, so higher categories should have
a longer average processing time.

---

# Part D: Identify Stock Processing Anomalies

## What this part does

A record is an **anomaly** if its `UnitsProcessed` is more than 15% above, or
more than 15% below, the average for the same zone on the same day. The steps
are:

1. **D1** – Loop through the data to total the units (and count the records)
   for every Day-Zone pair, then work out each pair's average. Results are
   stored in tables (matrices) with one row per day and one column per zone.
2. **D2** – Loop through every record, look up its daily zone average, and
   decide whether it is an anomaly.
3. **D3** – Display the first 10 anomalies.

## Step D1: Calculate the average units for each day and zone

```{r part-d-averages}
# Two tables with 28 rows (days) and 5 columns (zones), all starting at 0:
#   day_zone_total[d, z] = total units processed on day d in zone z
#   day_zone_count[d, z] = number of records on day d in zone z
day_zone_total <- matrix(0, nrow = num_days, ncol = num_zones)
day_zone_count <- matrix(0, nrow = num_days, ncol = num_zones)

# Go through every record once and add it to the correct Day-Zone cell
for (i in 1:nrow(warehouse_data)) {

  d <- warehouse_data$Day[i]    # day of this record
  z <- warehouse_data$Zone[i]   # zone of this record

  day_zone_total[d, z] <- day_zone_total[d, z] + warehouse_data$UnitsProcessed[i]
  day_zone_count[d, z] <- day_zone_count[d, z] + 1
}

# Daily zone average = total / count. Each Day-Zone pair has 6 categories,
# so the count in every cell should be 6.
day_zone_average <- matrix(0, nrow = num_days, ncol = num_zones)

for (d in 1:num_days) {
  for (z in 1:num_zones) {
    day_zone_average[d, z] <- day_zone_total[d, z] / day_zone_count[d, z]
  }
}
```

## Step D2: Flag each record as an anomaly (TRUE) or not (FALSE)

```{r part-d-anomalies}
# Create the results dataframe, the same size as warehouse_data (840 rows).
# Anomaly starts as FALSE for every row; the loop changes it to TRUE if needed.
processing_anomalies <- data.frame(
  Day              = numeric(total_rows),
  Zone             = numeric(total_rows),
  Category         = numeric(total_rows),
  UnitsProcessed   = numeric(total_rows),
  DailyZoneAverage = numeric(total_rows),
  Anomaly          = logical(total_rows)
)

# Go through every record in warehouse_data
for (i in 1:nrow(warehouse_data)) {

  # Read the details of this record
  d           <- warehouse_data$Day[i]
  z           <- warehouse_data$Zone[i]
  category_id <- warehouse_data$Category[i]
  units       <- warehouse_data$UnitsProcessed[i]

  # Look up the average for this record's day and zone (calculated in step D1)
  zone_day_avg <- day_zone_average[d, z]

  # Anomaly rule from the task:
  if (units > zone_day_avg * 1.15 || units < zone_day_avg * 0.85) {
    is_anomaly <- TRUE
  } else {
    is_anomaly <- FALSE
  }

  # Store the record and its anomaly result in the dataframe
  processing_anomalies$Day[i]              <- d
  processing_anomalies$Zone[i]             <- z
  processing_anomalies$Category[i]         <- category_id
  processing_anomalies$UnitsProcessed[i]   <- units
  processing_anomalies$DailyZoneAverage[i] <- round(zone_day_avg, 2)
  processing_anomalies$Anomaly[i]          <- is_anomaly
}
```

## Step D3: Display the first 10 anomalies

```{r part-d-display}
# Loop through the results and collect the row numbers where Anomaly is TRUE.
# We stop collecting once we have found 10 of them.
anomaly_rows <- c()   # empty vector to hold the row numbers

for (i in 1:nrow(processing_anomalies)) {
  if (processing_anomalies$Anomaly[i] == TRUE && length(anomaly_rows) < 10) {
    anomaly_rows <- c(anomaly_rows, i)   # add this row number to the vector
  }
}

# Also count ALL anomalies, for information
anomaly_count <- 0
for (i in 1:nrow(processing_anomalies)) {
  if (processing_anomalies$Anomaly[i] == TRUE) {
    anomaly_count <- anomaly_count + 1
  }
}

cat("First 10 rows where Anomaly is TRUE:\n")
print(processing_anomalies[anomaly_rows, ])

cat("\nTotal anomalies found in all", total_rows, "records:", anomaly_count, "\n")
```

**Reading the result:** an anomaly happens when a record's category effect plus
its random variation pushes it more than 15% away from the average of its zone
on that day. For example, a high-numbered category with a large positive random
variation can be flagged as unusually high.

---

# Part E: Identify the Most Productive Day for Each Zone

## What this part does

1. **E1** – Loop through the data to total the units for every Zone-Day pair.
2. **E2** – For each zone, loop through its 28 daily totals and keep track of
   the highest total and the day it happened.

```{r part-e}
# ---- Step E1: total units for every Zone-Day combination ----
# zone_day_total[z, d] = total units processed in zone z on day d
zone_day_total <- matrix(0, nrow = num_zones, ncol = num_days)

for (i in 1:nrow(warehouse_data)) {
  z <- warehouse_data$Zone[i]
  d <- warehouse_data$Day[i]
  zone_day_total[z, d] <- zone_day_total[z, d] + warehouse_data$UnitsProcessed[i]
}

# ---- Step E2: find the best day for each zone ----
# Create the results dataframe: one row per zone
peak_zone_days <- data.frame(
  Zone       = 1:num_zones,
  Day        = numeric(num_zones),
  TotalUnits = numeric(num_zones)
)

# OUTER LOOP: take each zone in turn
for (z in 1:num_zones) {

  # Start by assuming day 1 is the best day, then check the other days
  best_day   <- 1
  best_total <- zone_day_total[z, 1]

  # INNER LOOP: compare days 2 to 28 against the best day found so far
  for (d in 2:num_days) {

    # If this day has a higher total, it becomes the new best day.
    # ">" (not ">=") means that if two days tie, the EARLIER day is kept.
    if (zone_day_total[z, d] > best_total) {
      best_total <- zone_day_total[z, d]
      best_day   <- d
    }
  }

  # Save the winning day and its total for this zone
  peak_zone_days$Day[z]        <- best_day
  peak_zone_days$TotalUnits[z] <- best_total
}

# Display the completed dataframe
cat("peak_zone_days:\n")
print(peak_zone_days)
```

---

# Summary of Results

| Part | Dataframe               | Content                                                   |
|------|-------------------------|-----------------------------------------------------------|
| A    | `warehouse_data`        | 840 rows x 5 columns (Day, Zone, Category, UnitsProcessed, ProcessingTime) |
| B    | `zone_totals`           | Total units processed by each of the 5 zones              |
| C    | `category_average_time` | Average processing time for each of the 6 categories      |
| D    | `processing_anomalies`  | Every record with its daily zone average and anomaly flag |
| E    | `peak_zone_days`        | The highest-output day for each zone and its total        |
