Staffing Model

This document simulates the allocation of Project Managers (PMs) to a series of defined projects over time. The model dynamically assigns each project to a PM based on availability. The assumptions, parameters, and logic are described in detail below.

Assumptions and Key Parameters

  • Available hours per PM per day: 8 hours
  • Project effort: Defined in total hours required per project
  • Start day: Projects have fixed start dates
  • Maximum concurrent projects per PM per day: 3
  • Project duration rounding: Projects are scheduled in daily effort chunks (e.g., 8 hours/day). As a result, some projects may appear to exceed their expected duration by 1 day due to rounding in the final day of effort allocation. This is considered acceptable within the model.

1. Project Setup

This section initializes the project list and defines effort (in hours), fixed start dates, and sets placeholders for calculated end dates and PM assignments.

# Load required libraries
library(dplyr)
library(knitr)
library(kableExtra)

# Set seed for reproducibility so results are consistent each time
set.seed(42)

# Set the number of working hours a PM has available per day
available_hours_per_pm_per_day <- 8

# Set the max number of concurrent projects a PM can be assigned to at once
max_concurrent_projects <- 3

# Set the total number of days in the simulation horizon
max_day <- 360

# Define the effort (in total hours) required to complete each project
project_profile_durations <- c(100, 20, 100, 95, 90, 200, 95, 15, 90, 40, 85, 95, 130, 80)

# Define the fixed start day for each project
project_start_days <- c(1, 1, 5, 5, 10, 10, 15, 15, 20, 20, 25, 25, 30, 30)

# Calculate the total number of projects
n_projects <- length(project_profile_durations)

# Assign placeholder project names — these can be customized later
project_names <- paste("Project", 1:n_projects)

# Create the full project setup table (including fields used later in scheduling)
project_setup <- data.frame(
  Project_ID = 1:n_projects,                        # Unique project identifier
  Project_Name = project_names,                    # Human-readable project name
  Start_Day = project_start_days,                  # Fixed day the project starts
  Max_Duration = project_profile_durations,        # Maximum days allowed for completion
  Hours_Required = project_profile_durations,      # Total work effort required
  End_Day = NA,                                     # Placeholder — calculated after assignment
  Hours_Remaining = project_profile_durations,     # For future tracking of progress
  Assigned_PM = NA                                  # Placeholder — assigned during scheduling
)

# Display a clean summary table of the setup — exclude columns not yet relevant (e.g. End_Day, Assigned_PM)
project_setup %>%
  select(Project_ID, Project_Name, Start_Day, Hours_Required) %>%
  rename(
    `Project ID` = Project_ID,
    `Project Name` = Project_Name,
    `Start Day` = Start_Day,
    `Hours Required` = Hours_Required
  ) %>%
  kable(format = "html", caption = "Initial Project Setup Summary") %>%
  kable_styling(bootstrap_options = c("striped", "hover", "condensed", "responsive"), full_width = FALSE) %>%
  row_spec(0, bold = TRUE, background = "#f2f2f2")
Initial Project Setup Summary
Project ID Project Name Start Day Hours Required
1 Project 1 1 100
2 Project 2 1 20
3 Project 3 5 100
4 Project 4 5 95
5 Project 5 10 90
6 Project 6 10 200
7 Project 7 15 95
8 Project 8 15 15
9 Project 9 20 90
10 Project 10 20 40
11 Project 11 25 85
12 Project 12 25 95
13 Project 13 30 130
14 Project 14 30 80

2. PM Assignment Logic

This section describes the logic used to assign Project Managers (PMs) to projects while respecting critical constraints such as project start dates, duration limits, and limits on concurrent assignments.

Overview

The scheduling algorithm iteratively assigns each project to an available PM using the following principles:

  • Fixed Start Date: Each project has a defined start day.
  • Maximum Duration: Each project must be completed within a specified number of days.
  • Full-Day Effort: Each PM can contribute 8 hours per day to projects.
  • Concurrency Constraint: A PM may work on at most max_concurrent_projects at a time.

Step-by-Step Assignment Logic

  1. Initialization
    • A schedule table is created based on the initial project setup.
    • Two tracking lists (pm_daily_hours, pm_daily_projects) are initialized to keep daily records of each PM’s effort and number of active projects.
  2. Loop Over Each Project
    For each project in the schedule:
    • The algorithm checks whether any existing PM can handle the project within its duration and concurrency constraints.
  3. Feasibility Check for Existing PMs
    • For each PM, we simulate whether the project can be completed on time.
    • We look day by day across the project’s possible duration:
      • If the PM is already handling the maximum number of projects on any day, they are ineligible.
      • We deduct one day’s worth of effort (8 hours) for each available day.
      • If the required hours can be fully scheduled before the max duration is reached, the PM is deemed eligible.
  4. Assigning the Project to an Eligible PM
    • Once an eligible PM is found:
      • The project is scheduled day-by-day by adding 8 hours of work per day.
      • The projects_vec is updated to reflect the increased number of concurrent projects.
      • The PM is recorded in the schedule along with the calculated End_Day.
  5. If No PM Is Eligible
    • A new PM is added to the system.
    • The project is assigned to the new PM in the same day-by-day fashion.
    • The new PM’s daily effort and concurrency vectors are created and populated.
  6. Output
    • The final schedule table includes:
      • Assigned_PM: The ID of the assigned PM
      • End_Day: The last day of the assignment
    • The number of PMs used (n_pms) is returned along with the schedule.

This logic ensures that projects are never overbooked, always start on time, and respect each PM’s capacity, with new PMs added only when absolutely necessary.

# Function to assign projects to available PMs, enforcing start date, max duration, and concurrency limits
assign_projects_overlap_with_duration_check <- function(project_setup) {
  
  # Initialize output schedule table with placeholder values for Assigned_PM and End_Day
  schedule <- project_setup %>% mutate(Assigned_PM = NA, End_Day = NA)
  
  # Initialize lists to track each PM's daily workload and number of concurrent projects
  pm_daily_hours <- list()
  pm_daily_projects <- list()

  # Loop through each project to assign a PM
  for (i in 1:nrow(schedule)) {
    proj <- schedule[i, ]
    start_day <- proj$Start_Day
    duration <- proj$Max_Duration
    assigned <- FALSE  # Track whether the project has been successfully assigned

    # Try assigning to existing PMs first
    for (pm in seq_along(pm_daily_hours)) {
      hours_vec <- pm_daily_hours[[pm]]
      projects_vec <- pm_daily_projects[[pm]]

      # Extend vectors to cover the planning horizon if needed
      if (length(hours_vec) < max_day) {
        hours_vec <- c(hours_vec, rep(0, max_day - length(hours_vec)))
        projects_vec <- c(projects_vec, rep(0, max_day - length(projects_vec)))
      }

      # Check if this PM can handle the project within its duration limit
      remaining_hours <- proj$Hours_Required
      can_assign <- TRUE

      for (d in start_day:(start_day + duration - 1)) {
        if (projects_vec[d] >= max_concurrent_projects) {
          can_assign <- FALSE  # Too many concurrent projects on this day
          break
        }
        remaining_hours <- remaining_hours - available_hours_per_pm_per_day
        if (remaining_hours <= 0) break  # Project can be completed
      }

      # If the PM is eligible, assign the project day by day
      if (can_assign && remaining_hours <= 0) {
        remaining_hours <- proj$Hours_Required
        for (d in start_day:(start_day + duration - 1)) {
          if (remaining_hours <= 0) break
          hours_vec[d] <- hours_vec[d] + available_hours_per_pm_per_day
          projects_vec[d] <- projects_vec[d] + 1
          remaining_hours <- remaining_hours - available_hours_per_pm_per_day
        }
        schedule$Assigned_PM[i] <- pm
        schedule$End_Day[i] <- d
        pm_daily_hours[[pm]] <- hours_vec
        pm_daily_projects[[pm]] <- projects_vec
        assigned <- TRUE
        break  # Move on to the next project
      }
    }

    # If no existing PM could take the project, create a new PM for it
    if (!assigned) {
      new_hours <- numeric(max_day)
      new_projects <- numeric(max_day)
      remaining_hours <- proj$Hours_Required
      end_day <- start_day

      for (d in start_day:(start_day + proj$Max_Duration - 1)) {
        if (remaining_hours <= 0) break
        new_hours[d] <- available_hours_per_pm_per_day
        new_projects[d] <- 1
        remaining_hours <- remaining_hours - available_hours_per_pm_per_day
        end_day <- d
      }

      schedule$Assigned_PM[i] <- length(pm_daily_hours) + 1
      schedule$End_Day[i] <- end_day
      pm_daily_hours[[length(pm_daily_hours) + 1]] <- new_hours
      pm_daily_projects[[length(pm_daily_projects) + 1]] <- new_projects
    }
  }

  # Return the final schedule and the number of PMs used
  return(list(schedule = schedule, pm_count = length(pm_daily_hours)))
}

# Run the assignment function and store results
result <- assign_projects_overlap_with_duration_check(project_setup)

# Extract final schedule and PM count
schedule <- result$schedule
n_pms <- result$pm_count

3. Summarize PM Utilization

The table below provides a high-level overview of how Project Managers (PMs) have been assigned across all scheduled projects. It captures essential data points that allow you to quickly assess the distribution of workload among PMs and identify how early or late their project involvement spans.

What this table includes:

  • PM ID: A unique identifier for each Project Manager.
  • Number of Projects: The total number of projects assigned to the PM.
  • Project IDs: A comma-separated list of project identifiers that the PM is responsible for.
  • Start (Earliest): The first day the PM is scheduled to begin work on any assigned project.
  • End (Latest): The last day the PM is scheduled to finish their final assigned project.

This summary helps determine: - Whether PM resources are being efficiently reused. - How evenly projects are spread across available PMs. - Which PMs are engaged early or late in the planning window.

It is especially useful when optimizing for the minimum number of PMs required to complete all work within the given project durations.

Summary of PM Assignments
PM ID Number of Projects Project IDs Start (Earliest) End (Latest)
1 9 1, 2, 3, 4, 7, 9, 10, 11, 13 1 47
2 5 5, 6, 8, 12, 14 10 40

4. PM Assignment Timeline

The table below summarizes the day-by-day timeline of each project assigned to a Project Manager (PM). It includes key scheduling metrics that help evaluate performance and identify potential bottlenecks.

What this table shows:

  • PM ID: The identifier of the Project Manager assigned to the project.
  • Project ID: The unique identifier of the project.
  • Start Day: The day the project begins.
  • End Day: The day the project is completed based on allocated effort.
  • Hours Required: The total effort (in hours) needed to complete the project.
  • Duration (Days): The actual number of days the PM spent working on the project.
  • Expected Duration: The ideal number of days needed if the PM worked full capacity (8 hours/day).
  • Overrun: A flag that indicates whether the actual duration exceeded the expected duration by more than one day. A value of TRUE suggests a mild delay in execution.

This table is useful for identifying schedule overruns, verifying workload distribution across PMs, and assessing whether the assigned durations are realistic given PM capacity.

PM Project Timeline Overview
PM ID Project ID Start Day End Day Hours Required Duration (Days) Expected Duration Overrun
1 1 1 13 100 13 13 FALSE
1 2 1 4 20 4 3 FALSE
1 3 5 18 100 14 13 FALSE
1 4 5 17 95 13 12 FALSE
1 7 15 27 95 13 12 FALSE
1 9 20 32 90 13 12 FALSE
1 10 20 25 40 6 5 FALSE
1 11 25 36 85 12 11 FALSE
1 13 30 47 130 18 17 FALSE
2 5 10 21 90 12 12 FALSE
2 6 10 35 200 26 25 FALSE
2 8 15 17 15 3 2 FALSE
2 12 25 37 95 13 12 FALSE
2 14 30 40 80 11 10 FALSE

5. Gantt Chart

6. Daily PM Effort per Project

## # A tibble: 20 × 4
##    Assigned_PM Project_ID   Day Hours_Per_Project
##          <dbl>      <int> <int>             <dbl>
##  1           1          1     1              4   
##  2           1          2     1              4   
##  3           1          1     2              4   
##  4           1          2     2              4   
##  5           1          1     3              4   
##  6           1          2     3              4   
##  7           1          1     4              4   
##  8           1          2     4              4   
##  9           1          1     5              2.67
## 10           1          3     5              2.67
## 11           1          4     5              2.67
## 12           1          1     6              2.67
## 13           1          3     6              2.67
## 14           1          4     6              2.67
## 15           1          1     7              2.67
## 16           1          3     7              2.67
## 17           1          4     7              2.67
## 18           1          1     8              2.67
## 19           1          3     8              2.67
## 20           1          4     8              2.67