「ファミマ 5days チャレンジ」の舞台裏

本記事は、当社オウンドメディア「Doors」に移転しました。

約5秒後に自動的にリダイレクトします。

当社の取締役が、「食生活を最適化アルゴリズムに委ねてみる」ことにチャレンジしました。データサイエンティストもサポートし、アルゴリズム開発からスマホアプリの実装も含めてまずはやってみる!という、超短期間でのチャレンジの舞台裏をご紹介します!

こんにちは。今回は2月に当社取締役関口がチャレンジしたファミマ 5daysチャレンジ(関口個人のnoteにリンクします) の舞台裏、特にプロジェクトの進め方や技術的側面について紹介します。ファミマ 5daysチャレンジとは、関口の思いつきから始まったプロジェクトで、

  1. 1週間ファミリーマートの商品だけで生活する
  2. 何を食べるかはアルゴリズムに任せる

というものです。なぜそんなことをするはめになったのか、結果どうなったのか、については最後にご紹介するnoteを御覧ください。僕らはそんなチャレンジをする関口向けに、アルゴリズムの設計・構築からスマホアプリの実装まで行いました。正月休みを含めて1ヶ月(実質3週間弱)、業務の空き時間とノリだけで進めた短期プロジェクトです。

作ったもの

僕らが作ったのは、栄養価を気にしながら食事内容を提案するアプリです。 使える工数は有志の空き時間だけなので、無駄な労力を極力使わないで済むように考えた結果、以下のような Google Spreadsheet 中心の構成としました。

アプリの全体像

具体的な処理は以下のように進みます。

  1. 栄養素やカテゴリなど、今回のアプリに必要なマスターデータは、事前にクローリングして収集し、Google SpreadSheet に入れておきます。
  2. マスターデータとユーザーの食事履歴をもとに、1日に1回、GAS(Google App Script) を使って「明日食べるべき商品のリスト」を生成します(数理最適化処理)。結果は Google SpreadSheet に書き込みまれます。
  3. 食べるべき商品のリストは、Glideで実装されたUIを経由してユーザーに提案されます。
  4. ユーザーは、提案された商品を食べるように心がけなければなりません。ただし、自らの心の弱さのために提案と違う食事をしてしまった場合等は、必要に応じて履歴を修正することができます。

ちなみに、「明日食べるべき商品」は、厚生労働省が公開している日本人の食事摂取基準と、各商品の栄養データを照らし合わせて選択しています。詳細は後ほど説明します。

プロジェクト進行

開発の経緯

すべての始まりはここでした。

blog.brainpad.co.jp

上記は、同じく取締役の塩澤が「インターン生のアルゴリズムに沿って旅行をしてみる」というチャレンジをした記事です。 関口のnoteにも書かれている通り、 同じ取締役の塩澤さんの体を張ったチャレンジに嫉妬刺激を受けたようです。 早速、社内の分析官に声をかけたところ、あれよあれよという間に話が進み、

  1. 一週間の食事内容をすべてアルゴリズムに任せる
  2. アルゴリズムで提案される食事はすべてファミリーマートの商品

とする事になりました。はじめは「UIにはこだわらなくてよい。スプレッドシートに商品名の一覧があれば十分」という話だったのですが、実際のユースケースを考えるとそれは現実的でない1という話になり、簡単なアプリを作ることになりました。

マイルストーンとスケジュール

大まかにやることが決まったら、通常のプロジェクトと同様、マイルストーンを設定してスケジュールを確定させます。関口のスケジュールを確認させていただいたところ、毎日のように会食が入っていることがわかりました。会食中に関口一人だけにファミリーマートの商品を食べさせるわけにはいきません。そこで、直近2ヶ月程度でまだ会食の入っていない週を探し、そこをチャレンジ実施の週としたところ、以下の開発スケジュールとなりました。

日付 内容
12/20 関口取締役との雑談
12/27 データ収集(クローリング)と整形
1/7 メンバーを集めてキックオフ・プロトタイプ開発開始
1/16 アプリのプロトタイプ完成(第一弾)
1/20 プロトタイプの使用感調査と課題の洗い出し(プロトタイプの継続的な改善)
1/28 アプリ完成
2/3 チャレンジ開始

実装の詳細

タイトなスケジュールなうえに確保できる工数も少なく、要件も明確になっていない状態でのスタートだったので、前述の通り、可能な限り工数を削減することと修正依頼に素早く対応できるようにすることを重視し、以下の方針としました。

  1. Google SpreadSheet と GAS で行けるところまでやる(DB構築や綿密なデータ設計はしない)
  2. 画面はいわゆる「ノーコード」もしくは「ローコード」のものを利用する

前述のとおり、 Google SpreadSheet をハブとして、UI(Glide アプリ)とGASが連携するようなアーキテクチャにしたので、以下ではアプリ側とGAS側の2つに分けて紹介します。

アプリの実装

今回作成したアプリの画面は、今日のおすすめ商品タブ、今日食べたものタブ、健康度チェックタブ、の3つで構成されています。

  • 今日のおすすめ商品 タブでは、関口が今日の朝食、昼食、夕食それぞれで食べるべきもののリストを見られます。リストには商品写真がついているので、関口は、その写真や商品名を頼りに、対象商品を探すことになります。
  • 今日食べたもの タブでは、実際に食べたものの管理ができます。在庫切れで食べられなかったり、心の弱さからついついリストにない商品を食べてしまったときのために、編集できるようにしています。
  • 健康度チェック タブでは、各栄養素の推奨値との差異を確認することができます。

Glideをつかった画面の構築

前述の通り、画面の構築には Glide という、いわゆる「ノーコード」のサービスを使いました。 Glide はGoogle スプレッドシートと連携させて簡単にモバイルアプリを開発できるサービスで、以下のような特徴があります。

  • 非エンジニアでも簡単に作ることができる(ノンプログラミング / ノーコード)
  • 無料2で使える
  • 豊富なテンプレートが用意されている

使い方はとても簡単で、著者も今回のプロジェクトで初めて触ったのですが、使い方を調べながら仮のデータと連携させ、最低限の機能を作るまで 2 時間くらいでした。 使い方は Qiitaの入門記事Mediumの記事いくつかのサイトで紹介されているので、興味のある方は触ってみてください。

今回の例でいうと、各アプリ画面(タブ)毎に、「このシートを参照してこの列をこうやって表示してね」といったようなイメージで設定していくだけで、スマホアプリ(いわゆるPWA)を作ることができます。 Google Spreadsheet をDB のように扱い、最適化結果が格納されるシートを読みにいってアプリ画面に商品一覧を表示します。 また、画面からユーザーが編集した結果は、スプレッドシートに即時反映されるようになっています。

初めて使ってみての感想になりますが、ものの数時間で上記のような見栄えの良いアプリを簡単に作ることができるのはすごいなと思いました。今回のプロジェクトはキックオフ~リリースまで1ヵ月程で、かつ業務の合間を縫って進めていたため、「あくまでプロトタイプ」という意味では Glide を使って正解だったと思います。ただ、できることには限度があり、「コードで書いたら数行なのにできない・・・」というようなケースが多々あったというのも事実です。

全体として、「Quick and Dirty」思想の進め方には適していると感じました。もし業務で使うことを考えるのであれば、早い段階でクライアントにアプリのイメージを持って欲しいとか、開発序盤で使うのがよさそうですね。

GAS側の実装

数理最適化を用いたおすすめの食品リストの生成アルゴリズム

今回のアプリでは、厚生労働省が公開している日本人の食事摂取基準で定義されている、1日に摂取するべきタンパク質・脂質・カロリーの推奨量に近い栄養価を摂取できる食品メニューを選択するようにしました。 具体的には、食事メニューの選択フラグ x_iを変数として、各栄養素の推奨量と摂取量の差の総和を最小化します。

  • 目的関数
    \begin{align} \sum _ {k}|\sum _ {i}^{n}N _ {ki} x _ i - T _ k| \end{align}
  • 制約条件
    \begin{align} {x _ i \in \{0, 1\}} \end{align}

ここで  k は考慮する栄養素を表すインデックス、  n は商品数、  N _ {ki} は食品  i に含まれる栄養素  k の量、 T _ {k}は成人男性に対する栄養素 k の一日あたりの摂取推奨量に、前日までの不足分を足したものです。日本人の食事摂取基準には、推奨量以外にも推定平均必要量や目標量、耐容上限量なども定義されていますが、簡単のため推奨量だけを考慮しました。

さて、上の式では目的関数に絶対値が含まれていて、そのままでは最適化が難しいので、新たな変数を追加することで目的変数を絶対値を使用しない形で表現します。 推奨量からの差異を表す新しい変数  z _ k = |\sum _ {i} ^ {n} N _ {ki} x _ i - T _ k| を導入すると、最適化問題は以下の通り書き換えられます。

  • 目的関数
    \begin{align} \min \sum _ k z _ k \end{align}
  • 制約条件 \begin{align} \sum^{n} _ {i} N _ {ik} x _ i + z _ k &\geq T _ k \\ \sum _ {i}^{n} N _ {ik} x _ i - z _ k &\leq T _ k \\ z _ k &\geq 0 \\ x _ i &\in \{0, 1\} \end{align}

この形であれば、一般的なMIPソルバーで解くことができます。

最適化処理の実装

次に、数理最適化処理の実装面について説明したいと思います。 本プロジェクトでは、なるべくコスト(時間、金銭)をかけずに実装を済ませたいということで、Google Apps Script(以下、GAS)を利用しました。GAS とは、スプレッドシートなどのGoogleが提供する各種サービスと連携可能なJavaScriptベースのプログラミング環境です。特徴として、Googleアカウントとブラウザさえあれば、無料で利用でき、かなりお手軽な点があります。筆者も、GASは初めて、JavaScriptもほとんど素人という状態だったのですが、環境の準備も含め数時間程度の作業時間で以下の一連の機能を実装できました!

  • スプレッドシートとの連携(データの取得と書き込み)
  • 定式化した混合整数計画の実装
  • トリガーによる定期実行

以降では、それぞれ簡単に説明していきます。

スプレッドシートとの連携(データの取得と書き込み)

GASでは、Google スプレッドシートと連携することで、シート上のデータ読み込みや書き込みを始めとした複雑な処理を行えます。ExcelにおけるVBAをイメージするとわかりやすいでしょうか。 本プロジェクトでは、レコメンド対象の全商品一覧シートの取得と、最適解として得られたメニューのレコメンド商品シートへの書き込みにより Glide 上で表示されるレコメンド商品の更新を実現しました。

スプレッドシートシートの読み込み/書き込みの例を簡単に紹介します。

// スプレッドシートの読み込み
var ss = SpreadsheetApp.getActiveSpreadsheet()
// data シートの取得
var data_sheet = ss.getSheetByName('data')
// シート中のデータ範囲(開始行, 開始列, 取得行数, 取得列数)を指定して、データを取得
var items = data_sheet.getRange(2, 1, 10, 4).getValues()
// 次の行に取得したデータをコピー
data_sheet.getRange(13, 1, 10, 4).setValues(items)

上記の例では、取得したデータをそのまま次の行にコピーしているだけですが、もちろん、取得したデータを処理して別シートに書き込むなども可能です。

定式化した混合整数計画の実装

GASでは、簡単な線形計画問題を実装できる LinearOptimizationService というクラスが提供されています。今回は、こちらを利用して最適化部分を実装しました。 それでは、上述の定式に基づいて、摂取タンパク質の最適化部分の実装例を説明していきます。

  1. 最適化エンジンのインスタンス作成
     var engine = LinearOptimizationService.createEngine()
    
  2. 商品の選択フラグとなる0-1変数x_iの追加
     // all_items: 全商品データ(id, 商品名, kcal, タンパク質, 脂質, 炭水化物)
     // addVariable(変数名, 下限, 上限, 変数の型, [目的関数の係数])
     for(var i=0; i <= all_items.length-1; i++) {
         engine.addVariable('x' + i, 0, 1,
              LinearOptimizationService.VariableType.INTEGER)
     }
    
  3. 絶対値を外すための変数z_pの導入と目的関数の作成
     // The objective is 1 * zp
     engine.addVariable('zp', 0, Infinity,
         LinearOptimizationService.VariableType.CONTINUOUS, 1)
    
  4. 制約の追加
      // sum(p_i * x_i) - zp <= tp, tp <= zp + sum(p_i * x_i)
      // tp: タンパク質の摂取目標値, p_i: 商品iに含まれるタンパク質(all_items[i][3])
      // addConstraint(下限, 上限): 制約式の範囲を指定
      constraint_p_gt = engine.addConstraint(tp, Infinity)
      constraint_p_lt = engine.addConstraint(-Infinity, tp)
      // setCoefficient(変数名, 係数): 制約式に変数の項を追加
      // zp の項を追加
      constraint_p_gt.setCoefficient('zp',  1)
      constraint_p_lt.setCoefficient('zp',  -1)
     // sum(p_i * x_i) の項を追加
     for(var i=0; i <= all_items.length-1; i++) {
         constraint_p_gt.setCoefficient('x' + i, all_items[i][3])
         constraint_p_lt.setCoefficient('x' + i, all_items[i][3])
     }
    
  5. 最小化問題を解決
     engine.setMinimization()
     var solution = engine.solve()
     var recommended = []
     for (var i=0; i <= all_items.length-1; i++) {
         // 得られた解において、x_i = 1 となっている商品をレコメンドメニューに追加
         if (solution.getVariableValue('x' + i) == 1) {
             recommended.push(all_items[i])
         }
     }
    

同様に、脂質、炭水化物についての制約も加えたモデルを解いたものを1日のメニューとしてレコメンドします。また、追加で以下の工夫を施しています。

  • 一度食べた商品はレコメンドしない

    同じ商品を許容すると毎日同じメニューが延々とレコメンドされてしまうので辛いという理由から取り入れています。今回は、1週間限定のプロジェクトだったので、1商品1回でも良いと判断しました。

  • 前日までに食べた実績と達成目標との過不足分を加味して1日の目標値を補正する

    今回定式化した問題は、1日単位でメニューを最適化するものであり、食べ過ぎたり食事を取れない日が発生した場合に、全期間を通してみると目標値との差が大きくなってしまうという問題への対処として実施しました。

トリガーによる定期実行

GASでは、作成した関数に対してトリガーを設定できるので、定期実行も簡単に実現可能です。設定できるトリガーとしては、指定時間、スプレッドシートの編集時などいくつか種類がありますが、今回は毎日24:00時点で次の日のメニューをレコメンドするスクリプトを実施するようにトリガーを設定しました。以下はトリガーの追加方法です。

  1. GAS のエディタ画面からトリガー設定ページを開く
    トリガー追加手順
  2. 追加したいトリガーの条件を設定して保存(毎日0~1時の間で起動する設定例)
    トリガー追加手順

GASのエディタ画面から数回ポチポチするだけで追加でき、とても簡単です!

課題と今後の展望

実際に1週間アプリを使用した関口からのフィードバックも含め、以下のような課題や要望がありました。

最適化に関する課題

  • 朝食だけで1,000円オーバーしてしまい、継続すると出費が痛い
    ⇒ 金額に関するソフトな制約を入れれば良さそうです。
  • 品数が多く商品をそろえるのが大変
    ⇒ こちらも金額と同様、適切な制約を入れることで対処できそうです。
  • レコメンドされた商品が欠品していることが多く、商品が買えない
    ⇒ 本気でやるのであれば、ファミリーマート店舗の在庫情報と連携させる必要がありますが、実際のところ、どこまでできるのでしょうか。
  • 同じカテゴリの別の商品が複数レコメンドされてしまう
    (スープが1日に3種類、サラダが1日に数種類等)
    ⇒ 今回は不要な商品の除外用にカテゴリ情報を取得しています。この情報を最適化ロジックには組み込みこむことはできそうです。

上記課題に関する量を集計してみると下記のようになりました。確かに品数のバランスは気になりますね。

UI/UX に関する課題

UI/UXについては、以下のような要望があがりました。

  • おすすめ商品のリストで、食べたものはその画面で「食べたチェック」をいれたい
  • ファミペイアプリにすぐ飛べるようなリンクボタンが欲しい
  • 健康度チェックの見せ方に改良の余地がある
  • ファミリーマートのマップが欲しい、またはお近くのファミリーマートが知りたい

今回構築したのはあくまでチャレンジのための簡易的なアプリでしたが、実際に 1 週間使ってもらったことで(仮に、今後本格的に展開していくような話があった時の)課題となりそうなものが見えてきました。例えばファミペイアプリとの連携や欠品などは、実際にアプリを使ったからこそ出てきた要求だと感じました。

最後に

今回は取締役の思いつきとノリのよいメンバで行った、「ファミマ 5days チャレンジ」の舞台裏を紹介しました。 今回は社内プロジェクトだったため、企画から開発完了まで約1ヶ月(しかも間に正月をはさむ)という、とても短いプロジェクトでしたが、企画から仮実装、ユーザーからのフィードバック、最後のチャレンジなど、各々が楽しみながら、当初のノリをキープしたまま進めることができました。

ちなみに実際の案件では、3ヵ月~半年程度かけてプロトタイプを開発し、実地検証に進むことが多いです。今回と比べて期間は長いですし、考慮すべき点も多いですが、実地検証の結果を踏まえて課題を明らかにし、更に改善を加え、というループを繰り返していくという流れは変わりません。今回のアプリもとても簡易的なものでしたが、徐々に改善していって、どこかのタイミングで世に出せたら面白いなと思います。

関口のnoteはコチラ

最後の最後に

BrainPadでは、こんな仲間たちと一緒にクライアントのデータ活用を支援する仲間を募集しています!データサイエンティストもエンジニアも、興味のある方はお気軽にご連絡ください! www.brainpad.co.jp


  1. ファミリーマートの店内で毎日三回、弊社取締役が商品リストを片手にうろうろするのは、さすがに怪しいのではないか、という議論をしました。
  2. Glide には有料オプションがあります。今回は無料の範囲で十分だったのでアップグレードはしていませんが、アップグレードをすることで、スプレッドシートから読み込めるデータの行数やストレージの制限が解除されるようになります。