「本気のアドベントカレンダー」振り返り

ついに、12月25日になりました。
12月1日から始まったアドベントカレンダーも本日で締めとなります。
某社はこの期間に以下のアドベントカレンダーを進めてきました。

https://qiita.com/advent-calendar/2023/keycloak-by-openstandia

https://qiita.com/advent-calendar/2023/nri-openstandia

なかなか複数のカレンダーを同時に進めていくって珍しいのではないですかね？
推進してたのが同期っぽくて誇らしい限りです！

ここまでガチで進めてくれた同期への感謝の気持ちも込めて、「本気のアドベントカレンダー」振り返りをしようと思います。

# アドベントカレンダーとは
そもそもアドベントカレンダーとは何でしょうか？
最近はChat GPTくんが人気なので、ChatGPTくんに聞いてみました。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/119747/3c4e74fc-ff5f-254d-f1c2-3280af1704c9.png)

> アドベントカレンダーは、クリスマスに向けてのカウントダウンをするための伝統的なカレンダーです。一般的に12月1日から25日までの期間をカバーし、各日に小さな窓や扉がついた特別なカレンダーです。各窓を開けると、チョコレートや小さなお菓子、おもちゃ、メッセージ、写真などが隠されています。

ですって．．．。ブログじゃないんですね…
知っていましたか？

話は変わるのですが、わたくし、今年結婚しまして。。。（おめでとう！！！）
妻が知ってたんですよね。アドベントカレンダー！
そして、「（マジの方の）アドベントカレンダーをやろう！」と言っていたのでやってみることにしました。

![IMG_7544.jpeg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/119747/d02a2995-4b02-49a4-5750-32c3929a6830.jpeg)

こんな感じ！
妻と私でそれぞれ2000円ずつ、12日分と13日分ずつお菓子を買ってきて、（実は予算間違えて私は1000円以内で買ってましたが…（笑））
それぞれの持ち寄ったお菓子を混ぜて、「本気（マジ）のアドベントカレンダー」を作りました。

そして、、、実際作ってみて気づいたのですが、結構混ぜ方とか工夫しないと変な感じになるんですよね。
例えば、2日同じものを食べないといけなくなっちゃったり、個包装でめちゃめちゃ細かく分かれて、空けたときに平等に2人で分けて食べることができなかったり．．．
その辺、 **「いい感じにうまく」** できないだろうか？

# 自己紹介
またまた、いきなり話を変えますが、そういえば自己紹介が遅れていました。
少し自己紹介させてください。
私、某SIer？コンサルの会社？で数理最適化ソリューション [fiboat](https://atlaxblogs.nri.co.jp/entry/20230620) を提供しているチームでチームリーダーをしている土井というものです。
数年間、数理最適化に関わってきた中で気づいたのが、数理最適化って良くわからないんですよね（笑）
お前が言うなという感じかもしれませんが、厳密に言うと数理最適化そのものがわからないというより、数理最適化を使えそうな領域や使うべき領域をうまく専門外の人に伝えることが難しいという感じです。
公にはしていませんが、「The コンサル」っぽくどんな領域に数理最適化が使えたりするかをまとめたり、どういう典型的な問題があるかをまとめたりして社内で啓蒙活動をしたりしてましたが、結局実際に来る案件は、そんな典型的なものでもなかったりします。
（もしかしたら、潜在意識の中や更なる調査を重ねればあるかもです。勉強不足で申し訳ないです。）
なので、案件自体の獲得が難しいものなのかなという印象ではありますが、
うまく数理最適化案件に落とし込めた中でもいくつか共通点があることがわかってきたような気がします。
かなり話がそれるので、あくまで今回必要なものの紹介にとどめますが、その共通点の一つが、今回関係する共通点です。
それは、プロジェクトを進めていくにあたって、様々な場面で **「いい感じにうまく○○」** というワードが良く出てくるという点です。例えば、「いい感じにうまく配送時間を均一化したい」とか「いい感じにうまく計画をたてたい」だとかです。
似たような話で、機械学習等もそういった話は多いかと思いますが、どちらかというと「いい感じにうまく『予測したい』」といったように述語までちゃんと定義されますが、数理最適化はその辺結構微妙で、そもそも述語が共通に定義されず、抽象的な表現でとどまるあたりがポイントかなと思っていたりします。ちょっと感覚的な部分もあるので、興味ある方は、お酒の席などで（笑）

# アドベントカレンダーを数理最適化で振り返ってみる
さて、本題に戻ります。
今回、すでに「本気（マジ）のアドベントカレンダー」を作ってしまいましたが、改めて、 **「いい感じにうまく」** 自動的にアドベントカレンダーを作ることをできないか？と考えたときに、数理最適化を使えそうです。
ということで、数理最適化を使って、「本気（マジ）のアドベントカレンダー」を作りながら振り返ってみます。

## データの用意
まずは、データを用意します。
今回所与となる情報は、事前に私と妻が購入したお菓子となります。が、ここで早速問題が（笑）
レシートをもうすでに捨ててしまいました（笑）
データの再現ができません。なので、実際に近くにイオンに行って、何を買ったのかを思い出してみることにしました。

余談ですが、実地調査は大切です。データサイエンスあるあるですが、手元にある情報は見るけど、実際の現場を見なかったため、持っているデータと実態が異なり、使えないものになってしまうことは良くあります。
また、データがないことを理由にデータサイエンスをできないという高尚な主張を聞くこともありますが、現場に出向くことでデータを集めたり、推定できたりします。
個人的な意見ですが、その目で現場を見て、構造的に捉え、データを生み出すこともデータサイエンティストやエンジニアの仕事だと思っています。
なので、今回もそうすることにしました。

たぶん購入したお菓子をリスト化するとこんな感じ


| ID | 商品 | 値段（円） | 小分け数 | 1個あたりの大きさ | 補足 |
| --- | --- | --- | --- | --- | --- |
| 0 | [A. ブルボン プチチョコラングドシャ](https://www.amazon.co.jp/dp/B0BWYDL2YX) | 86 | 1 | 100 |  |
| 1 | [B. グリコ 冬のくちどけポッキー](https://www.amazon.co.jp/dp/B0CKVX1X98) | 181 | 2 | 100 |  |
| 2 | [C. グリコ 冬のきらめきポッキー](https://www.amazon.co.jp/dp/B0CKVZZCY2) | 181 | 2 | 100 |  |
| 3 | [D. 亀田製菓 亀田のうす焼えび](https://www.amazon.co.jp/dp/B09B9JL9P6) | 116 | 3 | 100 |  |
| 4 | [E. キタノ商事 トップオブザポップ バター](https://www.amazon.co.jp/dp/B00H9YWEL8) | 118 | 1 | 100 |  |
| 5 | [F. 江崎グリコ プリッツ(ロースト 塩バター)](https://www.amazon.co.jp/dp/B0B74XK3RN) | 138 | 2 | 100 |  |
| 6 | [G. ロッテ トッポ(ザ・ショコラ)](https://www.amazon.co.jp/dp/B0CKZ5NCKB) | 149 | 2 | 100 |  |
| 7 | [H. グリコ 生チーズのチーザ カマンベールチーズ](https://www.amazon.co.jp/dp/B0CNT911GM) | 224 | 1 | 100 |  |
| 8 | [I. 明治 メルティーキッスプレミアムショコラ](https://www.amazon.co.jp/dp/B0CH9HPWN6) | 246 | 14 | 5 | 56g入り、1袋4g = 14袋 |
| 9 | [J. 明治 メルティーキッスフルーティー濃いちご](https://www.amazon.co.jp/dp/B0CH9H4TMB) | 246 | 13 | 5 | 52g入り、1袋4g=13袋 |
| 10 | [K. 明治 メルティーキッス初摘み濃抹茶](https://www.amazon.co.jp/dp/B0CMCM49XL) | 246 | 13 | 5 | 52g入り、1袋4.1g=約13袋（繰り上げ） |
| 11 | [L. 亀田製菓 ハッピーターン](https://www.amazon.co.jp/dp/B0BQW37P6B) | 170 | 24 | 10 |  |
| 12 | [M. ネスレ日本 キットカット](https://www.amazon.co.jp/dp/B0BWJKFDBC) | 257 | 12 | 10 |  |
| 13 | [N. 亀田製菓 亀田のまがりせんべい](https://www.amazon.co.jp/dp/B09F91HRCF) | 149 | 8 | 15 |  |
| 14 | [O. 国産小麦の厚切りバウムクーヘン](https://greenbeans.com/products/14549414528999/details) | 301 | 9 | 15 |  |
| 15 | [P. ミックスゼリー(135g)](http://kinjoseika.com/i/item0001-166) | 237 | 22 | 5 | K22と書いてあったので22個かな |

※ 実際の実装ではこの表をdata.csvとして使っています。

アドベントカレンダーを作るにあたって、個包装ごとに混ぜ混ぜしていくので、小分け数も書いておきます。わからないものは、パッケージの仕様や名前から推定してます。
例えば、「メルティーキッス プレミアムショコラ」は56gで1袋あたり4gなので、14個といった具合です。「ミックスゼリー(135g)」はほとんど適当ですが、、、

そして、アドベントカレンダーの一つの袋に入る限界もあるので、一つの袋を100としたときのどれぐらいの大きさの比になるかを「大きさ」に示しています。
もし、「ハッピーターン」だけ積めるとすると大きさが10なので、10個入ります。

データが用意できたので、とりあえず何となく実装してみましょう

## 実装
今回はちょっと使ってみたかったこともあり、[Google OR ToolsのCP-SATソルバー](https://developers.google.com/optimization/cp/cp_solver?hl=ja) を使ってみることにします。

### 初期実装

#### import
まずは、必要なモジュールをimportし、データを読み込みましょう。

```python
import pandas as pd
from ortools.sat.python import cp_model

df = pd.read_csv('data.csv')
```

#### モデルと変数の定義
モデルと変数を定義します。
今回は、それぞれの小分けが12月1日～12月25日のどこかに割り当たると考え、「商品×小分け×日」で0, 1変数（bool値）を定義してあげます。

```python
x = {}
# 商品の小分け1つあたり1日～25日のどれかに割り当たる
for _, row in df.iterrows():
    product = row["商品"]
    for unit in range(row["小分け数"]): # 小分け数毎
        for day in range(1, 26, 1): # 1日～25日毎
            x[product, unit, day] = model.NewBoolVar(f"x[{product}, {unit}, {day}]")
```

#### 制約の定義
次に制約の定義をしていきます。

まずは、小分けを余らせることは許されないですし、一つの小分けが分裂することも許されません。
ということで、それぞれの小分けが必ずどこかの1日に割り当たるという制約を入れる必要があります。
これは「それぞれの小分けの12月1日～12月25日の変数を足したときに1になってなければならない」という実装で実現できそうです。

```python
# それぞれ必ずどこかの１日で食べる
for _, row in df.iterrows():
    product = row["商品"]
    for unit in range(row["小分け数"]): # 小分け数毎
        model.Add(sum(x[product, unit, day] for day in range(1, 26, 1)) == 1)
```

次にそれぞれの日で袋の大きさは100と定義しているので、袋の大きさ以内に収めるような制約をかけてあげます。
これはそれぞれの小分けがその日で1になったときに「大きさ」を足しこんでいくことでそれぞれの日のお菓子の大きさの和を取得できます。その値がそれぞれの日で100以下になれば良い。という制約を定義します。

```python
# 各日の袋の大きさの以内に積める（<= 100）
size_per_day = {i: 0 for i in range(1, 26, 1)}
for _, row in df.iterrows():
    product = row["商品"]
    for unit in range(row["小分け数"]):
        for day in range(1, 26, 1):
            size_per_day[day] += (x[product, unit, day] * row["1個あたりの大きさ"])

for day in range(1, 26, 1):
    model.Add(size_per_day[day] <= 100)
```

ひとまず制約はこれぐらいにして、最適化計算を実行し、結果を見てみましょう。

#### 計算実行・結果出力

以下のように記述することで最適化計算を実行できます。

```python
solver = cp_model.CpSolver()
status = solver.Solve(model)
```

最適化を実行すると、結果の状態（最適解を獲得できたのか、ただの実行可能解を獲得できただけなのか、もしくは、実行不可能解しか存在しないのか？計算時間が不足しているのか？等）をsolver.Solve()で取得できます。
ここは深堀しませんが、もし計算結果によって処理を分けたければ以下のようにすると良いです。

```python
if status == cp_model.OPTIMAL or status == cp_model.FEASIBLE:
    print(f"Total cost = {solver.ObjectiveValue()}\n")
```

最後にいい感じに結果を出力しておきます。

```python
res = pd.DataFrame([v for v in items.values()]).fillna("")
res.to_csv("result.csv", encoding="shift-jis")
```


#### 暫定結果確認
出力したCSVを確認してみます。
こんな感じです。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/119747/6c004225-7723-fe0a-ddb0-6149c8ce0b96.png)


0-indexではありますが、1日～25日のお菓子の中身が見れるようになっています。
行がそれぞれの日付を表していて、列がその日のお菓子を表しています。
列のインデックスに関しては、特に意味はありません。何となくの数の参考にしてください。
例えば、9行目に関しては、4列目まであるので、12月10日は5個のお菓子が入っているという解釈になります。

まずここで気づくのが、「25日のお菓子がない」ということです。（黄色塗りした行です。）
本当はすべての日にお菓子を入れるべきなので、ちゃんとお菓子を入れるように制約式を作っていく必要があります。

補足ですが、このようにして、数理最適化で何かしらの解を求めようとしたときに、「数理最適化により解が洗練され、より良いものが得られるのだろう！」と期待することが多いですが、そんなにすぐうまくいくことはなく、正しく定式化しなければ求めるものも得ることができません。
逆に考えると、数理最適化によって解を出力した時に想定と異なる結果が得られた場合、それを解消することにより、改めて暗黙的に考えていた要件があぶりだされることは良くあります。
今回で言うとそれは、「毎日必ず何かしらのお菓子が袋に入れられること」といった要件です。
一般的に多くのシステム開発は要件を決めてからスタートすることが多いですが、数理最適化（もしくは、「**いい感じにうまく**」系のシステム等）はこのようにして、まずは結果をざっくり出してみてから要件を改めて洗い出すという方法が有効だったりします。
なので、四の五の言う前にまずは手を動かす！ということが重要だったりする場面も多いでしょう。

本題に戻ります。
今回可視化することでまずは、「毎日必ず何かしらのお菓子が袋に入れられること」が足りなさそうということがわかったので、これを制約に入れることにします。
他にも何か足りなさそうですが、一旦、一つ一つ付け加えてみましょう！

### 制約の追加

#### 「毎日必ず何かしらのお菓子が袋に入れられること」
この定式化は先ほどの容量制約（各日の袋の大きさ以内に積める）のようにそれぞれの容量の和をとってそれが1以上になるようにすれば良さそうです。
一方で、一旦何かしらのお菓子を入れるということが少し違うところなので、厳密には大きさの和ではなくて、アイテム数の和が1以上になるような定式化にしてみましょう。（row["1個当たりの大きさ"]を勘案しなくなるイメージです。）


```python
# 各日の袋に一つ以上の商品を積める
cum_per_day = {i: 0 for i in range(1, 26, 1)}
for _, row in df.iterrows():
    product = row["商品"]
    for unit in range(row["小分け数"]):
        for day in range(1, 26, 1):
            cum_per_day[day] += x[product, unit, day]

for day in range(1, 26, 1):
    model.Add(cum_per_day[day] >= 1)
```

結果はこんな感じです。無事25日まで埋まっていることがわかるかと思います。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/119747/8e1923ba-6c84-6b76-7a20-915fd962c956.png)

．．．。
初日、ミックスゼリーだけかよ（笑）

というわけで、何となく、1日（一つの袋）に入れることのできるお菓子の数も制限してみたくなります。良い感じに分けたい！

#### 「同じ日に同じ商品を5個以上入れないようにする」
この定式化も一つ前の定式化と同じように実装できます。具体的には先ほど、アイテム数の和が1以上になるような定式化にしてみたところを1以上ではなく、N以下という定式化にしていきます。なんとなくどの商品も5個以上は入れられて欲しくないので、4個以下で制限をかけてみます。

```python
# 同じ日に同じ商品が複数個（5個）以上設定されないように
for _, row in df.iterrows():
    product = row["商品"]
    for day in range(1, 26, 1):
        cum_unit=0
        for unit in range(row["小分け数"]):
            cum_unit += x[product, unit, day]
        model.Add(cum_unit <= 4)
```

おまけで、トッポ、ポッキーは2個以上入ってるとちょっと多いので、これらにも1つ以下という制約も入れます。

```python
# 同じ日に同じ商品が複数個(2個)以上設定されないように（トッポ、ポッキー用）
for _, row in df.iterrows():
    product = row["商品"]
    if product in ["グリコ 冬のくちどけポッキー", "グリコ 冬のきらめきポッキー", 
                   "江崎グリコ プリッツ(ロースト 塩バター)", "ロッテ トッポ(ザ・ショコラ)"]:
        for day in range(1, 26, 1):
            cum_unit=0
            for unit in range(row["小分け数"]):
                cum_unit += x[product, unit, day]
            model.Add(cum_unit <= 1)
```

後で気づいたので、亀田の薄焼きえびにも同じように入れます。
（上とまとめれば良かった）

```python
# 同じ日に同じ商品が複数個(2個)以上設定されないように（亀田のうす焼きえび用）
for _, row in df.iterrows():
    product = row["商品"]
    if product in ["亀田製菓 亀田のうす焼えび"]:
        for day in range(1, 26, 1):
            cum_unit=0
            for unit in range(row["小分け数"]):
                cum_unit += x[product, unit, day]
            model.Add(cum_unit <= 1)
```

ここまでの制約を取り込んで得られた結果はこんな感じです。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/119747/5fc672c1-19db-f9bd-d9a7-a8cc7929fc12.png)

だいぶ良い感じになってきました！
また余談ですが、今回、ポッキーもトッポも亀田の薄焼きえびも大きさが100なので、絶対に2つ以上入れられることはありません。
にもかかわらず、定式化するのは、やはり要件を可視化するという意味では重要だったりします。

まだ微妙なのが、例えば、1日と2日を比べてみると全く同じ商品なのがわかるかと思います。
連日で同じ商品セットのお菓子はちょっと面白みが欠けますよね。
というわけで新たに「連続で同じ商品が選ばれるのは避けたい」を実装しようと思います。

#### 「連続で同じ商品が選ばれるのは避けたい」
こちらの定式化ですが、ある日（dayとします）の商品と次の日（day+1）の商品のどちらかが選ばれるはずなので、 ```商品[day] + 商品[day+1] <= 1``` という定式化がまず思い浮かびます。
ただ、今回は小分け単位で扱うことを考えなければなりません。
例えば、「亀田のうす焼えび」だと3つの小分けに分かれるので、

① data["亀田のうす焼えび"][0番目の小分け][1日]
② data["亀田のうす焼えび"][1番目の小分け][1日], 
③ data["亀田のうす焼えび"][2番目の小分け][1日] 

のどれかが1になった場合、

④ data["亀田のうす焼えび"][0番目の小分け][2日], 
⑤ data["亀田のうす焼えび"][1番目の小分け][2日], 
⑥ data["亀田のうす焼えび"][2番目の小分け][2日]

はすべて0でなければならない。
逆に④、⑤、⑥のいずれかが1となった場合に、①、②、③はすべて0出なければならない。
といった制約を加えてあげれば良くなります。
たとえば、①が1となったとき、②、③は1でも0でもどちらでも良いですが、④、⑤、⑥はゼロでなければなりません。
なので、

①+④ <= 1, ①+⑤ <= 1, ①+⑥ <= 1

といったような定式化が思い浮かびます。
同様に、

②+④ <= 1, ②+⑤ <= 1, ②+⑥ <= 1, 
③+④ <= 1, ③+⑤ <= 1, ③+⑥ <= 1

ということを定義すると、
①、②、③のうちどれかが1の時、④、⑤、⑥はすべて0、④、⑤、⑥のうちどれかが1の時、①、②、③はすべて0という制約を実現できます。

コードに表すと以下のようになります。

```python
# 連続は避けたい（昨日と同じは嫌だよね）
for _, row in df.iterrows():
    product = row["商品"]
    for day in range(1, 25, 1):
        cum_unit=0
        for today_unit in range(row["小分け数"]):
            for tom_unit in range(row["小分け数"]):
                model.Add(x[product, today_unit, day] + x[product, tom_unit, day + 1] <= 1)
```

全ての日に対して行うので、日にち毎のループを回すことを忘れないようにしましょう。

では、結果を見てみます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/119747/b1dd13e1-2313-33a9-9e72-55f69d431eec.png)

結構いい感じになってきましたが、１つ問題があります。
これ、妻と二人で食べるんですよね。。。
敢えて上の表でセルを大きく、そして、黄色塗りにしましたが、「メルティーキッス」は個包装の中に一つしか入ってないですし、二人で分けることもできないような大きさなので、１つしか入ってない場合、喧嘩になります。
もちろん小分けが奇数個で分かれているものもあるので、絶対に偶数個で分けられるように！はできないですが、二人が最低１個は食べることができるようにせめて最低2個小分けが同時に入れられて欲しいという制約を入れたくなります。ただ、「ハッピーターン」のようなものは半分に分けることができそうなので、分割できないぐらい小さい大きさの商品に関してこの制約を入れたくなります。

#### 「小さい大きさの商品に関して2個以上同時に袋に入れる」
定式化をしやすいように少し言い方を変えてみましょう。
着目する商品のとある小分けに着目したとき（「小分けA」とします）、その小分けが選択されていたら、（変数の値が1ならば、）

1個以上絶対に袋に入れたいときは、その他の小分けは0個以上選べば良い。
2個以上絶対に袋に入れたいときは、その他の小分けから1個以上選べば良い。
3個以上絶対に袋に入れたいときは、その他の小分けから2個以上選べば良い。

といった具合になります。
今着目したいのは、2個以上なので、「その他の小分けから1個以上選べば良い。」といった解釈になります。
逆に「小分けA」が選択されていない場合、つまり、変数の値が0の時は、自分自身以外で2個以上選ばれているか全くの0のどちらかとなります。

つまりすべての「小分けn」に対して、

変数[小分けn] <= 小分けn以外の変数[小分けi]の和

が成立していれば2個以上同時に袋に入れるということが成り立ちます。

コードに表すと以下のようになります。

```python
# 大きさが5より小さいものは1つしかないと寂しいので、2つ以上は入れる
for _, row in df.iterrows():
    product = row["商品"]
    if row["1個あたりの大きさ"] <= 5:
        for day in range(1, 26, 1):
            for base in range(row["小分け数"]):
                cum_unit=0
                for unit in range(row["小分け数"]):
                    if base == unit:
                        continue
                    cum_unit += x[product, unit, day]
                model.Add(x[product, base, day] <= cum_unit)

```

では、結果を見てみます。良く見てみるとちゃんとメルティーキッスが一つだけになっているのを防げていることがわかります。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/119747/fd039d3c-0295-3f08-c778-0f5506d129e5.png)

そして、初期実装と比べるとかなり良くなっているのがわかりますでしょうか？
実際、本気（マジ）のアドベントカレンダーはこんな感じだったかと思います。
というわけでこのようにしてアドベントカレンダーを再現できました。

# 考察：ちょっと真面目な話
今回アドベントカレンダーを数理最適化を用いて振り返ることで自分と妻がアドベントカレンダーを作る際に考えていた暗黙知を言語化してみました。
列挙すると、以下のようなものになります。

- 毎日必ず何かしらのお菓子が袋に入れられること
- 同じ日に同じ商品を5個以上入れないようにする
- 連続で同じ商品が選ばれるのは避けたい
- 小さい大きさの商品に関して2個以上同時に袋に入れる

正直、びっくりするぐらいそれっぽい結果が出てしまったので、この程度にとどまっているのですが、実践だとおそらくデータを変えることでさらに別の観点が出てきたりするものだと思います。
例えば、「それぞれの袋の中身をもっとバランシングしてほしい」等です。
もし「それぞれの袋の中身をもっとバランシングしてほしい」だった場合、例えば「ブルボン　プチチョコラングドシャ」は中身が結構あるけど、メルティは小分けで扱ってるので、全くお互いに重みが違います。このへんそもそもデータ集めからしっかりとやっていく必要があります。

データ集めをしっかりとやっていくという観点だと今回、レシートがなかったので、「たぶんこれを買っていたなぁ」という商品を後から思い出しながらデータを作っています。
なので、本来は実際に使ったデータを使用すべきです。
これまた思ったより良い解が出てしまったため、教訓として目に見える形ではわかりませんでしたが、多くの場合、こういった何となくの推測でデータを作るとだいたい違和感が残った解が出てきます。
良くデータはサクッと予測していい感じに最適化してよ～という話を聞くこともあり、確かに初動としては間違いでもない気もしますが、やはり予測するにしてもしっかりと現場を見るような活動を取り込み、より質の良いデータを作ることが大事だと思います。（くどいですが、今回はたまたまうまくいっただけです。）

そして、気づいた方もいるかと思うのですが、今回、数理最適化を用いてるのに目的関数もなく、何も最適化してないです。
これは間違ってるんじゃないの？と思うかもしれませんが、何も間違っていません。（あくまで業務的にはです。数学的？分野的？にはわかりません。）
結構、業務で数理最適化を用いる際も同じような罠に引っ掛かってしまうことがあり、数理最適化と聞くとどこからか「KPIおじさん」が現れてきます。
「KPIおじさん」は良く「今回何を最適化したいんだっけ？」、「何が減るの？」、「どこのコストが減るの？」という質問を投げかけてきますが、実際やりたいことは、「条件を満たす計画立案の自動化」であることが多かったりします。
今回の場合、敢えてそれぞれのお菓子の値段を表に入れていましたが、結局、この値を実装で用いることはありませんでした。にもかかわらず、それっぽい答えが出ています。改めて舞い戻って考えるとわかりますが、このお菓子の値段はあくまで所与でした。まずはお菓子が決まった状態で計画を立てるということを今回はやりたかったので、この値段が最適化に入ってくる余地がありません。ちなみに「KPIおじさん」の帽子をかぶって、なりきって指摘すると、「これっておかし代が安くなるんだっけ？」、「効率化されることでより質の高いお菓子が買えるのだっけ？」というような質問してきます。アドベントカレンダーを例にとったので、このような質問はややおかしな質問に聞こえるかと思いますが、実際には良くある話です。
こういったことがもたらす悪影響の一つが投資判断だったりするので太刀が悪く、数理最適化というワードの罠に引っ掛かってしまったら最後、「数理最適化により直接的にどれだけのコストが減るのか？」、「数理最適化を用いることでコストを回収できるのか？」といったような、「あれ？そんなことするシステムでしたっけ？」みたいな結論に落ちることがあるので要注意です。
もちろん、システム全体として作業効率化や自動化がされることでもたらされる効果をKPIとして投資判断はされるべきなので、KPIをおくことは間違いではないですが、あくまでシステム全体としての話であり、数理最適化によって最小化され、もたらされる効果ではないということが多くあるのは抑えておくべきです。（作業が効率化されて生産性があがる自動化システムを作ります！と言うと他のシステムと同じように評価され、単なる業務効率化がどれだけできそうかで投資判断がされるようになります。）
つまり、数理最適化はあくまで手段であり、目的ではないので、「指標を重視しない自動化や探索」なのか「何かしらの指標の最適化」のどちらが目的で適用しようとしているか？を意識しておく必要があります。


# まとめ
面白いテーマでそれなりの教訓を得ることができるかなと思って選んでみたテーマですが、まとめてみると、

- 数理最適化で結果を見せながら議論していくことで暗黙知をあぶりだすことができる
- データは大切だよ！予測に頼るだけじゃなくてちゃんと目で見よう！
- KPIおじさんにつられることなく、システム全体として本来の目的を見失わないようにしよう！数理最適化は単なる手法の名前だよ～

といった教訓をお見せすることができたかと思います。

というわけで、
某社の作ったアドベントカレンダーで『「本気の」アドベントカレンダー振り返り』ではなく、家で妻と仲良く作ったアドベントカレンダーで『「本気のアドベントカレンダー」振り返り』をしてみました。
メリークリスマス！良いお年を～！