OptimizeShifts / app.py
naohiro701's picture
Update app.py
1598d80 verified
import streamlit as st
import pandas as pd
from io import StringIO, BytesIO
import pulp
import xlsxwriter
import plotly.graph_objects as go
# Streamlit UI setup
st.set_page_config(page_title='シフト最適化', layout='wide')
st.title('シフト最適化')
# Introduction and explanations for first-time users
st.markdown("""
### 概要
シフトを作成するアプリケーションです。スタッフのシフト希望や連勤制約などの制約条件を考慮して、総出勤時間が最小となるシフトを作成します。線形最適化問題として定式化して、計算を行います。[ガイドはこちら](https://chatgpt.com/share/6702b7ca-a568-800e-994f-ea37dd3ad02a)。
---
#### 使用方法
1. **シフト設定の入力**: サイドバーにある「週間シフト設定」に、シフトの開始時間・終了時間・必要スタッフ数を入力します。
2. **スタッフ希望の入力**: サイドバーの「スタッフの希望」に、スタッフごとの希望シフトをCSV形式で入力します。
3. **制約条件の設定**: 最大連勤日数や週休最小日数を設定します。
4. **最適化の実行**: 入力が完了すると、アプリが自動的に最適化を行います。
5. **結果の確認**: 最適化されたスケジュールとスタッフの希望採用状況を確認します。
---
#### モデルの計算式
この最適化モデルは、**線形計画法**を使用しており、以下の目的関数と制約条件で構成されています。
- **目的関数**: スタッフの総勤務日数を最小化
$$
\min \sum_{{スタッフ}, {日}} {勤務変数}_{{スタッフ}, {日}}
$$
- **制約条件**:
1. **必要スタッフ数の確保**:
$$
\sum_{{スタッフ}} {勤務変数}_{{スタッフ}, {日}} \geq {その日の必要スタッフ数}
$$
2. **スタッフの希望の尊重**:
- スタッフが希望していない日に割り当てられないようにします。
3. **最大連勤日数の制限**:
- 各スタッフの連続勤務日数が設定した最大値を超えないようにします。
4. **週休最小日数の確保**:
- 各スタッフが週に取得する最低休日日数を確保します。
---
### 入力データ
""")
# Function to parse shift settings from user input
def parse_shift_settings(excel_data):
"""
ユーザーが提供したExcelデータからシフト設定を解析します。
"""
shift_settings_df = pd.read_csv(StringIO(excel_data), sep='\t')
shift_settings_df['Shift Start Time'] = pd.to_datetime(shift_settings_df['Shift Start Time'], format='%H:%M').dt.time
shift_settings_df['Shift End Time'] = pd.to_datetime(shift_settings_df['Shift End Time'], format='%H:%M').dt.time
return shift_settings_df
# Function to parse staff preferences from CSV
def parse_staff_preferences_csv(csv_data):
"""
ユーザーが提供したCSVデータからスタッフの希望を解析します。
"""
staff_df = pd.read_csv(StringIO(csv_data), sep='\t')
staff_preferences = []
for _, row in staff_df.iterrows():
staff_id = row['Staff ID']
for day in range(1, 8):
start_time = row.get(f'{day}_出勤')
end_time = row.get(f'{day}_退勤')
if pd.notna(start_time) and pd.notna(end_time) and start_time != 'x' and end_time != 'x':
staff_preferences.append({
'Staff ID': staff_id,
'Day': day,
'Preferred Start Time': start_time,
'Preferred End Time': end_time
})
staff_preferences_df = pd.DataFrame(staff_preferences)
if not staff_preferences_df.empty:
staff_preferences_df['Preferred Start Time'] = pd.to_datetime(staff_preferences_df['Preferred Start Time'], format='%H:%M').dt.time
staff_preferences_df['Preferred End Time'] = pd.to_datetime(staff_preferences_df['Preferred End Time'], format='%H:%M').dt.time
return staff_preferences_df
# Function to create the linear programming model
def create_lp_model(flat_schedule, staff_preferences, max_consecutive_days, min_days_off):
"""
スタッフスケジューリングの線形計画モデルを作成して返します。
"""
model = pulp.LpProblem("StaffSchedulingCostMinimization", pulp.LpMinimize)
# Staff IDs and days
staff_ids = staff_preferences['Staff ID'].unique()
days = flat_schedule['Day'].unique()
# Map staff availability
staff_availability = {}
for staff_id in staff_ids:
available_days = staff_preferences.loc[staff_preferences['Staff ID'] == staff_id, 'Day'].tolist()
staff_availability[staff_id] = available_days
# Define variables for each staff and day
staff_vars = {}
for staff_id in staff_ids:
for day in days:
if day in staff_availability[staff_id]:
staff_vars[(staff_id, day)] = pulp.LpVariable(f"staff_{staff_id}_{day}", cat='Binary')
else:
# Fix variable to 0 if staff is unavailable
staff_vars[(staff_id, day)] = pulp.LpVariable(f"staff_{staff_id}_{day}", cat='Binary', upBound=0, lowBound=0)
# Objective function: minimize total staff assigned
model += pulp.lpSum([staff_vars[(staff_id, day)] for (staff_id, day) in staff_vars]), "TotalStaffScheduled"
# Constraints to ensure required staff per day
for day in days:
required_staff = flat_schedule.loc[flat_schedule['Day'] == day, 'Staff'].iloc[0]
model += pulp.lpSum([staff_vars[(staff_id, day)] for staff_id in staff_ids]) >= required_staff, f"StaffRequirement_Day{day}"
# Constraints for each staff
for staff_id in staff_ids:
work_days = [staff_vars[(staff_id, day)] for day in days]
# Minimum days off per week
total_days_in_week = len(days)
model += pulp.lpSum(work_days) <= total_days_in_week - min_days_off, f"MaxDaysWorked_{staff_id}"
# Maximum consecutive working days
for i in range(total_days_in_week - max_consecutive_days):
model += pulp.lpSum(work_days[i:i + max_consecutive_days + 1]) <= max_consecutive_days, f"MaxConsecutiveDays_{staff_id}_{i}"
return model, staff_vars
# Streamlit Sidebar input fields for user to provide parameters
with st.sidebar:
st.header('入力パラメーター')
st.markdown("#### 1. 週間シフト設定 (Excelデータをコピーして貼り付け)")
excel_data = st.text_area("シフト設定をここに貼り付けてください:", value="Day\tShift Start Time\tShift End Time\tMinimum Staff Required\n1\t09:00\t18:00\t5\n2\t09:00\t18:00\t5\n3\t09:00\t18:00\t5\n4\t09:00\t18:00\t5\n5\t09:00\t18:00\t5\n6\t09:00\t18:00\t3\n7\t09:00\t18:00\t2")
st.markdown("#### 2. スタッフの希望 (CSVデータをコピーして貼り付け)")
csv_staff_data = st.text_area("スタッフ希望をここに貼り付けてください:", value="Staff ID\t1_出勤\t1_退勤\t2_出勤\t2_退勤\t3_出勤\t3_退勤\t4_出勤\t4_退勤\t5_出勤\t5_退勤\t6_出勤\t6_退勤\t7_出勤\t7_退勤\nA\t9:00\t18:00\t9:00\t18:00\t9:00\t18:00\t9:00\t18:00\t9:00\t18:00\t9:00\t18:00\tx\tx\nB\tx\tx\t9:00\t18:00\tx\tx\tx\tx\t9:00\t18:00\tx\tx\t9:00\t18:00\nC\tx\tx\t9:00\t18:00\t9:00\t18:00\tx\tx\t9:00\t18:00\t9:00\t18:00\t9:00\t18:00\nD\t9:00\t18:00\t9:00\t18:00\t9:00\t18:00\t9:00\t18:00\t9:00\t18:00\t9:00\t18:00\t9:00\t18:00\nE\tx\tx\t9:00\t18:00\tx\tx\t9:00\t18:00\t9:00\t18:00\tx\tx\t9:00\t18:00\nF\t9:00\t18:00\t9:00\t18:00\t9:00\t18:00\tx\tx\t9:00\t18:00\tx\tx\tx\tx\nG\tx\tx\t9:00\t18:00\t9:00\t18:00\tx\tx\t9:00\t18:00\tx\tx\tx\tx")
st.markdown("#### 3. 制約条件の設定")
max_consecutive_days = st.number_input("最大連勤日数", value=5, min_value=1, max_value=7, help="スタッフが連続して働ける最大日数")
min_days_off = st.number_input("週休最小日数", value=2, min_value=0, max_value=7, help="各スタッフが週に取得する最低休日日数")
# Main content
if excel_data and csv_staff_data:
shift_settings_df = parse_shift_settings(excel_data)
staff_preferences_df = parse_staff_preferences_csv(csv_staff_data)
st.markdown("#### シフト設定")
st.dataframe(shift_settings_df)
st.markdown("#### スタッフの希望")
st.dataframe(staff_preferences_df)
# Flatten the schedule into a format suitable for linear programming
flat_schedule = shift_settings_df[['Day', 'Minimum Staff Required']].copy()
flat_schedule.columns = ['Day', 'Staff']
flat_schedule['Staff'] = flat_schedule['Staff'].fillna(0)
# Create and solve the LP model
model, staff_vars = create_lp_model(flat_schedule, staff_preferences_df, max_consecutive_days, min_days_off)
solver = pulp.PULP_CBC_CMD(msg=1)
model.solve(solver)
# Extract optimized schedule
schedule = []
for (staff_id, day), var in staff_vars.items():
if var.varValue == 1:
schedule.append({'Staff ID': staff_id, 'Day': day})
optimized_schedule_df = pd.DataFrame(schedule)
st.markdown("### 最適化されたスタッフ別スケジュール")
st.dataframe(optimized_schedule_df)
# Display whether the staff preferences were fulfilled
st.markdown("### スタッフの希望の採用状況")
status_data = []
for staff_id in staff_preferences_df['Staff ID'].unique():
staff_schedule = optimized_schedule_df[optimized_schedule_df['Staff ID'] == staff_id]
for day in range(1, 8):
if day in staff_schedule['Day'].values:
color = 'green' # Staff is scheduled on this day
elif day in staff_preferences_df[(staff_preferences_df['Staff ID'] == staff_id) & (staff_preferences_df['Day'] == day)]['Day'].values:
color = 'red' # Staff wanted to work but was not scheduled
else:
color = 'grey' # Staff did not want to work on this day
status_data.append({'Staff ID': staff_id, 'Day': f"Day {day}", 'Status': color})
status_df = pd.DataFrame(status_data)
st.markdown("""
#### グラフの読み方
- **緑色(green)**: スタッフが希望した日にシフトが割り当てられています。
- **赤色(red)**: スタッフが希望したが、シフトが割り当てられていません。
- **灰色(grey)**: スタッフが希望していない日です。
""")
st.markdown("### 希望採用状況のカレンダー")
for staff_id in status_df['Staff ID'].unique():
staff_status = status_df[status_df['Staff ID'] == staff_id]
fig = go.Figure()
fig.add_trace(go.Bar(
x=staff_status['Day'],
y=[1]*len(staff_status),
marker_color=staff_status['Status'],
showlegend=False
))
fig.update_layout(
title_text=f"スタッフ {staff_id} のスケジュール状況",
yaxis=dict(visible=False),
xaxis_title="日",
xaxis=dict(type='category'),
barmode='stack',
height=200
)
st.plotly_chart(fig, use_container_width=True)
# Export the optimized schedule to Excel
output = BytesIO()
with pd.ExcelWriter(output, engine='xlsxwriter') as writer:
optimized_schedule_df.to_excel(writer, sheet_name='Optimized Schedule', index=False)
writer.close()
st.download_button(label='最適化スケジュールをExcelでダウンロード', data=output.getvalue(), file_name='optimized_schedule.xlsx', mime='application/vnd.openxmlformats-officedocument.spreadsheetml.sheet')