362 lines
14 KiB
Python
362 lines
14 KiB
Python
from django.db.models import F
|
|
from profiles.models import Pantry
|
|
from recipes.models import Ingredient, UnitConversion
|
|
import pandas as pd
|
|
import numpy as np
|
|
from sklearn.metrics.pairwise import cosine_similarity
|
|
from .models import Favorite
|
|
from django.db.models import Q
|
|
|
|
|
|
def get_owned_raw_ingredients(user):
|
|
pantry_ids = Pantry.objects.filter(user=user).values_list('id', flat=True)
|
|
unit_conversions = UnitConversion.objects.select_related(
|
|
'higher_unit', 'lower_unit').all()
|
|
|
|
owned_raw_ingredient_ids = set()
|
|
|
|
for pantry_id in pantry_ids:
|
|
pantry = Pantry.objects.select_related(
|
|
'unit', 'raw_ingredient').get(id=pantry_id)
|
|
raw_ingredient_id = pantry.raw_ingredient_id
|
|
|
|
ingredients = Ingredient.objects.filter(
|
|
Q(raw_ingredient_id=raw_ingredient_id) &
|
|
Q(raw_ingredient__pantry__user=user)
|
|
).select_related('unit')
|
|
|
|
for ingredient in ingredients:
|
|
if pantry.unit is None:
|
|
owned_raw_ingredient_ids.add(raw_ingredient_id)
|
|
elif ingredient.unit is None:
|
|
continue
|
|
else:
|
|
if ingredient.unit == pantry.unit:
|
|
if pantry.amount >= ingredient.amount:
|
|
owned_raw_ingredient_ids.add(raw_ingredient_id)
|
|
else:
|
|
if ingredient.unit.type == "measured" and pantry.unit.type == "measured":
|
|
if pantry.unit.hierarchy <= ingredient.unit.hierarchy:
|
|
higher = pantry
|
|
lower = ingredient
|
|
else:
|
|
higher = ingredient
|
|
lower = pantry
|
|
|
|
conversion = UnitConversion.objects.get(
|
|
higher_unit_id=higher.unit.pk, lower_unit_id=lower.unit.pk)
|
|
if conversion:
|
|
converted_amount = conversion.value * higher.amount
|
|
if higher == pantry and converted_amount >= lower.amount:
|
|
owned_raw_ingredient_ids.add(raw_ingredient_id)
|
|
elif higher == ingredient and lower.amount >= converted_amount:
|
|
owned_raw_ingredient_ids.add(raw_ingredient_id)
|
|
else:
|
|
common_unit_id = find_common_unit(
|
|
higher.unit.id, lower.unit.id, unit_conversions
|
|
)
|
|
if common_unit_id:
|
|
converted_higher_amount = convert_amount(
|
|
higher.amount, higher.unit.id, common_unit_id, unit_conversions
|
|
)
|
|
converted_lower_amount = convert_amount(
|
|
lower.amount, lower.unit.id, common_unit_id, unit_conversions
|
|
)
|
|
if converted_higher_amount >= converted_lower_amount:
|
|
owned_raw_ingredient_ids.add(
|
|
raw_ingredient_id)
|
|
else:
|
|
if ingredient.unit.category == pantry.unit.category:
|
|
if pantry.unit.hierarchy < ingredient.unit.hierarchy:
|
|
owned_raw_ingredient_ids.add(raw_ingredient_id)
|
|
elif pantry.unit.hierarchy == ingredient.unit.hierarchy:
|
|
if pantry.amount >= ingredient.amount:
|
|
owned_raw_ingredient_ids.add(
|
|
raw_ingredient_id)
|
|
elif pantry.unit.category == 'item':
|
|
owned_raw_ingredient_ids.add(raw_ingredient_id)
|
|
|
|
return owned_raw_ingredient_ids
|
|
|
|
|
|
def find_common_unit(higher_unit_id, lower_unit_id, unit_conversions):
|
|
if higher_unit_id == lower_unit_id:
|
|
return higher_unit_id
|
|
|
|
conversion = unit_conversions.filter(
|
|
higher_unit_id=higher_unit_id, lower_unit_id=lower_unit_id
|
|
).first()
|
|
if conversion:
|
|
return lower_unit_id
|
|
|
|
conversion = unit_conversions.filter(
|
|
higher_unit_id=lower_unit_id, lower_unit_id=higher_unit_id
|
|
).first()
|
|
if conversion:
|
|
return higher_unit_id
|
|
|
|
for conversion in unit_conversions:
|
|
if conversion.higher_unit_id == higher_unit_id:
|
|
common_unit = find_common_unit(
|
|
conversion.lower_unit_id, lower_unit_id, unit_conversions
|
|
)
|
|
if common_unit:
|
|
return common_unit
|
|
return None
|
|
|
|
|
|
def convert_amount(amount, from_unit_id, to_unit_id, unit_conversions):
|
|
if from_unit_id == to_unit_id:
|
|
return amount
|
|
|
|
conversion = unit_conversions.filter(
|
|
higher_unit_id=from_unit_id, lower_unit_id=to_unit_id
|
|
).first()
|
|
if conversion:
|
|
return amount * conversion.value
|
|
|
|
conversion = unit_conversions.filter(
|
|
higher_unit_id=to_unit_id, lower_unit_id=from_unit_id
|
|
).first()
|
|
if conversion:
|
|
return amount / conversion.value
|
|
|
|
common_unit_id = find_common_unit(
|
|
from_unit_id, to_unit_id, unit_conversions)
|
|
if common_unit_id:
|
|
return convert_amount(
|
|
convert_amount(amount, from_unit_id,
|
|
common_unit_id, unit_conversions),
|
|
common_unit_id, to_unit_id, unit_conversions
|
|
)
|
|
|
|
return None
|
|
|
|
|
|
def get_owned_recipe_ingredients(user, recipe_id):
|
|
pantry_ids = Pantry.objects.filter(user=user).values_list('id', flat=True)
|
|
unit_conversions = UnitConversion.objects.select_related(
|
|
'higher_unit', 'lower_unit').all()
|
|
|
|
owned_ingredient_ids = set()
|
|
|
|
for pantry_id in pantry_ids:
|
|
pantry = Pantry.objects.select_related(
|
|
'unit', 'raw_ingredient').get(id=pantry_id)
|
|
raw_ingredient_id = pantry.raw_ingredient_id
|
|
|
|
ingredients = Ingredient.objects.filter(
|
|
recipe_id=recipe_id, raw_ingredient_id=raw_ingredient_id, raw_ingredient__pantry__user=user).select_related('unit')
|
|
|
|
for ingredient in ingredients:
|
|
if pantry.unit is None:
|
|
owned_ingredient_ids.add(ingredient.id)
|
|
elif ingredient.unit is None:
|
|
continue
|
|
else:
|
|
if ingredient.unit == pantry.unit:
|
|
if pantry.amount >= ingredient.amount:
|
|
owned_ingredient_ids.add(ingredient.id)
|
|
else:
|
|
if ingredient.unit.type == "measured" and pantry.unit.type == "measured":
|
|
if pantry.unit.hierarchy <= ingredient.unit.hierarchy:
|
|
higher = pantry
|
|
lower = ingredient
|
|
else:
|
|
higher = ingredient
|
|
lower = pantry
|
|
|
|
conversion = UnitConversion.objects.get(
|
|
higher_unit_id=higher.unit.pk, lower_unit_id=lower.unit.pk)
|
|
if conversion:
|
|
converted_amount = conversion.value * higher.amount
|
|
if higher == pantry and converted_amount >= lower.amount:
|
|
owned_ingredient_ids.add(ingredient.id)
|
|
elif higher == ingredient and lower.amount >= converted_amount:
|
|
owned_ingredient_ids.add(ingredient.id)
|
|
else:
|
|
common_unit_id = find_common_unit(
|
|
higher.unit.id, lower.unit.id, unit_conversions
|
|
)
|
|
if common_unit_id:
|
|
converted_higher_amount = convert_amount(
|
|
higher.amount, higher.unit.id, common_unit_id, unit_conversions
|
|
)
|
|
converted_lower_amount = convert_amount(
|
|
lower.amount, lower.unit.id, common_unit_id, unit_conversions
|
|
)
|
|
if converted_higher_amount >= converted_lower_amount:
|
|
owned_ingredient_ids.add(ingredient.id)
|
|
else:
|
|
if ingredient.unit.category == pantry.unit.category:
|
|
if pantry.unit.hierarchy < ingredient.unit.hierarchy:
|
|
owned_ingredient_ids.add(ingredient.id)
|
|
elif pantry.unit.hierarchy == ingredient.unit.hierarchy:
|
|
if pantry.amount >= ingredient.amount:
|
|
owned_ingredient_ids.add(ingredient.id)
|
|
elif pantry.unit.category == 'item':
|
|
owned_ingredient_ids.add(ingredient.id)
|
|
|
|
return owned_ingredient_ids
|
|
|
|
|
|
def jaccard_similarity(owned, set1, set2):
|
|
intersection = len(owned)
|
|
union = len(set1.union(set2))
|
|
similarity = intersection / union
|
|
return similarity
|
|
|
|
|
|
def calculate_top_recipes_by_ingredients(user):
|
|
pantry = set(Pantry.objects.filter(user=user).values_list('raw_ingredient_id', flat=True))
|
|
# Get valid recipes
|
|
recipe_ingredients = Ingredient.objects.filter(
|
|
raw_ingredient__pantry__user=user
|
|
).values('recipe_id', 'raw_ingredient_id', 'id', user_id=F('raw_ingredient__pantry__user_id'))
|
|
|
|
data = {}
|
|
for ingredient in recipe_ingredients:
|
|
user_id = ingredient['user_id']
|
|
recipe_id = ingredient['recipe_id']
|
|
raw_ingredient_id = ingredient['raw_ingredient_id']
|
|
# ingredient_id = ingredient['id']
|
|
|
|
if recipe_id in data:
|
|
data[recipe_id]['raw_ingredient_ids'].add(raw_ingredient_id)
|
|
else:
|
|
data[recipe_id] = {
|
|
'user_id': user_id,
|
|
'recipe_id': recipe_id,
|
|
'raw_ingredient_ids': {raw_ingredient_id}
|
|
}
|
|
|
|
for recipe_id, recipe_data in data.items():
|
|
raw_ingredient_ids = set(Ingredient.objects.filter(recipe_id=recipe_id).values_list('raw_ingredient_id', flat=True))
|
|
owned_raw_ingredient = set(get_owned_recipe_ingredients(user, recipe_id))
|
|
similarity = jaccard_similarity(
|
|
owned_raw_ingredient, pantry, raw_ingredient_ids)
|
|
|
|
recipe_data['similarity'] = similarity * 100
|
|
|
|
sorted_recipes = sorted(
|
|
data.values(), key=lambda x: x['similarity'], reverse=True)
|
|
|
|
return sorted_recipes
|
|
|
|
|
|
def create_interaction_matrix():
|
|
favorites = Favorite.objects.all()
|
|
user_ids = []
|
|
recipe_ids = []
|
|
|
|
for favorite in favorites:
|
|
user_ids.append(favorite.user_id)
|
|
recipe_ids.append(favorite.recipe_id)
|
|
|
|
unique_user_ids = list(set(user_ids))
|
|
unique_recipe_ids = list(set(recipe_ids))
|
|
|
|
interaction_matrix = pd.DataFrame(
|
|
0, index=unique_user_ids, columns=unique_recipe_ids)
|
|
|
|
for i in range(len(user_ids)):
|
|
user_id = user_ids[i]
|
|
recipe_id = recipe_ids[i]
|
|
interaction_matrix.loc[user_id, recipe_id] = 1
|
|
|
|
return interaction_matrix
|
|
|
|
|
|
def create_interaction_matrix_hybrid():
|
|
favorites = Favorite.objects.all()
|
|
user_ids = []
|
|
recipe_ids = []
|
|
ratings = []
|
|
|
|
for favorite in favorites:
|
|
user_ids.append(favorite.user_id)
|
|
recipe_ids.append(favorite.recipe_id)
|
|
ratings.append(favorite.rating) # Menambahkan rating
|
|
|
|
unique_user_ids = list(set(user_ids))
|
|
unique_recipe_ids = list(set(recipe_ids))
|
|
|
|
interaction_matrix = pd.DataFrame(
|
|
0, index=unique_user_ids, columns=unique_recipe_ids)
|
|
|
|
for i in range(len(user_ids)):
|
|
user_id = user_ids[i]
|
|
recipe_id = recipe_ids[i]
|
|
rating = ratings[i]
|
|
# Menggunakan rating sebagai nilai
|
|
interaction_matrix.loc[user_id, recipe_id] = rating
|
|
|
|
return interaction_matrix
|
|
|
|
|
|
def calculate_cosine_similarity():
|
|
interaction_matrix = create_interaction_matrix()
|
|
similarity_matrix = cosine_similarity(interaction_matrix.T)
|
|
similarity_df = pd.DataFrame(
|
|
similarity_matrix, index=interaction_matrix.columns, columns=interaction_matrix.columns)
|
|
return similarity_df, interaction_matrix
|
|
|
|
|
|
def get_recommendations_for_user(user_id):
|
|
similarity_matrix, interaction_matrix = calculate_cosine_similarity()
|
|
|
|
if user_id in interaction_matrix.index:
|
|
scores = np.dot(interaction_matrix.values, similarity_matrix)
|
|
|
|
recommended_items = [interaction_matrix.columns[i]
|
|
for i in np.argsort(scores[interaction_matrix.index == user_id, :])[0][::-1]]
|
|
|
|
recommendations = []
|
|
|
|
min_score = np.min(scores)
|
|
max_score = np.max(scores)
|
|
|
|
if min_score == max_score:
|
|
return []
|
|
|
|
# Normalize the scores to a range of 0-100
|
|
normalized_scores = 100 * \
|
|
(scores - min_score) / (max_score - min_score)
|
|
|
|
for item in recommended_items:
|
|
score = scores[interaction_matrix.index ==
|
|
user_id, interaction_matrix.columns == item][0]
|
|
similarity_score = normalized_scores[interaction_matrix.index ==
|
|
user_id, interaction_matrix.columns == item][0]
|
|
if score > 0:
|
|
recommendation = {
|
|
'recipe_id': item,
|
|
'similarity': round(similarity_score, 2),
|
|
'score': round(score, 2)
|
|
}
|
|
recommendations.append(recommendation)
|
|
return recommendations
|
|
else:
|
|
return []
|
|
|
|
|
|
def get_hybrid_recommendation(user_id):
|
|
similarity_matrix_cbf, _ = calculate_cosine_similarity()
|
|
|
|
interaction_matrix = create_interaction_matrix() # Move this line here
|
|
|
|
if user_id in interaction_matrix.index:
|
|
similarity_matrix_ibcf = cosine_similarity(interaction_matrix.T)
|
|
similarity_matrix = 0.6 * similarity_matrix_cbf + 0.4 * similarity_matrix_ibcf
|
|
|
|
recommended_items = list(interaction_matrix.columns)
|
|
scores = np.dot(interaction_matrix.values, similarity_matrix)
|
|
scores_filtered = scores[interaction_matrix.index == user_id, :]
|
|
sorted_indices = np.argsort(scores_filtered[0])[::-1]
|
|
recommended_items_sorted = [interaction_matrix.columns[i]
|
|
for i in sorted_indices if interaction_matrix.columns[i] in recommended_items]
|
|
|
|
return recommended_items_sorted
|
|
else:
|
|
return []
|