# Name: Alexei Nica
# Student number: 2232919
# Module: ACCA7022 - Advanced Statistical Programming
# Assessment title: Data Manipulation
# File: warehouse_manipulation.r
# Scenario: Smart Warehouse Inventory Simulation
# Date: 23 October 2026


# This script simulates 28 days of warehouse activity across 5 zones
# and 6 product categories. It uses loops to generate the data, calculate
# summaries and identify unusual processing values.
# Two interpretations of the anomaly calculation are included to compare
# individual category records and combined daily totals for each zone.



# A. Generate Warehouse Data

# I kept a fixed seed so the data stays the same between runs. Calculations
# and results can be checked again using the same values.
set.seed(1)

BaseUnits <- 100
TimePerUnit <- 0.85

# The empty dataframe sets out the columns before any records are added.
# Each completed row is added with rbind(), which is easy to follow for this
# small dataset. Growing a table this way would be less efficient for much more data.
warehouse_data <- data.frame(
  Day = integer(),
  Zone = integer(),
  Category = integer(),
  UnitsProcessed = integer(),
  ProcessingTime = numeric()
)

# for loops are used because the ranges are known: 28 days, 5 zones and 6 categories.
# while would also work, but would need counters to be set and increased manually.
# for keeps this simpler, while is more useful when the number of repetitions
# is not known in advance and depends on a condition.
# The three nested loops create each combination once, giving 840 records.
for (Day in 1:28) {
  for (Zone in 1:5) {
    for (Category in 1:6) {
      
      # Pick a new, random variation for each record, then calculate units and time.
      RandomVariation <- sample(-12:12, 1)
      
      UnitsProcessed <- BaseUnits + (Zone * 8) +
        (Category * 5) + RandomVariation
      
      ProcessingTime <- UnitsProcessed * TimePerUnit
      
      # Keep these values together as one row before adding the next record.
      newRow <- data.frame(
        Day = Day,
        Zone = Zone,
        Category = Category,
        UnitsProcessed = UnitsProcessed,
        ProcessingTime = ProcessingTime
      )
      
      warehouse_data <- rbind(warehouse_data, newRow)
    }
  }
}

# Show a sample and the dimensions to check the generated table.
print(head(warehouse_data, 10))

print(nrow(warehouse_data))
print(ncol(warehouse_data))



# B. Total Units Processed per Zone

# I kept one summary row per zone, so the table starts empty and grows
# after each zone total is complete. The inner loop visits all records, the
# if condition selects the matching zone. Resetting TotalUnits inside the
# outer loop prevents the previous zone's total being carried forward.
zone_totals <- data.frame(
  Zone = integer(),
  TotalUnits = numeric()
)

for (Zone in 1:5) {
  
  # Start a fresh total for this zone.
  TotalUnits <- 0
  
  for (i in 1:nrow(warehouse_data)) {
    
    if (warehouse_data[i, "Zone"] == Zone) {
      TotalUnits <- TotalUnits +
        warehouse_data[i, "UnitsProcessed"]
    }
  }
  
  # All matching records have now been added, so the zone total is ready.
  newRow <- data.frame(
    Zone = Zone,
    TotalUnits = TotalUnits
  )
  
  zone_totals <- rbind(zone_totals, newRow)
}

print(zone_totals)


# C. Average Processing Time per Product Category

# For category averages, the table is also created first. A separate
# counter records how many matching values are added, so the average uses
# the actual record count. Both the count and time total restart for each
# category, and the result is stored only after its records have been checked.
category_average_time <- data.frame(
  Category = integer(),
  AverageProcessingTime = numeric()
)

for (Category in 1:6) {
  
  TotalProcessingTime <- 0
  RecordMatch <- 0
  
  for (i in 1:nrow(warehouse_data)) {
    
    if (warehouse_data[i, "Category"] == Category) {
      TotalProcessingTime <- TotalProcessingTime +
        warehouse_data[i, "ProcessingTime"]
      
      RecordMatch <- RecordMatch + 1
    }
  }
  
  # Divide only after the total and the matching record count are complete.
  AverageProcessingTime <- TotalProcessingTime / RecordMatch
  
  newRow <- data.frame(
    Category = Category,
    AverageProcessingTime = AverageProcessingTime
  )
  
  category_average_time <- rbind(category_average_time, newRow)
}

print(category_average_time)



# D. Identify Stock Processing Anomalies

# I decided to show both interpretations of the anomaly comparison
# separately. D compares each category with the average for its day and zone;
# D1 compares the combined daily production with the zone's average over 28 days.
# These compare different values, so different anomaly results are expected.

# Here the original 840 records are kept. Two vectors hold the extra results
# in the same row positions, making it simple to attach them to the original
# columns. FALSE is defined at the start, until a comparison finds an anomaly.
DailyZoneAverage <- numeric(nrow(warehouse_data))
Anomaly <- rep(FALSE, nrow(warehouse_data))

# Two inner loops are used for each day and zone: first find the average, then
# compare the matching records with it. The second loop is needed because
# the average must be known before the 15% comparison can be made.
for (Day in 1:28) {
  for (Zone in 1:5) {
    # Start again for each day and zone; this group contains six categories.
    TotalUnits <- 0
    RecordMatch <- 0
    
    for (i in 1:nrow(warehouse_data)) {
      if (warehouse_data[i, "Day"] == Day &
          warehouse_data[i, "Zone"] == Zone) {
        TotalUnits <- TotalUnits + warehouse_data[i, "UnitsProcessed"]
        RecordMatch <- RecordMatch + 1
      }
    }
    
    # The first loop has collected everything needed to calculate the average.
    AverageUnits <- TotalUnits / RecordMatch
    
    for (i in 1:nrow(warehouse_data)) {
      if (warehouse_data[i, "Day"] == Day &
          warehouse_data[i, "Zone"] == Zone) {
        # Keep the result in position i so it stays beside the original record.
        DailyZoneAverage[i] <- AverageUnits
        
        if (warehouse_data[i, "UnitsProcessed"] > AverageUnits * 1.15 |
            warehouse_data[i, "UnitsProcessed"] < AverageUnits * 0.85) {
          Anomaly[i] <- TRUE
        }
      }
    }
  }
}

# After calculating the averages and checking for anomalies, I add both
# result vectors to the original data to create the final table.
processing_anomalies <- data.frame(
  Day = warehouse_data$Day,
  Zone = warehouse_data$Zone,
  Category = warehouse_data$Category,
  UnitsProcessed = warehouse_data$UnitsProcessed,
  DailyZoneAverage = DailyZoneAverage,
  Anomaly = Anomaly
)

# Display only the first 10 records flagged as anomalies.
print(head(
  processing_anomalies[processing_anomalies$Anomaly == TRUE, ],
  10
))
# D1. Anomalies based on daily zone totals

# I used an empty table again here because each result is a new summary
# row, giving 28 x 5 = 140 rows. The zone total is already available from B;
# dividing it by 28 avoids repeating that calculation. Only the daily total
# needs to be found before adding each row. "All categories" labels the combined
# values, and the comparison itself supplies the TRUE/FALSE column.
processing_anomalies1 <- data.frame(
  Day = integer(),
  Zone = integer(),
  Category = character(),
  UnitsProcessed = numeric(),
  DailyZoneAverage = numeric(),
  Anomaly = logical()
)

for (Zone in 1:5) {
  
  # Use the total already stored in B for this zone and divide it by 28 days.
  AverageUnits <- zone_totals[zone_totals$Zone == Zone, "TotalUnits"] / 28
  
  for (Day in 1:28) {
    TotalUnits <- 0
    
    for (i in 1:nrow(warehouse_data)) {
      if (warehouse_data[i, "Day"] == Day &
          warehouse_data[i, "Zone"] == Zone) {
        TotalUnits <- TotalUnits + warehouse_data[i, "UnitsProcessed"]
      }
    }
    
    # This row represents all six categories together, not one product category.
    newRow <- data.frame(
      Day = Day,
      Zone = Zone,
      Category = "All categories",
      UnitsProcessed = TotalUnits,
      DailyZoneAverage = AverageUnits,
      Anomaly = TotalUnits > AverageUnits * 1.15 |
        TotalUnits < AverageUnits * 0.85
    )
    
    processing_anomalies1 <- rbind(processing_anomalies1, newRow)
  }
}

# An empty result is possible here if every daily total is within the limits.
print(head(
  processing_anomalies1[processing_anomalies1$Anomaly == TRUE, ],
  10
))



# E. Identify the Most Productive Day for Each Zone

# Each zone's daily totals are checked one at a time, keeping only the highest
# total and its day. Starting at zero works because all unit values are positive.
# Using > rather than >= keeps the first day if the highest total repeats more
# than once. One completed result is then added to the table for each zone.
peak_zone_days <- data.frame(
  Zone = integer(),
  Day = integer(),
  TotalUnits = numeric()
)

for (Zone in 1:5) {
  
  HighTotalValue <- 0
  PeakDay <- 1
  
  for (Day in 1:28) {
    
    TotalUnits <- 0
    
    for (i in 1:nrow(warehouse_data)) {
      
      if (warehouse_data[i, "Zone"] == Zone &
          warehouse_data[i, "Day"] == Day) {
        
        TotalUnits <- TotalUnits +
          warehouse_data[i, "UnitsProcessed"]
      }
    }
    
    # Update both values together so the stored day matches the highest total.
    if (TotalUnits > HighTotalValue) {
      HighTotalValue <- TotalUnits
      PeakDay <- Day
    }
  }
  
  # Save the result after all 28 days have been compared.
  newRow <- data.frame(
    Zone = Zone,
    Day = PeakDay,
    TotalUnits = HighTotalValue
  )
  
  peak_zone_days <- rbind(peak_zone_days, newRow)
}

print(peak_zone_days)


# Testing and validation
# I check selected results using the values displayed in the existing tables.
# The calculations below are compared with the stored averages, processing
# times and anomaly results. These values correspond to set.seed(1).


# Check processing time and category anomalies for day 1, zone 1.
# The first six rows contain the six categories for this day and zone.

View(warehouse_data[1:6, ])
View(processing_anomalies[1:6, ])

# The first record has 125 units. Compare this calculation with ProcessingTime.
print(125 * 0.85)

# Calculate the average from the six displayed UnitsProcessed values.
AverageUnits <- (125 + 109 + 117 + 116 + 122 + 148) / 6

print(AverageUnits)

# Compare the six values with these limits.
# Only values outside the limits should have Anomaly = TRUE.
print(AverageUnits * 0.85)
print(AverageUnits * 1.15)


# Check daily production and the peak day for zone 1.
# The first 28 rows contain the daily totals for this zone.

View(processing_anomalies1[1:28, ])

# Calculate the daily average from the 28 displayed UnitsProcessed values.
AverageUnits <- (
  737 + 732 + 749 + 749 + 739 + 780 + 761 +
    746 + 737 + 744 + 724 + 739 + 747 + 767 +
    761 + 743 + 758 + 740 + 766 + 740 + 731 +
    761 + 750 + 752 + 765 + 734 + 754 + 755
) / 28

# Compare this result with DailyZoneAverage.
print(AverageUnits)

# Check whether the daily totals fall within these limits.
print(AverageUnits * 0.85)
print(AverageUnits * 1.15)

# Find the highest daily total in the displayed table.
# Compare its day and value with the stored result for zone 1.
View(peak_zone_days[1, ])