シフト表の自動作成を Python・Pulpで挑戦してみた。

#目次
[1.はじめに](#1-はじめに)
[2.シフト作成時の条件](#2-シフト作成時の条件)
[3.実行環境](#3-実行環境)
[4.完成シフトイメージ](#4-完成シフトイメージ)
[5.pythonのコード](#5-pythonのコード)
[6.エクセルへの出力](#6-エクセルへの出力)
[7.考察](#7-考察)
[8.課題](#8-課題)
[9.参考サイト](#9-参考サイト)











## #1. はじめに
- 業務でシフト表の作成がかなりの時間をかけて行う仕事となっており、
 Pythonのプログラムで自動作成による仕事の効率化を図りたいと思いました。
目標としては、Google Colabでシフト表を自動で割り振り、作成したシフトをExcelに出力するというところにおきたいと思います。


## #2. シフト作成時の条件
- ４種類の勤務時間(０：休み、　１：午前、　２：午後、　３：フル）
- 従業員数３１名
- 実際のカレンダーを使用する
- 土日祝日を抜いた平日のみの勤務とする（営業日）
- 希望休を反映させる
- 1日の勤務時間、一人一人の月間勤務時間を平均的にする
## #3. 実行環境
- Google Colaboratory
- MacBook Pro (OS : macOS Ventura13.2)
- Microsoft Execel for Mac(バージョン16.69）





## #4. 完成シフトイメージ
![スクリーンショット 2023-01-25 9.53.40.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3113039/5108a21e-7433-8e27-6ecd-f77ed2ec8ccb.png)
従業員がA〜Eeの31人、日数が2023年1月の営業日19日間のシフト表です。
縦軸の一番下にその日の出勤者数、横軸の一番右に従業員ごとの勤務カテゴリごとの日数を表しています。
右側は月間営業日が増えれば使用するようになります。
## #5. pythonのコード

#### 最初に、今回のシフト自動作成に必要な『Pulp』、営業日を取得する『jpbizday』をインストールします
```python
!pip install pulp
!pip install jpbizday
```
続いて必要なライブラリをインストールします。
```python
import pandas as pd
import jpbizday
import datetime
import pulp
```
- jpbizday は日本の営業日を取得するライブラリです。
- PulPは、 Pythonで線形最適化を行うライブラリです。
- pandasは、python用のデータ解析ライブラリです。




#### 2023年1月の営業日を取得する
```python
biz_day = jpbizday.month_bizdays(2023, 1)

#2021年の営業日は以下の19日です。
print(biz_day)
[datetime.date(2023, 1, 4), datetime.date(2023, 1, 5), datetime.date(2023, 1, 6),
 datetime.date(2023, 1, 10), datetime.date(2023, 1, 11), datetime.date(2023, 1, 12),
 datetime.date(2023, 1, 13), datetime.date(2023, 1, 16), datetime.date(2023, 1, 17),
 datetime.date(2023, 1, 18), datetime.date(2023, 1, 19), datetime.date(2023, 1, 20),
 datetime.date(2023, 1, 23), datetime.date(2023, 1, 24), datetime.date(2023, 1, 25),
 datetime.date(2023, 1, 26), datetime.date(2023, 1, 27), datetime.date(2023, 1, 30),
 datetime.date(2023, 1, 31)
```
2023年1月の祝日は、1月1日（元旦）、1月2日（振替休日※1月1日が元旦のため）、1月9日（成人の日）の3日と、
1日以外の日曜日が（８、１５、２２、２９）土曜日が（７、１４、２１、２８）の4日です。
ここまでで、11日間です。3日は祝日ではないですが営業日から除外されています、
正月3日までは一般的に営業しないだろうということかと思います。

#### 使用するリストを定義する
```python
#リストの定義
#日付リスト
Day = list(range(len(biz_day)))
```
上で出した営業日リストの中から、len関数を使い直接の日付ではなく、要素数を抜き出します。
変数Dayは0〜１８までの１９個の数字のリストです。
<img width=75% src=https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3113039/61d2f04a-0925-7e73-d8a1-74735b1b758a.png>

```python
#従業員リスト
Emp = list(range(1,32))
```
こちらも、初めから従業員リストを代入するのではなく、従業員数を要素数として抜き出します。

```python
#(0: 休み）（1：　午前）（2：　午後）（３：　フル）
Shift = [0,1,2,3]

# 従業員希望休
H_hope_0 = [
          (1,1),(2,4),(3,7),(4,9),(8,14)
          ]
# 従業員希望出勤（午前）
H_hope_1 = [
          (30,4),(24,10)
          ]

```
従業員の希望休、希望出勤のリストです。
シフトを０〜３で表し『０』は休み『１』は午前出勤『２』は午後出勤、『３』はフル出勤となります。
従業員希望休の(1, 1)は一人目の従業員Aの2日目の希望休を表しています。
希望出勤の方も、（３０、　４）は３０番目の従業員Ddの5日目のシフトの希望が午前という意味です。
```python
#最適化モデルの定義
prob = pulp.LpProblem('ShiftFittingProblem',pulp.LpMinimize)
```
モデルを定義します。
Pulpは線型最適化を行うことができるライブラリで
'ShiftFittingPriblem'がモデル名でLpMinmizeによっては最小化のモデルを作ることができます。
以降のコードでの複数の条件式を満たした、最小の変数の値を導き出すモデルです。
シフト表は一般的に条件を満たした最小のものが求められるため、最小化モデルを作成します。

```python
#eは従業員、dは日にち、sはシフトを表している
ED = [(e,d) for e in Emp for d in Day]


x = {}
for e in Emp:
    for d in Day:
        for s in Shift:
            x[e, d, s] = pulp.LpVariable("x({:},{:},{:})".format(e,d,s), 0, 3, pulp.LpInteger)
```

```python
#希望休を守る
for e,d in H_hope_0:
  prob += x[e,d,s] == 0

#希望シフト１（午前）を守る
for e,d in H_hope_1:
  prob += x[e,d,s] == 1

# 週5日のうち4稼働日分以上稼働すること
for e in Emp:
  for d in Day[:-4]:
    for s in Shift:
      prob += x[e,d,s] + x[e,d+1,s] + x[e,d+2,s] +x[e,d+3,s]+x[e,d+4,s] == 12

#日にちごとの従業員全員の総仕事量が８０以上になるようにする
prob += pulp.lpSum([x[e,d,s] for e in Emp for s in Shift]) >=80
```
上のような、「希望休を守る」「希望シフトを守る」「4日以上稼働する」などの制約式を入れます。

```python
#目的関数の定義
'なし'

#求解
status = prob.solve()
print('Status:', pulp.LpStatus[status])

```
ソルバーを実行します。
ステータスが『optimal』と表示されると最適解が得られたということになります。

```python
#計算結果の表示
df = pd.DataFrame([[x[e,d,s].value() for d in Day] for e in Emp])
dic = {0.0:'休み',1.0:'午前',2.0:'午後',3.0:'フル'}
for column in df.columns:
  df.loc[:,column] = df.loc[:,column].map(dic)
day_list = []
for i in range(len(biz_day)):
  day_list.append(biz_day[i].day)
print(day_list)
df.column =day_list
index1 =["A", "B", "C", "D","E", "F", "G","H","I","J","K", "L","M","N","O","P","Q","R","S","T","U","V","W","X","Y","Z","Aa","Bb","Cc","Dd","Ee"]
df.index = index1
df.columns=day_list
df

```
出力は以下のようになります。
![スクリーンショット 2023-01-26 17.52.46.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3113039/1bc9debc-d8de-bb4b-e957-a5dea0849ae1.png)


## #6. エクセルへの出力
```python
df.to_csv("shift.csv", encoding="shift_jis")
```
df.to_csv()でcsvファイルとして出力、エンコードは”shift_jis"で行いました。

計算結果を表示します。内部の計算で用いた数字をそれぞれ、休み・午前・午後・フルに置き換えています。
さらにここでカラムに実際の日付、インデックスを仮の従業員名に置き換えています。
するとGoogle Clabolatoryのフォルダに”shift.csv"といったファイルが出力されます。
<img width=50% src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3113039/ae291f77-8f05-5e08-51d5-64b2b3adb99e.png"><img width=50% src=https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3113039/c35d0078-322a-a486-0155-f195096b658e.png>
これをダウンロードして保存します。
<img=50% src=https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3113039/f4c1f448-3b56-1e6e-1e0e-b860a91ebdf5.png>
次にExecelで新規作成を選びA1セルに合わせて、データ→データファイルを指定する→テキストから（レガシ）を選択します。そして先ほどダウンロードしたShift.csvファイルを開きます。
<img= 75% src=https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3113039/1403a266-b6da-910f-4cf0-16303b298d1d.png>
テキストファイルウィザードの選択項目は２/３ページ目の項目を　カンマ　に変更してください。
その他の変更は必要ありません。
![スクリーンショット 2023-01-25 13.24.14.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3113039/de938e0e-146d-07ec-97dd-a69c6c26d4fc.png)
すると上の写真のように出力されます。
これを事前に用意しておいたExcelの表にXLOOKUP関数を使って参照するようにします。
![スクリーンショット 2023-01-25 13.51.59.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3113039/5645e18e-e20f-4043-f709-ef3f2caba185.png)

## #7. 考察
- 出勤者数、出勤シフト数にばらつきがある日があり、現状では一日休みがない人も出ている。これは条件式に6日のうちに休みを1日以上入れるなどを加えれば改善されるかもしれない。
- 従業員ごとに考えても明らかに公平ではないシフトが出てきているので、希望休、希望シフトの後にランダムで午前、午後のシフトを割り振る必要がありそうである。
- 今回は休みも含めて4種類のシフトを０、１、２、３の４つの数字で計算した。０、１などで複数のシフトを計算することができればさらに適正値に近づきそうである。



## #8. 課題
- 仕事内容によって数人ごとのグループに分け、他のグループが不足する場合にヘルプに行くことも想定したシフトを作れると良いと思う。　
- 現状改善点ばかりで未完成であるが実際はさらに複雑な条件分岐のため、設定を細かくしていき少しづつ手直しをして使えるようなものに改良していきたい。
- 自分一人では作れなかったものなので教えていただいた方々、参考にさせてもらったブログの著者の方々には感謝したい。
　　
## #9. 参考サイト
https://qiita.com/ookamikujira/items/862ec78914d5deb549c1

https://docs.pyq.jp/python/math_opt/pulp.html

https://pypi.org/project/jpbizday/