bigroll commited on
Commit
105bb6d
·
verified ·
1 Parent(s): 2492660

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +20 -98
app.py CHANGED
@@ -1,52 +1,7 @@
1
- import streamlit as st
2
- import pandas as pd
3
- import plotly.express as px
4
- import duckdb
5
- import os
6
-
7
- # --- Set page layout ---
8
- st.set_page_config(layout="wide", page_title="Sentiment Analysis")
9
-
10
- # --- CSS for Flag Toggle ---
11
- st.markdown("""
12
- <style>
13
- .flag-container { display: flex; justify-content: center; gap: 10px; }
14
- .flag { cursor: pointer; width: 40px; height: 30px; }
15
- </style>
16
- """, unsafe_allow_html=True)
17
-
18
- # --- State & Language ---
19
- flag_selection = st.radio("", ["🇬🇧", "🇧🇬"], horizontal=True, label_visibility="collapsed")
20
- LANG = "English" if flag_selection == "🇬🇧" else "Български"
21
-
22
- T = {
23
- "English": {
24
- "title": "📊 Sentiment Analysis Dashboard",
25
- "select_entities": "Select Entities",
26
- "select_domains": "Select Domains",
27
- "score_type": "Select Score Type",
28
- "group_by_domain": "Group by Domain",
29
- "all_scores": "Show All Score Types",
30
- "no_data": "No data matches the selected filters.",
31
- "avg_over_time": "Average {} Over Time",
32
- "scores_over_time": "Sentiment Scores Over Time",
33
- "select_entities_prompt": "Please select at least one entity."
34
- },
35
- "Български": {
36
- "title": "📊 Табло за анализ на настроенията",
37
- "select_entities": "Изберете обекти",
38
- "select_domains": "Изберете източници",
39
- "score_type": "Изберете тип оценка",
40
- "group_by_domain": "Групирай по сайт",
41
- "all_scores": "Покажи всички типове оценки",
42
- "no_data": "Няма данни за избраните филтри.",
43
- "avg_over_time": "Средна стойност на {} във времето",
44
- "scores_over_time": "Оценки на настроенията във времето",
45
- "select_entities_prompt": "Моля, изберете поне един обект."
46
- }
47
  }
48
 
49
  # --- MotherDuck Connection ---
 
50
  @st.cache_resource
51
  def get_connection():
52
  try:
@@ -58,20 +13,17 @@ def get_connection():
58
 
59
  con = get_connection()
60
 
61
- # --- Metadata for Filters ---
62
  @st.cache_data(ttl=600)
63
  def get_filter_options():
64
- # Trim and count for cleaner lookups
65
- entities = con.sql("SELECT trim(entity) as entity, COUNT(*) as c FROM sentiment_analysis GROUP BY 1 ORDER BY c DESC").df()
66
- domains = con.sql("SELECT trim(domain) as domain, COUNT(*) as c FROM sentiment_analysis GROUP BY 1 ORDER BY c DESC").df()
67
  return entities, domains
68
-
69
- df_entities, df_domains = get_filter_options()
70
-
71
- # --- Sidebar ---
72
  st.title(T[LANG]["title"])
73
 
74
  with st.sidebar:
 
75
  entity_lookup = dict(zip(df_entities['entity'], df_entities['c']))
76
  domain_lookup = dict(zip(df_domains['domain'], df_domains['c']))
77
 
@@ -88,59 +40,32 @@ with st.sidebar:
88
  )
89
 
90
  score_type = st.selectbox(T[LANG]["score_type"], ["entity_score", "title_score", "overall_score"])
91
- group_by_domain = st.checkbox(T[LANG]["group_by_domain"])
92
- all_scores = st.checkbox(T[LANG]["all_scores"])
93
-
94
- timeframes = {"All": 9999, "Last 7 Days": 7, "Last 30 Days": 30, "Last Year": 365}
95
- time_choice = st.selectbox("Timeframe", list(timeframes.keys()))
96
-
97
- # --- Logic: Stop if no selection ---
98
- if not selected_entities:
99
  st.info(T[LANG]["select_entities_prompt"])
100
  st.stop()
101
 
102
- # --- Dynamic Granularity Logic (Targeting ~50 points) ---
103
- days = timeframes[time_choice]
104
- if days <= 50:
105
- bucket = "1 day"
106
- elif days <= 365:
107
- bucket = "1 week"
108
- else:
109
- bucket = "1 month"
110
-
111
- # --- Query Building ---
112
  where_clause = f"WHERE entity IN ({str(selected_entities)[1:-1]})"
113
  if selected_domains:
114
  where_clause += f" AND domain IN ({str(selected_domains)[1:-1]})"
115
  if time_choice != "All":
116
- where_clause += f" AND created_at >= (epoch(now()) - {days * 86400})"
 
117
 
118
  if all_scores:
 
119
  sql_query = f"""
120
  SELECT
121
- time_bucket(interval '{bucket}', created_at) as date,
122
  entity,
123
  domain,
124
  score_name as score_type,
125
- AVG(score_value) as score
126
- FROM (
127
- UNPIVOT sentiment_analysis
128
- ON entity_score, title_score, overall_score
129
- INTO NAME score_name VALUE score_value
130
- )
131
- {where_clause}
132
- GROUP BY ALL
133
- """
134
  else:
135
  sql_query = f"""
136
  SELECT
137
- time_bucket(interval '{bucket}', created_at) as date,
138
  entity,
139
  domain,
140
  AVG({score_type}) as score
141
- FROM sentiment_analysis
142
- {where_clause}
143
- GROUP BY ALL
144
  """
145
 
146
  # --- Execution & Plotting ---
@@ -153,34 +78,31 @@ except Exception as e:
153
  if filtered_df.empty:
154
  st.warning(T[LANG]["no_data"])
155
  else:
156
- # Build legend labels
157
  color_col = "entity"
158
  if group_by_domain:
159
  filtered_df["label"] = filtered_df["entity"] + " | " + filtered_df["domain"]
160
  color_col = "label"
161
 
162
  if all_scores:
163
- current_base = filtered_df["label"] if group_by_domain else filtered_df["entity"]
164
- filtered_df["label"] = current_base + " | " + filtered_df["score_type"]
165
  color_col = "label"
166
 
167
- # Plot
168
  fig = px.line(
169
  filtered_df.sort_values("date"),
170
  x="date", y="score", color=color_col,
171
- title=f"{T[LANG]['scores_over_time']} (Grouped by {bucket})",
172
- labels={"score": "Score", "date": "Date"}
173
  )
174
 
175
- # Layout styling
176
  fig.update_layout(
177
  yaxis=dict(range=[-10, 10], gridcolor="lightgrey"),
178
  plot_bgcolor="white",
179
  legend=dict(orientation="h", y=-0.2, x=0.5, xanchor="center")
180
  )
181
 
182
- # Reference lines
183
  for val in [-5, 0, 5]:
184
- fig.add_hline(y=val, line_width=2 if val==0 else 1, line_dash="dash", line_color="black")
185
-
186
- st.plotly_chart(fig, use_container_width=True)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  }
2
 
3
  # --- MotherDuck Connection ---
4
+
5
  @st.cache_resource
6
  def get_connection():
7
  try:
 
13
 
14
  con = get_connection()
15
 
16
+ # --- Cached Metadata for Filters ---
17
  @st.cache_data(ttl=600)
18
  def get_filter_options():
19
+ # Fast counts via DuckDB
20
+ entities = con.sql("SELECT entity, COUNT(*) as c FROM sentiment_analysis GROUP BY 1 ORDER BY c DESC").df()
21
+ domains = con.sql("SELECT domain, COUNT(*) as c FROM sentiment_analysis GROUP BY 1 ORDER BY c DESC").df()
22
  return entities, domains
 
 
 
 
23
  st.title(T[LANG]["title"])
24
 
25
  with st.sidebar:
26
+ # Create lookup dictionaries to avoid filtering dataframes in the lambda (prevents crashes)
27
  entity_lookup = dict(zip(df_entities['entity'], df_entities['c']))
28
  domain_lookup = dict(zip(df_domains['domain'], df_domains['c']))
29
 
 
40
  )
41
 
42
  score_type = st.selectbox(T[LANG]["score_type"], ["entity_score", "title_score", "overall_score"])
 
 
 
 
 
 
 
 
43
  st.info(T[LANG]["select_entities_prompt"])
44
  st.stop()
45
 
46
+ # Dynamic SQL construction
 
 
 
 
 
 
 
 
 
47
  where_clause = f"WHERE entity IN ({str(selected_entities)[1:-1]})"
48
  if selected_domains:
49
  where_clause += f" AND domain IN ({str(selected_domains)[1:-1]})"
50
  if time_choice != "All":
51
+ where_clause += f" AND created_at >= (epoch(now()) - {timeframes[time_choice] * 86400})"
52
+
53
 
54
  if all_scores:
55
+
56
  sql_query = f"""
57
  SELECT
58
+ time_bucket(interval '1 day', created_at) as date,
59
  entity,
60
  domain,
61
  score_name as score_type,
 
 
 
 
 
 
 
 
 
62
  else:
63
  sql_query = f"""
64
  SELECT
65
+ time_bucket(interval '1 day', created_at) as date,
66
  entity,
67
  domain,
68
  AVG({score_type}) as score
 
 
 
69
  """
70
 
71
  # --- Execution & Plotting ---
 
78
  if filtered_df.empty:
79
  st.warning(T[LANG]["no_data"])
80
  else:
81
+ # Build dynamic labels for the legend
82
  color_col = "entity"
83
  if group_by_domain:
84
  filtered_df["label"] = filtered_df["entity"] + " | " + filtered_df["domain"]
85
  color_col = "label"
86
 
87
  if all_scores:
88
+ current_label = filtered_df["label"] if group_by_domain else filtered_df["entity"]
89
+ filtered_df["label"] = current_label + " | " + filtered_df["score_type"]
90
  color_col = "label"
91
 
 
92
  fig = px.line(
93
  filtered_df.sort_values("date"),
94
  x="date", y="score", color=color_col,
95
+ title=T[LANG]["scores_over_time"] if all_scores else T[LANG]["avg_over_time"].format(score_type.replace('_', ' ').title()),
96
+ labels={"score": "Sentiment Score", "date": "Date", "label": "Entity/Source"}
97
  )
98
 
99
+ # Styling
100
  fig.update_layout(
101
  yaxis=dict(range=[-10, 10], gridcolor="lightgrey"),
102
  plot_bgcolor="white",
103
  legend=dict(orientation="h", y=-0.2, x=0.5, xanchor="center")
104
  )
105
 
106
+ # Static horizontal reference lines
107
  for val in [-5, 0, 5]:
108
+ fig.add_hline(y=val, line_width=2 if val==0 else 1, line_dash="dash", line_color="black")