text-topic-map / app /streamlit_app.py
DDDDD-433's picture
Deploy precomputed topic map app
3e24942 verified
Raw
History Blame Contribute Delete
4.45 kB
"""Interactive topic map dashboard for BBC News articles.
Reads only the precomputed artifacts produced by `python -m src.pipeline`:
- data/processed/embedded_topics.parquet
- data/processed/topic_summary.csv
"""
from pathlib import Path
import pandas as pd
import plotly.express as px
import streamlit as st
PROJECT_ROOT = Path(__file__).resolve().parent.parent
EMBEDDED_TOPICS_PARQUET = PROJECT_ROOT / "data" / "processed" / "embedded_topics.parquet"
TOPIC_SUMMARY_CSV = PROJECT_ROOT / "data" / "processed" / "topic_summary.csv"
MAX_PLOT_POINTS = 3000
HOVER_PREVIEW_CHARS = 180
st.set_page_config(page_title="Text Topic Map", page_icon="🗺️", layout="wide")
@st.cache_data
def load_data():
df = pd.read_parquet(EMBEDDED_TOPICS_PARQUET)
summary = pd.read_csv(TOPIC_SUMMARY_CSV)
df["preview"] = df["text"].str.slice(0, HOVER_PREVIEW_CHARS) + "…"
return df, summary
if not EMBEDDED_TOPICS_PARQUET.exists():
st.error(
"Precomputed data not found. Run `bash scripts/run_pipeline.sh` first to "
"generate `data/processed/embedded_topics.parquet`."
)
st.stop()
df, topic_summary = load_data()
# ---------------------------------------------------------------- header
st.title("🗺️ Text Topic Map — BBC News")
st.caption(
f"{len(df):,} BBC News articles embedded with MiniLM, clustered with BERTopic, "
"and projected to 2D with UMAP. Each point is one article."
)
# ---------------------------------------------------------------- sidebar
st.sidebar.header("Filters")
topic_options = sorted(df["topic_label"].unique(), key=lambda s: int(s.split(":")[0]))
selected_topics = st.sidebar.multiselect(
"Topics", topic_options, default=topic_options
)
if "label" in df.columns:
label_options = sorted(df["label"].unique())
selected_labels = st.sidebar.multiselect(
"BBC section label", label_options, default=label_options
)
else:
selected_labels = None
search_query = st.sidebar.text_input("Search article text", placeholder="e.g. election, champions league…")
# ---------------------------------------------------------------- filtering
filtered = df[df["topic_label"].isin(selected_topics)]
if selected_labels is not None:
filtered = filtered[filtered["label"].isin(selected_labels)]
if search_query.strip():
filtered = filtered[filtered["text"].str.contains(search_query.strip(), case=False, regex=False)]
# ---------------------------------------------------------------- KPIs
k1, k2, k3 = st.columns(3)
k1.metric("Total articles", f"{len(df):,}")
k2.metric("Topics", df["topic_id"].nunique())
k3.metric("Articles matching filters", f"{len(filtered):,}")
# ---------------------------------------------------------------- scatter map
if filtered.empty:
st.warning("No articles match the current filters. Broaden your topic/label selection or clear the search box.")
else:
plot_df = filtered
if len(plot_df) > MAX_PLOT_POINTS:
plot_df = plot_df.sample(n=MAX_PLOT_POINTS, random_state=0)
st.caption(f"Showing a random sample of {MAX_PLOT_POINTS:,} points for speed.")
fig = px.scatter(
plot_df,
x="x",
y="y",
color="topic_label",
hover_data={"preview": True, "x": False, "y": False, "topic_label": True},
color_discrete_sequence=px.colors.qualitative.Bold,
height=620,
)
fig.update_traces(marker=dict(size=6, opacity=0.75))
fig.update_layout(
legend_title_text="Topic",
xaxis=dict(visible=False),
yaxis=dict(visible=False),
margin=dict(l=10, r=10, t=10, b=10),
plot_bgcolor="rgba(245,246,250,1)",
)
st.plotly_chart(fig, width="stretch")
# ---------------------------------------------------------------- topic panel
st.subheader("Topic summary")
left, right = st.columns([1, 1])
with left:
display_summary = topic_summary.copy()
display_summary["share"] = (display_summary["count"] / len(df) * 100).round(1).astype(str) + "%"
st.dataframe(display_summary, width="stretch", hide_index=True)
with right:
inspect_topic = st.selectbox("Inspect a topic", topic_options)
examples = df[df["topic_label"] == inspect_topic].head(5)
st.markdown(f"**Top 5 example articles — {inspect_topic}**")
for _, row in examples.iterrows():
label_note = f" · _{row['label']}_" if "label" in df.columns else ""
st.markdown(f"- {row['preview']}{label_note}")