histlearn commited on
Commit
a44adb0
·
verified ·
1 Parent(s): 6b9ba8d

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +564 -0
app.py CHANGED
@@ -1,3 +1,567 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  pdf.cell(0, 7, f'- {disc}:', 0, new_x=XPos.LMARGIN, new_y=YPos.NEXT, align='L')
2
  pdf.set_font('Helvetica', '', 10)
3
  for aviso in avisos:
 
1
+ import gradio as gr
2
+ import camelot
3
+ import pandas as pd
4
+ import matplotlib.pyplot as plt
5
+ import numpy as np
6
+ from fpdf import FPDF
7
+ from fpdf.enums import XPos, YPos
8
+ import tempfile
9
+ import os
10
+ import matplotlib
11
+ import shutil
12
+ import colorsys
13
+ from datetime import datetime
14
+ matplotlib.use('Agg')
15
+
16
+ # Configurações globais
17
+ ESCALA_MAXIMA_NOTAS = 12
18
+ LIMITE_APROVACAO_NOTA = 5
19
+ LIMITE_APROVACAO_FREQ = 75
20
+ BIMESTRES = ['1º Bimestre', '2º Bimestre', '3º Bimestre', '4º Bimestre']
21
+ CONCEITOS_VALIDOS = ['ES', 'EP', 'ET']
22
+
23
+ # Definição das disciplinas de formação básica
24
+ FORMACAO_BASICA = {
25
+ 'fundamental': {
26
+ 'LINGUA PORTUGUESA',
27
+ 'MATEMATICA',
28
+ 'HISTORIA',
29
+ 'GEOGRAFIA',
30
+ 'CIENCIAS',
31
+ 'LINGUA ESTRANGEIRA INGLES',
32
+ 'ARTE',
33
+ 'EDUCACAO FISICA'
34
+ },
35
+ 'medio': {
36
+ 'LINGUA PORTUGUESA',
37
+ 'MATEMATICA',
38
+ 'HISTORIA',
39
+ 'GEOGRAFIA',
40
+ 'BIOLOGIA',
41
+ 'FISICA',
42
+ 'QUIMICA',
43
+ 'INGLÊS',
44
+ 'FILOSOFIA',
45
+ 'SOCIOLOGIA',
46
+ 'ARTE',
47
+ 'EDUCACAO FISICA'
48
+ }
49
+ }
50
+
51
+ def detectar_nivel_ensino(disciplinas):
52
+ """Detecta se é ensino fundamental ou médio baseado nas disciplinas presentes."""
53
+ disciplinas_set = set(disciplinas)
54
+ disciplinas_exclusivas_medio = {'BIOLOGIA', 'FISICA', 'QUIMICA', 'FILOSOFIA', 'SOCIOLOGIA'}
55
+ return 'medio' if any(d in disciplinas_set for d in disciplinas_exclusivas_medio) else 'fundamental'
56
+
57
+ def separar_disciplinas_por_categoria(disciplinas_dados):
58
+ """Separa as disciplinas em formação básica e diversificada."""
59
+ disciplinas = [d['disciplina'] for d in disciplinas_dados]
60
+ nivel = detectar_nivel_ensino(disciplinas)
61
+
62
+ formacao_basica = []
63
+ diversificada = []
64
+
65
+ for disc_data in disciplinas_dados:
66
+ if disc_data['disciplina'] in FORMACAO_BASICA[nivel]:
67
+ formacao_basica.append(disc_data)
68
+ else:
69
+ diversificada.append(disc_data)
70
+
71
+ return {
72
+ 'nivel': nivel,
73
+ 'formacao_basica': formacao_basica,
74
+ 'diversificada': diversificada
75
+ }
76
+
77
+ def converter_nota(valor):
78
+ """Converte valor de nota para float, tratando casos especiais e conceitos."""
79
+ if pd.isna(valor) or valor == '-' or valor == 'N' or valor == '' or valor == 'None':
80
+ return None
81
+
82
+ if isinstance(valor, str):
83
+ valor_limpo = valor.strip().upper()
84
+ if valor_limpo in CONCEITOS_VALIDOS:
85
+ conceitos_map = {'ET': 10, 'ES': 8, 'EP': 6}
86
+ return conceitos_map.get(valor_limpo)
87
+
88
+ try:
89
+ return float(valor_limpo.replace(',', '.'))
90
+ except:
91
+ return None
92
+
93
+ if isinstance(valor, (int, float)):
94
+ return float(valor)
95
+
96
+ return None
97
+
98
+ def calcular_media_bimestres(notas):
99
+ """Calcula média considerando apenas bimestres com notas válidas."""
100
+ notas_validas = [nota for nota in notas if nota is not None]
101
+ if not notas_validas:
102
+ return 0
103
+ return sum(notas_validas) / len(notas_validas)
104
+
105
+ def calcular_frequencia_media(frequencias):
106
+ """Calcula média de frequência considerando apenas bimestres cursados."""
107
+ freq_validas = []
108
+ for freq in frequencias:
109
+ try:
110
+ if isinstance(freq, str):
111
+ freq = freq.strip().replace('%', '').replace(',', '.')
112
+ if freq and freq != '-':
113
+ valor = float(freq)
114
+ if valor > 0:
115
+ freq_validas.append(valor)
116
+ except:
117
+ continue
118
+
119
+ if not freq_validas:
120
+ return 0
121
+ return sum(freq_validas) / len(freq_validas)
122
+
123
+ def extrair_tabelas_pdf(pdf_path):
124
+ """Extrai tabelas do PDF usando stream apenas para o nome e lattice para notas."""
125
+ try:
126
+ # Extrair nome do aluno usando stream
127
+ tables_header = camelot.read_pdf(
128
+ pdf_path,
129
+ pages='1',
130
+ flavor='stream',
131
+ edge_tol=500
132
+ )
133
+
134
+ info_aluno = {}
135
+
136
+ # Procurar apenas o nome do aluno
137
+ for table in tables_header:
138
+ df = table.df
139
+ for i in range(len(df)):
140
+ for j in range(len(df.columns)):
141
+ texto = str(df.iloc[i,j]).strip()
142
+ if 'Nome do Aluno' in texto:
143
+ try:
144
+ if j + 1 < len(df.columns):
145
+ nome = str(df.iloc[i,j+1]).strip()
146
+ elif i + 1 < len(df):
147
+ nome = str(df.iloc[i+1,j]).strip()
148
+ if nome and nome != 'Nome do Aluno:':
149
+ info_aluno['nome'] = nome
150
+ break
151
+ except:
152
+ continue
153
+
154
+ # Extrair tabela de notas usando lattice
155
+ tables_notas = camelot.read_pdf(
156
+ pdf_path,
157
+ pages='all',
158
+ flavor='lattice'
159
+ )
160
+
161
+ # Encontrar tabela de notas (procurar a maior tabela com 'Disciplina')
162
+ df_notas = None
163
+ max_rows = 0
164
+
165
+ for table in tables_notas:
166
+ df_temp = table.df
167
+ if len(df_temp) > max_rows and 'Disciplina' in str(df_temp.iloc[0,0]):
168
+ max_rows = len(df_temp)
169
+ df_notas = df_temp.copy()
170
+ df_notas = df_notas.rename(columns={
171
+ 0: 'Disciplina',
172
+ 1: 'Nota B1', 2: 'Freq B1', 3: '%Freq B1', 4: 'AC B1',
173
+ 5: 'Nota B2', 6: 'Freq B2', 7: '%Freq B2', 8: 'AC B2',
174
+ 9: 'Nota B3', 10: 'Freq B3', 11: '%Freq B3', 12: 'AC B3',
175
+ 13: 'Nota B4', 14: 'Freq B4', 15: '%Freq B4', 16: 'AC B4',
176
+ 17: 'CF', 18: 'Nota Final', 19: 'Freq Final', 20: 'AC Final'
177
+ })
178
+
179
+ if df_notas is None:
180
+ raise ValueError("Tabela de notas não encontrada")
181
+
182
+ # Adicionar apenas o nome ao DataFrame
183
+ df_notas.attrs['nome'] = info_aluno.get('nome', 'Nome não encontrado')
184
+
185
+ return df_notas
186
+
187
+ except Exception as e:
188
+ print(f"Erro na extração das tabelas: {str(e)}")
189
+ raise
190
+
191
+ def obter_disciplinas_validas(df):
192
+ """Identifica disciplinas válidas no boletim com seus dados."""
193
+ colunas_notas = ['Nota B1', 'Nota B2', 'Nota B3', 'Nota B4']
194
+ colunas_freq = ['%Freq B1', '%Freq B2', '%Freq B3', '%Freq B4']
195
+
196
+ disciplinas_dados = []
197
+
198
+ for _, row in df.iterrows():
199
+ disciplina = row['Disciplina']
200
+ if pd.isna(disciplina) or disciplina == '':
201
+ continue
202
+
203
+ notas = []
204
+ freqs = []
205
+ bimestres_cursados = []
206
+
207
+ for i, (col_nota, col_freq) in enumerate(zip(colunas_notas, colunas_freq), 1):
208
+ nota = converter_nota(row[col_nota])
209
+ freq = row[col_freq] if col_freq in row else None
210
+
211
+ if nota is not None or (freq and freq != '-'):
212
+ bimestres_cursados.append(i)
213
+ notas.append(nota if nota is not None else 0)
214
+ freqs.append(freq)
215
+ else:
216
+ notas.append(None)
217
+ freqs.append(None)
218
+
219
+ if bimestres_cursados:
220
+ media_notas = calcular_media_bimestres(notas)
221
+ media_freq = calcular_frequencia_media(freqs)
222
+
223
+ disciplinas_dados.append({
224
+ 'disciplina': disciplina,
225
+ 'notas': notas,
226
+ 'frequencias': freqs,
227
+ 'media_notas': media_notas,
228
+ 'media_freq': media_freq,
229
+ 'bimestres_cursados': bimestres_cursados
230
+ })
231
+
232
+ return disciplinas_dados
233
+
234
+ def gerar_paleta_cores(n_cores):
235
+ """Gera uma paleta de cores distintas para o número de disciplinas."""
236
+ cores_base = [
237
+ '#1f77b4', '#ff7f0e', '#2ca02c', '#d62728', '#9467bd',
238
+ '#8c564b', '#e377c2', '#7f7f7f', '#bcbd22', '#17becf',
239
+ '#393b79', '#637939', '#8c6d31', '#843c39', '#7b4173'
240
+ ]
241
+
242
+ if n_cores > len(cores_base):
243
+ HSV_tuples = [(x/n_cores, 0.7, 0.85) for x in range(n_cores)]
244
+ cores_extras = ['#%02x%02x%02x' % tuple(int(x*255) for x in colorsys.hsv_to_rgb(*hsv))
245
+ for hsv in HSV_tuples]
246
+ return cores_extras
247
+
248
+ return cores_base[:n_cores]
249
+
250
+ def plotar_evolucao_bimestres(disciplinas_dados, temp_dir, titulo=None, nome_arquivo=None):
251
+ """Plota gráfico de evolução das notas por bimestre com visualização refinada."""
252
+ n_disciplinas = len(disciplinas_dados)
253
+
254
+ if n_disciplinas == 0:
255
+ raise ValueError("Nenhuma disciplina válida encontrada para plotar.")
256
+
257
+ plt.figure(figsize=(11.69, 8.27))
258
+
259
+ cores = gerar_paleta_cores(n_disciplinas)
260
+ marcadores = ['o', 's', '^', 'D', 'v', '<', '>', 'p', 'h', '*']
261
+ estilos_linha = ['-', '--', '-.', ':', '-', '--', '-.', ':', '-', '--']
262
+
263
+ plt.grid(True, linestyle='--', alpha=0.3, zorder=0)
264
+
265
+ # Deslocamento ainda menor e mais refinado
266
+ deslocamentos = np.linspace(-0.03, 0.03, n_disciplinas)
267
+
268
+ # Estrutura para armazenar as posições das anotações já utilizadas
269
+ anotacoes_usadas = {} # formato: {bimestre: [(y, texto)]}
270
+
271
+ # Primeira passagem: coletar todos os valores e determinar grupos
272
+ grupos_notas = {} # {bimestre: {nota: [índices]}}
273
+ for idx, disc_data in enumerate(disciplinas_dados):
274
+ notas = pd.Series(disc_data['notas'])
275
+ bimestres_cursados = disc_data['bimestres_cursados']
276
+
277
+ if bimestres_cursados:
278
+ notas_validas = [nota for i, nota in enumerate(notas, 1) if i in bimestres_cursados and nota is not None]
279
+ bimestres = [bim for bim in bimestres_cursados if notas[bim-1] is not None]
280
+
281
+ for bim, nota in zip(bimestres, notas_validas):
282
+ if nota is not None:
283
+ if bim not in grupos_notas:
284
+ grupos_notas[bim] = {}
285
+ if nota not in grupos_notas[bim]:
286
+ grupos_notas[bim][nota] = []
287
+ grupos_notas[bim][nota].append(idx)
288
+
289
+ # Segunda passagem: plotar e anotar
290
+ for idx, disc_data in enumerate(disciplinas_dados):
291
+ notas = pd.Series(disc_data['notas'])
292
+ bimestres_cursados = disc_data['bimestres_cursados']
293
+ desloc = deslocamentos[idx]
294
+
295
+ if bimestres_cursados:
296
+ notas_validas = [nota for i, nota in enumerate(notas, 1) if i in bimestres_cursados and nota is not None]
297
+ bimestres = [bim for bim in bimestres_cursados if notas[bim-1] is not None]
298
+ bimestres_deslocados = [bim + desloc for bim in bimestres]
299
+
300
+ if notas_validas:
301
+ # Plotar linha e pontos
302
+ plt.plot(bimestres_deslocados, notas_validas,
303
+ color=cores[idx % len(cores)],
304
+ marker=marcadores[idx % len(marcadores)],
305
+ markersize=7,
306
+ linewidth=1.5,
307
+ label=disc_data['disciplina'],
308
+ linestyle=estilos_linha[idx % len(estilos_linha)],
309
+ alpha=0.8)
310
+
311
+ # Adicionar anotações com posicionamento otimizado
312
+ for bim_orig, bim_desloc, nota in zip(bimestres, bimestres_deslocados, notas_validas):
313
+ if nota is not None:
314
+ # Verificar se é o primeiro índice para esta nota neste bimestre
315
+ if grupos_notas[bim_orig][nota][0] == idx:
316
+ # Determinar posição vertical da anotação
317
+ if bim_orig not in anotacoes_usadas:
318
+ anotacoes_usadas[bim_orig] = []
319
+
320
+ # Encontrar posição vertical disponível
321
+ y_base = nota
322
+ y_offset = 10
323
+ texto = f"{nota:.1f}"
324
+
325
+ # Verificar sobreposição com anotações existentes
326
+ while any(abs(y - (y_base + y_offset/20)) < 0.4 for y, _ in anotacoes_usadas.get(bim_orig, [])):
327
+ y_offset += 5
328
+
329
+ # Adicionar anotação
330
+ plt.annotate(texto,
331
+ (bim_orig, nota),
332
+ textcoords="offset points",
333
+ xytext=(0, y_offset),
334
+ ha='center',
335
+ va='bottom',
336
+ fontsize=8,
337
+ bbox=dict(facecolor='white',
338
+ edgecolor='none',
339
+ alpha=0.8,
340
+ pad=0.5))
341
+
342
+ anotacoes_usadas[bim_orig].append((nota + y_offset/20, texto))
343
+
344
+ # Usar título personalizado se fornecido
345
+ titulo_grafico = titulo or 'Evolução das Médias por Disciplina ao Longo dos Bimestres'
346
+ plt.title(titulo_grafico, pad=20, fontsize=12, fontweight='bold')
347
+
348
+ plt.xlabel('Bimestres', fontsize=10)
349
+ plt.ylabel('Notas', fontsize=10)
350
+ plt.xticks([1, 2, 3, 4], ['1º Bim', '2º Bim', '3º Bim', '4º Bim'])
351
+ plt.ylim(0, ESCALA_MAXIMA_NOTAS)
352
+
353
+ # Adicionar linha de aprovação
354
+ plt.axhline(y=LIMITE_APROVACAO_NOTA, color='r', linestyle='--', alpha=0.3)
355
+ plt.text(0.02, LIMITE_APROVACAO_NOTA + 0.1, 'Média mínima para aprovação',
356
+ transform=plt.gca().get_yaxis_transform(), color='r', alpha=0.5)
357
+
358
+ # Ajustar legenda
359
+ if n_disciplinas > 8:
360
+ plt.legend(bbox_to_anchor=(1.05, 1), loc='upper left', fontsize=8,
361
+ ncol=max(1, n_disciplinas // 12))
362
+ else:
363
+ plt.legend(bbox_to_anchor=(1.05, 1), loc='upper left', ncol=1)
364
+
365
+ plt.tight_layout()
366
+
367
+ # Usar nome de arquivo personalizado se fornecido
368
+ nome_arquivo = nome_arquivo or 'evolucao_notas.png'
369
+ plot_path = os.path.join(temp_dir, nome_arquivo)
370
+ plt.savefig(plot_path, bbox_inches='tight', dpi=300)
371
+ plt.close()
372
+ return plot_path
373
+
374
+ def plotar_graficos_destacados(disciplinas_dados, temp_dir):
375
+ """Plota gráficos de médias e frequências com destaques."""
376
+ n_disciplinas = len(disciplinas_dados)
377
+
378
+ if not n_disciplinas:
379
+ raise ValueError("Nenhuma disciplina válida encontrada no boletim.")
380
+
381
+ # Criar figura
382
+ plt.figure(figsize=(12, 10))
383
+
384
+ disciplinas = [d['disciplina'] for d in disciplinas_dados]
385
+ medias_notas = [d['media_notas'] for d in disciplinas_dados]
386
+ medias_freq = [d['media_freq'] for d in disciplinas_dados]
387
+
388
+ # Criar subplot com mais espaço entre os gráficos
389
+ fig, (ax1, ax2) = plt.subplots(2, 1, figsize=(12, 10), height_ratios=[1, 1])
390
+ plt.subplots_adjust(hspace=0.5) # Aumentar espaço entre os gráficos
391
+
392
+ # Definir cores baseadas nos limites de aprovação
393
+ cores_notas = ['red' if media < LIMITE_APROVACAO_NOTA else '#2ecc71' for media in medias_notas]
394
+ cores_freq = ['red' if media < LIMITE_APROVACAO_FREQ else '#2ecc71' for media in medias_freq]
395
+
396
+ # Calcular médias globais
397
+ media_global = np.mean(medias_notas)
398
+ freq_global = np.mean(medias_freq)
399
+
400
+ # Gráfico de notas
401
+ barras_notas = ax1.bar(disciplinas, medias_notas, color=cores_notas)
402
+ ax1.set_title('Média de Notas por Disciplina', pad=20, fontsize=12, fontweight='bold')
403
+ ax1.set_ylim(0, ESCALA_MAXIMA_NOTAS)
404
+ ax1.grid(True, axis='y', alpha=0.3, linestyle='--')
405
+
406
+ # Melhorar a apresentação dos rótulos
407
+ ax1.set_xticklabels(disciplinas, rotation=45, ha='right', va='top')
408
+ ax1.set_ylabel('Notas', fontsize=10, labelpad=10)
409
+
410
+ # Adicionar linha de média mínima
411
+ ax1.axhline(y=LIMITE_APROVACAO_NOTA, color='r', linestyle='--', alpha=0.3)
412
+ ax1.text(0.02, LIMITE_APROVACAO_NOTA + 0.1, 'Média mínima (5,0)',
413
+ transform=ax1.get_yaxis_transform(), color='r', alpha=0.7)
414
+
415
+ # Valores nas barras de notas
416
+ for barra in barras_notas:
417
+ altura = barra.get_height()
418
+ ax1.text(barra.get_x() + barra.get_width()/2., altura,
419
+ f'{altura:.1f}',
420
+ ha='center', va='bottom', fontsize=8)
421
+
422
+ # Gráfico de frequências
423
+ barras_freq = ax2.bar(disciplinas, medias_freq, color=cores_freq)
424
+ ax2.set_title('Frequência Média por Disciplina', pad=20, fontsize=12, fontweight='bold')
425
+ ax2.set_ylim(0, 110)
426
+ ax2.grid(True, axis='y', alpha=0.3, linestyle='--')
427
+
428
+ # Melhorar a apresentação dos rótulos
429
+ ax2.set_xticklabels(disciplinas, rotation=45, ha='right', va='top')
430
+ ax2.set_ylabel('Frequência (%)', fontsize=10, labelpad=10)
431
+
432
+ # Adicionar linha de frequência mínima
433
+ ax2.axhline(y=LIMITE_APROVACAO_FREQ, color='r', linestyle='--', alpha=0.3)
434
+ ax2.text(0.02, LIMITE_APROVACAO_FREQ + 1, 'Frequência mínima (75%)',
435
+ transform=ax2.get_yaxis_transform(), color='r', alpha=0.7)
436
+
437
+ # Valores nas barras de frequência
438
+ for barra in barras_freq:
439
+ altura = barra.get_height()
440
+ ax2.text(barra.get_x() + barra.get_width()/2., altura,
441
+ f'{altura:.1f}%',
442
+ ha='center', va='bottom', fontsize=8)
443
+
444
+ # Título global com informações de média
445
+ plt.suptitle(
446
+ f'Desempenho Geral\nMédia Global: {media_global:.1f} | Frequência Global: {freq_global:.1f}%',
447
+ y=0.98, fontsize=14, fontweight='bold'
448
+ )
449
+
450
+ # Aviso de risco de reprovação se necessário
451
+ if freq_global < LIMITE_APROVACAO_FREQ:
452
+ plt.figtext(0.5, 0.02,
453
+ "Atenção: Risco de Reprovação por Baixa Frequência",
454
+ ha="center", fontsize=11, color="red", weight='bold')
455
+
456
+ plt.tight_layout()
457
+
458
+ # Salvar o gráfico
459
+ plot_path = os.path.join(temp_dir, 'medias_frequencias.png')
460
+ plt.savefig(plot_path, bbox_inches='tight', dpi=300)
461
+ plt.close()
462
+
463
+ return plot_path
464
+
465
+ def gerar_relatorio_pdf(df, disciplinas_dados, grafico_basica, grafico_diversificada, grafico_medias):
466
+ """Gera relatório PDF com os gráficos e análises."""
467
+ pdf = FPDF()
468
+ pdf.set_auto_page_break(auto=True, margin=15)
469
+
470
+ # Primeira página - Informações e Formação Básica
471
+ pdf.add_page()
472
+ pdf.set_font('Helvetica', 'B', 18)
473
+ pdf.cell(0, 10, 'Relatório de Desempenho Escolar', 0, new_x=XPos.LMARGIN, new_y=YPos.NEXT, align='C')
474
+ pdf.ln(15)
475
+
476
+ # Informações do aluno
477
+ pdf.set_font('Helvetica', 'B', 12)
478
+ pdf.cell(0, 10, 'Informações do Aluno', 0, new_x=XPos.LMARGIN, new_y=YPos.NEXT, align='L')
479
+ pdf.line(10, pdf.get_y(), 200, pdf.get_y())
480
+ pdf.ln(5)
481
+
482
+ # Mostrar apenas o nome
483
+ if hasattr(df, 'attrs') and 'nome' in df.attrs:
484
+ pdf.set_font('Helvetica', 'B', 11)
485
+ pdf.cell(30, 7, 'Nome:', 0, 0)
486
+ pdf.set_font('Helvetica', '', 11)
487
+ pdf.cell(0, 7, df.attrs['nome'], 0, new_x=XPos.LMARGIN, new_y=YPos.NEXT)
488
+
489
+ pdf.ln(10)
490
+
491
+ # Data do relatório
492
+ data_atual = datetime.now().strftime('%d/%m/%Y')
493
+ pdf.set_font('Helvetica', 'I', 10)
494
+ pdf.cell(0, 5, f'Data de geração: {data_atual}', 0, new_x=XPos.LMARGIN, new_y=YPos.NEXT, align='R')
495
+ pdf.ln(15)
496
+
497
+ # Gráfico de evolução da formação básica
498
+ pdf.set_font('Helvetica', 'B', 14)
499
+ pdf.cell(0, 10, 'Evolução das Notas - Formação Geral Básica', 0, new_x=XPos.LMARGIN, new_y=YPos.NEXT, align='L')
500
+ pdf.line(10, pdf.get_y(), 200, pdf.get_y())
501
+ pdf.ln(10)
502
+ pdf.image(grafico_basica, x=10, w=190)
503
+
504
+ # Segunda página - Parte Diversificada
505
+ pdf.add_page()
506
+ pdf.set_font('Helvetica', 'B', 14)
507
+ pdf.cell(0, 10, 'Evolução das Notas - Parte Diversificada', 0, new_x=XPos.LMARGIN, new_y=YPos.NEXT, align='L')
508
+ pdf.line(10, pdf.get_y(), 200, pdf.get_y())
509
+ pdf.ln(10)
510
+ pdf.image(grafico_diversificada, x=10, w=190)
511
+
512
+ # Terceira página - Médias e Frequências
513
+ pdf.add_page()
514
+ pdf.set_font('Helvetica', 'B', 14)
515
+ pdf.cell(0, 10, 'Análise de Médias e Frequências', 0, new_x=XPos.LMARGIN, new_y=YPos.NEXT, align='L')
516
+ pdf.line(10, pdf.get_y(), 200, pdf.get_y())
517
+ pdf.ln(10)
518
+ pdf.image(grafico_medias, x=10, w=190)
519
+
520
+ # Quarta página - Análise Detalhada
521
+ pdf.add_page()
522
+ pdf.set_font('Helvetica', 'B', 14)
523
+ pdf.cell(0, 10, 'Análise Detalhada', 0, new_x=XPos.LMARGIN, new_y=YPos.NEXT, align='L')
524
+ pdf.line(10, pdf.get_y(), 200, pdf.get_y())
525
+ pdf.ln(10)
526
+
527
+ # Calcular médias globais
528
+ medias_notas = [d['media_notas'] for d in disciplinas_dados]
529
+ medias_freq = [d['media_freq'] for d in disciplinas_dados]
530
+ media_global = np.mean(medias_notas)
531
+ freq_global = np.mean(medias_freq)
532
+
533
+ # Resumo geral
534
+ pdf.set_font('Helvetica', 'B', 12)
535
+ pdf.cell(0, 7, 'Resumo Geral:', 0, new_x=XPos.LMARGIN, new_y=YPos.NEXT, align='L')
536
+ pdf.ln(5)
537
+
538
+ pdf.set_font('Helvetica', '', 11)
539
+ pdf.cell(0, 7, f'Média Global: {media_global:.1f}', 0, new_x=XPos.LMARGIN, new_y=YPos.NEXT, align='L')
540
+ pdf.cell(0, 7, f'Frequência Global: {freq_global:.1f}%', 0, new_x=XPos.LMARGIN, new_y=YPos.NEXT, align='L')
541
+ pdf.ln(10)
542
+
543
+ # Avisos Importantes
544
+ pdf.set_font('Helvetica', 'B', 12)
545
+ pdf.cell(0, 10, 'Pontos de Atenção:', 0, new_x=XPos.LMARGIN, new_y=YPos.NEXT, align='L')
546
+ pdf.ln(5)
547
+
548
+ pdf.set_font('Helvetica', '', 10)
549
+
550
+ # Disciplinas com baixo desempenho
551
+ disciplinas_risco = []
552
+ for disc_data in disciplinas_dados:
553
+ avisos = []
554
+ if disc_data['media_notas'] < LIMITE_APROVACAO_NOTA:
555
+ avisos.append(f"Média de notas abaixo de {LIMITE_APROVACAO_NOTA} ({disc_data['media_notas']:.1f})")
556
+ if disc_data['media_freq'] < LIMITE_APROVACAO_FREQ:
557
+ avisos.append(f"Frequência abaixo de {LIMITE_APROVACAO_FREQ}% ({disc_data['media_freq']:.1f}%)")
558
+
559
+ if avisos:
560
+ disciplinas_risco.append((disc_data['disciplina'], avisos))
561
+
562
+ if disciplinas_risco:
563
+ for disc, avisos in disciplinas_risco:
564
+ pdf.set_font('Helvetica', 'B', 10)
565
  pdf.cell(0, 7, f'- {disc}:', 0, new_x=XPos.LMARGIN, new_y=YPos.NEXT, align='L')
566
  pdf.set_font('Helvetica', '', 10)
567
  for aviso in avisos: