histlearn commited on
Commit
f691fbc
·
verified ·
1 Parent(s): 9048432

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +356 -202
app.py CHANGED
@@ -9,26 +9,40 @@ from fpdf import FPDF
9
  from typing import Tuple, Dict, List
10
  import logging
11
  import warnings
 
 
 
 
12
 
 
13
  warnings.filterwarnings('ignore')
 
14
 
15
  # Configuração de logging
16
  logging.basicConfig(
17
  level=logging.INFO,
18
- format='%(asctime)s - %(levelname)s - %(message)s'
 
 
 
 
19
  )
20
 
21
  class DataProcessor:
22
  @staticmethod
23
  def parse_duration(duration_str: str) -> timedelta:
 
24
  try:
25
- h, m, s = map(int, duration_str.split(':'))
26
- return timedelta(hours=h, minutes=m, seconds=s)
27
- except:
28
- return timedelta(0)
 
 
29
 
30
  @staticmethod
31
  def format_timedelta(td: timedelta) -> str:
 
32
  total_seconds = int(td.total_seconds())
33
  hours, remainder = divmod(total_seconds, 3600)
34
  minutes, seconds = divmod(remainder, 60)
@@ -40,6 +54,7 @@ class DataProcessor:
40
 
41
  @staticmethod
42
  def normalize_html_to_csv(input_html_path: str, output_csv_path: str) -> None:
 
43
  try:
44
  html_data = pd.read_html(input_html_path)
45
  data = html_data[0]
@@ -51,6 +66,7 @@ class DataProcessor:
51
 
52
  @staticmethod
53
  def normalize_excel_to_csv(input_excel_path: str, output_csv_path: str) -> None:
 
54
  try:
55
  excel_data = pd.read_excel(input_excel_path)
56
  unnecessary_columns = [col for col in excel_data.columns if 'Unnamed' in str(col)]
@@ -64,11 +80,13 @@ class DataProcessor:
64
 
65
  class StudentAnalyzer:
66
  def __init__(self, tarefas_df: pd.DataFrame, alunos_df: pd.DataFrame):
 
67
  self.tarefas_df = tarefas_df
68
  self.alunos_df = alunos_df
69
  self.processor = DataProcessor()
70
 
71
  def prepare_data(self) -> pd.DataFrame:
 
72
  self.tarefas_df.columns = self.tarefas_df.columns.str.strip()
73
  self.alunos_df.columns = self.alunos_df.columns.str.strip()
74
 
@@ -80,7 +98,8 @@ class StudentAnalyzer:
80
  return self.match_students()
81
 
82
  def match_students(self) -> pd.DataFrame:
83
- def generate_aluno_pattern(ra, dig_ra):
 
84
  ra_str = str(ra).zfill(9)
85
  return f"{ra_str[1]}{ra_str[2:]}{dig_ra}-sp".lower()
86
 
@@ -88,7 +107,7 @@ class StudentAnalyzer:
88
  lambda row: generate_aluno_pattern(row['RA'], row['Dig. RA']), axis=1
89
  )
90
 
91
- def extract_pattern(nome):
92
  if isinstance(nome, str):
93
  match = re.search(r'\d+.*', nome.lower())
94
  return match.group(0) if match else None
@@ -98,6 +117,7 @@ class StudentAnalyzer:
98
  return self.calculate_metrics()
99
 
100
  def calculate_metrics(self) -> pd.DataFrame:
 
101
  metrics_df = pd.DataFrame()
102
 
103
  for _, aluno in self.alunos_df.iterrows():
@@ -120,12 +140,32 @@ class StudentAnalyzer:
120
  return metrics_df.sort_values('Acertos Absolutos', ascending=False)
121
 
122
  class ReportGenerator:
 
 
123
  def __init__(self, data: pd.DataFrame):
124
  self.data = data
125
  self.stats = self.calculate_statistics()
126
  self.data['Nível'] = self.data['Acertos Absolutos'].apply(self.classify_performance)
127
-
128
- def classify_performance(self, acertos):
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
129
  if acertos >= 10:
130
  return 'Avançado'
131
  elif acertos >= 5:
@@ -134,136 +174,269 @@ class ReportGenerator:
134
  return 'Necessita Atenção'
135
 
136
  def calculate_statistics(self) -> Dict:
137
- basic_stats = {
138
- 'media_acertos': float(self.data['Acertos Absolutos'].mean()),
139
- 'desvio_padrao': float(self.data['Acertos Absolutos'].std()),
140
- 'mediana_acertos': float(self.data['Acertos Absolutos'].median()),
141
- 'total_alunos': len(self.data),
142
- 'media_tarefas': float(self.data['Tarefas Completadas'].mean()),
143
- 'media_tempo': str(pd.to_timedelta(self.data['Total Tempo']).mean())
144
- }
145
-
146
- top_students = self.data.nlargest(3, 'Acertos Absolutos')[
147
- ['Nome do Aluno', 'Acertos Absolutos']
148
- ].values.tolist()
149
- basic_stats['top_performers'] = top_students
150
-
151
- return basic_stats
 
 
 
 
 
152
 
153
- def generate_graphs(self) -> List[plt.Figure]:
154
- graphs = []
155
-
156
- # Configurações globais
157
- plt.rcParams['figure.figsize'] = [15, 10] # Aumentado para acomodar rótulos
158
- plt.rcParams['font.size'] = 10
159
- plt.rcParams['axes.titlesize'] = 12
160
-
161
- # 1. Distribuição por nível
162
- plt.figure()
163
  nivel_counts = self.data['Nível'].value_counts()
164
- colors = {'Avançado': '#2ecc71', 'Intermediário': '#f1c40f', 'Necessita Atenção': '#e74c3c'}
165
-
166
- bars = plt.bar(nivel_counts.index, nivel_counts.values)
 
167
  for i, bar in enumerate(bars):
168
- bar.set_color(colors[nivel_counts.index[i]])
169
- plt.text(bar.get_x() + bar.get_width()/2, bar.get_height(),
170
- str(nivel_counts.values[i]),
171
- ha='center', va='bottom')
172
-
173
- plt.title('Distribuição dos Alunos por Nível de Desempenho')
 
174
  plt.ylabel('Número de Alunos')
175
- graphs.append(plt.gcf())
176
- plt.close()
 
177
 
178
- # 2. Todos os alunos por acertos
179
- plt.figure(figsize=(15, 12))
 
180
  students_data = self.data.sort_values('Acertos Absolutos', ascending=True)
 
 
181
  bars = plt.barh(range(len(students_data)), students_data['Acertos Absolutos'])
182
-
183
- plt.yticks(range(len(students_data)), students_data['Nome do Aluno'], fontsize=8)
184
- plt.title('Ranking Completo - Acertos Absolutos')
185
- plt.xlabel('Número de Acertos')
186
-
 
 
 
187
  for i, bar in enumerate(bars):
188
- plt.text(bar.get_width(), i, f' {bar.get_width():.0f}',
189
- va='center')
190
-
191
- plt.tight_layout()
192
- graphs.append(plt.gcf())
193
- plt.close()
 
 
194
 
195
- # 3. Relação tempo x acertos com rótulos
 
196
  plt.figure(figsize=(15, 10))
197
- for nivel in colors:
 
 
198
  mask = self.data['Nível'] == nivel
199
  tempo = pd.to_timedelta(self.data[mask]['Total Tempo']).dt.total_seconds() / 60
200
-
201
  plt.scatter(tempo, self.data[mask]['Acertos Absolutos'],
202
- c=colors[nivel], label=nivel, alpha=0.6, s=100)
203
-
204
- # Adicionar rótulos para cada ponto
205
  for i, (x, y, nome) in enumerate(zip(tempo,
206
  self.data[mask]['Acertos Absolutos'],
207
  self.data[mask]['Nome do Aluno'])):
208
- plt.annotate(nome, (x, y), xytext=(0, -10),
209
- textcoords='offset points', ha='center',
210
- va='top', rotation=45, fontsize=8)
211
-
212
- plt.title('Relação entre Tempo e Acertos por Nível')
 
 
 
 
 
 
 
 
 
213
  plt.xlabel('Tempo Total (minutos)')
214
  plt.ylabel('Número de Acertos')
215
- plt.legend()
216
- plt.tight_layout()
217
- graphs.append(plt.gcf())
218
- plt.close()
219
 
220
- # 4. Relação Tarefas x Acertos com rótulos e linha de tendência
 
221
  plt.figure(figsize=(15, 10))
222
- plt.scatter(self.data['Tarefas Completadas'], self.data['Acertos Absolutos'],
223
- color='#3498db', alpha=0.6, s=100)
224
-
225
- # Adicionar rótulos para cada ponto
 
 
 
 
 
226
  for i, row in self.data.iterrows():
 
 
 
 
 
 
 
227
  plt.annotate(row['Nome do Aluno'],
228
  (row['Tarefas Completadas'], row['Acertos Absolutos']),
229
- xytext=(0, -10), textcoords='offset points',
230
- ha='center', va='top', rotation=45, fontsize=8)
231
-
 
232
  # Linha de tendência
233
- z = np.polyfit(self.data['Tarefas Completadas'], self.data['Acertos Absolutos'], 1)
 
234
  p = np.poly1d(z)
235
- x_range = np.linspace(self.data['Tarefas Completadas'].min(),
236
- self.data['Tarefas Completadas'].max(), 100)
237
- plt.plot(x_range, p(x_range), "r--", alpha=0.8, label='Tendência')
238
-
239
- plt.title('Relação entre Tarefas Completadas e Acertos')
 
 
240
  plt.xlabel('Número de Tarefas Completadas')
241
  plt.ylabel('Número de Acertos')
242
- plt.legend()
243
- plt.tight_layout()
244
- graphs.append(plt.gcf())
245
- plt.close()
 
 
 
 
 
 
 
 
 
 
 
 
 
246
 
247
- return graphs
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
248
 
249
  def generate_pdf(self, output_path: str, graphs: List[plt.Figure]) -> None:
250
  """Gera relatório em PDF com análise detalhada."""
251
- class PDF(FPDF):
252
- def header(self):
253
- self.set_font('Arial', 'B', 15)
254
- self.set_fill_color(240, 240, 240)
255
- self.cell(0, 15, 'Relatório de Desempenho - Análise Detalhada', 0, 1, 'C', True)
256
- self.ln(10)
257
-
258
- pdf = PDF('L', 'mm', 'A4')
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
259
 
260
- # Introdução
261
- pdf.add_page()
262
  pdf.set_font('Arial', 'B', 14)
263
  pdf.set_fill_color(240, 240, 240)
264
  pdf.cell(0, 10, 'Introdução', 0, 1, 'L', True)
265
  pdf.ln(5)
266
  pdf.set_font('Arial', '', 11)
 
267
  intro_text = """
268
  Este relatório apresenta uma análise abrangente do desempenho dos alunos nas atividades realizadas.
269
  Os dados são analisados considerando três aspectos principais:
@@ -278,9 +451,9 @@ class ReportGenerator:
278
  - Necessita Atenção: Menos de 5 acertos - Requer suporte adicional
279
  """
280
  pdf.multi_cell(0, 7, intro_text)
281
-
282
- # Visão Geral
283
- pdf.add_page()
284
  pdf.set_font('Arial', 'B', 14)
285
  pdf.cell(0, 10, 'Visão Geral da Turma', 0, 1, 'L', True)
286
  pdf.ln(5)
@@ -303,7 +476,8 @@ class ReportGenerator:
303
  """
304
  pdf.multi_cell(0, 7, stats_text)
305
 
306
- # Destaques
 
307
  pdf.ln(5)
308
  pdf.set_font('Arial', 'B', 12)
309
  pdf.cell(0, 10, 'Destaques de Desempenho', 0, 1)
@@ -314,7 +488,8 @@ class ReportGenerator:
314
  for aluno, acertos in self.stats['top_performers']:
315
  pdf.cell(0, 7, f"- {aluno}: {acertos:.0f} acertos", 0, 1)
316
 
317
- # Gráficos e Análises
 
318
  for i, graph in enumerate(graphs):
319
  pdf.add_page()
320
  graph_path = f'temp_graph_{i}.png'
@@ -322,8 +497,7 @@ class ReportGenerator:
322
  pdf.image(graph_path, x=10, y=30, w=270)
323
  os.remove(graph_path)
324
 
325
- # Título e explicação para cada gráfico
326
- pdf.ln(150) # Espaço após o gráfico
327
  pdf.set_font('Arial', 'B', 12)
328
 
329
  if i == 0:
@@ -370,43 +544,16 @@ class ReportGenerator:
370
  - Alunos acima da linha superam a expectativa da turma
371
  """)
372
 
373
- # Detalhamento por Nível
 
374
  for nivel in ['Avançado', 'Intermediário', 'Necessita Atenção']:
375
  alunos_nivel = self.data[self.data['Nível'] == nivel]
376
  if not alunos_nivel.empty:
377
  pdf.add_page()
378
- pdf.set_font('Arial', 'B', 14)
379
- pdf.cell(0, 10, f'Detalhamento - Nível {nivel}', 0, 1, 'L', True)
380
- pdf.ln(5)
381
-
382
- # Tabela
383
- colunas = [
384
- ('Nome do Aluno', 80),
385
- ('Acertos', 25),
386
- ('Tarefas', 25),
387
- ('Tempo Total', 35)
388
- ]
389
-
390
- # Cabeçalho da tabela
391
- pdf.set_font('Arial', 'B', 10)
392
- pdf.set_fill_color(230, 230, 230)
393
- for titulo, largura in colunas:
394
- pdf.cell(largura, 8, titulo, 1, 0, 'C', True)
395
- pdf.ln()
396
 
397
- # Dados
398
- pdf.set_font('Arial', '', 10)
399
- for _, row in alunos_nivel.iterrows():
400
- tempo = pd.to_timedelta(row['Total Tempo'])
401
- tempo_str = f"{int(tempo.total_seconds() // 60)}min {int(tempo.total_seconds() % 60)}s"
402
-
403
- pdf.cell(80, 7, str(row['Nome do Aluno'])[:40], 1)
404
- pdf.cell(25, 7, f"{row['Acertos Absolutos']:.0f}", 1, 0, 'C')
405
- pdf.cell(25, 7, str(row['Tarefas Completadas']), 1, 0, 'C')
406
- pdf.cell(35, 7, tempo_str, 1, 0, 'C')
407
- pdf.ln()
408
-
409
- # Recomendações Finais
410
  pdf.add_page()
411
  pdf.set_font('Arial', 'B', 14)
412
  pdf.cell(0, 10, 'Recomendações e Próximos Passos', 0, 1, 'L', True)
@@ -436,9 +583,7 @@ class ReportGenerator:
436
  """
437
  pdf.multi_cell(0, 7, recom_text)
438
 
439
- pdf.output(output_path)
440
-
441
- def process_files(html_file, excel_files) -> Tuple[str, str, str]:
442
  """Processa arquivos e gera relatório."""
443
  try:
444
  temp_dir = "temp_files"
@@ -453,6 +598,7 @@ def process_files(html_file, excel_files) -> Tuple[str, str, str]:
453
  with open(html_path, "wb") as f:
454
  f.write(html_file)
455
 
 
456
  excel_paths = []
457
  for i, excel_file in enumerate(excel_files):
458
  excel_path = os.path.join(temp_dir, f"tarefa_{i}.xlsx")
@@ -465,6 +611,7 @@ def process_files(html_file, excel_files) -> Tuple[str, str, str]:
465
  alunos_csv_path = os.path.join(temp_dir, "alunos.csv")
466
  processor.normalize_html_to_csv(html_path, alunos_csv_path)
467
 
 
468
  tarefas_df = pd.DataFrame()
469
  for excel_path in excel_paths:
470
  csv_path = excel_path.replace('.xlsx', '.csv')
@@ -472,12 +619,11 @@ def process_files(html_file, excel_files) -> Tuple[str, str, str]:
472
  df = pd.read_csv(csv_path)
473
  tarefas_df = pd.concat([tarefas_df, df], ignore_index=True)
474
 
475
- # Análise
476
  alunos_df = pd.read_csv(alunos_csv_path)
477
  analyzer = StudentAnalyzer(tarefas_df, alunos_df)
478
  results_df = analyzer.prepare_data()
479
 
480
- # Gerar relatório
481
  report_generator = ReportGenerator(results_df)
482
  graphs = report_generator.generate_graphs()
483
 
@@ -493,69 +639,77 @@ def process_files(html_file, excel_files) -> Tuple[str, str, str]:
493
  logging.error(f"Erro no processamento: {str(e)}")
494
  raise
495
 
496
- # Interface Gradio
497
- theme = gr.themes.Default(
498
- primary_hue="blue",
499
- secondary_hue="gray",
500
- font=["Arial", "sans-serif"],
501
- font_mono=["Courier New", "monospace"],
502
- )
503
-
504
- with gr.Blocks(theme=theme) as interface:
505
- gr.Markdown("""
506
- # Sistema de Análise de Desempenho Acadêmico
507
-
508
- Este sistema analisa o desempenho dos alunos e gera um relatório detalhado com:
509
- - Análise estatística completa
510
- - Visualizações gráficas
511
- - Recomendações personalizadas
512
- """)
513
-
514
- with gr.Row():
515
- with gr.Column():
516
- gr.Markdown("## Lista de Alunos")
517
- html_file = gr.File(
518
- label="Arquivo HTML com lista de alunos (.htm)",
519
- type="binary",
520
- file_types=[".htm", ".html"]
521
- )
522
-
523
- with gr.Column():
524
- gr.Markdown("## Relatórios de Tarefas")
525
- excel_files = gr.Files(
526
- label="Arquivos Excel com dados das tarefas (.xlsx)",
527
- type="binary",
528
- file_count="multiple",
529
- file_types=[".xlsx"]
530
- )
531
 
532
- with gr.Row():
533
- generate_btn = gr.Button("Gerar Relatório", variant="primary", size="lg")
 
534
 
535
- with gr.Row():
536
- output_html = gr.HTML()
 
 
 
537
 
538
- with gr.Row():
539
- with gr.Column():
540
- download_html_btn = gr.File(
541
- label="Download Relatório HTML",
542
- type="filepath",
543
- interactive=False
544
- )
545
- with gr.Column():
546
- download_pdf_btn = gr.File(
547
- label="Download Relatório PDF",
548
- type="filepath",
549
- interactive=False
 
 
 
 
 
 
 
 
 
 
 
550
  )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
551
 
552
- generate_btn.click(
553
- fn=process_files,
554
- inputs=[html_file, excel_files],
555
- outputs=[output_html, download_html_btn, download_pdf_btn]
556
- )
557
 
558
  if __name__ == "__main__":
 
559
  interface.launch(
560
  share=False,
561
  server_name="0.0.0.0",
 
9
  from typing import Tuple, Dict, List
10
  import logging
11
  import warnings
12
+ import seaborn as sns
13
+ from matplotlib.colors import LinearSegmentedColormap
14
+ from matplotlib.ticker import MaxNLocator
15
+ from math import ceil
16
 
17
+ # Configura o estilo seaborn e suprime warnings
18
  warnings.filterwarnings('ignore')
19
+ sns.set_style("whitegrid")
20
 
21
  # Configuração de logging
22
  logging.basicConfig(
23
  level=logging.INFO,
24
+ format='%(asctime)s - %(levelname)s - %(message)s',
25
+ handlers=[
26
+ logging.FileHandler("app.log"),
27
+ logging.StreamHandler()
28
+ ]
29
  )
30
 
31
  class DataProcessor:
32
  @staticmethod
33
  def parse_duration(duration_str: str) -> timedelta:
34
+ """Converte string de duração em objeto timedelta."""
35
  try:
36
+ if isinstance(duration_str, str) and ':' in duration_str:
37
+ h, m, s = map(int, duration_str.split(':'))
38
+ return timedelta(hours=h, minutes=m, seconds=s)
39
+ except Exception as e:
40
+ logging.warning(f"Erro ao processar duração '{duration_str}': {str(e)}")
41
+ return timedelta(0)
42
 
43
  @staticmethod
44
  def format_timedelta(td: timedelta) -> str:
45
+ """Formata timedelta em string legível."""
46
  total_seconds = int(td.total_seconds())
47
  hours, remainder = divmod(total_seconds, 3600)
48
  minutes, seconds = divmod(remainder, 60)
 
54
 
55
  @staticmethod
56
  def normalize_html_to_csv(input_html_path: str, output_csv_path: str) -> None:
57
+ """Converte arquivo HTML para CSV."""
58
  try:
59
  html_data = pd.read_html(input_html_path)
60
  data = html_data[0]
 
66
 
67
  @staticmethod
68
  def normalize_excel_to_csv(input_excel_path: str, output_csv_path: str) -> None:
69
+ """Converte arquivo Excel para CSV."""
70
  try:
71
  excel_data = pd.read_excel(input_excel_path)
72
  unnecessary_columns = [col for col in excel_data.columns if 'Unnamed' in str(col)]
 
80
 
81
  class StudentAnalyzer:
82
  def __init__(self, tarefas_df: pd.DataFrame, alunos_df: pd.DataFrame):
83
+ """Inicializa o analisador com DataFrames de tarefas e alunos."""
84
  self.tarefas_df = tarefas_df
85
  self.alunos_df = alunos_df
86
  self.processor = DataProcessor()
87
 
88
  def prepare_data(self) -> pd.DataFrame:
89
+ """Prepara os dados para análise."""
90
  self.tarefas_df.columns = self.tarefas_df.columns.str.strip()
91
  self.alunos_df.columns = self.alunos_df.columns.str.strip()
92
 
 
98
  return self.match_students()
99
 
100
  def match_students(self) -> pd.DataFrame:
101
+ """Realiza o match entre alunos e tarefas."""
102
+ def generate_aluno_pattern(ra: str, dig_ra: str) -> str:
103
  ra_str = str(ra).zfill(9)
104
  return f"{ra_str[1]}{ra_str[2:]}{dig_ra}-sp".lower()
105
 
 
107
  lambda row: generate_aluno_pattern(row['RA'], row['Dig. RA']), axis=1
108
  )
109
 
110
+ def extract_pattern(nome: str) -> str:
111
  if isinstance(nome, str):
112
  match = re.search(r'\d+.*', nome.lower())
113
  return match.group(0) if match else None
 
117
  return self.calculate_metrics()
118
 
119
  def calculate_metrics(self) -> pd.DataFrame:
120
+ """Calcula métricas de desempenho dos alunos."""
121
  metrics_df = pd.DataFrame()
122
 
123
  for _, aluno in self.alunos_df.iterrows():
 
140
  return metrics_df.sort_values('Acertos Absolutos', ascending=False)
141
 
142
  class ReportGenerator:
143
+ """Classe responsável pela geração de relatórios e visualizações."""
144
+
145
  def __init__(self, data: pd.DataFrame):
146
  self.data = data
147
  self.stats = self.calculate_statistics()
148
  self.data['Nível'] = self.data['Acertos Absolutos'].apply(self.classify_performance)
149
+ self.colors = {
150
+ 'Avançado': '#2ecc71',
151
+ 'Intermediário': '#f1c40f',
152
+ 'Necessita Atenção': '#e74c3c'
153
+ }
154
+ self.setup_plot_style()
155
+
156
+ def setup_plot_style(self):
157
+ """Configura o estilo padrão dos gráficos."""
158
+ plt.style.use('seaborn')
159
+ plt.rcParams['figure.figsize'] = [15, 10]
160
+ plt.rcParams['font.size'] = 11
161
+ plt.rcParams['axes.titlesize'] = 14
162
+ plt.rcParams['axes.labelsize'] = 12
163
+ plt.rcParams['axes.grid'] = True
164
+ plt.rcParams['grid.alpha'] = 0.3
165
+ plt.rcParams['grid.linestyle'] = '--'
166
+
167
+ def classify_performance(self, acertos: float) -> str:
168
+ """Classifica o desempenho do aluno baseado no número de acertos."""
169
  if acertos >= 10:
170
  return 'Avançado'
171
  elif acertos >= 5:
 
174
  return 'Necessita Atenção'
175
 
176
  def calculate_statistics(self) -> Dict:
177
+ """Calcula estatísticas básicas do desempenho dos alunos."""
178
+ try:
179
+ basic_stats = {
180
+ 'media_acertos': float(self.data['Acertos Absolutos'].mean()),
181
+ 'desvio_padrao': float(self.data['Acertos Absolutos'].std()),
182
+ 'mediana_acertos': float(self.data['Acertos Absolutos'].median()),
183
+ 'total_alunos': len(self.data),
184
+ 'media_tarefas': float(self.data['Tarefas Completadas'].mean()),
185
+ 'media_tempo': str(pd.to_timedelta(self.data['Total Tempo']).mean())
186
+ }
187
+
188
+ top_students = self.data.nlargest(3, 'Acertos Absolutos')[
189
+ ['Nome do Aluno', 'Acertos Absolutos']
190
+ ].values.tolist()
191
+ basic_stats['top_performers'] = top_students
192
+
193
+ return basic_stats
194
+ except Exception as e:
195
+ logging.error(f"Erro ao calcular estatísticas: {str(e)}")
196
+ raise
197
 
198
+ def create_distribution_plot(self) -> plt.Figure:
199
+ """Cria o gráfico de distribuição por nível."""
200
+ plt.figure(figsize=(15, 8))
 
 
 
 
 
 
 
201
  nivel_counts = self.data['Nível'].value_counts()
202
+ total_alunos = len(self.data)
203
+
204
+ bars = plt.bar(nivel_counts.index, nivel_counts.values, width=0.6)
205
+
206
  for i, bar in enumerate(bars):
207
+ bar.set_color(self.colors[nivel_counts.index[i]])
208
+ percentage = (nivel_counts.values[i] / total_alunos) * 100
209
+ plt.text(bar.get_x() + bar.get_width()/2, bar.get_height(),
210
+ f'{nivel_counts.values[i]}\n({percentage:.1f}%)',
211
+ ha='center', va='bottom', fontsize=12, fontweight='bold')
212
+
213
+ plt.title('Distribuição dos Alunos por Nível de Desempenho', pad=20)
214
  plt.ylabel('Número de Alunos')
215
+ plt.grid(True, axis='y', alpha=0.3)
216
+
217
+ return plt.gcf()
218
 
219
+ def create_ranking_plot(self) -> plt.Figure:
220
+ """Cria o gráfico de ranking completo dos alunos."""
221
+ plt.figure(figsize=(15, max(10, len(self.data) * 0.4)))
222
  students_data = self.data.sort_values('Acertos Absolutos', ascending=True)
223
+
224
+ colors = [self.colors[nivel] for nivel in students_data['Nível']]
225
  bars = plt.barh(range(len(students_data)), students_data['Acertos Absolutos'])
226
+
227
+ for bar, color in zip(bars, colors):
228
+ bar.set_color(color)
229
+ bar.set_alpha(0.8)
230
+
231
+ plt.yticks(range(len(students_data)), students_data['Nome do Aluno'],
232
+ fontsize=10)
233
+
234
  for i, bar in enumerate(bars):
235
+ plt.text(bar.get_width(), i, f' {bar.get_width():.0f}',
236
+ va='center', fontsize=10, fontweight='bold')
237
+
238
+ plt.title('Ranking Completo - Acertos Absolutos', pad=20)
239
+ plt.xlabel('Número de Acertos')
240
+ plt.grid(True, axis='x', alpha=0.3)
241
+
242
+ return plt.gcf()
243
 
244
+ def create_time_performance_plot(self) -> plt.Figure:
245
+ """Cria o gráfico de relação entre tempo e acertos."""
246
  plt.figure(figsize=(15, 10))
247
+
248
+ # Scatter plot com cores por nível
249
+ for nivel, color in self.colors.items():
250
  mask = self.data['Nível'] == nivel
251
  tempo = pd.to_timedelta(self.data[mask]['Total Tempo']).dt.total_seconds() / 60
252
+
253
  plt.scatter(tempo, self.data[mask]['Acertos Absolutos'],
254
+ c=color, label=nivel, alpha=0.7, s=150)
255
+
256
+ # Adiciona rótulos otimizados
257
  for i, (x, y, nome) in enumerate(zip(tempo,
258
  self.data[mask]['Acertos Absolutos'],
259
  self.data[mask]['Nome do Aluno'])):
260
+ if x > np.median(tempo.values):
261
+ ha = 'right'
262
+ offset = (-5, 0)
263
+ else:
264
+ ha = 'left'
265
+ offset = (5, 0)
266
+
267
+ plt.annotate(nome, (x, y),
268
+ xytext=offset, textcoords='offset points',
269
+ ha=ha, va='center', rotation=30, fontsize=8,
270
+ bbox=dict(facecolor='white', edgecolor='none',
271
+ alpha=0.7))
272
+
273
+ plt.title('Relação entre Tempo e Acertos por Nível', pad=20)
274
  plt.xlabel('Tempo Total (minutos)')
275
  plt.ylabel('Número de Acertos')
276
+ plt.legend(bbox_to_anchor=(1.05, 1), loc='upper left')
277
+ plt.grid(True, alpha=0.3)
278
+
279
+ return plt.gcf()
280
 
281
+ def create_tasks_performance_plot(self) -> plt.Figure:
282
+ """Cria o gráfico de relação entre tarefas e acertos."""
283
  plt.figure(figsize=(15, 10))
284
+
285
+ # Scatter plot com cores por nível
286
+ for nivel, color in self.colors.items():
287
+ mask = self.data['Nível'] == nivel
288
+ plt.scatter(self.data[mask]['Tarefas Completadas'],
289
+ self.data[mask]['Acertos Absolutos'],
290
+ c=color, alpha=0.7, s=150, label=nivel)
291
+
292
+ # Adiciona rótulos otimizados
293
  for i, row in self.data.iterrows():
294
+ if row['Tarefas Completadas'] > np.median(self.data['Tarefas Completadas']):
295
+ ha = 'right'
296
+ offset = (-5, 0)
297
+ else:
298
+ ha = 'left'
299
+ offset = (5, 0)
300
+
301
  plt.annotate(row['Nome do Aluno'],
302
  (row['Tarefas Completadas'], row['Acertos Absolutos']),
303
+ xytext=offset, textcoords='offset points',
304
+ ha=ha, va='center', rotation=30, fontsize=8,
305
+ bbox=dict(facecolor='white', edgecolor='none', alpha=0.7))
306
+
307
  # Linha de tendência
308
+ z = np.polyfit(self.data['Tarefas Completadas'],
309
+ self.data['Acertos Absolutos'], 1)
310
  p = np.poly1d(z)
311
+ x_range = np.linspace(self.data['Tarefas Completadas'].min(),
312
+ self.data['Tarefas Completadas'].max(), 100)
313
+
314
+ plt.plot(x_range, p(x_range), "--", color='#e74c3c', alpha=0.8,
315
+ label='Tendência', linewidth=2)
316
+
317
+ plt.title('Relação entre Tarefas Completadas e Acertos', pad=20)
318
  plt.xlabel('Número de Tarefas Completadas')
319
  plt.ylabel('Número de Acertos')
320
+ plt.legend(bbox_to_anchor=(1.05, 1), loc='upper left')
321
+ plt.grid(True, alpha=0.3)
322
+
323
+ return plt.gcf()
324
+
325
+ def generate_graphs(self) -> List[plt.Figure]:
326
+ """Gera todos os gráficos para o relatório."""
327
+ try:
328
+ graphs = []
329
+
330
+ # 1. Distribuição por nível
331
+ graphs.append(self.create_distribution_plot())
332
+ plt.close()
333
+
334
+ # 2. Ranking completo
335
+ graphs.append(self.create_ranking_plot())
336
+ plt.close()
337
 
338
+ # 3. Relação tempo x acertos
339
+ graphs.append(self.create_time_performance_plot())
340
+ plt.close()
341
+
342
+ # 4. Relação tarefas x acertos
343
+ graphs.append(self.create_tasks_performance_plot())
344
+ plt.close()
345
+
346
+ return graphs
347
+ except Exception as e:
348
+ logging.error(f"Erro ao gerar gráficos: {str(e)}")
349
+ raise
350
+
351
+ def generate_table_section(self, pdf: FPDF, nivel: str, alunos_nivel: pd.DataFrame):
352
+ """Gera uma seção de tabela com formatação melhorada."""
353
+ try:
354
+ pdf.set_font('Arial', 'B', 14)
355
+ pdf.set_fill_color(240, 240, 240)
356
+ pdf.cell(0, 10, f'Detalhamento - Nível {nivel}', 0, 1, 'L', True)
357
+ pdf.ln(5)
358
+
359
+ # Configuração da tabela
360
+ colunas = [
361
+ ('Nome do Aluno', 80),
362
+ ('Acertos', 25),
363
+ ('Tarefas', 25),
364
+ ('Tempo Total', 35)
365
+ ]
366
+
367
+ # Cabeçalho
368
+ pdf.set_font('Arial', 'B', 10)
369
+ pdf.set_fill_color(230, 230, 230)
370
+ for titulo, largura in colunas:
371
+ pdf.cell(largura, 8, titulo, 1, 0, 'C', True)
372
+ pdf.ln()
373
+
374
+ # Dados com cores alternadas
375
+ pdf.set_font('Arial', '', 10)
376
+ for i, (_, row) in enumerate(alunos_nivel.iterrows()):
377
+ # Cor de fundo alternada
378
+ fill_color = (245, 245, 245) if i % 2 == 0 else (255, 255, 255)
379
+ pdf.set_fill_color(*fill_color)
380
+
381
+ tempo = pd.to_timedelta(row['Total Tempo'])
382
+ tempo_str = f"{int(tempo.total_seconds() // 60)}min {int(tempo.total_seconds() % 60)}s"
383
+
384
+ pdf.cell(80, 7, str(row['Nome do Aluno'])[:40], 1, 0, 'L', True)
385
+ pdf.cell(25, 7, f"{row['Acertos Absolutos']:.0f}", 1, 0, 'R', True)
386
+ pdf.cell(25, 7, str(row['Tarefas Completadas']), 1, 0, 'R', True)
387
+ pdf.cell(35, 7, tempo_str, 1, 0, 'R', True)
388
+ pdf.ln()
389
+ except Exception as e:
390
+ logging.error(f"Erro ao gerar seção de tabela: {str(e)}")
391
+ raise
392
 
393
  def generate_pdf(self, output_path: str, graphs: List[plt.Figure]) -> None:
394
  """Gera relatório em PDF com análise detalhada."""
395
+ try:
396
+ class PDF(FPDF):
397
+ def header(self):
398
+ """Define o cabeçalho padrão do PDF."""
399
+ self.set_font('Arial', 'B', 15)
400
+ self.set_fill_color(240, 240, 240)
401
+ self.cell(0, 15, 'Relatório de Desempenho - Análise Detalhada', 0, 1, 'C', True)
402
+ self.ln(10)
403
+
404
+ pdf = PDF('L', 'mm', 'A4')
405
+
406
+ # Introdução
407
+ pdf.add_page()
408
+ self._add_introduction_section(pdf)
409
+
410
+ # Visão Geral
411
+ pdf.add_page()
412
+ self._add_overview_section(pdf)
413
+
414
+ # Destaques
415
+ self._add_highlights_section(pdf)
416
+
417
+ # Gráficos e Análises
418
+ self._add_graphs_section(pdf, graphs)
419
+
420
+ # Detalhamento por Nível
421
+ self._add_detailed_sections(pdf)
422
+
423
+ # Recomendações Finais
424
+ self._add_recommendations_section(pdf)
425
+
426
+ pdf.output(output_path)
427
+
428
+ except Exception as e:
429
+ logging.error(f"Erro ao gerar PDF: {str(e)}")
430
+ raise
431
 
432
+ def _add_introduction_section(self, pdf: FPDF) -> None:
433
+ """Adiciona a seção de introdução ao PDF."""
434
  pdf.set_font('Arial', 'B', 14)
435
  pdf.set_fill_color(240, 240, 240)
436
  pdf.cell(0, 10, 'Introdução', 0, 1, 'L', True)
437
  pdf.ln(5)
438
  pdf.set_font('Arial', '', 11)
439
+
440
  intro_text = """
441
  Este relatório apresenta uma análise abrangente do desempenho dos alunos nas atividades realizadas.
442
  Os dados são analisados considerando três aspectos principais:
 
451
  - Necessita Atenção: Menos de 5 acertos - Requer suporte adicional
452
  """
453
  pdf.multi_cell(0, 7, intro_text)
454
+
455
+ def _add_overview_section(self, pdf: FPDF) -> None:
456
+ """Adiciona a seção de visão geral ao PDF."""
457
  pdf.set_font('Arial', 'B', 14)
458
  pdf.cell(0, 10, 'Visão Geral da Turma', 0, 1, 'L', True)
459
  pdf.ln(5)
 
476
  """
477
  pdf.multi_cell(0, 7, stats_text)
478
 
479
+ def _add_highlights_section(self, pdf: FPDF) -> None:
480
+ """Adiciona a seção de destaques ao PDF."""
481
  pdf.ln(5)
482
  pdf.set_font('Arial', 'B', 12)
483
  pdf.cell(0, 10, 'Destaques de Desempenho', 0, 1)
 
488
  for aluno, acertos in self.stats['top_performers']:
489
  pdf.cell(0, 7, f"- {aluno}: {acertos:.0f} acertos", 0, 1)
490
 
491
+ def _add_graphs_section(self, pdf: FPDF, graphs: List[plt.Figure]) -> None:
492
+ """Adiciona a seção de gráficos ao PDF."""
493
  for i, graph in enumerate(graphs):
494
  pdf.add_page()
495
  graph_path = f'temp_graph_{i}.png'
 
497
  pdf.image(graph_path, x=10, y=30, w=270)
498
  os.remove(graph_path)
499
 
500
+ pdf.ln(150)
 
501
  pdf.set_font('Arial', 'B', 12)
502
 
503
  if i == 0:
 
544
  - Alunos acima da linha superam a expectativa da turma
545
  """)
546
 
547
+ def _add_detailed_sections(self, pdf: FPDF) -> None:
548
+ """Adiciona as seções detalhadas por nível ao PDF."""
549
  for nivel in ['Avançado', 'Intermediário', 'Necessita Atenção']:
550
  alunos_nivel = self.data[self.data['Nível'] == nivel]
551
  if not alunos_nivel.empty:
552
  pdf.add_page()
553
+ self.generate_table_section(pdf, nivel, alunos_nivel)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
554
 
555
+ def _add_recommendations_section(self, pdf: FPDF) -> None:
556
+ """Adiciona a seção de recomendações ao PDF."""
 
 
 
 
 
 
 
 
 
 
 
557
  pdf.add_page()
558
  pdf.set_font('Arial', 'B', 14)
559
  pdf.cell(0, 10, 'Recomendações e Próximos Passos', 0, 1, 'L', True)
 
583
  """
584
  pdf.multi_cell(0, 7, recom_text)
585
 
586
+ def process_files(html_file, excel_files) -> Tuple[str, str, str]:
 
 
587
  """Processa arquivos e gera relatório."""
588
  try:
589
  temp_dir = "temp_files"
 
598
  with open(html_path, "wb") as f:
599
  f.write(html_file)
600
 
601
+ # Processar arquivos Excel
602
  excel_paths = []
603
  for i, excel_file in enumerate(excel_files):
604
  excel_path = os.path.join(temp_dir, f"tarefa_{i}.xlsx")
 
611
  alunos_csv_path = os.path.join(temp_dir, "alunos.csv")
612
  processor.normalize_html_to_csv(html_path, alunos_csv_path)
613
 
614
+ # Concatenar dados das tarefas
615
  tarefas_df = pd.DataFrame()
616
  for excel_path in excel_paths:
617
  csv_path = excel_path.replace('.xlsx', '.csv')
 
619
  df = pd.read_csv(csv_path)
620
  tarefas_df = pd.concat([tarefas_df, df], ignore_index=True)
621
 
622
+ # Análise e geração de relatório
623
  alunos_df = pd.read_csv(alunos_csv_path)
624
  analyzer = StudentAnalyzer(tarefas_df, alunos_df)
625
  results_df = analyzer.prepare_data()
626
 
 
627
  report_generator = ReportGenerator(results_df)
628
  graphs = report_generator.generate_graphs()
629
 
 
639
  logging.error(f"Erro no processamento: {str(e)}")
640
  raise
641
 
642
+ def create_interface():
643
+ """Cria a interface Gradio."""
644
+ theme = gr.themes.Default(
645
+ primary_hue="blue",
646
+ secondary_hue="gray",
647
+ font=["Arial", "sans-serif"],
648
+ font_mono=["Courier New", "monospace"],
649
+ )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
650
 
651
+ with gr.Blocks(theme=theme) as interface:
652
+ gr.Markdown("""
653
+ # Sistema de Análise de Desempenho Acadêmico
654
 
655
+ Este sistema analisa o desempenho dos alunos e gera um relatório detalhado com:
656
+ - Análise estatística completa
657
+ - Visualizações gráficas
658
+ - Recomendações personalizadas
659
+ """)
660
 
661
+ with gr.Row():
662
+ with gr.Column():
663
+ gr.Markdown("## Lista de Alunos")
664
+ html_file = gr.File(
665
+ label="Arquivo HTML com lista de alunos (.htm)",
666
+ type="binary",
667
+ file_types=[".htm", ".html"]
668
+ )
669
+
670
+ with gr.Column():
671
+ gr.Markdown("## Relatórios de Tarefas")
672
+ excel_files = gr.Files(
673
+ label="Arquivos Excel com dados das tarefas (.xlsx)",
674
+ type="binary",
675
+ file_count="multiple",
676
+ file_types=[".xlsx"]
677
+ )
678
+
679
+ with gr.Row():
680
+ generate_btn = gr.Button(
681
+ "Gerar Relatório",
682
+ variant="primary",
683
+ size="lg"
684
  )
685
+
686
+ with gr.Row():
687
+ output_html = gr.HTML()
688
+
689
+ with gr.Row():
690
+ with gr.Column():
691
+ download_html_btn = gr.File(
692
+ label="Download Relatório HTML",
693
+ type="filepath",
694
+ interactive=False
695
+ )
696
+ with gr.Column():
697
+ download_pdf_btn = gr.File(
698
+ label="Download Relatório PDF",
699
+ type="filepath",
700
+ interactive=False
701
+ )
702
+
703
+ generate_btn.click(
704
+ fn=process_files,
705
+ inputs=[html_file, excel_files],
706
+ outputs=[output_html, download_html_btn, download_pdf_btn]
707
+ )
708
 
709
+ return interface
 
 
 
 
710
 
711
  if __name__ == "__main__":
712
+ interface = create_interface()
713
  interface.launch(
714
  share=False,
715
  server_name="0.0.0.0",