非エンジニアがラーメン二郎の営業店舗検索システムをGoogle Colab上で実装してみた

JADEの今年のアドベントカレンダー15日目。エンジニアではない江越が、生成AIの力を借りながらラーメン二郎の営業時間検索システムを作成しました。要件定義から実装までの思考プロセスと、できあがったコードの解説をご共有です。

こんにちは。アドベントカレンダー3日目で一風変わった入社エントリを書いたところ、妙に「リトルエゴシ」と連呼されて困っている(?)江越です。

blog.ja.dev

この記事は、JADE Advent Calendar 2024 の15日目の記事です。

 

前回のアドベントカレンダーでは入社経緯について話しましたが、今回はより実践的な内容をお届け。JADEに入社してから、自分の気になる機能や使ってみたい機能を生成AIの力を借りながら実装することが趣味の一つとなっているのですが、今日はその一例をご紹介したいと思います。

この記事は、私の大好きなラーメン二郎の営業店舗検索機能を作ってみた話です。非エンジニアである私が、生成AIの発達した現代でどこまで「モノづくり」ができるのか。そしてどのような思考フローでコードを実装していくのか。興味のある方はぜひ最後までお付き合いください。

 

課題:「この日」の「この時間」に営業しているラーメン二郎の一覧を、楽に探せるソースがない

通称野猿二郎。久々に行ったら店内がリニューアルされててめちゃ綺麗になっていた。

 

皆さんご存知のラーメン二郎。いわゆる「ラーメン二郎」の名前を冠している直系店と言われる店舗は2024/12/17現在、全国に45店舗存在します(朝倉街道駅前店さん、開店おめでとうございます)。

ラーメン二郎朝倉街道駅前店 (@r26akgd) on X

スープも麺もブタもヤサイも店舗によって十人十色。自分は現在までに39(既に閉店してしまった赤羽店を含めると40)店舗を回っていますが、それでも全く食べ飽きさせないバリエーションと魅力がそこにあります。

 

江越のラーメン二郎スタンプラリー。朝倉街道駅前店が開店する前で作ったのでこの時点では全44店舗。

 

……とまぁ本題からの脱線はこのあたりにして。

営業時間も店舗によってさまざまです。朝から食べられる店舗もあれば、実質平日に有給を取らなければ行けない店舗もあります。よって「今日ラーメン二郎を食いたい気分!」と思ったら、まず「どの店舗が自分の行きたい時間に営業しているのか」を調べる必要があるのです。

そこで直面する問題が「この曜日のこの時間帯に営業している店舗を一覧で把握するのが難しい」という点。

ブログ執筆時点で筆者が観測した範囲では、表形式で各店舗の営業時間をまとめてくださっているものはあるものの、フィルタや検索が上手く効くものは未だ発見できておりません(単純に見つけられていないだけであれば、すみません)。

……とここで天使の囁き。「無いなら、自分で作ればいいじゃない?」

 

要件を言語化して生成AIに投げる:実際の動きから必要な機能を想像して言語化。コードを作ってもらう

思い出の相模大野二郎。通称スモジ。ポン酢の酸味も二郎と見事なハーモニーを奏でられることを知った。

 

ということで自分の私利私欲のために検索システムを作ることを決心した私は、サクッとコードを書いていきます。

……というのは冗談で、非エンジニアの私はそう簡単にはいきません。どういうコードを使えばいいのか見当もつかないため、まずは要件の言語化から始めることにしました。営業時間の検索を実現するにはどんな動きが必要なのか。この段階ではざっくりとした整理で構わないので書き出していきます。

自分は実際にユーザーとマシンの間でどんなやり取りがされるかを想像→必要な機能を整理するという順番で考えるのが楽に感じています。が、我流なので皆さんの意見もぜひ聞いてみたいところです!

 

🍜ユーザーとマシンの間で行われるやり取り

  1. まずは自分の探したい条件を検索窓に入力する
  2. 検索窓に入力した値と営業時間データベースの値を比較する
  3. 検索条件にヒットしたカラムだけを絞って描画する

 

🍥必要な機能

「まずは自分の探したい条件を検索窓に入力する」に関わる機能

  • 変換のためにEnterを押したつもりが条件確定にならないようにする検索窓を用意する。
  • 検索窓へのユーザーの入力を何らかの形で制限 or 補助する。
  • 複数の曜日×時間の掛け合わせが入力できるようにしたい。

「検索窓に入力した値と営業時間データベースの値を比較する」に関わる機能

  • 営業時間データベースを作成する必要がある。とりあえずスプレッドシートで。
  • ユーザーの入力値とデータベースの値を比較するための工夫。入力値やデータベースの値を区切り文字でバラして比較するなど。
  • 複数の曜日×時間の掛け合わせがあった場合の比較はOR条件で。

「検索条件にヒットしたカラムだけを絞って描画する」に関わる機能

  • まず検索条件に当てはまる店舗の情報をデータベースからフィルタして持ってくる。
  • ただフィルタするだけだと複数条件で検索した場合、どの店舗がどの検索条件に当てはまっているのか分かりにくいので別途これがわかるように数表を作成する。 

 

……と、ブログの体裁上一気に書いてしまいましたが。実際には思いついた部分だけで生成AIに投げて、作成してもらったコードの実行結果を元に整理した部分も多いです。

「ユーザーとマシンの間で必要なやり取り」を一通り想像できたら一旦生成AIに投げてみて実際にコードを実行してみる。そして使いにくい部分や想像との乖離がある部分があれば、機能要望を生成AIに投げて少しずつチューニングしていく。

こうして一気に要件を投げずに少しずつやっていくメリットとしては、チューニング前とチューニング後のコードの違いから、どのブロックでどの機能を実装してくれたのかが分かりやすく、学習に向いているな〜と感じています。

 

実装してもらったコードを確認してみる

というわけで作った(Claudeに作らせた)ものがこちらになります。着ドーン(丼)。

データベース。今回は趣味の範囲でやっているため情報をやや簡略化しており、実際の営業時間と乖離がある箇所があります。

 

# 必要なライブラリのインポート
import pandas as pd
from datetime import datetime, time
import re
import ipywidgets as widgets
from IPython.display import display, clear_output, HTML
import gspread
from google.colab import auth
from google.auth import default
from collections import defaultdict

# Google認証の設定
auth.authenticate_user()
creds, _ = default()
gc = gspread.authorize(creds)

# スプレッドシートのURLを指定
ss_url = '<https://docs.google.com/spreadsheets/d/hogehoge>'
spreadsheet = gc.open_by_url(ss_url)

# シートデータの取得
sheets = spreadsheet.sheet1
all_data = []

# データフレームの作成
data = sheets.get_all_values()
df = pd.DataFrame(data[1:], columns=data[0])

# 時間文字列をtime型に変換する関数
def parse_time(time_str):
    return datetime.strptime(time_str, '%H:%M').time()

# 店舗が指定時間帯に営業しているかを判定する関数
def is_open(opening_hours, time_ranges):
    if opening_hours == '24時間営業':
        return True
    elif opening_hours == '休業':
        return False
    else:
        store_ranges = [tuple(map(parse_time, tr.split('-'))) for tr in opening_hours.split('/')]
        for start, end in time_ranges:
            for store_start, store_end in store_ranges:
                if (store_start < start < store_end) or \
                   (store_start < end < store_end) or \
                   (start < store_start < store_end < end):
                    return True
        return False

# 指定した曜日と時間帯で営業している店舗を検索する関数
def find_open_stores(day, time_ranges):
    return df[day].apply(lambda x: is_open(x, time_ranges))

# 検索条件を解析する関数
def parse_conditions(input_string):
    conditions = defaultdict(list)
    for condition in input_string.split(','):
        match = re.match(r'^([月火水木金土日]曜日)\s+(\d{2}:\d{2})-(\d{2}:\d{2})$', condition.strip())
        if match:
            day, start, end = match.groups()
            start_time = parse_time(start)
            end_time = parse_time(end)
            conditions[day].append((start_time, end_time))
        else:
            print(f"無効な入力です: {condition}")
    return conditions

# 検索結果のスタイル設定関数
def style_dataframe(df):
    return df.style.set_properties(**{
        'text-align': 'center',
        'font-size': '12px',
        'border-color': 'black',
        'border-style': 'solid',
        'border-width': '1px',
        'padding': '5px'
    }).set_table_styles([
        {'selector': 'th', 'props': [('background-color', 'F2F2F2_1'), ('font-weight', 'bold')]},
        {'selector': 'td', 'props': [('border', '1px solid black')]},
        {'selector': '', 'props': [('border-collapse', 'collapse')]}
    ])

# 店舗を検索して結果を表示する関数
def search_stores(b):
    # 入力された条件の取得と解析
    conditions = input_widget.value
    parsed_conditions = parse_conditions(conditions)
    clear_output(wait=True)
    display(input_widget, search_button)

    # 検索条件の表示
    print("\n指定された条件:")
    for day, time_ranges in parsed_conditions.items():
        for start_time, end_time in time_ranges:
            print(f"{day} {start_time.strftime('%H:%M')}-{end_time.strftime('%H:%M')}")

    # 条件に合う店舗の検索
    results = {day: find_open_stores(day, time_ranges) for day, time_ranges in parsed_conditions.items()}
    final_result = pd.concat(results.values(), axis=1).any(axis=1)

    result_df = df[final_result].copy()

    # 営業時間表の作成
    hours_df = result_df[['店舗名'] + list(parsed_conditions.keys())]
    styled_hours_df = style_dataframe(hours_df)

    # ○×表の作成
    ox_df = pd.DataFrame({'店舗名': result_df['店舗名']})
    for day, time_ranges in parsed_conditions.items():
        for start_time, end_time in time_ranges:
            condition = f"{day}\n{start_time.strftime('%H:%M')}-{end_time.strftime('%H:%M')}"
            ox_df[condition] = results[day][final_result].map({True: '○', False: '×'})
    styled_ox_df = style_dataframe(ox_df).apply(lambda x: ['background-color: D4EDDA_1' if v == '○' else '' for v in x], axis=1)

    # 検索結果の表示
    print("\n検索結果:")
    if final_result.any():
        print("営業時間表:")
        display(HTML(styled_hours_df.to_html()))
        print("\n○×表:")
        display(HTML(styled_ox_df.to_html()))
    else:
        print("条件を満たす店舗はありません。")

# 検索用UIを作成
input_widget = widgets.Text(
    value='',
    placeholder='例: 月曜日 14:00-15:00, 火曜日 15:00-17:00',
    description='検索条件:',
    disabled=False,
    layout=widgets.Layout(width='50%')
)

search_button = widgets.Button(
    description='検索',
    disabled=False,
    button_style='',
    tooltip='クリックして検索を実行',
    icon='search'
)

search_button.on_click(search_stores)

# 検索用UIを表示
display(input_widget, search_button)

 

そして実際の実行結果が以下のようになります。

検索条件に当てはまった具体的な営業時間を確認できる数表と、各条件ごとにどの店舗が営業しているかを表す○×表を作成させる形にしています。

 

これで自分の行きたい曜日×時間帯に合わせて、営業中の店舗を簡単に吟味できるというわけです。

 

個人的に苦労したのは「検索条件を打ち込む時にEnterを押すと勝手にinputが確定しないようにするにはどういった仕様にすれば良いか」「検索条件を複数用意した場合にどうデータベースと比較させるか」あたりでしょうか。しかしこれらも課題を言語化して生成AIと対話していくことで解決できました。生成AIすごい。

ただ今回は遊びの範疇ということで、Webサイトへのアップロードや仕様の細かい調整はせずに放置しています。あとは全国のジロリアンに夢を託そうかと。

とはいえ生成AIでここまで作れる時代。学びを得た上に私のラーメン二郎ライフのQualityも爆上がりしてしまった。いい時代になりました。最高ですね。

 

最後に

湘南藤沢の二郎。海には目も暮れず、ただ二郎へ向かうのみ。

 

ブログがとっても文字文字しそうだったので、自分が今年行ったラーメン二郎の画像を随所に散りばめてみましたが、いかがでしたでしょうか?ラーメン食べたくなりましたか?

実は私、前回のアドベントカレンダー書いてからあまり間を開けずにドイツに旅立ったこともあって、ラーメンしばらく食べられていないんです。もう我慢の限界。

今日の仕事終わらせたら、ラーメン屋に駆け込もうと思います。ろくでもないあとがきになってしまってすみません!行ってきます!

明日以降のアドベントカレンダーもお楽しみに!

adventar.org