bigroll commited on
Commit
da8e6ab
·
verified ·
1 Parent(s): c9cd4a4

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +100 -20
app.py CHANGED
@@ -1,5 +1,52 @@
1
- # --- MotherDuck Connection ---
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2
 
 
3
  @st.cache_resource
4
  def get_connection():
5
  try:
@@ -11,17 +58,20 @@ def get_connection():
11
 
12
  con = get_connection()
13
 
14
- # --- Cached Metadata for Filters ---
15
  @st.cache_data(ttl=600)
16
  def get_filter_options():
17
- # Fast counts via DuckDB
18
- entities = con.sql("SELECT entity, COUNT(*) as c FROM sentiment_analysis GROUP BY 1 ORDER BY c DESC").df()
19
- domains = con.sql("SELECT domain, COUNT(*) as c FROM sentiment_analysis GROUP BY 1 ORDER BY c DESC").df()
20
  return entities, domains
 
 
 
 
21
  st.title(T[LANG]["title"])
22
 
23
  with st.sidebar:
24
- # Create lookup dictionaries to avoid filtering dataframes in the lambda (prevents crashes)
25
  entity_lookup = dict(zip(df_entities['entity'], df_entities['c']))
26
  domain_lookup = dict(zip(df_domains['domain'], df_domains['c']))
27
 
@@ -38,32 +88,59 @@ with st.sidebar:
38
  )
39
 
40
  score_type = st.selectbox(T[LANG]["score_type"], ["entity_score", "title_score", "overall_score"])
 
 
 
 
 
 
 
 
41
  st.info(T[LANG]["select_entities_prompt"])
42
  st.stop()
43
 
44
- # Dynamic SQL construction
 
 
 
 
 
 
 
 
 
45
  where_clause = f"WHERE entity IN ({str(selected_entities)[1:-1]})"
46
  if selected_domains:
47
  where_clause += f" AND domain IN ({str(selected_domains)[1:-1]})"
48
  if time_choice != "All":
49
- where_clause += f" AND created_at >= (epoch(now()) - {timeframes[time_choice] * 86400})"
50
-
51
 
52
  if all_scores:
53
-
54
  sql_query = f"""
55
  SELECT
56
- time_bucket(interval '1 day', created_at) as date,
57
  entity,
58
  domain,
59
  score_name as score_type,
 
 
 
 
 
 
 
 
 
60
  else:
61
  sql_query = f"""
62
  SELECT
63
- time_bucket(interval '1 day', created_at) as date,
64
  entity,
65
  domain,
66
  AVG({score_type}) as score
 
 
 
67
  """
68
 
69
  # --- Execution & Plotting ---
@@ -76,31 +153,34 @@ except Exception as e:
76
  if filtered_df.empty:
77
  st.warning(T[LANG]["no_data"])
78
  else:
79
- # Build dynamic labels for the legend
80
  color_col = "entity"
81
  if group_by_domain:
82
  filtered_df["label"] = filtered_df["entity"] + " | " + filtered_df["domain"]
83
  color_col = "label"
84
 
85
  if all_scores:
86
- current_label = filtered_df["label"] if group_by_domain else filtered_df["entity"]
87
- filtered_df["label"] = current_label + " | " + filtered_df["score_type"]
88
  color_col = "label"
89
 
 
90
  fig = px.line(
91
  filtered_df.sort_values("date"),
92
  x="date", y="score", color=color_col,
93
- title=T[LANG]["scores_over_time"] if all_scores else T[LANG]["avg_over_time"].format(score_type.replace('_', ' ').title()),
94
- labels={"score": "Sentiment Score", "date": "Date", "label": "Entity/Source"}
95
  )
96
 
97
- # Styling
98
  fig.update_layout(
99
  yaxis=dict(range=[-10, 10], gridcolor="lightgrey"),
100
  plot_bgcolor="white",
101
  legend=dict(orientation="h", y=-0.2, x=0.5, xanchor="center")
102
  )
103
 
104
- # Static horizontal reference lines
105
  for val in [-5, 0, 5]:
106
- fig.add_hline(y=val, line_width=2 if val==0 else 1, line_dash="dash", line_color="black")
 
 
 
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
 
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
  )
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
  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)