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.
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")
| 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 |
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.
The scheduling algorithm iteratively assigns each project to an available PM using the following principles:
max_concurrent_projects at a time.pm_daily_hours,
pm_daily_projects) are initialized to keep daily records of
each PM’s effort and number of active projects.projects_vec is updated to reflect the increased
number of concurrent projects.End_Day.Assigned_PM: The ID of the assigned PMEnd_Day: The last day of the assignmentn_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
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:
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.
| 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 |
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:
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 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 |
## # 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