ナップザック問題で最適なグルメを求める(北海道編) 食べログのpythonスクレイピングと分析 #17

スポンサーリンク
tech系(python)
スポンサーリンク

前回までのあらすじと今回やること

前回は遺伝的アルゴリズムを使ってスコアと価格帯の最適解を求めようとして失敗しました。

今回はナップザック問題を用いてある金額の中でベストなグルメジャンルを求めてみます。

対象のデータは北海道の食べログのデータです。

ナップザック問題とは

ナップザック問題とはWikipediaさまの文言を引用すると、

$n$種類の品物(各々、価値$v_i$、重量$w_i$)が与えられたとき、重量の合計が$W$を超えない範囲で品物のいくつかをナップザックに入れて、その入れた品物の価値の合計を最大化するには入れる品物の組み合わせをどのように選べばよいか

https://ja.wikipedia.org/wiki/%E3%83%8A%E3%83%83%E3%83%97%E3%82%B5%E3%83%83%E3%82%AF%E5%95%8F%E9%A1%8C

という問題です。

つまり、超ざっくりいうと、できるだけ多くナップザックに入れるにはどうすればよいか?という問題です。

具体的な今回やること

ナップザック問題を食べログに適用するには、

ランチとディナーのジャンル毎の価格帯とスコアの平均を求めます。

例えば、「とんかつ」というジャンルのランチの価格帯の3でディナーの価格帯は5、スコアの平均は3.2とします。

この時の平均を基に「とんかつ」の値を設定します。

各ジャンル毎に求めたら、その値を基にナップザック問題を解きます。

予算内に収まるように、スコアを最大にし、その時のジャンルを一覧として求めます。

予算はランチ、ディナー共に「¥10,000~¥14,999」(価格帯は14、価格帯の詳細はこちらに記載)

これにより、その予算内に食べられる評価値を最大限にしたジャンルが求まります。

では、やることがわかったので、実装に移っていきます。

実装

以下が今回のpythonプログラムです。

ハイライトのところをlunch_value_meanからdinner_value_meanに変えるとディナーにおけるナップザック問題が解けます。

import pandas as pd

# カラム内のリストの要素を1つのリストに結合
result = [item for sublist in restaurant_data['ganre_tags'] for item in sublist]
unique_list = list(set(result))

ganre_info = {}

# ジャンル毎の平均をランチとディナーで求める
for genre_item in unique_list:
    selected_rows = restaurant_data[restaurant_data['ganre_tags'].apply(lambda x: genre_item in x)]
    lunch_value_mean = selected_rows['lunch_value_label'].astype(float).dropna().mean()
    dinner_value_mean = selected_rows['dinner_value_label'].astype(float).dropna().mean()
    selected_rows = selected_rows[selected_rows['score'] != '-']
    score_mean = selected_rows['score'].astype(float).dropna().mean()
    ganre_info[genre_item] = {
        'genre' : genre_item,
        'lunch_value_mean': lunch_value_mean,
        'dinner_value_mean': dinner_value_mean,
        'score_mean': score_mean,
    }

selected_keys = ['lunch_value_mean', 'score_mean', 'genre']
## キーの各valueを取得する。
## lunch_value_mean、score_mean、genreの各値が入ったリストのリストを作る(入れ子のリストになる)
value_list = [[d[key] for key in selected_keys] for d in ganre_info.values()]

N = len(ganre_info)  # 荷物の数
W = 14  # ナップサックの許容量 (価格帯は14)
filtered_data = [inner_list for inner_list in value_list if not any(np.isnan(x) for x in inner_list[:-1])]
filtered_data = [sublist for sublist in filtered_data if sublist[0] < 14]
wv = filtered_data
N = len(filtered_data)

# dpテーブルをベクトルで管理する
dp = [0] * (W + 1)
choice = [""] * (W + 1)

## ナップザック問題を解く
## 逆回しをしてdpテーブル更新の重複を避ける
for w, v, g in wv:
    for prev in range(W - int(w), -1, -1):
        new_v = dp[prev] + v
        if new_v > dp[prev + int(w)]:
            dp[prev + int(w)] = new_v
            choice[prev + int(w)] = choice[prev] + "1"
        else:
            choice[prev + int(w)] = choice[prev + int(w)] + "0"
    # 遷移できない場合はそのメニューを選べないため、0を付加する
    for i in range(int(w)):
        choice[i] += "0"
            
print(dp[-1], choice[-1])

## ジャンルを求める
best_genre = [wv[i][2] for i, bit in enumerate(choice[-1]) if bit == '1']
print(best_genre)

結果

ランチ

結果です。

ランチから。

まず、最適解(価格の中で評価値の平均を最大にした値)は以下になりました。

46.589382165394426

この最適グルメは以下です。

'飲茶・点心' 1件
'担々麺' 1件
'刀削麺' 1件
'立ち食いそば' 1件
'カステラ' 1件
'ロシア料理' 1件
'バインミー' 1件
'汁なし担々麺' 1件
'立ち飲み' 1件
'カレーうどん' 1件
'ドイツ料理' 1件
'四川料理' 1件
'麦とろ' 1件
'油そば・まぜそば' 1件

北海道に来て、ロシア料理、ドイツ料理、四川料理を食べることなんてあるんかいな...。

ディナー

次にディナー。

最適解(価格の中で評価値の平均を最大にした値)は以下です。

45.823170523526954

最適グルメは以下です。

'とんかつ', 1件
'親子丼', 1件
'バインミー', 1件
'肉まん', 1件
'ちゃんぽん', 1件 
'餃子', 1件 
'汁なし担々麺', 1件 
'油そば・まぜそば', 1件 
'天丼', 1件 
'立ち食いそば', 1件 
'つけ麺', 1件 
'焼きそば', 1件 
'ホットドッグ', 1件 
'カステラ' 1件

北海道っぽいものが無いですね...。

まとめ

今回はナップザック問題でグルメを求めてみました。

限られた予算内でできるだけ評価値の合計を上げる必要があるということなのですが、

結果を見た感じ、安くて評価値の平均ががある程度あるジャンルをできるだけ入れ込む結果のように見えますね。

なんか、惜しいところまで行っているような気がします。

あと少し、なような気がしています。

次で一回区切りとなるかもです。


タイトルとURLをコピーしました