Spaces:
Sleeping
Sleeping
Commit ·
90df448
1
Parent(s): d896e62
Initial commit
Browse files- app.py +132 -0
- books_summary.csv +0 -0
- requirements.txt +6 -0
- semantic_embeddings.npy +3 -0
app.py
ADDED
|
@@ -0,0 +1,132 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import pandas as pd
|
| 2 |
+
from sklearn.feature_extraction.text import TfidfVectorizer
|
| 3 |
+
from sklearn.metrics.pairwise import cosine_similarity
|
| 4 |
+
from sklearn.neighbors import NearestNeighbors
|
| 5 |
+
from sentence_transformers import SentenceTransformer
|
| 6 |
+
import numpy as np
|
| 7 |
+
import streamlit as st
|
| 8 |
+
import os
|
| 9 |
+
from rapidfuzz import process
|
| 10 |
+
import re
|
| 11 |
+
|
| 12 |
+
def load_data(filepath):
|
| 13 |
+
"""Load and deduplicate the dataset."""
|
| 14 |
+
data = pd.read_csv(filepath)
|
| 15 |
+
data = data.drop_duplicates(subset=['book_name'], keep='first').reset_index(drop=True)
|
| 16 |
+
return data
|
| 17 |
+
|
| 18 |
+
def preprocess_data(data):
|
| 19 |
+
"""Preprocess the dataset by cleaning text fields."""
|
| 20 |
+
data['book_name'] = data['book_name'].fillna('').str.lower()
|
| 21 |
+
data['summaries'] = data['summaries'].fillna('').str.lower()
|
| 22 |
+
data['categories'] = data['categories'].fillna('').str.lower()
|
| 23 |
+
data['combined_text'] = (
|
| 24 |
+
data['book_name'] + ' ' +
|
| 25 |
+
data['summaries'] * 2 + ' ' +
|
| 26 |
+
data['categories']
|
| 27 |
+
)
|
| 28 |
+
return data
|
| 29 |
+
|
| 30 |
+
def feature_engineering(data):
|
| 31 |
+
"""Generate features using TF-IDF and SentenceTransformer embeddings."""
|
| 32 |
+
# TF-IDF Vectorization
|
| 33 |
+
vectorizer = TfidfVectorizer(stop_words='english')
|
| 34 |
+
tfidf_matrix = vectorizer.fit_transform(data['combined_text'])
|
| 35 |
+
|
| 36 |
+
# SentenceTransformer embeddings
|
| 37 |
+
model = SentenceTransformer('paraphrase-MiniLM-L3-v2')
|
| 38 |
+
embeddings_file = "semantic_embeddings.npy"
|
| 39 |
+
if not os.path.exists(embeddings_file):
|
| 40 |
+
data['semantic_embeddings'] = data['combined_text'].apply(lambda x: model.encode(x))
|
| 41 |
+
np.save(embeddings_file, np.vstack(data['semantic_embeddings']))
|
| 42 |
+
else:
|
| 43 |
+
semantic_embeddings = np.load(embeddings_file)
|
| 44 |
+
data['semantic_embeddings'] = [embedding for embedding in semantic_embeddings]
|
| 45 |
+
|
| 46 |
+
# Fit NearestNeighbors model
|
| 47 |
+
nn_model = NearestNeighbors(metric='cosine', algorithm='auto')
|
| 48 |
+
nn_model.fit(np.vstack(data['semantic_embeddings']))
|
| 49 |
+
|
| 50 |
+
return vectorizer, tfidf_matrix, model, nn_model
|
| 51 |
+
|
| 52 |
+
def calculate_similarity(input_title, data, vectorizer, tfidf_matrix, model, nn_model, n_recommendations=5, min_similarity=0.3):
|
| 53 |
+
"""Calculate similarities and generate recommendations."""
|
| 54 |
+
try:
|
| 55 |
+
# Compute TF-IDF similarity
|
| 56 |
+
input_vector = vectorizer.transform([input_title.lower()])
|
| 57 |
+
cosine_similarities = cosine_similarity(input_vector, tfidf_matrix).flatten()
|
| 58 |
+
min_similarity = max(0.1, cosine_similarities.mean() - cosine_similarities.std())
|
| 59 |
+
|
| 60 |
+
# Compute semantic similarity using NearestNeighbors
|
| 61 |
+
input_embedding = model.encode(input_title.lower())
|
| 62 |
+
distances, indices = nn_model.kneighbors([input_embedding], n_neighbors=len(data))
|
| 63 |
+
|
| 64 |
+
# Combine similarities
|
| 65 |
+
recommended_books = []
|
| 66 |
+
seen_titles = set()
|
| 67 |
+
|
| 68 |
+
for idx, dist in zip(indices[0], distances[0]):
|
| 69 |
+
book_title = data['book_name'].iloc[idx]
|
| 70 |
+
round_sim_value = round(1 - dist, 2) # Convert distance to similarity
|
| 71 |
+
if book_title not in seen_titles and round_sim_value >= min_similarity and str(book_title).lower() != str(input_title).lower():
|
| 72 |
+
recommended_books.append((book_title, round_sim_value))
|
| 73 |
+
seen_titles.add(book_title)
|
| 74 |
+
if len(recommended_books) == n_recommendations:
|
| 75 |
+
break
|
| 76 |
+
|
| 77 |
+
# If no meaningful recommendations, return fallback message
|
| 78 |
+
if not any(score >= min_similarity for _, score in recommended_books):
|
| 79 |
+
return [("No meaningful recommendations found.", 0.0)]
|
| 80 |
+
|
| 81 |
+
return recommended_books
|
| 82 |
+
except Exception as e:
|
| 83 |
+
return [(f"An error occurred: {str(e)}", 0.0)]
|
| 84 |
+
|
| 85 |
+
def validate_input(input_title, data, threshold=70):
|
| 86 |
+
"""Validate input title using fuzzy matching against dataset titles."""
|
| 87 |
+
titles = data['book_name'].tolist()
|
| 88 |
+
closest_match, score, _ = process.extractOne(input_title, titles)
|
| 89 |
+
if score >= threshold:
|
| 90 |
+
return closest_match
|
| 91 |
+
return None
|
| 92 |
+
|
| 93 |
+
def deploy_ui(data, vectorizer, tfidf_matrix, model, nn_model):
|
| 94 |
+
"""Streamlit-based UI for interacting with the recommendation system."""
|
| 95 |
+
st.title("Content-Based Book Recommender")
|
| 96 |
+
st.write("Enter a book title to get up to 5 similar recommendations.")
|
| 97 |
+
book_title = st.text_input("Enter a book title")
|
| 98 |
+
|
| 99 |
+
if st.button("Submit"):
|
| 100 |
+
# Input validation
|
| 101 |
+
if len(book_title.strip()) < 3:
|
| 102 |
+
st.write("Input is too short. Please enter a meaningful book title.")
|
| 103 |
+
return
|
| 104 |
+
|
| 105 |
+
# Fuzzy matching for known titles
|
| 106 |
+
closest_title = validate_input(book_title, data)
|
| 107 |
+
if closest_title:
|
| 108 |
+
st.write(f"Did you mean: {closest_title}?")
|
| 109 |
+
book_title = closest_title
|
| 110 |
+
|
| 111 |
+
# Generate recommendations
|
| 112 |
+
recommendations = calculate_similarity(book_title, data, vectorizer, tfidf_matrix, model, nn_model, min_similarity=0.3)
|
| 113 |
+
if "error" in recommendations[0][0]:
|
| 114 |
+
st.write("Error occured. Please try again later")
|
| 115 |
+
|
| 116 |
+
elif recommendations is None or len(recommendations) == 0 or recommendations[0][0] == "No meaningful recommendations found.":
|
| 117 |
+
st.write("No meaningful recommendations found for the input. Please try again with a different title.")
|
| 118 |
+
else:
|
| 119 |
+
st.write("Recommended Books:")
|
| 120 |
+
idx = 1
|
| 121 |
+
for book, score in recommendations:
|
| 122 |
+
cleaned_book = re.sub(r"[^A-Za-z0-9]+$", "", book)
|
| 123 |
+
st.write(f"{idx}: {cleaned_book}")
|
| 124 |
+
idx += 1
|
| 125 |
+
|
| 126 |
+
|
| 127 |
+
if __name__ == "__main__":
|
| 128 |
+
filepath = "books_summary.csv"
|
| 129 |
+
data = load_data(filepath)
|
| 130 |
+
data = preprocess_data(data)
|
| 131 |
+
vectorizer, tfidf_matrix, model, nn_model = feature_engineering(data)
|
| 132 |
+
deploy_ui(data, vectorizer, tfidf_matrix, model, nn_model)
|
books_summary.csv
ADDED
|
The diff for this file is too large to render.
See raw diff
|
|
|
requirements.txt
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
streamlit
|
| 2 |
+
pandas
|
| 3 |
+
scikit-learn
|
| 4 |
+
sentence-transformers
|
| 5 |
+
numpy
|
| 6 |
+
rapidfuzz
|
semantic_embeddings.npy
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:fca7d1a53c7299e5278cdbb7fe42495072d0eb5d96676431ece0d6b68ebf4043
|
| 3 |
+
size 1890944
|