Как создать приложение для рекомендации коктейлей с нуля
Парсинг ингредиентов коктейлей, разработка простого алгоритма схожести коктейлей и создание приложения для рекомендации коктейлей с использованием Streamlit
В этой статье мы рассмотрим, как создать простое приложение для рекомендации коктейлей от начала до конца. Основные шаги:
-
Получение данных о коктейлях
-
Разработка алгоритма схожести коктейлей
-
Создание пользовательского интерфейса (веб-приложение)
1. Получение данных о коктейлях
Сайт TheCocktailDB.com предлагает бесплатное API, которое можно использовать для получения данных о коктейлях. Мы будем использовать метод API "List all cocktails by first letter". Сначала создадим список URL-адресов:
import pandas as pd
import numpy as np
import string
def get_url_list():
'''получает URL-адрес для каждой возможной первой буквы названия коктейля - a, b, c... 1, 2 и т. д. и сохраняет их в списке'''
url_list = []
main_url = 'https://www.thecocktaildb.com/api/json/v1/1/search.php?f='
for i in string.printable:
url_list.append(main_url+i)
return url_list
Затем мы получаем ингредиенты и инструкции для приготовления коктейлей из этих URL-адресов:
import requests
def scrape_cocktail_list():
'''парсит список URL-адресов для получения информации о коктейлях'''
cocktail_list = []
url_list = get_url_list()
for i in url_list:
try:
r = requests.get(i, verify=False)
cocktail_list.append(r.json())
except:
pass
return cocktail_list
Ниже показаны первые два элемента возвращенного списка:
cocktail_list[0:2]
[{'drinks': None},
{'drinks': [{'dateModified': '2016-10-05 12:36:28',
'idDrink': '15346',
'strAlcoholic': 'Alcoholic',
'strCategory': 'Cocktail',
'strCreativeCommonsConfirmed': 'No',
'strDrink': '155 Belmont',
'strDrinkAlternate': None,
'strDrinkThumb': 'https://www.thecocktaildb.com/images/media/drink/yqvvqs1475667388.jpg',
'strGlass': 'White wine glass',
'strIBA': None,
'strImageAttribution': None,
'strImageSource': None,
'strIngredient1': 'Темный ром',
'strIngredient10': None,
'strIngredient11': None,
'strIngredient12': None,
'strIngredient13': None,
'strIngredient14': None,
'strIngredient15': None,
'strIngredient2': 'Светлый ром',
'strIngredient3': 'Водка',
'strIngredient4': 'Апельсиновый сок',
'strIngredient5': None,
'strIngredient6': None,
'strIngredient7': None,
'strIngredient8': None,
'strIngredient9': None,
'strInstructions': 'Смешайте с льдом. Подавайте в бокале для белого вина. Украсьте морковью.',
'strInstructionsDE': 'Смешайте с льдом. Подавайте в бокале для белого вина. Украсьте морковью.',
'strInstructionsES': None,
'strInstructionsFR': None,
'strInstructionsIT': 'Смешайте с льдом. Подавайте в бокале для белого вина. Украсьте морковью.',
'strInstructionsZH-HANS': None,
'strInstructionsZH-HANT': None,
'strMeasure1': '1 шот ',
'strMeasure10': None,
'strMeasure11': None,
'strMeasure12': None,
'strMeasure13': None,
'strMeasure14': None,
'strMeasure15': None,
'strMeasure2': '2 шота ',
'strMeasure3': '1 шот ',
'strMeasure4': '1 шот ',
'strMeasure5': None,
'strMeasure6': None,
'strMeasure7': None,
'strMeasure8': None,
'strMeasure9': None,
'strTags': None,
'strVideo': None},
{'dateModified': '2016-07-18 22:27:04',
'idDrink': '15395',
'strAlcoholic': 'Alcoholic',
'strCategory': 'Shot',
'strCreativeCommonsConfirmed': 'No',
'strDrink': '1-900-FUK-MEUP',
'strDrinkAlternate': None,
'strDrinkThumb': 'https://www.thecocktaildb.com/images/media/drink/uxywyw1468877224.jpg',
'strGlass': 'Old-fashioned glass',
'strIBA': None,
'strImageAttribution': None,
'strImageSource': None,
'strIngredient1': 'Absolut Kurant',
'strIngredient10': None,
'strIngredient11': None,
'strIngredient12': None,
'strIngredient13': None,
'strIngredient14': None,
'strIngredient15': None,
'strIngredient2': 'Grand Marnier',
'strIngredient3': 'Chambord raspberry liqueur',
'strIngredient4': 'Midori melon liqueur',
'strIngredient5': 'Malibu rum',
'strIngredient6': 'Amaretto',
'strIngredient7': 'Cranberry juice',
'strIngredient8': 'Pineapple juice',
'strIngredient9': None,
'strInstructions': 'Встряхните ингредиенты в шейкере с кубиками льда. Процедите в рокс-стакан.',
'strInstructionsDE': 'Встряхните ингредиенты в шейкере с кубиками льда. Процедите в рокс-стакан.',
'strInstructionsES': None,
'strInstructionsFR': None,
'strInstructionsIT': 'Встряхните ингредиенты в шейкере с кубиками льда. Процедите в рокс-стакан.',
'strInstructionsZH-HANS': None,
'strInstructionsZH-HANT': None,
'strMeasure1': '1/2 унции ',
'strMeasure10': None,
'strMeasure11': None,
'strMeasure12': None,
'strMeasure13': None,
'strMeasure14': None,
'strMeasure15': None,
'strMeasure2': '1/4 унции ',
'strMeasure3': '1/4 унции ',
'strMeasure4': '1/4 унции ',
'strMeasure5': '1/4 унции ',
'strMeasure6': '1/4 унции ',
'strMeasure7': '1/2 унции ',
'strMeasure8': '1/4 унции ',
'strMeasure9': None,
'strTags': None,
'strVideo': None},
{'dateModified': '2016-02-03 14:51:57',
'idDrink': '15423',
'strAlcoholic': 'Alcoholic',
'strCategory': 'Beer',
'strCreativeCommonsConfirmed': 'No',
'strDrink': '110 in the shade',
'strDrinkAlternate': None,
'strDrinkThumb': 'https://www.thecocktaildb.com/images/media/drink/xxyywq1454511117.jpg',
'strGlass': 'Beer Glass',
'strIBA': None,
'strImageAttribution': None,
'strImageSource': None,
'strIngredient1': 'Лагер',
'strIngredient10': None,
'strIngredient11': None,
'strIngredient12': None,
'strIngredient13': None,
'strIngredient14': None,
'strIngredient15': None,
'strIngredient2': 'Текила',
'strIngredient3': None,
'strIngredient4': None,
'strIngredient5': None,
'strIngredient6': None,
'strIngredient7': None,
'strIngredient8': None,
'strIngredient9': None,
'strInstructions': 'Бросьте шутера в стакан. Наполните пивом',
'strInstructionsDE': 'Бросьте шутера в стакан. Наполните пивом',
'strInstructionsES': None,
'strInstructionsFR': None,
'strInstructionsIT': 'Бросьте шутера в стакан. Наполните пивом',
'strInstructionsZH-HANS': None,
'strInstructionsZH-HANT': None,
'strMeasure1': '16 унций ',
'strMeasure10': None,
'strMeasure11': None,
'strMeasure12': None,
'strMeasure13': None,
'strMeasure14': None,
'strMeasure15': None,
'strMeasure2': '1.5 унции ',
'strMeasure3': None,
'strMeasure4': None,
'strMeasure5': None,
'strMeasure6': None,
'strMeasure7': None,
'strMeasure8': None,
'strMeasure9': None,
'strTags': None,
'strVideo': None},
{'dateModified': '2016-07-18 22:28:43',
'idDrink': '14588',
'strAlcoholic': 'Alcoholic',
'strCategory': 'Shake',
'strCreativeCommonsConfirmed': 'No',
'strDrink': '151 Florida Bushwacker',
'strDrinkAlternate': None,
'strDrinkThumb': 'https://www.thecocktaildb.com/images/media/drink/rvwrvv1468877323.jpg',
'strGlass': 'Beer mug',
'strIBA': None,
'strImageAttribution': None,
'strImageSource': None,
'strIngredient1': 'Malibu rum',
'strIngredient10': None,
'strIngredient11': None,
'strIngredient12': None,
'strIngredient13': None,
'strIngredient14': None,
'strIngredient15': None,
'strIngredient2': 'Светлый ром',
'strIngredient3': '151 proof rum',
'strIngredient4': 'Темный крем-де-какао',
'strIngredient5': 'Cointreau',
'strIngredient6': 'Молоко',
'strIngredient7': 'Кокосовый ликер',
'strIngredient8': 'Ванильное мороженое',
'strIngredient9': None,
'strInstructions': 'Смешайте все ингредиенты. Взбейте до гладкости. Украсьте шоколадной стружкой, если хотите.',
'strInstructionsDE': 'Смешайте все ингредиенты. Взбейте до гладкости. Украсьте шоколадной стружкой, если хотите.',
'strInstructionsES': None,
'strInstructionsFR': None,
'strInstructionsIT': 'Смешайте все ингредиенты. Взбейте до гладкости. Украсьте шоколадной стружкой, если хотите.',
'strInstructionsZH-HANS': None,
'strInstructionsZH-HANT': None,
'strMeasure1': '1/2 унции ',
'strMeasure10': None,
'strMeasure11': None,
'strMeasure12': None,
'strMeasure13': None,
'strMeasure14': None,
'strMeasure15': None,
'strMeasure2': '1/2 унции ',
'strMeasure3': '1/2 унции Bacardi ',
'strMeasure4': '1 унция ',
'strMeasure5': '1 унция ',
'strMeasure6': '3 унции ',
'strMeasure7': '1 унция ',
'strMeasure8': '1 чашка ',
'strMeasure9': None,
'strTags': None,
'strVideo': None}]}]
Структура данных следующая: Список, содержащий Словари коктейлей, начинающихся с одной и той же буквы. В каждом из этих словарей находится еще один Список, содержащий Словарь для каждого коктейля.
Для алгоритма рекомендации и пользовательского интерфейса нам понадобятся только название коктейля (strDrink), ингредиенты (strIngredient1, strIngredient2 и т. д.) и их количество (strMeasure1, strMeasure2 и т. д.), а также инструкции по приготовлению коктейля на английском языке (strInstructions).
Необходимо выполнить некоторую предварительную обработку данных, чтобы:
- Объединить ингредиенты для каждого коктейля в одну строку (вместо наличия одного столбца на каждый ингредиент). Это поможет настроить алгоритм схожести.
- Сопоставить ингредиенты каждого коктейля с их соответствующими количествами, а затем также объединить пары "ингредиент - количество" в одну строку для каждого коктейля. Это потребуется для веб-приложения.
- Создать конечную структуру данных только с необходимой информацией для каждого коктейля.
Для предварительной обработки данных используется следующий код:
def get_ingredients(cocktail_list, col_name)
'''объединяет несколько столбцов с именами столбцов col_name1, col_name2 и т. д. в одну строку'''
ingredient = cocktail_list[col_name + '1']
i = 2
ingredient_list = ""
while ingredient:
ingredient_list = ingredient + ", " + ingredient_list
ingredient = cocktail_list[col_name + str(i)]
i = i+1
return ingredient_list
# создание столбцов, показывающих пары "ингредиент - количество"
for i in range(len(cocktail_list)):
try:
for j in range(len(cocktail_list[i]['drinks'])):
for k in range(1,16):
if cocktail_list[i]['drinks'][j]['strIngredient'+str(k)] is not None:
try:
cocktail_list[i]['drinks'][j]['ingredient_and_quantity'+str(k)] = cocktail_list[i]['drinks'][j]['strIngredient'+str(k)] + " - " + cocktail_list[i]['drinks'][j]['strMeasure'+str(k)]
except:
cocktail_list[i]['drinks'][j]['ingredient_and_quantity'+str(k)] = cocktail_list[i]['drinks'][j]['strIngredient'+str(k)]
else:
cocktail_list[i]['drinks'][j]['ingredient_and_quantity'+str(k)] = None
except:
pass
def cocktail_data_clean(cocktail_list):
'''создает список словарей, один для каждого коктейля, с ключами: drink, ingredients, ingredients_and_quantities, instructions'''
cocktails_info = []
for i in range(len(cocktail_list)):
try:
for j in range(len(cocktail_list[i]['drinks'])):
cocktail = {}
cocktail['drink'] = cocktail_list[i]['drinks'][j]['strDrink']
cocktail['ingredients'] = get_ingredients(cocktail_list[i]['drinks'][j],'strIngredient')
cocktail['ingredients_and_quantities'] = get_ingredients(cocktail_list[i]['drinks'][j],'ingredient_and_quantity')
cocktail['instructions'] = cocktail_list[i]['drinks'][j]['strInstructions']
cocktails_info.append(cocktail)
except:
pass #игнорировать коктейли, для которых нет данных
return cocktails_info
2. Разработка алгоритма схожести коктейлей
После парсинга данных и их приведения к удобному формату, пришло время разработать алгоритм рекомендаций. Простым методом является рекомендация пользователю коктейлей с похожими ингредиентами. Поскольку мы уже создали строку со всеми ингредиентами для каждого коктейля, нам нужно только сравнить эти строки и найти наиболее похожие. Мы можем использовать комбинацию векторизатора tf-idf и косинусного сходства полученных векторов.
На высоком уровне, векторизатор tf-idf используется для присвоения "оценки" каждому слову в документе (в нашем случае, слово = ингредиент и документ = все ингредиенты коктейля) в коллекции документов (полный список ингредиентов всех коктейлей), давая более высокую "оценку" словам, которые часто встречаются внутри этого документа, но наказывая слова, которые часто встречаются в целом, т.е. в коллекции документов. Например, если многие коктейли содержат сахар в качестве ингредиента, векторизатор tf-idf даст сахару низкую "оценку".
После применения векторизатора tf-idf получается вектор чисел для каждого коктейля. Косинусное сходство можно использовать для измерения схожести каждой пары векторов. Чем больше (т.е. ближе к 1) значение косинусного сходства, тем более похожи векторы (их угол ближе к 0). Ниже приведена реализация алгоритма схожести:
from sklearn.feature_extraction.text import TfidfVectorizer
from sklearn.metrics.pairwise import cosine_similarity
cocktail_df = pd.DataFrame(cocktails_info)
def similar_cocktail(cocktail_df, chosen_cocktail):
# предварительная обработка строк
cocktail_df['ingredients'] = cocktail_df['ingredients'].str.lower().str.replace('[^\w\s]','')
# применение векторизатора tf-idf к ингредиентам
vectorizer = TfidfVectorizer()
X = vectorizer.fit_transform(cocktail_df['ingredients'])
arr = X.toarray()
# создание таблицы попарного косинусного сходства
similarity_table = pd.DataFrame(cosine_similarity(arr), columns=cocktail_df['drink'], index=cocktail_df['drink'])
for column in similarity_table.columns:
similarity_table[column] = np.where(similarity_table[column] >= 1, 0, similarity_table[column])
# выбор коктейля с наибольшим косинусным сходством в качестве наиболее похожего на выбранный пользователем коктейль
similar_cocktail= similarity_table.idxmax()
new_cocktail = similar_cocktail[chosen_cocktail]
return new_cocktail
3. Создание пользовательского интерфейса (веб-приложение)
Последний шаг - создание пользовательского интерфейса, который позволит пользователям получать рекомендации. Мы можем создать веб-приложение с использованием Streamlit, дружественного к пользователю фреймворка для создания веб-приложений на Python. Основные элементы приложения Streamlit:
- Файл .py для каждой страницы, которую мы хотим показать (здесь называется файл recommend_page.py)
- Файл app.py для запуска приложения
Нам также понадобится вспомогательный файл .py, который содержит код парсинга и алгоритм схожести (здесь называется scrape_and_model.py)
Собирая код из предыдущих шагов, это файл scrape_and_model.py:
import pandas as pd
import numpy as np
import requests
import string
from sklearn.feature_extraction.text import TfidfVectorizer
from sklearn.metrics.pairwise import cosine_similarity
def get_url_list():
''' получает URL для каждой потенциальной первой буквы названия коктейля - a, b, c.. 1, 2 и т. д. и сохраняет их в списке'''
url_list = []
main_url = 'https://www.thecocktaildb.com/api/json/v1/1/search.php?f='
for i in string.printable:
url_list.append(main_url+i)
return url_list
def scrape_cocktail_list():
'''парсит список URL-адресов для информации о коктейле'''
cocktail_list = []
url_list = get_url_list()
for i in url_list:
try:
r = requests.get(i, verify=False)
cocktail_list.append(r.json())
except:
pass
return cocktail_list
cocktail_list = scrape_cocktail_list()
for i in range(len(cocktail_list)):
try:
for j in range(len(cocktail_list[i]['drinks'])):
for k in range(1,16):
if cocktail_list[i]['drinks'][j]['strIngredient'+str(k)] is not None:
try:
cocktail_list[i]['drinks'][j]['ingredient_and_quantity'+str(k)] = cocktail_list[i]['drinks'][j]['strIngredient'+str(k)] + " - " + cocktail_list[i]['drinks'][j]['strMeasure'+str(k)]
except:
cocktail_list[i]['drinks'][j]['ingredient_and_quantity'+str(k)] = cocktail_list[i]['drinks'][j]['strIngredient'+str(k)]
else:
cocktail_list[i]['drinks'][j]['ingredient_and_quantity'+str(k)] = None
except:
pass
def get_ingredients(cocktail_list, col_name):
'''объединяет несколько столбцов с именами столбцов col_name1, col_name2 и т. д. в одну строку'''
ingredient = cocktail_list[col_name + '1']
i = 2
ingredient_list = ""
while ingredient:
ingredient_list = ingredient + ", " + ingredient_list
ingredient = cocktail_list[col_name + str(i)]
i = i+1
return ingredient_list
def cocktail_data_clean(cocktail_list):
'''создает список словарей, один для каждого коктейля, с колонками: drink, ingredients, ingredients_and_quantities, instructions'''
cocktails_info = []
for i in range(len(cocktail_list)):
try:
for j in range(len(cocktail_list[i]['drinks'])):
cocktail = {}
cocktail['drink'] = cocktail_list[i]['drinks'][j]['strDrink']
cocktail['ingredients'] = get_ingredients(cocktail_list[i]['drinks'][j],'strIngredient')
cocktail['ingredients_and_quantities'] = get_ingredients(cocktail_list[i]['drinks'][j],'ingredient_and_quantity')
cocktail['instructions'] = cocktail_list[i]['drinks'][j]['strInstructions']
cocktails_info.append(cocktail)
except:
pass #игнорировать коктейли, для которых нет данных
return cocktails_info
cocktails_info = cocktail_data_clean(cocktail_list)
cocktail_df = pd.DataFrame(cocktails_info)
cocktail_df.drop_duplicates(inplace=True)
def similar_cocktail(cocktail_df, chosen_cocktail):
# предварительная обработка строки
cocktail_df['ingredients'] = cocktail_df['ingredients'].str.lower().str.replace('[^\w\s]','')
# реализация векторизатора tf-idf на ингредиентах
vectorizer = TfidfVectorizer()
X = vectorizer.fit_transform(cocktail_df['ingredients'])
arr = X.toarray()
# создание таблицы попарной косинусной схожести
similarity_table = pd.DataFrame(cosine_similarity(arr), columns=cocktail_df['drink'], index=cocktail_df['drink'])
for column in similarity_table.columns:
similarity_table[column] = np.where(similarity_table[column] >= 1, 0, similarity_table[column])
# выбираем коктейль с наивысшей косинусной схожестью как наиболее похожий на выбранный пользователем коктейль
similar_cocktail= similarity_table.idxmax()
new_cocktail = similar_cocktail[chosen_cocktail]
return new_cocktail
Далее recommend_page.py, где мы создаем приложение Streamlit:
import streamlit as st
from scrape_and_model import similar_cocktail, cocktail_df
def show_recommend_page():
st.title("Рекомендация коктейлей")
st.write("""#### Выберите коктейль, который вам нравится, и мы порекомендуем похожий!""")
cocktail_names = cocktail_df.drink.unique()
chosen_cocktail = st.selectbox("**Коктейль**:", cocktail_names)
chosen_cocktail_ingredients_and_quantities = cocktail_df[cocktail_df.drink == chosen_cocktail].ingredients_and_quantities.values
st.write(f"Коктейль **{chosen_cocktail}** содержит следующие ингредиенты:")
for ingredient in chosen_cocktail_ingredients_and_quantities[0].rstrip().rstrip(",").split(","):
st.write(f"- {ingredient}")
button_clicked = st.button("Рекомендовать коктейль")
if button_clicked:
new_cocktail = similar_cocktail(cocktail_df, chosen_cocktail)
st.subheader(f"Вам может понравиться коктейль: {new_cocktail}")
new_cocktail_ingredients_and_quantities = cocktail_df[cocktail_df.drink == new_cocktail].ingredients_and_quantities.values
new_cocktail_instructions = cocktail_df[cocktail_df.drink == new_cocktail].instructions.values
st.write(f"**Ингредиенты:**")
for ingredient in new_cocktail_ingredients_and_quantities[0].rstrip().rstrip(",").split(","):
st.write(f"- {ingredient}")
st.write(f"**Инструкции**:")
st.write(new_cocktail_instructions[0])
Наконец, файл app.py довольно прост:
from recommend_page import show_recommend_page
show_recommend_page()
Чтобы запустить приложение локально, нужно ввести:
streamlit run app.py
Вот несколько скриншотов получившегося приложения. Главная страница:
Если мы выберем коктейль из выпадающего меню и нажмем на "Рекомендовать коктейль", мы получим похожий коктейль:
Вот и все! Ваше приложение для рекомендации коктейлей готово.
Спасибо за чтение моей статьи! Я надеюсь, вам понравился этот учебник. Вы можете получить полный код здесь: https://github.com/joakark/cocktail_recommendation