| import streamlit as st |
| import pandas as pd |
| import plotly.express as px |
| import duckdb |
| import os |
|
|
| |
| st.set_page_config(layout="wide") |
|
|
| |
| flag_style = """ |
| <style> |
| .flag-container { |
| display: flex; |
| justify-content: center; |
| gap: 10px; |
| } |
| .flag { |
| cursor: pointer; |
| width: 40px; |
| height: 30px; |
| } |
| </style> |
| """ |
| st.markdown(flag_style, unsafe_allow_html=True) |
|
|
| flag_selection = st.radio( |
| "", |
| ["🇬🇧", "🇧🇬"], |
| horizontal=True, |
| label_visibility="collapsed" |
| ) |
| LANG = "English" if flag_selection == "🇬🇧" else "Български" |
|
|
| T = { |
| "English": { |
| "title": "📊 Sentiment Analysis Dashboard", |
| "select_entities": "Select Entities", |
| "select_domains": "Select Domains (News Sites)", |
| "score_type": "Select Score Type", |
| "group_by_domain": "Group by Domain", |
| "all_scores": "Show All Score Types", |
| "no_data": "No data matches the selected filters.", |
| "avg_over_time": "Average {} Over Time", |
| "scores_over_time": "Sentiment Scores Over Time", |
| "select_entities_prompt": "Please select at least one entity to view the graph." |
| }, |
| "Български": { |
| "title": "📊 Табло за анализ на настроенията", |
| "select_entities": "Изберете обекти", |
| "select_domains": "Изберете източници (сайтове)", |
| "score_type": "Изберете тип оценка", |
| "group_by_domain": "Групирай по сайт", |
| "all_scores": "Покажи всички типове оценки", |
| "no_data": "Няма данни за избраните филтри.", |
| "avg_over_time": "Средна стойност на {} във времето", |
| "scores_over_time": "Оценки на настроенията във времето", |
| "select_entities_prompt": "Моля, изберете поне един обект, за да видите графиката." |
| } |
| } |
|
|
| |
| if "MOTHERDUCK_TOKEN" not in st.secrets: |
| st.error("MOTHERDUCK_TOKEN not found in secrets.") |
| st.stop() |
|
|
| MOTHERDUCK_TOKEN = st.secrets["MOTHERDUCK_TOKEN"] |
|
|
| |
| @st.cache_data(ttl=300) |
| def get_data(): |
| try: |
| |
| con = duckdb.connect(f'md:?token={MOTHERDUCK_TOKEN}') |
| |
| |
| query = """ |
| SELECT entity, entity_score, domain, title_score, overall_score, created_at |
| FROM sentiment_analysis |
| """ |
| |
| df = con.sql(query).df() |
| |
| |
| if pd.api.types.is_numeric_dtype(df["created_at"]): |
| df["created_at"] = pd.to_datetime(df["created_at"], unit="s") |
| else: |
| df["created_at"] = pd.to_datetime(df["created_at"]) |
| |
| |
| if pd.api.types.is_datetime64_any_dtype(df["created_at"]): |
| if df["created_at"].dt.tz is not None: |
| df["created_at"] = df["created_at"].dt.tz_localize(None) |
|
|
| return df |
| except Exception as e: |
| st.error(f"Failed to fetch data: {e}") |
| return pd.DataFrame() |
|
|
| df = get_data() |
| if df.empty: |
| st.warning("No data available.") |
| st.stop() |
|
|
| |
| st.title(T[LANG]["title"]) |
|
|
| |
| with st.sidebar: |
| |
| entity_counts = df["entity"].value_counts() |
| entity_labels = [f"{ent} ({count})" for ent, count in entity_counts.items()] |
| entity_lookup = {f"{ent} ({count})": ent for ent, count in entity_counts.items()} |
| selected_labels = st.multiselect(T[LANG]["select_entities"], entity_labels) |
| entities = [entity_lookup[label] for label in selected_labels] |
|
|
| |
| domain_counts = df["domain"].value_counts() |
| domain_labels = [f"{dom} ({count})" for dom, count in domain_counts.items()] |
| domain_lookup = {f"{dom} ({count})": dom for dom, count in domain_counts.items()} |
| selected_domain_labels = st.multiselect(T[LANG]["select_domains"], domain_labels) |
| domains = [domain_lookup[label] for label in selected_domain_labels] |
|
|
| |
| score_type = st.selectbox(T[LANG]["score_type"], ["entity_score", "title_score", "overall_score"]) |
| group_by_domain = st.checkbox(T[LANG]["group_by_domain"]) |
| group_by_score_type = st.checkbox(T[LANG]["all_scores"]) |
|
|
| |
| timeframes = ["All", "Last 7 Days", "Last 30 Days", "Last Year", "Custom"] |
| selected_timeframe = st.selectbox("Select Timeframe", timeframes) |
|
|
| |
| start_date, end_date = None, None |
| if selected_timeframe == "Custom": |
| col1, col2 = st.columns(2) |
| with col1: |
| start_date = st.date_input("Start Date") |
| with col2: |
| end_date = st.date_input("End Date") |
| if start_date > end_date: |
| st.error("Start date must be before or equal to the end date.") |
| st.stop() |
|
|
| |
| filtered_df = df.copy() |
| if entities: |
| filtered_df = filtered_df[filtered_df["entity"].isin(entities)] |
| if domains: |
| filtered_df = filtered_df[filtered_df["domain"].isin(domains)] |
|
|
| if selected_timeframe != "All": |
| now = pd.Timestamp.now() |
| if selected_timeframe == "Last 7 Days": |
| start_date = now - pd.Timedelta(days=7) |
| end_date = now |
| elif selected_timeframe == "Last 30 Days": |
| start_date = now - pd.Timedelta(days=30) |
| end_date = now |
| elif selected_timeframe == "Last Year": |
| start_date = now - pd.Timedelta(days=365) |
| end_date = now |
| |
| if start_date and end_date: |
| filtered_df = filtered_df[ |
| (filtered_df["created_at"] >= pd.Timestamp(start_date)) & |
| (filtered_df["created_at"] <= pd.Timestamp(end_date)) |
| ] |
|
|
| |
| if not entities: |
| st.info(T[LANG]["select_entities_prompt"]) |
| st.stop() |
|
|
| |
| if not filtered_df.empty: |
| |
| |
| |
| |
| rank_group_cols = ["entity"] |
| if group_by_domain: |
| rank_group_cols.append("domain") |
| |
| |
| rank_df = filtered_df.groupby(rank_group_cols)["entity_score"].mean().reset_index() |
| |
| rank_lookup = { |
| tuple(row[col] for col in rank_group_cols): row["entity_score"] |
| for _, row in rank_df.iterrows() |
| } |
|
|
| |
| min_date = filtered_df["created_at"].min() |
| max_date = filtered_df["created_at"].max() |
| days_span = (max_date - min_date).days + 1 |
| |
| if days_span <= 20: |
| freq = "D" |
| else: |
| step = int(days_span / 20) |
| step = max(1, step) |
| freq = f"{step}D" |
| |
| filtered_df["date"] = filtered_df["created_at"].dt.floor(freq) |
| |
|
|
| if group_by_score_type: |
| score_cols = ["entity_score", "title_score", "overall_score"] |
| melted = filtered_df.melt( |
| id_vars=["date", "entity", "domain"], |
| value_vars=score_cols, |
| var_name="score_type", |
| value_name="score" |
| ) |
|
|
| group_cols = ["date", "entity"] |
| if group_by_domain: |
| group_cols.append("domain") |
| group_cols.append("score_type") |
|
|
| grouped = ( |
| melted.groupby(group_cols)["score"] |
| .mean() |
| .reset_index() |
| ) |
| |
| |
| grouped["score"] = grouped["score"].round(2) |
|
|
| stats_group = ["entity", "score_type"] |
| if group_by_domain: |
| stats_group.append("domain") |
|
|
| stats = ( |
| melted.groupby(stats_group)["score"] |
| .agg(["mean", "count"]) |
| .round(2) |
| .reset_index() |
| ) |
|
|
| stat_lookup = { |
| tuple(row[col] for col in stats_group): f"{row['mean']:.2f}, n={int(row['count'])}" |
| for _, row in stats.iterrows() |
| } |
|
|
| def build_label(row): |
| key = tuple(row[col] for col in stats_group) |
| label = f"{row['entity']}" |
| if group_by_domain: |
| label += f" | {row['domain']}" |
| label += f" | {row['score_type'].replace('_', ' ').title()}" |
| label += f" (avg: {stat_lookup.get(key, '')})" |
| return label |
|
|
| grouped["label"] = grouped.apply(build_label, axis=1) |
|
|
| |
| def get_rank_score(row): |
| key_cols = [row["entity"]] |
| if group_by_domain: |
| key_cols.append(row["domain"]) |
| return rank_lookup.get(tuple(key_cols), -999) |
|
|
| grouped["rank_score"] = grouped.apply(get_rank_score, axis=1) |
| |
| |
| grouped = grouped.sort_values(by=["rank_score", "label", "date"], ascending=[False, True, True]) |
|
|
| fig = px.line( |
| grouped, |
| x="date", |
| y="score", |
| color="label", |
| labels={"date": "Date", "score": "Score", "label": "Legend"}, |
| title=T[LANG]["scores_over_time"] |
| ) |
| fig.update_traces(mode="lines+markers") |
|
|
| else: |
| group_cols = ["date", "entity"] |
| if group_by_domain: |
| group_cols.append("domain") |
|
|
| grouped = ( |
| filtered_df.groupby(group_cols)[score_type] |
| .mean() |
| .reset_index() |
| ) |
|
|
| |
| grouped[score_type] = grouped[score_type].round(2) |
|
|
| stats_group = ["entity"] |
| if group_by_domain: |
| stats_group.append("domain") |
| stats = ( |
| filtered_df.groupby(stats_group)[score_type] |
| .agg(["mean", "count"]) |
| .round(2) |
| .reset_index() |
| ) |
|
|
| stat_lookup = { |
| tuple(row[col] for col in stats_group): f"{row['mean']:.2f}, n={int(row['count'])}" |
| for _, row in stats.iterrows() |
| } |
|
|
| def build_label(row): |
| key = tuple(row[col] for col in stats_group) |
| label = f"{row['entity']}" |
| if group_by_domain: |
| label += f" | {row['domain']}" |
| label += f" (avg: {stat_lookup.get(key, '')})" |
| return label |
|
|
| grouped["label"] = grouped.apply(build_label, axis=1) |
| |
| |
| def get_rank_score(row): |
| key_cols = [row["entity"]] |
| if group_by_domain: |
| key_cols.append(row["domain"]) |
| return rank_lookup.get(tuple(key_cols), -999) |
|
|
| grouped["rank_score"] = grouped.apply(get_rank_score, axis=1) |
| |
| |
| grouped = grouped.sort_values(by=["rank_score", "label", "date"], ascending=[False, True, True]) |
|
|
| fig = px.line( |
| grouped, |
| x="date", |
| y=score_type, |
| color="label", |
| labels={"date": "Date", score_type: "Score", "label": "Legend"}, |
| title=T[LANG]["avg_over_time"].format(score_type.replace("_", " ").title()) |
| ) |
| fig.update_traces(mode="lines+markers") |
|
|
| |
| shapes = [] |
| for y in range(-10, 11): |
| shapes.append({ |
| "type": "line", |
| "xref": "paper", |
| "x0": 0, |
| "x1": 1, |
| "yref": "y", |
| "y0": y, |
| "y1": y, |
| "line": { |
| "color": "Black", |
| "width": 1 if y in [-5, 5] else 0.2, |
| "dash": "solid" |
| } |
| }) |
|
|
| fig.update_layout( |
| xaxis=dict(type="date"), |
| yaxis=dict(range=[-10, 10]), |
| plot_bgcolor="white", |
| shapes=shapes, |
| legend=dict( |
| orientation="h", |
| yanchor="top", |
| y=-0.3, |
| xanchor="center", |
| x=0.5, |
| title="", |
| traceorder="normal" |
| ) |
| ) |
|
|
| st.plotly_chart(fig, use_container_width=True) |
| else: |
| st.warning(T[LANG]["no_data"]) |