| import streamlit as st |
| import pandas as pd |
| import numpy as np |
|
|
| from sklearn.feature_extraction.text import TfidfVectorizer |
| from sklearn.metrics.pairwise import cosine_similarity |
|
|
| import neattext as ntx |
|
|
|
|
| |
| |
| |
|
|
| def clean_text(text: str) -> str: |
| if not isinstance(text, str): |
| return "" |
| text = text.lower() |
| text = ntx.remove_stopwords(text) |
| text = ntx.remove_multiple_spaces(text) |
| text = ntx.normalize(text) |
| return text |
|
|
|
|
| @st.cache_data(show_spinner=False) |
| def load_data(path: str) -> pd.DataFrame: |
| df = pd.read_csv(path, encoding="ISO-8859-1") |
| df = df.dropna(subset=["Title", "Article"]).copy() |
| df["article_clean"] = df["Article"].apply(clean_text) |
| return df |
|
|
|
|
| @st.cache_resource(show_spinner=False) |
| def build_vectorizer(corpus: pd.Series): |
| vectorizer = TfidfVectorizer() |
| matrix = vectorizer.fit_transform(corpus) |
| return vectorizer, matrix |
|
|
|
|
| def find_similar_by_text( |
| input_text: str, |
| df: pd.DataFrame, |
| vectorizer: TfidfVectorizer, |
| article_matrix, |
| top_n: int = 5, |
| ): |
| query_clean = clean_text(input_text) |
| if not query_clean.strip(): |
| return [] |
| query_vec = vectorizer.transform([query_clean]) |
| sims = cosine_similarity(query_vec, article_matrix).flatten() |
| top_idx = np.argsort(-sims)[:top_n] |
| results = [] |
| for i in top_idx: |
| results.append({ |
| "title": df.iloc[i]["Title"], |
| "article": df.iloc[i]["Article"], |
| "score": float(sims[i]), |
| }) |
| return results |
|
|
|
|
| def find_similar_by_title( |
| title: str, |
| df: pd.DataFrame, |
| vectorizer: TfidfVectorizer, |
| article_matrix, |
| top_n: int = 5, |
| ): |
| matches = df.index[df["Title"] == title].tolist() |
| if matches: |
| idx = matches[0] |
| vec = article_matrix[idx] |
| sims = cosine_similarity(vec, article_matrix).flatten() |
| sims[idx] = -np.inf |
| top_idx = np.argsort(-sims)[:top_n] |
| results = [] |
| for i in top_idx: |
| results.append({ |
| "title": df.iloc[i]["Title"], |
| "article": df.iloc[i]["Article"], |
| "score": float(sims[i]), |
| }) |
| return results |
| return find_similar_by_text(title, df, vectorizer, article_matrix, top_n) |
|
|
|
|
| |
| |
| |
|
|
| def main(): |
| st.set_page_config(page_title="Article Recommender", page_icon="📰", layout="centered") |
| st.title("📰 Article Recommendation System") |
| st.caption("Content-based recommendations using TF-IDF and cosine similarity") |
|
|
| df = load_data("https://raw.githubusercontent.com/amankharwal/Website-data/master/articles.csv") |
| vectorizer, article_matrix = build_vectorizer(df["article_clean"]) |
|
|
| with st.sidebar: |
| st.header("Settings") |
| top_n = st.number_input("Top N results", min_value=1, max_value=20, value=5, step=1) |
|
|
| mode = st.radio("Input type", ("Title", "Article content")) |
|
|
| results = [] |
|
|
| if mode == "Title": |
| title = st.selectbox("Select title", options=sorted(df["Title"].unique().tolist())) |
| if title: |
| st.subheader(title) |
| st.write(df.loc[df["Title"] == title, "Article"].iloc[0]) |
| if st.button("Find similar articles", type="primary"): |
| results = find_similar_by_title(title, df, vectorizer, article_matrix, top_n) |
| else: |
| article_text = st.text_area("Paste article content", height=200) |
| if st.button("Find similar articles", type="primary"): |
| if not article_text.strip(): |
| st.warning("Please paste some article content.") |
| else: |
| results = find_similar_by_text(article_text, df, vectorizer, article_matrix, top_n) |
|
|
| if results: |
| st.subheader("Similar Articles") |
| for i, item in enumerate(results, start=1): |
| with st.expander(f"{i}. {item['title']} :yellow-badge[:material/star: {item['score']:.3f}]", expanded=False): |
| st.write(item["article"]) |
|
|
|
|
| if __name__ == "__main__": |
| main() |
|
|