第1回:毎日の献立問題をデータで解く

はじめに — 「今日の晩ごはん、何にしよう?」

毎日の食事作りを担う方なら、誰でも一度はこの問いに頭を悩ませた経験があるはずです。

栄養バランスを考えて、家族の好みに合わせて、冷蔵庫の中身と相談して、昨日と同じものは避けて…。主婦の方はもちろん、一人暮らしの方や料理を担当しているすべての方にとって、献立を考える作業は、料理そのものよりも負担が大きいと感じることが少なくありません。

実はこのプロジェクト、AIブームが到来するよりもずっと前 --- 10年以上前に始めたものです。当時はディープラーニングという言葉すらまだ一般的ではなく、機械学習といえばSVM(サポートベクターマシン)やランダムフォレストが主流の時代でした。

「毎日の献立に悩む人を、テクノロジーで助けられないだろうか」

そんな素朴な動機から、古典的な機械学習の手法だけを武器に、レシピデータの分析と献立提案の仕組みを作り始めました。ChatGPTもなければ、Transformerアーキテクチャもない時代です。使えるのはコサイン類似度、TF-IDF、そしてLSTMといった、今では「クラシカル」と呼ばれる手法ばかりでした。

しかし、だからこそ面白い。最先端のLLMに頼らず、データそのものと真正面から向き合い、特徴量を自分の手で設計し、一つ一つアルゴリズムを積み上げていくプロセスには、AIの本質を理解するうえで代えがたい学びがありました。

この連載では、その10年越しの取り組みを振り返りながら、コサイン類似度からLSTM、そして最終的にChatGPTに至るまでの技術的変遷を追っていきます。

第1回となる本記事では、すべての土台となるデータパイプラインの構築について解説します。


3つのデータソース

献立提案システムを構築するにあたり、まず必要なのは大量のレシピデータです。今回使用したのは、以下の3つのCSVファイルです。

ファイルレコード数内容
recipe.csv19,902件レシピ本体(名前、手順、カテゴリなど)
material.csv196,126件各レシピの材料と分量
nutrition.csv各レシピの栄養素データ

約2万件のレシピに対して、約20万件の材料データと栄養素データが紐づいている構造です。これらを recipe_id をキーにして統合し、機械学習に投入できる形に整えるのが、このパイプラインの目的です。


データクレンジング — NULLとの戦い

生データは当然ながら完璧ではありません。最初に取り組んだのは、欠損値の処理です。

レシピ本文の欠損を除去

レシピ手順(recipe カラム)が空のデータは、献立提案に使えません。まずこれを除外します。

# レシピ手順が空のレコードを除外
data = data.dropna(subset=['recipe'])

キーワードの補完

検索やフィルタリングに使う keyword カラムが空の場合、レシピ名(name)で自動補完します。これにより、キーワード検索時の網羅性を確保しました。

# キーワードが空ならレシピ名で補完
data['keyword'] = data.apply(
    lambda row: row['name'] if pd.isna(row['keyword']) or row['keyword'].strip() == ''
    else row['keyword'], axis=1
)

一見シンプルな処理ですが、約2万件のデータに対してこの補完を行うだけで、後続の自然言語処理の精度に大きな影響がありました。機械学習において「データの品質がすべて」とよく言われますが、まさにその通りです。


レシピ手順のフォーマット変換

元データのレシピ手順はJSON配列として格納されていました。たとえば以下のような形式です。

["豚肉を一口大に切る", "フライパンに油を熱する", "豚肉を炒める"]

これを機械学習や表示に適した番号付きテキスト形式に変換します。

def format_recipe(recipe_json):
    """JSON形式のレシピ手順を番号付きテキストに変換"""
    try:
        recipe_list = json.loads(recipe_json)
        return ' '.join(f"{idx + 1}. {step}" for idx, step in enumerate(recipe_list))
    except json.JSONDecodeError:
        return None

変換後の出力はこのようになります。

1. 豚肉を一口大に切る 2. フライパンに油を熱する 3. 豚肉を炒める

この変換により、レシピ手順が一つの連続したテキストになり、後続のTF-IDF処理やテキスト類似度計算に直接投入できるようになります。json.JSONDecodeError をキャッチしているのは、ごくまれに不正なJSON形式のデータが混在しているためです。


材料と栄養素の集約

材料テーブル(material.csv)と栄養素テーブル(nutrition.csv)は、1つのレシピに対して複数行が存在するリレーショナル構造です。これを recipe_id ごとに1行にまとめます。

栄養素の集約

栄養素名と日本語名、分量を結合してから、レシピ単位で集約します。

# 栄養素名 + 日本語名 + 分量単位を結合
nutrition['nutrition_name'] = nutrition['name'] + ' ' + nutrition['name_japanese'] + ' ' + nutrition['amount_unit']

# recipe_idごとにカンマ区切りで集約
nutrition_grouped = nutrition.groupby('recipe_id')['nutrition_name'].apply(lambda x: ', '.join(x)).reset_index()

材料の集約

同様に、材料名と分量を結合してレシピ単位にまとめます。

# 材料名 + 分量単位を結合
material['material_combined'] = material['material'] + ' ' + material['amount_unit']

# recipe_idごとにカンマ区切りで集約
material_grouped = material.groupby('recipe_id')['material_combined'].apply(lambda x: ', '.join(x)).reset_index()

この groupby + join のパターンは、リレーショナルデータを機械学習用のフラットなテーブルに変換する際の定石です。1レシピ = 1行という構造にすることで、後続の特徴量抽出やベクトル化がシンプルになります。


最終マージ — recipe_master.csv の完成

3つのテーブルを recipe_id をキーにして結合し、最終的なマスターデータを作成します。

# レシピと栄養素を結合
recipe_with_nutrition = pd.merge(recipe, nutrition_grouped, on='recipe_id', how='left')

# さらに材料を結合
final_recipe = pd.merge(recipe_with_nutrition, material_grouped, on='recipe_id', how='left')

how='left' を指定しているのは、栄養素や材料データが欠損しているレシピも残すためです。すべてのレシピを保持しつつ、紐づくデータがあれば付加する、という方針です。

最終的に生成された recipe_master.csv の概要は以下の通りです。

項目
レコード数19,312件
主なカラムrecipe_id, name, keyword, recipe(手順), nutrition_name, material_combined
元データからの減少19,902 → 19,312(レシピ手順が空のレコードを除外)

約600件の不完全なレコードを除外し、19,312件のクリーンなレシピデータが完成しました。


データパイプライン全体像

ここまでの処理を図にまとめます。

┌─────────────┐   ┌─────────────────┐   ┌────────────────┐
│ recipe.csv  │   │  material.csv   │   │ nutrition.csv  │
│ 19,902件    │   │  196,126件      │   │                │
└──────┬──────┘   └───────┬─────────┘   └───────┬────────┘
       │                  │                     │
       ▼                  ▼                     ▼
  ┌──────────┐    ┌──────────────┐    ┌──────────────────┐
  │ NULLを除外│    │ 材料名+分量  │    │ 栄養素名+日本語名│
  │ keyword  │    │  を結合      │    │ +分量を結合      │
  │  を補完   │    └──────┬───────┘    └────────┬─────────┘
  └────┬─────┘           │                     │
       │                  ▼                     ▼
       │          ┌──────────────┐    ┌──────────────────┐
       │          │ recipe_id で │    │ recipe_id で     │
       │          │  groupby     │    │  groupby         │
       │          └──────┬───────┘    └────────┬─────────┘
       │                  │                     │
       └────────┬─────────┴─────────────────────┘


       ┌────────────────┐
       │   pd.merge     │
       │  (left join)   │
       └───────┬────────┘


     ┌──────────────────┐
     │ recipe_master.csv│
     │  19,312件        │
     └──────────────────┘

3つのCSVファイルから出発し、クレンジング、フォーマット変換、集約、結合を経て、1つのマスターデータに統合する。シンプルですが、堅実なETLパイプラインです。


このデータで何ができるか

recipe_master.csv が完成したことで、以下のような分析・機械学習が可能になります。

  • 栄養素ベクトルによるレシピ類似度検索 --- 「この料理と栄養バランスが似ている別の料理」を見つける
  • 材料テキストによるレシピ推薦 --- 「冷蔵庫にあるもの」から作れるレシピを提案する
  • 献立の自動生成 --- 栄養バランスを考慮した1週間分の献立を自動提案する

次回は、このデータを使った最初のアプローチ --- コサイン類似度による栄養ベクトル検索について解説します。「同じ栄養バランスで、違う料理」を見つける仕組みです。


次回: 第2回「栄養ベクトルとコサイン類似度で『同じ栄養、違う料理』を実現する」

この記事をシェア

関連記事