import streamlit as st import pandas as pd import joblib import os from sklearn.feature_extraction.text import TfidfVectorizer from sklearn.metrics.pairwise import cosine_similarity # --- Configuration and Constants --- # File paths for model persistence VECTORIZER_PATH = 'src/article_vectorizer.joblib' TFIDF_MATRIX_PATH = 'src/article_matrix.joblib' DATA_FILE_PATH = 'src/articles.csv' # Number of recommendations to display NUM_RECOMMENDATIONS = 5 # --- Data Loading and Preparation --- @st.cache_data def load_and_prepare_data(): """ Loads data ONLY from articles.csv and prepares the DataFrame. If the file is not found or reading fails, it returns an empty DataFrame. """ df = pd.DataFrame() # Initialize an empty DataFrame if os.path.exists(DATA_FILE_PATH): try: df = pd.read_csv(DATA_FILE_PATH,encoding='latin1') # Removed st.success(f"Data loaded successfully...") except Exception as e: st.error(f"Error reading {DATA_FILE_PATH}. Please check file integrity and format (CSV). Error: {e}") else: st.error(f"CRITICAL ERROR: {DATA_FILE_PATH} not found. Please upload the file to your application directory.") if df.empty or 'Article' not in df.columns or 'Title' not in df.columns: # If loading failed or columns are missing, return empty or incomplete DataFrame return pd.DataFrame() # 1. Ensure the index is a clean, contiguous 0-based positional index df = df.reset_index(drop=True) # Ensure the article column is string type df['Article'] = df['Article'].astype(str) return df # --- Model Fitting/Loading Logic --- @st.cache_data def fit_or_load_models(df): """ Fits the TF-IDF model and calculates Cosine Similarity, or loads them from disk if available. """ # Check if the necessary columns exist before proceeding if df.empty or 'Article' not in df.columns or 'Title' not in df.columns: return None, None, None if os.path.exists(VECTORIZER_PATH) and os.path.exists(TFIDF_MATRIX_PATH): try: # Load existing models tfidf = joblib.load(VECTORIZER_PATH) tfidf_matrix = joblib.load(TFIDF_MATRIX_PATH) # Recalculate cosine similarity (This is fast on the loaded matrix) cosine_sim = cosine_similarity(tfidf_matrix) # Removed st.success("Models loaded successfully...") return tfidf, tfidf_matrix, cosine_sim except Exception as e: st.error(f"Error loading models. Recalculating: {e}") # Fallback to calculation below pass # Calculate and save models if they don't exist or loading failed with st.spinner('Calculating TF-IDF and Cosine Similarity (First run/Model not found)...'): articles = df["Article"].tolist() # 1. TF-IDF Vectorizer Setup tfidf = TfidfVectorizer(stop_words='english') tfidf_matrix = tfidf.fit_transform(articles) # 2. Cosine Similarity Calculation cosine_sim = cosine_similarity(tfidf_matrix) # Save models for future runs joblib.dump(tfidf, VECTORIZER_PATH) joblib.dump(tfidf_matrix, TFIDF_MATRIX_PATH) # Removed st.success("Model calculation complete...") return tfidf, tfidf_matrix, cosine_sim # --- Recommendation Function --- def get_recommendations(article_index, cosine_sim_matrix, df, num_recommendations=NUM_RECOMMENDATIONS): """ Returns the top N article recommendations for a given article index. """ if article_index >= len(df) or article_index < 0: return [] # Get the similarity scores for the article similarity_scores = list(enumerate(cosine_sim_matrix[article_index])) # Sort the scores in descending order similarity_scores = sorted(similarity_scores, key=lambda x: x[1], reverse=True) # Exclude the article itself (index 0, as similarity is 1.0) and take the top N top_n_recommendations = similarity_scores[1:num_recommendations + 1] # Get the indices of the top N recommended articles (these are positional indices) top_n_indices = [i[0] for i in top_n_recommendations] # Use list indexing instead of DataFrame .iloc to guarantee positional lookup title_list = df["Title"].tolist() recommended_titles = [title_list[i] for i in top_n_indices] # Get the similarity scores for display (rounded) recommended_scores = [round(i[1] * 100, 2) for i in top_n_recommendations] return list(zip(recommended_titles, recommended_scores)) # --- Streamlit App Layout --- def app(): st.set_page_config(page_title="Article Recommendation System", layout="wide") st.title("📄 Content-Based Article Recommender") st.markdown("Select an article from the sidebar to see the top **5** most relevant recommendations.") # 1. Load Data df = load_and_prepare_data() # Check if data loading was successful and essential columns exist if df.empty or 'Article' not in df.columns or 'Title' not in df.columns: st.error("Application cannot start: Please ensure 'articles.csv' is uploaded and contains 'Title' and 'Article' columns.") return # 2. Load/Fit Models tfidf, tfidf_matrix, cosine_sim = fit_or_load_models(df) # Check if models were loaded/calculated successfully if cosine_sim is None: st.error("Application cannot start: Model calculation failed.") return # --- Sidebar for Selection --- # Create a list of titles for the selectbox article_titles = df['Title'].tolist() st.sidebar.header("Select an Article") selected_title = st.sidebar.selectbox( "Which article is the reader currently viewing?", article_titles ) # Find the index of the selected article if selected_title: # Use .tolist().index() to guarantee a 0-based positional index try: selected_index = df['Title'].tolist().index(selected_title) except ValueError: st.error("Error finding article index. Check data consistency.") return else: # Should not happen if article_titles is non-empty st.error("No article selected.") return # --- Main Content Display --- st.header(f"Recommendations for: **{selected_title}**") # 3. Get Recommendations recommendations_list = get_recommendations(selected_index, cosine_sim, df) # 4. Display Results if recommendations_list: st.subheader(f"Top {NUM_RECOMMENDATIONS} Most Similar Articles:") # Create a visually appealing table or list col1, col2 = st.columns([1, 4]) with col1: st.markdown("### Rank") for i in range(1, NUM_RECOMMENDATIONS + 1): st.write(f"**#{i}**") with col2: st.markdown("### Article Title (Similarity Score)") for title, score in recommendations_list: st.markdown(f"**{title}** - *({score}%)*") if __name__ == "__main__": app()