#----------------------------------------------------------------------
# Student name: Nadejda Toma
# Student number: 2232920
# Module code: ACCA7022 Advance Statistical Programming
# Assessment title: Data Manipulation
# File: warehouse_manipulation.R
# Date: 23 October 2026
# Scenario: Smart Warehouse Inventory Simulation.
#-----------------------------------------------------------------------


# This program simulates warehouse activity and uses loops to organise
# and analyse the records. It creates the data, summarises work by zone
# and category, checks for unusual values and finds each zone's busiest day.
# I have kept two anomaly analyses because they compare different groups:
# categories within a day and zone, and daily totals within a zone.
# Results are displayed as each stage is completed.


#-----------------------------------------------------------------------
# PART A - GENERATE WAREHOUSE DATA
#-----------------------------------------------------------------------

# I keep the seed fixed so the same data can be checked again after each run.
# The vectors are prepared before the loops, with matching positions for each
# record. Working through days, zones and categories in order makes it easier
# to keep the calculated units and processing times beside the right details.
set.seed(11)

# Keep the fixed settings together before generating the records.
BaseUnits <- 100
TimePerUnit <- 0.85

total_rows <- 28 * 5 * 6

Day <- rep(0, total_rows)
Zone <- rep(0, total_rows)
Category <- rep(0, total_rows)
UnitsProcessed <- rep(0, total_rows)
ProcessingTime <- rep(0, total_rows)

row_index <- 1

for (day in 1:28) {
  for (zone in 1:5) {
    for (category in 1:6) {

      Day[row_index] <- day
      Zone[row_index] <- zone
      Category[row_index] <- category

      # Draw a fresh variation for the current record.
      RandomVariation <- sample(-12:12, 1)

      UnitsProcessed[row_index] <- BaseUnits + (Zone[row_index] * 8) +
        (Category[row_index] * 5) + RandomVariation

      ProcessingTime[row_index] <- UnitsProcessed[row_index] * TimePerUnit

      # Move on only after all values for this record have been stored.
      row_index <- row_index + 1
    }
  }
}

# The completed vectors now become columns in the warehouse table.
# A short preview and the dimensions help check its structure without
# printing the full dataset.
warehouse_data <- data.frame(Day, Zone, Category, UnitsProcessed, ProcessingTime)

print(head(warehouse_data, 10))

print(nrow(warehouse_data))

print(ncol(warehouse_data))


#-----------------------------------------------------------------------
# PART B - TOTAL UNITS PROCESSED PER ZONE
#-----------------------------------------------------------------------

# This section brings all records for a zone together into one total.
# A separate running total is used for each zone so results do not carry over
# from the previous group. I keep the totals in a vector first, then create
# the result table once the calculations are finished.
Zone <- 1:5
TotalUnits <- rep(0, 5)

for (zone in 1:5) {
  total <- 0

  for (i in 1:nrow(warehouse_data)) {
    if (warehouse_data$Zone[i] == zone) {
      total <- total + warehouse_data$UnitsProcessed[i]
    }
  }

  # Save the total after all records for this zone have been checked.
  TotalUnits[zone] <- total
}

zone_totals <- data.frame(Zone, TotalUnits)

print(zone_totals)


#-----------------------------------------------------------------------
# PART C - AVERAGE PROCESSING TIME PER PRODUCT CATEGORY
#-----------------------------------------------------------------------

# Here I want to compare the processing time of the product categories.
# Each category is checked across all days and zones. The loop collects its
# processing times and counts the matching records, so the average describes
# that category alone. Both values start fresh when the category changes.
average_time <- rep(0, 6)

for (category in 1:6) {
  total_time <- 0
  record_count <- 0

  for (i in 1:nrow(warehouse_data)) {
    if (warehouse_data$Category[i] == category) {
      # Count the same records whose processing times are being added.
      total_time <- total_time + warehouse_data$ProcessingTime[i]
      record_count <- record_count + 1
    }
  }

  # Store the completed average before moving to the next category.
  average_time[category] <- total_time / record_count
}

# The averages are ready before the table is created. Filling it row by row
# keeps each category beside its result and makes the storage stage separate
# from the calculations.
category_average_time <- data.frame(
  Category = numeric(0),
  AverageProcessingTime = numeric(0)
)

for (category in 1:6) {
  category_average_time[category, ] <- c(
    category,
    average_time[category]
  )
}

print(category_average_time)


#-----------------------------------------------------------------------
# PART D - IDENTIFY STOCK PROCESSING ANOMALIES
#-----------------------------------------------------------------------

# This version looks for differences between categories on the same day
# and in the same zone. The first scan collects the matching records and
# calculates their average. A second scan gives that average to every record
# in the group. This avoids calculating it again for each category while
# keeping the results in the same order as the original data.
daily_zone_average <- rep(0, nrow(warehouse_data))

for (day in 1:28) {
  for (zone in 1:5) {
    # Start a fresh total and count for the current day and zone.
    total_units <- 0
    record_count <- 0

    for (i in 1:nrow(warehouse_data)) {
      if (warehouse_data$Day[i] == day &
          warehouse_data$Zone[i] == zone) {
        total_units <- total_units + warehouse_data$UnitsProcessed[i]
        record_count <- record_count + 1
      }
    }

    # The matching records form the group used for this average.
    average_units <- total_units / record_count

    # Copy the group average to each matching position in the result vector.
    for (i in 1:nrow(warehouse_data)) {
      if (warehouse_data$Day[i] == day &
          warehouse_data$Zone[i] == zone) {
        daily_zone_average[i] <- average_units
      }
    }
  }
}

# The result table keeps the original record details beside their average.
# Each record is then checked against the allowed range and given a logical
# flag. I select flagged rows in a separate table for display, so the complete
# results remain available for inspection.
processing_anomalies <- data.frame(
  Day = numeric(0),
  Zone = numeric(0),
  Category = numeric(0),
  UnitsProcessed = numeric(0),
  DailyZoneAverage = numeric(0),
  Anomaly = logical(0)
)

for (i in 1:nrow(warehouse_data)) {
  processing_anomalies[i, "Day"] <- warehouse_data$Day[i]
  processing_anomalies[i, "Zone"] <- warehouse_data$Zone[i]
  processing_anomalies[i, "Category"] <- warehouse_data$Category[i]
  processing_anomalies[i, "UnitsProcessed"] <- warehouse_data$UnitsProcessed[i]
  processing_anomalies[i, "DailyZoneAverage"] <- daily_zone_average[i]

  # Either an unusually high or low value is enough to flag the record.
  if (processing_anomalies$UnitsProcessed[i] >
      processing_anomalies$DailyZoneAverage[i] * 1.15 |
      processing_anomalies$UnitsProcessed[i] <
      processing_anomalies$DailyZoneAverage[i] * 0.85) {
    processing_anomalies[i, "Anomaly"] <- TRUE
  } else {
    processing_anomalies[i, "Anomaly"] <- FALSE
  }
}

# Filter a separate copy so unflagged records stay in the full table.
anomaly_rows <- processing_anomalies[
  processing_anomalies$Anomaly == TRUE, ]

print(head(anomaly_rows, 10))


#-----------------------------------------------------------------------
# PART D1 - ANOMALIES IN DAILY ZONE TOTALS
#-----------------------------------------------------------------------

# This alternative looks at changes between days rather than categories.
# All categories are combined into a daily total for each zone. Those totals
# are saved while the zone total is collected, then used to find the zone's
# average daily activity. Keeping this analysis separate makes the different
# comparison clear and leaves the original anomaly results available.
daily_zone_totals <- rep(0, 28 * 5)

zone_daily_average <- rep(0, 5)

row_index <- 1

for (zone in 1:5) {
  zone_total <- 0

  for (day in 1:28) {
    # Keep each daily total separate from the previous day.
    daily_total <- 0

    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]
      }
    }

    # Save this day separately before adding it to the whole zone total.
    daily_zone_totals[row_index] <- daily_total

    zone_total <- zone_total + daily_total

    row_index <- row_index + 1
  }

  # The daily average is ready once all days for this zone have been checked.
  zone_daily_average[zone] <- zone_total / 28
}

# The table is filled in the same zone and day order as the saved totals.
# Each total is compared with its own zone's daily average, and the flag is
# stored beside it. Categories are not listed separately here because they
# have already been combined. The final display selects only flagged days.
processing_anomalies1 <- data.frame(
  Day = numeric(0),
  Zone = numeric(0),
  TotalUnits = numeric(0),
  ZoneDailyAverage = numeric(0),
  Anomaly = logical(0)
)

row_index <- 1

for (zone in 1:5) {
  for (day in 1:28) {
    processing_anomalies1[row_index, "Day"] <- day
    processing_anomalies1[row_index, "Zone"] <- zone

    processing_anomalies1[row_index, "TotalUnits"] <-
      daily_zone_totals[row_index]

    processing_anomalies1[row_index, "ZoneDailyAverage"] <-
      zone_daily_average[zone]

    # Use this zone's average, rather than an average across different zones.
    if (daily_zone_totals[row_index] >
        zone_daily_average[zone] * 1.15 |
        daily_zone_totals[row_index] <
        zone_daily_average[zone] * 0.85) {
      processing_anomalies1[row_index, "Anomaly"] <- TRUE
    } else {
      processing_anomalies1[row_index, "Anomaly"] <- FALSE
    }

    row_index <- row_index + 1
  }
}

print(head(processing_anomalies1, 10))
print(nrow(processing_anomalies1))

# An empty result here means no daily total fell outside its allowed range.
anomaly_rows1 <- processing_anomalies1[
  processing_anomalies1$Anomaly == TRUE, ]

print(head(anomaly_rows1, 10))


#-----------------------------------------------------------------------
# PART E - IDENTIFY THE MOST PRODUCTIVE DAY FOR EACH ZONE
#-----------------------------------------------------------------------

# To find the busiest day, each zone is considered separately.
# The daily totals include all categories. As the days are checked, the best
# total and its day are kept together and updated only when a larger total
# is found. A tie therefore keeps the earlier day rather than replacing it.
peak_day <- rep(0, 5)
peak_total <- rep(0, 5)

for (zone in 1:5) {
  highest_total <- 0
  best_day <- 0

  for (day in 1:28) {
    # Keep each daily total separate from the previous day.
    daily_total <- 0

    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]
      }
    }

    # Keep the total and day together whenever a new maximum is found.
    if (daily_total > highest_total) {
      highest_total <- daily_total
      best_day <- day
    }
  }

  # Store the zone's result only after its daily totals have all been compared.
  peak_day[zone] <- best_day
  peak_total[zone] <- highest_total
}

# After every day has been checked, the saved results are written into
# the table row by row. Keeping the zone, chosen day and total together
# makes it clear which result belongs to each zone.
peak_zone_days <- data.frame(
  Zone = numeric(0),
  Day = numeric(0),
  TotalUnits = numeric(0)
)

for (zone in 1:5) {
  peak_zone_days[zone, ] <- c(
    zone,
    peak_day[zone],
    peak_total[zone]
  )
}

print(peak_zone_days)
