はじめに
NearMeエンジニアの柿野上 拓真(Takuma Kakinoue)です。私は、数理最適化や機械学習をはじめとする高度なアルゴリズムを含むテクノロジーによって実社会の問題を解決することに高いモチベーションを持っており、NearMeでは主に自動配車システムや相乗りマッチングシステムの開発に携わっております。単に高度なテクノロジーを使ったシステムを作るだけではなく、オペレーションを含め全体の業務を「デザイン」していくことに興味関心があります。
さて、今回のテーマは、直近新たな課題として社内で挙がった「シフト組業務の自動化およびシフトの効率化プロジェクト」です。NearMeは相乗りマッチングや車両割り当ての制御を行うプラットフォームの開発に焦点を当てており、車両やドライバーのマネジメントや実際の運行は他社のハイヤー会社(以下、運行会社と呼ぶ)に委託しています。なのでドライバーのシフト組業務は運行会社側で行っておりましたが、より効率的なシフトを組みたいという要望が運行会社から上がり「シフト組業務の自動化およびシフトの効率化プロジェクト」を実施するに至りました。
本記事では、シフト最適化システムの要件やその要件をどのように数理最適化のフレームワークに落とし込んだのか、また、AIエージェントを使ったインタラクティブにシフト組みを行うシステムに関して述べていきます。
ヒアリング結果&要件定義
ある運行会社にヒアリングした結果、シフトの効率面の観点で以下の改善点があることがわかりました。
・配車依頼件数の増減に応じて出勤するドライバーの人数を最適化したい
・ドライバーの勤務時間帯を均し、使う車両台数をなるべく減らしたい
また、手動でシフト組を行っている際に考慮している制約としては以下の点が挙がりました。
制約はハード制約(必ず守らなければならない制約)とソフト制約(なるべく守りたい制約)に分類して考えることにしました。
・法令で定められた勤務間隔(ハード制約)
・ドライバーの休暇希望(ソフト制約)
・各ドライバーの出勤日数が月の最小出勤日数〜最大出勤日数の範囲に収まること(ハード制約)
・車両をいくつか予備として残しておきたいので、1日に出勤するドライバーの人数に上限を設けること(ハード制約)
・月ごとにシフト組を行うが、対象月の前月の最終週のシフトもちゃんと考慮すること(ハード制約)
また現状の運用方法としては以下のようでした。
・ドライバーの休暇希望日や希望出勤日数はcsvなどで管理している
・手動でシフト組を行い、各ドライバーに出勤日を伝える
・ドライバーからシフトの変更要望があれば再度シフトを微調整する
シフト最適化システムの設計
私は、上記のヒアリング結果をもとにシステムの設計を行いました。先ずは、設定値をYAMLファイルから読み込んでシフト最適化結果を画面に表示するというプリミティブなCUIアプリケーションにしようと考えました。以下は大まかなシステム設計の内容です。
入力
YAMLファイルで以下の設定値を指定
全体向けの設定
法令で定められた勤務間隔
曜日ごとに必要なドライバー数を指定
目的関数の各項の重み(w1, w2, w3)
各ドライバーの設定
最小勤務日数
最大勤務日数
休暇希望日
前月の最終週の出勤状況
など
出力
画面に下図のように結果を出力
左側にドライバーの名前を表示(今回は名字の最初の1文字のみ写しています)
各日のシフトを色で分類して表示(緑:出勤、オレンジ:希望休、白:希望休以外での休み)
最適化における目的関数
w1 * [希望出勤数との絶対誤差] + w2 * [休暇希望だが出勤になったシフト数] + w3 * [車両を効率的に使えているかどうか]
最適化のフレームワーク
上記の目的関数および各種制約関数の元で充足可能性問題 (SAT問題)ソルバーで求解
上記をPythonで実装し、M1 Macで10秒ほど計算を回し解を求めてみました。
実装の雰囲気を理解していただくため、以下に簡易版のコードを載せています。
(1) 従業員・日数などの情報を準備
employees = [ Employee("Alice", 5, 10), Employee("Bob", 3, 8), ] settings = Setting(target_workers_per_day=[2,2,2,2,2,2,2], max_workers_per_day=3)
・各従業員に、週に何日シフトに入るかの 最小・最大回数 を設定。 ・各日ごとの 希望人数(target)と 上限人数(max)を設定。
(2) シフトに入るかどうかの変数を作る
work[e_idx, d_idx] = self.model.NewBoolVar(...)
・work[e, d] は、従業員 e が日 d にシフトに入るかどうかの ブール変数(0 or 1)。
(3) 従業員ごとのシフト回数の制約を追加
emp.min_shifts <= sum(work[e_idx, d]) <= emp.max_shifts
・各従業員が週の中で 希望の回数だけシフトに入るよう制約。
(4) 各日の人数が目標からずれた分をペナルティにする
dev = self.model.NewIntVar(...) self.model.AddAbsEquality(dev, actual - target)
・各日の実際の人数と希望人数の差(絶対値)を dev に入れ、最小化対象に。
(5) 各日で上限人数を超えた分もペナルティにする
ex = self.model.NewIntVar(...)
self.model.Add(ex >= actual - max)
・上限を超えた人数分 ex を計算し、これもペナルティ。
(6) 目的関数(ペナルティ合計)を最小化
self.model.Minimize(sum(deviations) + sum(excesses))
・「人数のずれ」や「上限超過」を なるべく減らすように最適化。
(7) ソルバーで実行して解を求める
from ortools.sat.python import cp_model solver = cp_model.CpSolver() solver.Solve(self.model)
・OR-Tools の CP-SAT ソルバーを使って解決。
結果比較
まず、シフト最適化システムが出したシフト組が各種制約を守っているかは運行会社のシフト組担当者と私とでダブルチェックをして確認しました。その上で、手動でシフト組した結果とシステムでシフト組した結果に対して、各曜日のドライバー出勤数がいかに需要にフィットしているかを比較しました。
水曜と木曜が注文数が少なく、土曜が注文数が多い傾向がデータ分析により確認できたので以下のドライバー数を目標出勤数としました。
・水曜:5人
・木曜:4人
・土曜:8人
・それ以外:6人
以下は横軸:曜日、縦軸:平均出勤ドライバー数のグラフです。システム(自動)でシフト組した場合、手動の場合と比較して各曜日の理想出勤数(目標値)との差分が小さいことがわかると思います。
AIエージェントを使ってインタラクティブにシフト組みを行うシステム
現在、インタラクティブなシフト組みシステムを開発しています。用途としては、作成したシフトに対して「このドライバーはやはりこの日を休みにしてほしい」などといった追加要望が来た場合に、柔軟にシフトに反映していくためです。エンジニアが対応しても良いのですが、工数削減のためにAIエージェントを活用することにしました。
設計としては、最適化計算はOR-Toolsに任せ、各種制約など最適化計算への入力情報を書くYAMLファイルをAIエージェントによって生成しています。
プロンプトとして、各設定値がどういう意味を持つかを与えています。YAMLのような構造化されたデータはAIエージェントと相性が良いと考え、このような設計にしました。
また、初めはボタンなどを使ったGUIを考えていたのですが、以下の理由によりAIエージェントをインターフェースとして採用しました。
・柔軟性の高い返答が可能になる
・既成のモデルを使うことで素早く開発ができる(UIのウィジェットはほぼテキストボックスのみで良い)
・会話形式なので使い方を覚えるまでもなく直感的に使える
以下は、概念実証のためにインターン生が作ってくれたUIおよびプロンプトです。
実際に、「佐藤ドライバーは木曜休みでお願い」と打つと、それ通りにYAMLの設定を上書きし、再度最適化プログラムを走らせて結果を表示してくれます。Webアプリとして実装しており、フレームワークはStreamlitを用いています。
以下のコードはAIエージェントにリクエストを投げる部分の実装を要約したものです。
(1)プロンプトの作成
※プロンプトの内容はかなり省略して載せています。
def build_system_prompt(year, month): return f""" あなたは、{year}年{month}月の従業員のシフト希望を記述した日本語の自然文から、YAML更新用の差分JSONを生成するAIです。 # 🛠 更新内容の記法 以下の例のように対象の従業員名と更新内容をJSON形式で出力して # 🧾 入力例 「田中の水曜日をすべて希望休にして(2025年の7月の例)」 # 📤 出力例 {{ "diff_type": "update", "name": "田中", "updates": {{ "preferred_days_off": {{ "add": [2, 9, 16, 23, 30] // 例: 2025年7月の水曜日 }} }} }} """.strip()
・最適化の各設定を記述するYAMLをどのように更新するかを出力してもらうように指示しています
(2)Azure OpenAIクライアントの初期化
client = AzureOpenAI( api_key=os.getenv("AI_AGENT_AZURE_OPEN_AI_API_KEY"), api_version=os.getenv("AI_AGENT_AZURE_OPEN_AI_API_VERSION"), azure_endpoint=os.getenv("AI_AGENT_AZURE_OPEN_AI_ENDPOINT", ""), )
・環境変数からキーやエンドポイントなどを読み込みます
(3)チャットAIへの入力メッセージを構築
messages=[ {"role": "system", "content": build_system_prompt(year, month)}, {"role": "user", "content": chat_input}, ]
・メッセージは、「(1)で作成したプロンプト "system"」と「ユーザが入力した文章 "user"」の2つから構成されています。
(4)Azure OpenAIクライアントにリクエストを送信
response = client.chat.completions.create( model=os.getenv("AI_AGENT_AZURE_OPEN_AI_DEPLOYMENT_NAME"), messages=messages, temperature=0.0, max_tokens=1000, top_p=1.0, frequency_penalty=0.0, presence_penalty=0.0, stop=None, ) return json.loads(response.choices[0].message.content.strip())
・AIエージェントにプロンプト+チャットの内容を送ります。
・チャットで送られた要望を満たすために、最適化計算への入力が書かれたYAMLのどの値を変更するかがAIエージェントから返ってくるので、それに基づいてYAMLを上書きします。
今後の展望
シフト最適化システムは日単位での最適化を前提に開発してきましたが、更なる発展として、時間単位で最適化するシステムも開発中です。
また、AIエージェントを使ったシステムは以下のような展望を考えています。
・要望に対して複数の設定値パターンを考え、複数のシフト組結果を提案する機能の開発
・数理最適化の観点で曖昧な指示をしたとしても、意図を汲み取りなるべく要望に沿うアウトプットを出せるようにする機能の開発
・日単位ではなく時間単位で最適化する高速なアルゴリズムの開発
・どの制約に一番多く引っかかっているか(解が弾かれているか)を判定し、設定値のチューニングを補助する機能
・従業員の名前の判定精度の向上("希望休暇さん"などといった名前とは無関係の単語を名前と認識してしまうことがある)
おわりに
本記事では、「シフト組み業務の自動化およびシフトの効率化プロジェクト」について述べてきました。
DXにおいて、エンジニアがDX対象の業務について理解するだけではなく、業務を行っている人がソフトウェアについて理解があるとより一層システム構築が円滑に進むと考えています。NearMeのオペレーションチームには、エンジニア経験のある方やシステム開発の理解が深い方が在籍しているので、「要件のヒアリング→要件定義→システム設計→実装→検証」というシステム開発の一連のプロセスを私がスムーズに執り行えたのは、オペレーションチームの方々のサポートあってのことであると実感しています。
技術を研究段階で留めるのではなく、実業務に対して適用することに興味関心があるエンジニアにとってNearMeは最高の環境だと思います!
最後になりますが、NearMeではエンジニアを募集しています!ご興味のある方はぜひ以下をご覧ください。
Author: Takuma Kakinoue