Roster Generation not allocating all shifts

Results not coming out as expected. Have created a loop to go through it three times, but it still leaves lots of shifts as unallocated.

Using the below code, it does the job fairly well, but I want to figure out why its not grabbing the remaining shifts and attempting to create lines of work out of it.

Last time I ran it, it left approx. 57 shifts as “unallocated” but when I went through them manually, I was able to place most of the shifts in a line of work with other unallocated shifts.

import pandas as pd
import random
from collections import defaultdict

# Define parse_days function
def parse_days(days_string):
    if pd.isna(days_string):
        return []
    return list(days_string.lower())

# Load Excel file
file_path = r'E:Header.xlsx'
sheet_name = input('Please type the Sheet Name to generate rosters from: ')
df = pd.read_excel(file_path, sheet_name=sheet_name)

# Mapping for days
day_map = {
    'm': 'Monday',
    'u': 'Tuesday',
    'w': 'Wednesday',
    't': 'Thursday',
    'f': 'Friday',
    'a': 'Saturday',
    's': 'Sunday'
}

# Define line limits (easily editable)
MAX_MONDAY_FRIDAY_LINES = int(input('How many Monday-Friday Lines would you like? '))
MAX_MONDAY_SATURDAY_LINES = int(input('How many Monday-Saturday Lines would you like? '))
MAX_MONDAY_SUNDAY_LINES = int(input('How many Monday-Sunday Lines would you like? '))
MAX_SHIFTS = 5  # set to 5 for Full-Timers, 3 for Flexi's etc.

# Hour constraints
MAX_HOURS = 39.3333
MIN_HOURS = 37.0000
BUFFER_HOURS = 33.0000

MAX_START_TIME_DIFF = pd.Timedelta(minutes=90)  # Max allowable start time difference between days within the same line - EBA states 90 minutes, this limits it to 1 hour. To change to 90 minutes, change to minutes=90

# Prepare data structure to store shifts
shifts = []
for idx, row in df.iterrows():
    try:
        days = parse_days(row['Days'])
    except KeyError:
        print(f"Warning: 'Days' column is missing or invalid in row {idx}. Skipping...")
        continue

    try:
        start_time = (pd.Timestamp('1900-01-01') + row['ON 1']).time()  # Convert timedelta to time
    except KeyError:
        print(f"Warning: StartTime not found in row {idx}. Skipping...")
        continue

    try:
        hours = row['HRS WKD']
        if isinstance(hours, pd.Timedelta):
            hours = hours.total_seconds() / 3600  # Convert Timedelta to hours
    except KeyError:
        print(f"Warning: Hours not found in row {idx}. Skipping...")
        continue

    try:
        shift_number = str(row['Shift'])  # Ensure ShiftNumber is a string
    except KeyError:
        print(f"Warning: Shift not found in row {idx}. Skipping...")
        continue

    for day in days:
        if day not in day_map:
            print(f"Warning: Day '{day}' from row {idx} not found in day_map. Skipping...")
            continue
        
        shift = {
            'ShiftNumber': shift_number,
            'Day': day_map[day],
            'StartTime': start_time,
            'Hours': hours
        }
        shifts.append(shift)

# Shuffle shifts to randomize
random.shuffle(shifts)

# Prepare output structure
roster = defaultdict(lambda: defaultdict(list))
line_hours = defaultdict(float)
shift_count = defaultdict(int)
line_notes = defaultdict(str)
unallocated_shifts = []

# Define the different line categories
monday_friday_lines = 0
monday_saturday_lines = 0
monday_sunday_lines = 0

def can_allocate_shift(line, shift):
    """Check if a shift can be allocated to a line."""
    if shift_count[line] >= MAX_SHIFTS or line_hours[line] + shift['Hours'] > MAX_HOURS:
        return False
    if shift['Day'] in roster[line] and any(s.get('Day') == shift['Day'] for s in roster[line][shift['Day']]):
        return False
    if line_notes[line] == 'Monday_Friday' and shift['Day'] in ['Saturday', 'Sunday']:
        return False
    if line_notes[line] == 'Monday_Saturday' and shift['Day'] == 'Sunday':
        return False
    
    # Ensure all shifts between Monday and Friday start within the allowed time difference
    if shift['Day'] in ['Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday']:
        start_time = pd.Timestamp.combine(pd.Timestamp('1900-01-01'), shift['StartTime'])
        for existing_shifts in roster[line].values():
            for existing_shift in existing_shifts:
                existing_start_time = pd.Timestamp.combine(pd.Timestamp('1900-01-01'), existing_shift['StartTime'])
                if abs(start_time - existing_start_time) > MAX_START_TIME_DIFF:
                    return False
    return True

def allocate_shift(shift, max_lines, line_counter, line_note):
    """Attempt to allocate a shift to an appropriate line."""
    global monday_friday_lines, monday_saturday_lines, monday_sunday_lines
    for line, hours in sorted(line_hours.items(), key=lambda x: abs(x[1] + shift['Hours'] - (MAX_HOURS + MIN_HOURS) / 2)):
        if line_notes[line] == line_note and can_allocate_shift(line, shift):
            roster[line][shift['Day']].append({
                'ShiftNumber': shift['ShiftNumber'],
                'StartTime': shift['StartTime'],
                'Hours': shift['Hours'],
                'Day': shift['Day']
            })
            line_hours[line] += shift['Hours']
            shift_count[line] += 1
            return True
    
    # If shift could not be added to any existing line, create a new line
    if line_counter < max_lines:
        new_line = max([key for key in roster.keys() if isinstance(key, int)], default=0) + 1
        line_notes[new_line] = line_note
        roster[new_line][shift['Day']] = [{
            'ShiftNumber': shift['ShiftNumber'],
            'StartTime': shift['StartTime'],
            'Hours': shift['Hours'],
            'Day': shift['Day']
        }]
        line_hours[new_line] += shift['Hours']
        shift_count[new_line] += 1
        if line_note == 'Monday_Friday':
            monday_friday_lines += 1
        elif line_note == 'Monday_Saturday':
            monday_saturday_lines += 1
        elif line_note == 'Monday_Sunday':
            monday_sunday_lines += 1
        return True
    return False

# Process shifts to generate roster
for shift in shifts:
    allocated = False
    if shift['Day'] in ['Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday']:
        allocated = allocate_shift(shift, MAX_MONDAY_FRIDAY_LINES, monday_friday_lines, 'Monday_Friday')
    if not allocated and shift['Day'] in ['Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday']:
        allocated = allocate_shift(shift, MAX_MONDAY_SATURDAY_LINES, monday_saturday_lines, 'Monday_Saturday')
    if not allocated:
        if not allocate_shift(shift, MAX_MONDAY_SUNDAY_LINES, monday_sunday_lines, 'Monday_Sunday'):
            unallocated_shifts.append(shift)

# Attempt to reallocate unallocated shifts
def reallocate_unallocated_shifts():
    """Attempt to reallocate unallocated shifts into new lines."""
    global unallocated_shifts, monday_friday_lines, monday_saturday_lines, monday_sunday_lines
    remaining_shifts = []
    for shift in unallocated_shifts:
        allocated = False
        if shift['Day'] in ['Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday']:
            allocated = allocate_shift(shift, MAX_MONDAY_FRIDAY_LINES, monday_friday_lines, 'Monday_Friday')
        if not allocated and shift['Day'] in ['Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday']:
            allocated = allocate_shift(shift, MAX_MONDAY_SATURDAY_LINES, monday_saturday_lines, 'Monday_Saturday')
        if not allocated:
            allocated = allocate_shift(shift, MAX_MONDAY_SUNDAY_LINES, monday_sunday_lines, 'Monday_Sunday')
        if not allocated:
            remaining_shifts.append(shift)
    unallocated_shifts = remaining_shifts

# Loop through reallocation 3 times
for _ in range(3):
    reallocate_unallocated_shifts()

# Additional reallocation into lines with <= 3 shifts
def reallocate_into_lines_with_few_shifts():
    """Reallocate unallocated shifts into lines with 3 or fewer shifts if possible."""
    global unallocated_shifts
    remaining_shifts = []
    for shift in unallocated_shifts:
        allocated = False
        for line in [line for line, count in shift_count.items() if count <= 3]:
            if can_allocate_shift(line, shift):
                roster[line][shift['Day']].append({
                    'ShiftNumber': shift['ShiftNumber'],
                    'StartTime': shift['StartTime'],
                    'Hours': shift['Hours'],
                    'Day': shift['Day']
                })
                line_hours[line] += shift['Hours']
                shift_count[line] += 1
                allocated = True
                break
        if not allocated:
            remaining_shifts.append(shift)
    unallocated_shifts = remaining_shifts

reallocate_into_lines_with_few_shifts()

# Ensure lines are within the required hour constraints
def enforce_hour_constraints():
    """Ensure lines are within the required hour constraints, marking those that need additional hours."""
    global roster, line_hours, line_notes
    for line, hours in line_hours.items():
        if hours < MIN_HOURS and hours >= BUFFER_HOURS:
            line_notes[line] += ' (TBA)'

enforce_hour_constraints()

# Check if the roster is empty
if not roster:
    print("Roster is empty. No shifts were added.")
else:
    print("Roster has been populated.")

# Prepare output DataFrame
rows = []
for line, days in roster.items():
    line_data = {'Line': line, 'Monday': '', 'Tuesday': '', 'Wednesday': '', 'Thursday': '', 'Friday': '', 'Saturday': '', 'Sunday': '', 'Total Hours': '', 'Notes': ''}
    for day, shifts in days.items():
        shift_numbers = ', '.join([shift['ShiftNumber'] for shift in shifts])
        line_data[day] = shift_numbers
    line_data['Total Hours'] = line_hours[line]
    line_data['Notes'] = line_notes.get(line, 'Unallocated')
    rows.append(line_data)

# Add unallocated shifts to the DataFrame
for shift in unallocated_shifts:
    unallocated_data = {'Line': 'Unallocated', 'Monday': '', 'Tuesday': '', 'Wednesday': '', 'Thursday': '', 'Friday': '', 'Saturday': '', 'Sunday': '', 'Total Hours': shift['Hours'], 'Notes': 'Unallocated'}
    unallocated_data[shift['Day']] = shift['ShiftNumber']
    rows.append(unallocated_data)

output = pd.DataFrame(rows, columns=['Line', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday', 'Sunday', 'Total Hours', 'Notes'])

# Save to Excel
output_file = f'output_{sheet_name}_roster.xlsx'  # Replace with your desired output file path
output.to_excel(output_file, index=False)

print(f'Roster generated and saved to {output_file}')

New contributor

Paul Gartner is a new contributor to this site. Take care in asking for clarification, commenting, and answering.
Check out our Code of Conduct.

Trang chủ Giới thiệu Sinh nhật bé trai Sinh nhật bé gái Tổ chức sự kiện Biểu diễn giải trí Dịch vụ khác Trang trí tiệc cưới Tổ chức khai trương Tư vấn dịch vụ Thư viện ảnh Tin tức - sự kiện Liên hệ Chú hề sinh nhật Trang trí YEAR END PARTY công ty Trang trí tất niên cuối năm Trang trí tất niên xu hướng mới nhất Trang trí sinh nhật bé trai Hải Đăng Trang trí sinh nhật bé Khánh Vân Trang trí sinh nhật Bích Ngân Trang trí sinh nhật bé Thanh Trang Thuê ông già Noel phát quà Biểu diễn xiếc khỉ Xiếc quay đĩa Dịch vụ tổ chức sự kiện 5 sao Thông tin về chúng tôi Dịch vụ sinh nhật bé trai Dịch vụ sinh nhật bé gái Sự kiện trọn gói Các tiết mục giải trí Dịch vụ bổ trợ Tiệc cưới sang trọng Dịch vụ khai trương Tư vấn tổ chức sự kiện Hình ảnh sự kiện Cập nhật tin tức Liên hệ ngay Thuê chú hề chuyên nghiệp Tiệc tất niên cho công ty Trang trí tiệc cuối năm Tiệc tất niên độc đáo Sinh nhật bé Hải Đăng Sinh nhật đáng yêu bé Khánh Vân Sinh nhật sang trọng Bích Ngân Tiệc sinh nhật bé Thanh Trang Dịch vụ ông già Noel Xiếc thú vui nhộn Biểu diễn xiếc quay đĩa Dịch vụ tổ chức tiệc uy tín Khám phá dịch vụ của chúng tôi Tiệc sinh nhật cho bé trai Trang trí tiệc cho bé gái Gói sự kiện chuyên nghiệp Chương trình giải trí hấp dẫn Dịch vụ hỗ trợ sự kiện Trang trí tiệc cưới đẹp Khởi đầu thành công với khai trương Chuyên gia tư vấn sự kiện Xem ảnh các sự kiện đẹp Tin mới về sự kiện Kết nối với đội ngũ chuyên gia Chú hề vui nhộn cho tiệc sinh nhật Ý tưởng tiệc cuối năm Tất niên độc đáo Trang trí tiệc hiện đại Tổ chức sinh nhật cho Hải Đăng Sinh nhật độc quyền Khánh Vân Phong cách tiệc Bích Ngân Trang trí tiệc bé Thanh Trang Thuê dịch vụ ông già Noel chuyên nghiệp Xem xiếc khỉ đặc sắc Xiếc quay đĩa thú vị
Trang chủ Giới thiệu Sinh nhật bé trai Sinh nhật bé gái Tổ chức sự kiện Biểu diễn giải trí Dịch vụ khác Trang trí tiệc cưới Tổ chức khai trương Tư vấn dịch vụ Thư viện ảnh Tin tức - sự kiện Liên hệ Chú hề sinh nhật Trang trí YEAR END PARTY công ty Trang trí tất niên cuối năm Trang trí tất niên xu hướng mới nhất Trang trí sinh nhật bé trai Hải Đăng Trang trí sinh nhật bé Khánh Vân Trang trí sinh nhật Bích Ngân Trang trí sinh nhật bé Thanh Trang Thuê ông già Noel phát quà Biểu diễn xiếc khỉ Xiếc quay đĩa
Thiết kế website Thiết kế website Thiết kế website Cách kháng tài khoản quảng cáo Mua bán Fanpage Facebook Dịch vụ SEO Tổ chức sinh nhật