No.24 『席の割り振り(座席表)をCorticonで実装する』
2017.12.19 Progress Corticon
本エントリーは株式会社アシスト様が寄稿したエントリー(https://www.ashisuto.co.jp/product/category/brms/progress_corticon/column/detail/brmstech24.html)を転載したものとなります。
2016年4月からProgress製品を担当しています棚橋浩志です。いよいよ2017年も年の瀬が押し迫り、忘年会および新年会の準備に忙しい日々をお過ごしの事と思います。私は11月上旬に子供のクリスマスプレゼントを既に購入して自宅に隠してあり、まずは一息ついているところですが「年賀状」を書くように催促されて忙しくなってきました。さて今回は忘年会・新年会に関わらず日々忙しいお店の『席の割り振り(座席表)』について書いてみました。
席とり
昔、参加者より少ない椅子を丸く並べ、その椅子の外側を参加者が音楽に合わせて廻り、ある時点で笛が鳴って椅子を奪い合うという遊びをしました。これは「早い物勝ち」というルールに基づいて行われたゲームでしたがお昼のランチや居酒屋でも早い者勝ちで着席します。たまに複数人のグループで待っている時に先に1名を着席させる謎のルールも見かけますがこの効果についても機会がありましたら検討する予定です。
今回は、以下の条件で『席の割り振り』をどのように作るのか?その作り方を推論型AI:Corticonで実施してみます(notエクセル)。
- a) 4名分の席があります
- b) 1ターン毎に退席・グループの来店・着席が行われます
- c) 着席は1グループ単位で来店順に行われます
- d) グループを構成する人数は席数以上の人数のグループはありません (上記の場合は最大4名です)
- e) グループの人数は1名、2名、3名、4名で均等な確率で来店します
- f) 0名の来店は考慮しません
- g) 1グループは着席後3ターンまで席を使用します
身近な例ですと4席ある『牛丼店』があります。お客さんはグループ単位で1名から4名で来店して全員が同時に着席出来る迄待ちます。退席およびグループの来店および着席は5分毎に行われ、食事にかかる時間は15分(1ターン5分と想定)です。
お客さんが来店しない事は考えません。という条件ですね。想像してみて下さい。
以下5ターンを試行した結果になります。
6ターンの開店で『閉店』するまでに15ターンまでかかりました。(0基数)
稼働率100%の理想の値は
来店累計人数13名 × 3ターン = 39 (名・ターン)
になりますが、実際には
席4席×15ターン=60(席・ターン)
要しました。今回は人数 = 席と見なせるので
60(席・ターン) - 39(名・ターン) = 21(席・ターン) 空席状態が発生して、#4グループが最長5ターン待つ事になりました。
試行回数を増やすと何か傾向や特徴が見えてきそうですね。そこで Corticon で実装してみましょう。
Corticon での実装
まずは語彙を作成します。各属性は後述します。
統計データを取得する属性名は "_"を先頭に付与しました。ルールとして実装するのであれば不要な属性になります。
続いてルールですが、試行錯誤した結果、下記のルールフローになりました。
【shop.erf】
初期処理
init と make_seat 2つのルールシートを含めてサブフローを作成しました。
[init.ers]
処理をする為に属性を初期化しています。0列を使用しています。
[make_seat.ers]
使用する席を作成しています。
エンティティを動的に作成するために new 演算子を使用して作成する個数(ship.NUMBERofSEAT)までアドバンス推論自己トリガを設定して繰り返し処理します。
次に処理のメインとなる来店 - 席の割り当て - 着席 - 離席のルールを実装します。
私自身プログラミング言語を書いていて様々な条件が加わると 離席 -来店 - 席の割り当て - 着席 と逆に処理を記載した方が楽になる場合が多々ありますが、今回はブランチで条件判定をさせましたのでそのまま記載しました。
今回の『席とり』では、開店時間となった場合には来店を受け付けずに待っているグループを順次着席させるという処理にしているので少し複雑なロジックになります。そのロジックを制御しているのが shop.status で "開店中""準備中""閉店"の3つの状態を持たせています。開店時間が終了して待っているグループを順次着席させている状態を"準備中"としました。
また"開店中"のサブフローにはループを設定して、"閉店"になる迄1ターンとして繰り返し処理しています。
[in.ers]
来店処理になります。
まずは来店するグループの人数をアクションA行で処理させています。この乱数発生のロジックは『No.20 『機械学習をCorticonで実装する』(2017年9月1日)』 で検証したものを使用しています。修正箇所は0を含まず最大席数の人数を抽選するという制御をしています。人数が確定したらグループのエンティティをNew演算子で作成します。もしも何千、何万ターンも検証するのであれば パフォーマンスに影響が出る部分なので考慮する必要があります。なおこの処理は"開店中"だけ処理します。
[Judge.ers]
後から出てきますが、席には、着席しているグループ番号(SEAT_UNIT.group_number)を、空席の場合には-1を設定しています。これは init.ers でも設定しています。そこで空席をフィルタリングして空席の数を ->size で求め、待っているグループ(shop.group_num + 1)をフィルタリングして着席可能かどうか判断しています。プログラミング言語であれば可能な時の処理を続けて書くのですが、フィルタを設定したままでは欲しいデータを呼び出せないので、shop.judgeに着席する人数、shop.group_num に着席するグループ番号を設定します。
[set.ers]
Judge.ers で着席可能なフラグ(shop.judge)に値が設定された場合のみ着席可能なのでshop.judge>0 という条件の場合の処理を記述します。sortedBy や ->next が出てきて難しそうに感じるかもしれませんが、単に 着席するグループの情報を着席するグループの人数分の席に設定しています。 もし4人であれば4席に着席するグループの情報を設定、2人であれば2席に着席するグループ情報を設定します。その席数分ループさせる為に sortedBy と ->next を使用して繰り返しています。
席に設定する情報はグループ番号(ship.group_number)とseat.status で使用するターン数を設定しています。ここまでの設定で実行すると永久ループに陥る為に席にグループ情報を設定したらフラグ(shop.judge)を-1して 0以上の間繰り返させています。Corticon を使い慣れないと発想が難しい部分ですね。
[Time_past.ers]
このルールシートは ターン(shop.times)をインクリメントしているだけです。
[past_group.ers]
このルールシートは統計用の処理で、待ちグループがいる場合には累計待ち時間の属性(shop.group_unit._wait_t)をインクリメントしています。
グループは毎ターン来店しますが、着席出来ている最終グループは shop.group_num に保持されています。そのためにshop.group_num より大きいグループ番号をフィルタリングして累計待ち時間の属性をインクリメントしています。
[_past_seat.ers]
このルールシートも統計用の処理で席の状態を判断して、着席であれば 累積着席時間 seat._t_occupy_t をインクリメントして、空席であれば 累積空席時間 seat._t_empty_t をインクリメントしています。
[out.ers]
離席の処理になります。
着席している時間は shop.seat.status で管理しています。Time_past.ers で1ターン進めたので着席時間を1ターン減らします。厳密に着席しているかどうか判定して着席している場合のみ1ターン減らす方法が正確ですが、今回実装したように全部の席の時間から1ターン減らして、空席状態の場合には空席の値を設定するという少々横暴な方法もしばしばとります。言い方を変えると横暴な方法を採れるようにデータ構造を考えます。
[Past.ers]
最後の処理はお店の状態を判定する処理です。既定ターンが終了したら"準備中"に変更して、待っているグループが無くて、席にお客さんがいなければ"閉店"にしています。今回このルールを作成中に、待ちグループがなくなったら"閉店"として値がおかしくなり頭を悩ませた部分でした。待ちグループがいないからといって店内で食事中のお客様がいるにも関わらず照明を消す様な事をしてしまって反省しました。
以上で処理は終わるのですが統計値を計算するルールシートを加えました。
[statistics.ers]
集計した値の最大値や最小値および平均値をCorticonにある演算子で計算しています。
検証
本稿冒頭で検証した5ターンで4席の場合を実行してみましょう。
1グループの人数は同じ確率で生成していますが4名グループが多くなると満席状態および待ち状態が多くなり、その結果席は3ターン空かなくなり閉店は18ターンになってしまいました。
この結果を冒頭の表の様に纏めてみます。
席の割り当て方は特段制御していないために席2を最優先で割り振って、席3は優先度を一番低く割り振っている様子が伺えます。
統計情報を確認してみると
グループの平均人数 (_ave_group_num) は 約2.83人でほぼ均等な人数ですね。
グループの平均待ち時間 (_ave_group_wait) は 5ターン です。もしも1ターンが5分であれば25分程度になり少し長いですね。
席の平均空き時間 (_ave_seat_empty) は 5.25ターンです。
席の平均占有時間 (_ave_seat_occupy) は 12.75 ターンです。
グループの最大待ち時間 (_max_group_wait) は 10ターンです。もしも1ターンが5分であれば50分になり非常に長いですね。
席の最大累積空き時間 (_max_seat_empty) は 9ターンで席3が該当します。
席の最大累積占有時間 (_max_seat_occupy) は 18ターンで席2が該当します。
グループの人数がランダムに生成している為に同じ条件では比較出来ませんが、『グループの平均人数』を指標として席数を倍の8席にして実行してみましょう。NUMBERofSEAT を 8 に変更して実行します。数回実行してグループの平均人数 (_ave_group_num) は 約2.67人 の結果を掲載します。
席番号への割り振りは正確ではありません。あくまでもイメージとして見て下さい。
グループの平均待ち時間 (_ave_group_wait) は 0.6ターン、グループの最大待ち時間 (_max_group_wait) は 1ターン、『準備中』の時間は3ターンと稼働率は66%程度と良い数字になりました。
最後に実際のお店を想定して
ターン数 を72ターン、席数を30席、最大グループ数を6名、席を占有する時間を4ターンとして数パターン実行してみましょう。
作成されたエンティティの詳細画像は割愛させて頂き、統計情報のみ表示します。
グループの平均人数 (_ave_group_num)は 3.76 と平均値で待ち時間は0と優秀ですが、準備中が3ターンあり、稼働率が約50%と経営者には厳しい数字となりました。
数十回試行後グループ平均人数が高いデータが作成できました。試行回数が多くなるとグループ人数も平均値に近づくのでなかなか高い値が出にくくなります。
グループの平均人数 (_ave_group_num)は 3.9 で待ち時間は0と優秀ですが、準備中が4ターンあり、稼働率が約50%と経営者には厳しい数字には変わりないですね。
席数を20に下げてみましょう。
グループの平均人数 (_ave_group_num)は 平均で最大待ち時間1と優秀ですが、稼働率が70%以上になりました。一方では準備中が5ターンに増えました。経営者としてはうれしい数字だとおもいます。
今回の席割りは1ターンに1グループを割り振るという要件やグループでの着席は空き席数を比較していて『グループ同士が同じテーブルで着席する』『相席にはしない』等の要望は実装していません。このような実装はまた機会がありましたら実装してみたいと思います。