File size: 7,253 Bytes
3a921f3
29c143e
 
 
 
 
 
 
 
 
 
8b49811
29c143e
 
 
 
450f90e
29c143e
 
8b49811
 
 
 
 
 
 
 
450f90e
 
8b49811
 
 
 
 
 
 
 
 
 
ca02dba
8b49811
29c143e
8b49811
29c143e
 
 
 
 
 
 
 
 
 
8b49811
 
 
 
29c143e
 
 
 
 
 
8b49811
29c143e
450f90e
29c143e
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
450f90e
29c143e
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
ca02dba
29c143e
 
450f90e
8b49811
 
29c143e
 
 
 
 
 
 
 
 
 
 
 
 
450f90e
29c143e
8b49811
29c143e
8b49811
 
 
 
 
 
 
29c143e
 
8b49811
 
 
 
 
29c143e
 
 
 
 
 
 
 
 
 
 
 
 
8b49811
ca02dba
 
 
 
 
29c143e
8b49811
29c143e
 
 
 
 
 
 
8b49811
29c143e
 
8b49811
29c143e
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
450f90e
29c143e
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
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()