GuglielmoTor commited on
Commit
0dd5951
·
verified ·
1 Parent(s): 379a2b6

Create home_tab_module.py

Browse files
Files changed (1) hide show
  1. services/home_tab_module.py +407 -0
services/home_tab_module.py ADDED
@@ -0,0 +1,407 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # services/home_tab_module.py
2
+
3
+ import gradio as gr
4
+ import pandas as pd
5
+ import logging
6
+ import matplotlib.pyplot as plt
7
+ import numpy as np
8
+ import html
9
+ import ast
10
+ from datetime import datetime, timedelta
11
+
12
+ # Import the data filtering function
13
+ from data_processing.analytics_data_processing import prepare_filtered_analytics_data
14
+ # Import the theme-aware styler for our donut chart
15
+ from ui.analytics_plot_generator import _apply_theme_aware_styling, create_placeholder_plot
16
+
17
+ logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(module)s - %(message)s')
18
+
19
+ def _parse_eb_label(label_data):
20
+ if isinstance(label_data, list): return label_data
21
+ if isinstance(label_data, str):
22
+ try:
23
+ parsed = ast.literal_eval(label_data)
24
+ return parsed if isinstance(parsed, list) else [str(parsed)]
25
+ except (ValueError, SyntaxError):
26
+ return [label_data.strip()] if label_data.strip() else []
27
+ return [] if pd.isna(label_data) else [str(label_data)]
28
+
29
+
30
+ def _format_kpi_list(data_series: pd.Series, unit="ER") -> str:
31
+ """
32
+ Formats a pandas Series into a clean HTML list for the Analisi Strategica.
33
+ """
34
+ if data_series is None or data_series.empty:
35
+ return "<div class='kpi-list-item'>Nessun dato disponibile.</div>"
36
+
37
+ html_items = ""
38
+ for index, value in data_series.items():
39
+ html_items += f"""
40
+ <div class='kpi-list-item'>
41
+ <span class='kpi-list-label'>{html.escape(str(index))}</span>
42
+ <span class='kpi-list-value'>{unit}: <strong>{value:.1f}%</strong></span>
43
+ </div>
44
+ """
45
+ return html_items
46
+
47
+ def _calculate_brand_sentiment(posts_df: pd.DataFrame, mentions_df: pd.DataFrame, comments_df: pd.DataFrame) -> float:
48
+ """
49
+ FIX KPI: Calcola sentiment medio tra commenti e menzioni (posts_df non usato più)
50
+ """
51
+ try:
52
+ sentiment_map = {
53
+ 'Positive 👍': 1, 'Neutral 😐': 0, 'Negative 👎': -1,
54
+ 'Positive': 1, 'Neutral': 0, 'Negative': -1
55
+ }
56
+
57
+ # FIX: Usa comments_df invece di posts_df
58
+ total_comments = 0
59
+ comment_score = 0
60
+
61
+ if not comments_df.empty:
62
+ sentiment_col = 'sentiment_label' if 'sentiment_label' in comments_df.columns else 'sentiment'
63
+ if sentiment_col in comments_df.columns:
64
+ comment_sentiments = comments_df[sentiment_col].value_counts()
65
+ total_comments = comment_sentiments.sum()
66
+ comment_score = sum(
67
+ comment_sentiments.get(key, 0) * sentiment_map.get(key, 0)
68
+ for key in sentiment_map
69
+ )
70
+ logging.info(f"Comment sentiment - Total: {total_comments}, Score: {comment_score}")
71
+
72
+ # Mentions (invariato)
73
+ total_mentions = 0
74
+ mention_score = 0
75
+
76
+ if not mentions_df.empty and 'sentiment_label' in mentions_df.columns:
77
+ mention_sentiments = mentions_df['sentiment_label'].value_counts()
78
+ total_mentions = mention_sentiments.sum()
79
+ mention_score = sum(
80
+ mention_sentiments.get(key, 0) * sentiment_map.get(key, 0)
81
+ for key in sentiment_map
82
+ )
83
+ logging.info(f"Mention sentiment - Total: {total_mentions}, Score: {mention_score}")
84
+
85
+ total_volume = total_comments + total_mentions
86
+ if total_volume == 0:
87
+ logging.warning("No sentiment data available")
88
+ return None
89
+
90
+ total_score = comment_score + mention_score
91
+ avg_score = total_score / total_volume
92
+ sentiment_percentage = (avg_score + 1) / 2 * 100
93
+
94
+ logging.info(f"Final brand sentiment: {sentiment_percentage:.1f}%")
95
+ return sentiment_percentage
96
+
97
+ except Exception as e:
98
+ logging.error(f"Error calculating brand sentiment: {e}", exc_info=True)
99
+ return None
100
+
101
+ import plotly.graph_objects as go
102
+
103
+ def generate_home_engagement_plotly(df):
104
+ df_copy = df.copy()
105
+ df_copy['date'] = pd.to_datetime(df_copy['published_at'])
106
+ df_copy['engagement'] = pd.to_numeric(df_copy['engagement'], errors='coerce')
107
+ df_copy = df_copy.dropna().sort_values('date')
108
+
109
+ fig = go.Figure()
110
+ fig.add_trace(go.Scatter(
111
+ x=df_copy['date'],
112
+ y=df_copy['engagement'],
113
+ mode='lines+markers',
114
+ name='Engagement Rate',
115
+ line=dict(color='#F472B6', width=2),
116
+ marker=dict(size=6)
117
+ ))
118
+
119
+ fig.update_layout(
120
+ #title='Performance Contenuti (Engagement Rate)',
121
+ xaxis_title='Date',
122
+ yaxis_title='Engagement Rate (%)',
123
+ yaxis=dict(range=[0, None]),
124
+ template='plotly_white',
125
+ height=325
126
+ )
127
+
128
+ return fig
129
+
130
+
131
+ def refresh_home_tab_ui(token_state_value, date_filter_option, custom_start_date, custom_end_date):
132
+ """
133
+ Fetches all data, calculates KPIs, and returns updates for the home tab.
134
+
135
+ Returns 7 values:
136
+ 1. New Followers (markdown)
137
+ 2. Growth Rate (markdown)
138
+ 3. Sentiment Chart (plot)
139
+ 4. ER Plot Data (lineplot data)
140
+ 5. Topics HTML (html)
141
+ 6. Formats HTML (html)
142
+ 7. Combined Follower Persona (html)
143
+ """
144
+ logging.info(f"Refreshing Home Tab. Filter: {date_filter_option}")
145
+
146
+ # Get Filtered Data
147
+ try:
148
+ # MODIFICATO: Unpacking include comments_df
149
+ (filtered_merged_posts_df,
150
+ filtered_mentions_df,
151
+ filtered_comments_df, # AGGIUNTO
152
+ date_filtered_follower_stats_df,
153
+ raw_follower_stats_df,
154
+ _, _) = prepare_filtered_analytics_data(
155
+ token_state_value, date_filter_option, custom_start_date, custom_end_date
156
+ )
157
+
158
+ logging.info(f"Data loaded - Posts: {len(filtered_merged_posts_df)}, Mentions: {len(filtered_mentions_df)}")
159
+
160
+ except Exception as e:
161
+ logging.error(f"Error during data preparation: {e}", exc_info=True)
162
+ placeholder_fig = create_placeholder_plot("Data Error")
163
+ placeholder_text = gr.update(value="<div class='kpi-value-error'>Error</div>")
164
+ placeholder_plot_data = pd.DataFrame(columns=['date', 'engagement', 'label'])
165
+ placeholder_persona = gr.update(value="""
166
+ <div class='persona-card'>
167
+ <div class='persona-avatar'>👤</div>
168
+ <div class='persona-details'>
169
+ <div class='persona-title'>Target Follower Persona</div>
170
+ <div class='persona-item'><span class='persona-label'>Error:</span><span class='persona-value'>Data unavailable</span></div>
171
+ </div>
172
+ </div>
173
+ """)
174
+
175
+ return (
176
+ placeholder_text, placeholder_text, gr.update(value=placeholder_fig),
177
+ placeholder_plot_data, placeholder_text, placeholder_text, placeholder_persona
178
+ )
179
+
180
+ # KPI 1: Nuovi Follower
181
+ try:
182
+ gains_df = date_filtered_follower_stats_df[
183
+ date_filtered_follower_stats_df['follower_count_type'] == 'follower_gains_monthly'
184
+ ]
185
+ total_new_followers = pd.to_numeric(gains_df['follower_count_organic'], errors='coerce').sum() + \
186
+ pd.to_numeric(gains_df['follower_count_paid'], errors='coerce').sum()
187
+ kpi_new_followers_update = gr.update(value=f"<div class='kpi-value'>{int(total_new_followers)}</div>")
188
+ logging.info(f"New followers: {int(total_new_followers)}")
189
+ except Exception as e:
190
+ total_new_followers = 0
191
+ logging.error(f"Error calculating new followers: {e}")
192
+ kpi_new_followers_update = gr.update(value="<div class='kpi-value-error'>N/A</div>")
193
+
194
+ # KPI 2: Growth Rate
195
+ try:
196
+ geo_df = raw_follower_stats_df[
197
+ raw_follower_stats_df['follower_count_type'] == 'follower_geo'
198
+ ]
199
+
200
+ if geo_df.empty:
201
+ end_count = 0
202
+ else:
203
+ end_count = pd.to_numeric(geo_df['follower_count_organic'], errors='coerce').sum() + \
204
+ pd.to_numeric(geo_df['follower_count_paid'], errors='coerce').sum()
205
+
206
+ gains_in_period = total_new_followers
207
+ start_count = end_count - gains_in_period
208
+
209
+ if start_count > 0:
210
+ growth_rate = (gains_in_period / start_count) * 100
211
+ color = 'green' if growth_rate >= 0 else 'red'
212
+ sign = '+' if growth_rate >= 0 else ''
213
+ kpi_growth_update = gr.update(
214
+ value=f"<div class='kpi-value'>{growth_rate:.1f}%</div><div class='kpi-change' style='color:{color};'>{sign}{growth_rate:.1f}%</div>"
215
+ )
216
+ elif end_count > 0:
217
+ kpi_growth_update = gr.update(value=f"<div class='kpi-value' style='color:green;'>+100%</div><div class='kpi-change'>Nuovo</div>")
218
+ else:
219
+ kpi_growth_update = gr.update(value="<div class='kpi-value-error'>N/A</div>")
220
+
221
+ except Exception as e:
222
+ logging.error(f"Error calculating growth rate: {e}")
223
+ kpi_growth_update = gr.update(value="<div class='kpi-value-error'>N/A</div>")
224
+
225
+ # KPI 3: Brand Sentiment
226
+ try:
227
+ sentiment_percentage = _calculate_brand_sentiment(filtered_merged_posts_df, filtered_mentions_df, filtered_comments_df)
228
+ donut_fig = _create_small_donut_chart(sentiment_percentage, "Positivo")
229
+ kpi_sentiment_update = gr.update(value=donut_fig)
230
+ except Exception as e:
231
+ logging.error(f"Donut chart error: {e}")
232
+ kpi_sentiment_update = gr.update(value=create_placeholder_plot("Sentiment Error"))
233
+
234
+ # KPI 4: Engagement Rate Plot - FIXED VERSION
235
+ try:
236
+ er_plot_data = generate_home_engagement_plotly(
237
+ filtered_merged_posts_df
238
+ )
239
+
240
+ kpi_er_plot_update = gr.update(value=er_plot_data)
241
+ logging.info(f"Engagement plot updated with {len(filtered_merged_posts_df)} data points")
242
+
243
+ except Exception as e:
244
+ logging.error(f"Error creating ER plot data: {e}", exc_info=True)
245
+ kpi_er_plot_update = gr.update(value=pd.DataFrame(columns=['date', 'engagement', 'label']))
246
+
247
+ # KPI 5 & 6: Topics & Formats
248
+ try:
249
+ topics_col = 'li_eb_label'
250
+ if not filtered_merged_posts_df.empty and topics_col in filtered_merged_posts_df.columns:
251
+ topics_df = filtered_merged_posts_df.copy()
252
+ topics_df[topics_col] = topics_df[topics_col].apply(_parse_eb_label)
253
+ topics_exploded = topics_df.explode(topics_col)
254
+ topics_exploded = topics_exploded[topics_exploded[topics_col].notna() & (topics_exploded[topics_col] != '')]
255
+
256
+ if not topics_exploded.empty:
257
+ topics_er = topics_exploded.groupby(topics_col)['engagement'].mean().nlargest(4)
258
+ kpi_topics_update = gr.update(value=_format_kpi_list(topics_er, unit="ER"))
259
+ else:
260
+ kpi_topics_update = gr.update(value="<div class='kpi-list-item'>Nessun dato disponibile</div>")
261
+ else:
262
+ kpi_topics_update = gr.update(value="<div class='kpi-list-item'>N/A</div>")
263
+
264
+ formats_col = 'media_type'
265
+ if not filtered_merged_posts_df.empty and formats_col in filtered_merged_posts_df.columns:
266
+ formats_df = filtered_merged_posts_df.copy()
267
+ formats_df = formats_df[formats_df[formats_col].notna() & (formats_df[formats_col] != '')]
268
+
269
+ if not formats_df.empty:
270
+ formats_er = formats_df.groupby(formats_col)['engagement'].mean().nlargest(4)
271
+ kpi_formats_update = gr.update(value=_format_kpi_list(formats_er, unit="ER"))
272
+ else:
273
+ kpi_formats_update = gr.update(value="<div class='kpi-list-item'>Nessun dato disponibile</div>")
274
+ else:
275
+ kpi_formats_update = gr.update(value="<div class='kpi-list-item'>N/A</div>")
276
+
277
+ except Exception as e:
278
+ logging.error(f"Error in Analisi Strategica: {e}")
279
+ kpi_topics_update = gr.update(value="<div class='kpi-list-item'>Error</div>")
280
+ kpi_formats_update = gr.update(value="<div class='kpi-list-item'>Error</div>")
281
+
282
+ # KPI 7: Follower Persona
283
+ def get_top_demo(df, demo_type):
284
+ try:
285
+ if df.empty: return None
286
+ demo_df = df[df['follower_count_type'] == demo_type].copy()
287
+ if demo_df.empty: return None
288
+
289
+ demo_df['total_follower_count'] = pd.to_numeric(demo_df['follower_count_organic'], errors='coerce').fillna(0) + \
290
+ pd.to_numeric(demo_df['follower_count_paid'], errors='coerce').fillna(0)
291
+ return demo_df.groupby('category_name')['total_follower_count'].sum().idxmax()
292
+ except Exception as e:
293
+ logging.error(f"Error getting top demo for {demo_type}: {e}")
294
+ return None
295
+
296
+ try:
297
+ top_role = get_top_demo(raw_follower_stats_df, 'follower_function')
298
+ top_industry = get_top_demo(raw_follower_stats_df, 'follower_industry')
299
+ top_seniority = get_top_demo(raw_follower_stats_df, 'follower_seniority')
300
+ top_country = get_top_demo(raw_follower_stats_df, 'follower_geo')
301
+
302
+ role_display = html.escape(str(top_role)) if top_role else "N/A"
303
+ industry_display = html.escape(str(top_industry)) if top_industry else "N/A"
304
+ seniority_display = html.escape(str(top_seniority)) if top_seniority else "N/A"
305
+ country_display = html.escape(str(top_country)) if top_country else "N/A"
306
+
307
+ persona_html = f"""
308
+ <div class='persona-card'>
309
+ <div class='persona-avatar'>👤</div>
310
+ <div class='persona-details'>
311
+ <div class='persona-item'>
312
+ <span class='persona-label'>Role:</span>
313
+ <span class='persona-value'>{role_display}</span>
314
+ </div>
315
+ <div class='persona-item'>
316
+ <span class='persona-label'>Industry:</span>
317
+ <span class='persona-value'>{industry_display}</span>
318
+ </div>
319
+ <div class='persona-item'>
320
+ <span class='persona-label'>Seniority:</span>
321
+ <span class='persona-value'>{seniority_display}</span>
322
+ </div>
323
+ <div class='persona-item'>
324
+ <span class='persona-label'>Country:</span>
325
+ <span class='persona-value'>{country_display}</span>
326
+ </div>
327
+ </div>
328
+ </div>
329
+ """
330
+
331
+ kpi_persona_update = gr.update(value=persona_html)
332
+
333
+ except Exception as e:
334
+ logging.error(f"Error creating follower persona: {e}")
335
+ kpi_persona_update = gr.update(value="""
336
+ <div class='persona-card'>
337
+ <div class='persona-avatar'>👤</div>
338
+ <div class='persona-details'>
339
+ <div class='persona-title'>Target Follower Persona</div>
340
+ <div class='persona-item'><span class='persona-label'>Error:</span><span class='persona-value'>Data unavailable</span></div>
341
+ </div>
342
+ </div>
343
+ """)
344
+
345
+ return (
346
+ kpi_new_followers_update,
347
+ kpi_growth_update,
348
+ kpi_sentiment_update,
349
+ kpi_er_plot_update,
350
+ kpi_topics_update,
351
+ kpi_formats_update,
352
+ kpi_persona_update
353
+ )
354
+
355
+
356
+ def _create_small_donut_chart(percentage: float, title: str):
357
+ """
358
+ Creates a smaller theme-aware Matplotlib donut chart for the KPI row.
359
+ """
360
+ try:
361
+ fig, ax = plt.subplots(figsize=(2.5, 2.5))
362
+ _apply_theme_aware_styling(fig, ax, is_pie=True)
363
+
364
+ if percentage is None or pd.isna(percentage) or not 0 <= percentage <= 100:
365
+ percentage = 0
366
+ title = "No Data"
367
+
368
+ percentage_value = percentage / 100.0
369
+ remaining = 1.0 - percentage_value
370
+
371
+ PRIMARY_COLOR = plt.rcParams.get('axes.prop_cycle').by_key()['color'][0]
372
+ GRID_COLOR = plt.rcParams.get('grid.color', '#4B5563')
373
+ TEXT_COLOR = plt.rcParams.get('text.color', '#E5E7EB')
374
+ BG_COLOR = plt.rcParams.get('figure.facecolor', '#111827')
375
+
376
+ data = [percentage_value, remaining]
377
+ colors = [PRIMARY_COLOR, GRID_COLOR]
378
+
379
+ wedges, _ = ax.pie(
380
+ data,
381
+ colors=colors,
382
+ startangle=90,
383
+ counterclock=False,
384
+ wedgeprops=dict(width=0.3, edgecolor=BG_COLOR, linewidth=1.5)
385
+ )
386
+
387
+ ax.text(
388
+ 0, 0, f"{percentage:.0f}%",
389
+ ha='center', va='center',
390
+ fontsize=18, fontweight='bold',
391
+ color=TEXT_COLOR
392
+ )
393
+ ax.text(
394
+ 0, -0.4, title,
395
+ ha='center', va='center',
396
+ fontsize=8,
397
+ color=TEXT_COLOR,
398
+ alpha=0.8
399
+ )
400
+
401
+ ax.axis('equal')
402
+ fig.tight_layout()
403
+ return fig
404
+
405
+ except Exception as e:
406
+ logging.error(f"Error creating small donut chart: {e}", exc_info=True)
407
+ return create_placeholder_plot(title="Chart Error", message=str(e))