Spaces:
Sleeping
Sleeping
| 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') | |