histlearn commited on
Commit
a9b8de4
·
verified ·
1 Parent(s): c38caaf

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +299 -197
app.py CHANGED
@@ -10,12 +10,70 @@ import os
10
  import matplotlib
11
  import shutil
12
  import colorsys
 
13
  matplotlib.use('Agg')
14
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
15
  def extrair_tabelas_pdf(pdf_path):
16
  """Extrai tabelas do PDF e retorna um DataFrame processado."""
17
  try:
18
- # Extrair tabelas do PDF usando o método 'lattice'
19
  tables = camelot.read_pdf(pdf_path, pages='all', flavor='lattice')
20
  print(f"Tabelas extraídas: {len(tables)}")
21
 
@@ -25,146 +83,159 @@ def extrair_tabelas_pdf(pdf_path):
25
  # Processar a primeira tabela
26
  df = tables[0].df
27
 
28
- # Verificar se a tabela tem conteúdo
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
29
  if df.empty:
30
  raise ValueError("A tabela extraída está vazia.")
31
-
32
- # Salvar todas as tabelas extraídas em CSV (para debug)
33
- temp_dir = os.path.dirname(pdf_path)
34
- for i, table in enumerate(tables):
35
- csv_path = os.path.join(temp_dir, f'boletim_extraido_{i+1}.csv')
36
- table.to_csv(csv_path)
37
- print(f"Tabela {i+1} salva como CSV em {csv_path}")
38
 
39
  return df
 
40
  except Exception as e:
41
  print(f"Erro na extração das tabelas: {str(e)}")
42
  raise
43
 
44
- def converter_nota(valor):
45
- """Converte valor de nota para float, tratando casos especiais."""
46
- if pd.isna(valor) or valor == '-' or valor == 'N' or valor == '' or valor == 'None':
47
- return 0
48
- try:
49
- if isinstance(valor, str):
50
- # Remover possíveis espaços e substituir vírgula por ponto
51
- valor_limpo = valor.strip().replace(',', '.')
52
- # Se depois de limpar ainda estiver vazio, retorna 0
53
- if not valor_limpo:
54
- return 0
55
- return float(valor_limpo)
56
- elif isinstance(valor, (int, float)):
57
- return float(valor)
58
- return 0
59
- except:
60
- return 0
61
-
62
  def obter_disciplinas_validas(df):
63
- """Identifica disciplinas válidas no boletim."""
64
- # Colunas de notas e frequências
65
  colunas_notas = ['Nota B1', 'Nota B2', 'Nota B3', 'Nota B4']
66
  colunas_freq = ['%Freq B1', '%Freq B2', '%Freq B3', '%Freq B4']
67
 
68
- # Converter notas para numérico, tratando valores inválidos
69
- for col in colunas_notas:
70
- if col in df.columns:
71
- df[col] = df[col].apply(lambda x: converter_nota(x))
72
-
73
- # Converter frequências, tratando valores inválidos
74
- for col in colunas_freq:
75
- if col in df.columns:
76
- df[col] = df[col].replace('%', '', regex=True)
77
- df[col] = df[col].apply(lambda x: converter_nota(x) if pd.notna(x) else 0)
78
 
79
- # Identificar disciplinas que têm pelo menos uma nota ou frequência
80
- disciplinas_validas = []
81
  for _, row in df.iterrows():
82
  disciplina = row['Disciplina']
83
  if pd.isna(disciplina) or disciplina == '':
84
  continue
 
 
 
 
 
 
 
 
 
85
 
86
- notas = row[colunas_notas].astype(float)
87
- freq = row[colunas_freq].astype(float)
 
 
 
 
 
88
 
89
- if (notas > 0).any() or (freq > 0).any():
90
- disciplinas_validas.append(disciplina)
 
 
 
 
 
 
 
 
 
 
 
 
 
91
 
92
- return disciplinas_validas
93
-
94
  def gerar_paleta_cores(n_cores):
95
  """Gera uma paleta de cores distintas para o número de disciplinas."""
96
  cores_base = [
97
- '#DC143C', '#4169E1', '#9370DB', '#32CD32', '#FF8C00',
98
- '#00CED1', '#FF69B4', '#8B4513', '#4B0082', '#556B2F',
99
- '#B8860B', '#483D8B', '#008B8B', '#8B008B', '#8B0000'
100
  ]
101
 
102
  if n_cores > len(cores_base):
103
- HSV_tuples = [(x/n_cores, 0.8, 0.9) for x in range(n_cores)]
104
  cores_extras = ['#%02x%02x%02x' % tuple(int(x*255) for x in colorsys.hsv_to_rgb(*hsv))
105
  for hsv in HSV_tuples]
106
  return cores_extras
107
 
108
  return cores_base[:n_cores]
109
 
110
- def plotar_evolucao_bimestres(df_filtrado, temp_dir):
111
  """Plota gráfico de evolução das notas por bimestre."""
112
- # Obter disciplinas válidas
113
- disciplinas_validas = obter_disciplinas_validas(df_filtrado)
114
- n_disciplinas = len(disciplinas_validas)
115
 
116
  if n_disciplinas == 0:
117
  raise ValueError("Nenhuma disciplina válida encontrada para plotar.")
118
 
119
- # Calcular tamanho da figura baseado no número de disciplinas
120
- altura_figura = max(6, n_disciplinas * 0.4)
121
- plt.figure(figsize=(14, altura_figura))
122
 
123
  cores = gerar_paleta_cores(n_disciplinas)
124
- marcadores = ['o', 's', '^', 'D', 'v', '<', '>', 'p', 'h', '8', '*', 'H', '+', 'x', 'd']
125
- estilos_linha = ['-', '--', '-.', ':', '-', '--', '-.', ':', '-', '--', '-.', ':', '-', '--', '-.']
126
 
127
  plt.grid(True, linestyle='--', alpha=0.3, zorder=0)
128
 
129
- colunas_notas = ['Nota B1', 'Nota B2', 'Nota B3', 'Nota B4']
130
-
131
- for idx, disciplina in enumerate(disciplinas_validas):
132
- dados_disciplina = df_filtrado[df_filtrado['Disciplina'] == disciplina]
133
- if not dados_disciplina.empty:
134
- # Converter notas para Series do pandas
135
- notas = pd.Series(dados_disciplina[colunas_notas].values[0])
136
- notas_validas = pd.to_numeric(notas, errors='coerce').replace([np.nan, 0], 0) > 0
137
 
138
- if notas_validas.any():
139
- bimestres = np.arange(1, len(colunas_notas) + 1)[notas_validas]
140
- notas_filtradas = pd.to_numeric(notas[notas_validas], errors='coerce').replace(np.nan, 0)
141
-
142
- plt.plot(bimestres, notas_filtradas,
143
  color=cores[idx % len(cores)],
144
  marker=marcadores[idx % len(marcadores)],
145
  markersize=8,
146
  linewidth=2,
147
- label=disciplina,
148
  linestyle=estilos_linha[idx % len(estilos_linha)],
149
  alpha=0.8)
150
 
151
- for x, y in zip(bimestres, notas_filtradas):
152
- plt.annotate(f"{y:.1f}", (x, y),
153
- textcoords="offset points",
154
- xytext=(0, 5),
155
- ha='center',
156
- fontsize=8)
157
-
158
- plt.title('Evolução das Médias por Disciplina ao Longo dos Bimestres')
159
- plt.xlabel('Bimestres')
160
- plt.ylabel('Média de Notas')
161
- plt.xticks([1, 2, 3, 4], ['B1', 'B2', 'B3', 'B4'])
162
- plt.ylim(0, 10)
163
-
164
- if n_disciplinas > 10:
165
- plt.legend(bbox_to_anchor=(1.05, 1), loc='upper left', fontsize=8)
 
 
 
 
 
 
 
 
 
166
  else:
167
- plt.legend(bbox_to_anchor=(1.05, 1), loc='upper left')
168
 
169
  plt.tight_layout()
170
 
@@ -173,67 +244,75 @@ def plotar_evolucao_bimestres(df_filtrado, temp_dir):
173
  plt.close()
174
  return plot_path
175
 
176
- def plotar_graficos_destacados(df_boletim_clean, temp_dir):
177
  """Plota gráficos de médias e frequências com destaques."""
178
- # Obter disciplinas válidas
179
- disciplinas_validas = obter_disciplinas_validas(df_boletim_clean)
180
 
181
- if not disciplinas_validas:
182
  raise ValueError("Nenhuma disciplina válida encontrada no boletim.")
183
 
184
- n_disciplinas = len(disciplinas_validas)
185
-
186
- # Calcular tamanho da figura baseado no número de disciplinas
187
- altura_figura = max(6, n_disciplinas * 0.4)
188
- plt.figure(figsize=(14, altura_figura))
189
-
190
- df_filtrado = df_boletim_clean[df_boletim_clean['Disciplina'].isin(disciplinas_validas)]
191
- disciplinas = df_filtrado['Disciplina'].astype(str)
192
 
193
- # Processar frequências
194
- colunas_freq = ['%Freq B1', '%Freq B2', '%Freq B3', '%Freq B4']
195
- freq_data = df_filtrado[colunas_freq].replace('%', '', regex=True).astype(float)
196
- medias_frequencia = freq_data.replace([np.nan, 0], 0).mean(axis=1)
197
-
198
- # Processar notas
199
- colunas_notas = ['Nota B1', 'Nota B2', 'Nota B3', 'Nota B4']
200
- notas_data = df_filtrado[colunas_notas].astype(float)
201
- medias_notas = notas_data.replace([np.nan, 0], 0).mean(axis=1)
202
 
203
- cores_notas = ['red' if media < 5 else 'blue' for media in medias_notas]
204
- cores_frequencias = ['red' if media < 75 else 'green' for media in medias_frequencia]
 
 
205
 
206
- frequencia_global_media = medias_frequencia.mean()
 
207
 
208
- plt.subplot(1, 2, 1)
 
209
  barras_notas = plt.bar(disciplinas, medias_notas, color=cores_notas)
210
- plt.title('Média de Notas por Disciplina (Vermelho: < 5)')
211
- plt.xticks(rotation=45, ha='right')
212
- plt.ylim(0, 10)
213
 
 
 
 
 
 
 
214
  for barra in barras_notas:
215
  altura = barra.get_height()
216
  plt.text(barra.get_x() + barra.get_width()/2., altura,
217
  f'{altura:.1f}',
218
- ha='center', va='bottom')
 
 
 
 
 
 
 
219
 
220
- plt.subplot(1, 2, 2)
221
- barras_freq = plt.bar(disciplinas, medias_frequencia, color=cores_frequencias)
222
- plt.title('Média de Frequência por Disciplina (Vermelho: < 75%)')
223
- plt.xticks(rotation=45, ha='right')
224
- plt.ylim(0, 100)
225
 
 
226
  for barra in barras_freq:
227
  altura = barra.get_height()
228
  plt.text(barra.get_x() + barra.get_width()/2., altura,
229
  f'{altura:.1f}%',
230
- ha='center', va='bottom')
231
 
232
- plt.suptitle(f"Frequência Global Média: {frequencia_global_media:.2f}%")
 
 
 
233
 
234
- if frequencia_global_media < 75:
235
- plt.figtext(0.5, 0.02, "Cuidado: Risco de Reprovação por Baixa Frequência",
236
- ha="center", fontsize=12, color="red")
 
237
 
238
  plt.tight_layout()
239
 
@@ -242,57 +321,95 @@ def plotar_graficos_destacados(df_boletim_clean, temp_dir):
242
  plt.close()
243
  return plot_path
244
 
245
- def gerar_relatorio_pdf(df, grafico1_path, grafico2_path):
246
  """Gera relatório PDF com os gráficos e análises."""
247
  pdf = FPDF()
 
248
  pdf.add_page()
249
 
 
250
  pdf.set_font('Helvetica', 'B', 16)
251
  pdf.cell(0, 10, 'Relatório de Desempenho Escolar', 0, new_x=XPos.LMARGIN, new_y=YPos.NEXT, align='C')
252
- pdf.ln(10)
253
 
254
- if 'Nome do Aluno' in df.columns:
255
- pdf.set_font('Helvetica', '', 12)
256
- pdf.cell(0, 10, f'Aluno: {df["Nome do Aluno"].iloc[0]}', 0, new_x=XPos.LMARGIN, new_y=YPos.NEXT, align='L')
 
 
 
 
 
 
 
 
257
 
 
 
 
 
 
 
 
 
 
258
  pdf.image(grafico1_path, x=10, w=190)
259
  pdf.ln(10)
260
  pdf.image(grafico2_path, x=10, w=190)
261
  pdf.ln(10)
262
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
263
  pdf.set_font('Helvetica', 'B', 12)
264
  pdf.cell(0, 10, 'Avisos Importantes:', 0, new_x=XPos.LMARGIN, new_y=YPos.NEXT, align='L')
265
- pdf.set_font('Helvetica', '', 10)
266
 
267
- disciplinas_validas = obter_disciplinas_validas(df)
268
- df_filtrado = df[df['Disciplina'].isin(disciplinas_validas)]
269
-
270
- # Calcular médias
271
- colunas_notas = ['Nota B1', 'Nota B2', 'Nota B3', 'Nota B4']
272
- notas_data = df_filtrado[colunas_notas].astype(float)
273
- medias_notas = notas_data.replace([np.nan, 0], 0).mean(axis=1)
274
 
275
- # Processar frequências
276
- colunas_freq = ['%Freq B1', '%Freq B2', '%Freq B3', '%Freq B4']
277
- freq_data = df_filtrado[colunas_freq].replace('%', '', regex=True).astype(float)
278
- medias_freq = freq_data.replace([np.nan, 0], 0).mean(axis=1)
 
 
 
 
 
 
 
279
 
280
- # Adicionar média global
281
- media_global = medias_notas.mean()
282
- freq_global = medias_freq.mean()
 
 
 
283
 
284
- pdf.cell(0, 10, f'Média Global: {media_global:.1f}', 0, new_x=XPos.LMARGIN, new_y=YPos.NEXT, align='L')
285
- pdf.cell(0, 10, f'Frequência Global: {freq_global:.1f}%', 0, new_x=XPos.LMARGIN, new_y=YPos.NEXT, align='L')
286
  pdf.ln(5)
287
 
288
- for idx, (disciplina, media_nota, media_freq) in enumerate(zip(df_filtrado['Disciplina'], medias_notas, medias_freq)):
289
- if media_nota < 5:
290
- pdf.cell(0, 10, f'- {disciplina}: Média de notas abaixo de 5 ({media_nota:.1f})', 0,
291
- new_x=XPos.LMARGIN, new_y=YPos.NEXT, align='L')
292
- if media_freq < 75:
293
- pdf.cell(0, 10, f'- {disciplina}: Frequência abaixo de 75% ({media_freq:.1f}%)', 0,
294
- new_x=XPos.LMARGIN, new_y=YPos.NEXT, align='L')
295
 
 
296
  temp_pdf = tempfile.NamedTemporaryFile(delete=False, suffix='.pdf')
297
  pdf_path = temp_pdf.name
298
  pdf.output(pdf_path)
@@ -302,31 +419,25 @@ def processar_boletim(file):
302
  """Função principal que processa o boletim e gera o relatório."""
303
  temp_dir = None
304
  try:
305
- # Verificar se o arquivo é válido
306
  if file is None:
307
  return None, "Nenhum arquivo foi fornecido."
308
 
309
- # Criar diretório temporário
310
  temp_dir = tempfile.mkdtemp()
311
  print(f"Diretório temporário criado: {temp_dir}")
312
 
313
- # Verificar se o arquivo tem conteúdo
314
  if not hasattr(file, 'name') or not os.path.exists(file.name):
315
  return None, "Arquivo inválido ou corrompido."
316
 
317
  if os.path.getsize(file.name) == 0:
318
  return None, "O arquivo está vazio."
319
 
320
- # Copiar o arquivo para o diretório temporário
321
  temp_pdf = os.path.join(temp_dir, 'boletim.pdf')
322
  shutil.copy2(file.name, temp_pdf)
323
  print(f"PDF copiado para: {temp_pdf}")
324
 
325
- # Verificar se a cópia foi bem sucedida
326
  if not os.path.exists(temp_pdf) or os.path.getsize(temp_pdf) == 0:
327
  return None, "Erro ao copiar o arquivo."
328
 
329
- # Extrair tabelas do PDF
330
  print("Iniciando extração das tabelas...")
331
  df = extrair_tabelas_pdf(temp_pdf)
332
  print("Tabelas extraídas com sucesso")
@@ -334,47 +445,38 @@ def processar_boletim(file):
334
  if df is None or df.empty:
335
  return None, "Não foi possível extrair dados do PDF."
336
 
337
- # Renomear colunas para o formato esperado
338
  try:
339
- df.columns = ['Disciplina', 'Nota B1', 'Freq B1', '%Freq B1', 'AC B1',
340
- 'Nota B2', 'Freq B2', '%Freq B2', 'AC B2',
341
- 'Nota B3', 'Freq B3', '%Freq B3', 'AC B3',
342
- 'Nota B4', 'Freq B4', '%Freq B4', 'AC B4',
343
- 'CF', 'Nota Final', 'Freq Final', 'AC Final']
344
- except:
345
- return None, "O formato do PDF não corresponde ao esperado."
346
-
347
- # Processar notas
348
- colunas_notas = ['Nota B1', 'Nota B2', 'Nota B3', 'Nota B4']
349
- for col in colunas_notas:
350
- if col in df.columns:
351
- df[col] = df[col].apply(converter_nota)
352
- print("Notas processadas")
353
-
354
- # Gerar gráficos
355
- print("Gerando gráficos...")
356
- grafico1_path = plotar_evolucao_bimestres(df, temp_dir)
357
- grafico2_path = plotar_graficos_destacados(df, temp_dir)
358
- print("Gráficos gerados")
359
-
360
- # Gerar PDF
361
- print("Gerando relatório PDF...")
362
- pdf_path = gerar_relatorio_pdf(df, grafico1_path, grafico2_path)
363
- print("Relatório PDF gerado")
364
-
365
- # Criar arquivo temporário para retorno
366
- output_file = tempfile.NamedTemporaryFile(delete=False, suffix='.pdf')
367
- output_path = output_file.name
368
- shutil.copy2(pdf_path, output_path)
369
 
370
- return output_path, "Relatório gerado com sucesso!"
 
371
 
372
  except Exception as e:
373
  print(f"Erro durante o processamento: {str(e)}")
374
  return None, f"Erro ao processar o boletim: {str(e)}"
375
 
376
  finally:
377
- # Limpar arquivos temporários
378
  if temp_dir and os.path.exists(temp_dir):
379
  try:
380
  shutil.rmtree(temp_dir)
 
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 # Aumentado para melhor visualização
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'] # Conceitos não numéricos válidos
22
+
23
+ def converter_nota(valor):
24
+ """Converte valor de nota para float, tratando casos especiais e conceitos."""
25
+ if pd.isna(valor) or valor == '-' or valor == 'N' or valor == '' or valor == 'None':
26
+ return None
27
+
28
+ # Se for string, limpar e verificar se é conceito
29
+ if isinstance(valor, str):
30
+ valor_limpo = valor.strip().upper()
31
+ if valor_limpo in CONCEITOS_VALIDOS:
32
+ # Converter conceitos para valores numéricos
33
+ conceitos_map = {'ET': 10, 'ES': 8, 'EP': 6}
34
+ return conceitos_map.get(valor_limpo)
35
+
36
+ # Tentar converter para número
37
+ try:
38
+ return float(valor_limpo.replace(',', '.'))
39
+ except:
40
+ return None
41
+
42
+ # Se for número, retornar diretamente
43
+ if isinstance(valor, (int, float)):
44
+ return float(valor)
45
+
46
+ return None
47
+
48
+ def calcular_media_bimestres(notas):
49
+ """Calcula média considerando apenas bimestres com notas válidas."""
50
+ notas_validas = [nota for nota in notas if nota is not None]
51
+ if not notas_validas:
52
+ return 0
53
+ return sum(notas_validas) / len(notas_validas)
54
+
55
+ def calcular_frequencia_media(frequencias):
56
+ """Calcula média de frequência considerando apenas bimestres cursados."""
57
+ freq_validas = []
58
+ for freq in frequencias:
59
+ try:
60
+ # Limpar string e converter para número
61
+ if isinstance(freq, str):
62
+ freq = freq.strip().replace('%', '').replace(',', '.')
63
+ if freq and freq != '-':
64
+ valor = float(freq)
65
+ if valor > 0: # Considerar apenas frequências positivas
66
+ freq_validas.append(valor)
67
+ except:
68
+ continue
69
+
70
+ if not freq_validas:
71
+ return 0
72
+ return sum(freq_validas) / len(freq_validas)
73
+
74
  def extrair_tabelas_pdf(pdf_path):
75
  """Extrai tabelas do PDF e retorna um DataFrame processado."""
76
  try:
 
77
  tables = camelot.read_pdf(pdf_path, pages='all', flavor='lattice')
78
  print(f"Tabelas extraídas: {len(tables)}")
79
 
 
83
  # Processar a primeira tabela
84
  df = tables[0].df
85
 
86
+ # Extrair nome do aluno e outras informações se disponível
87
+ info_aluno = {}
88
+ for i, row in df.iterrows():
89
+ if 'Nome do Aluno' in str(row[0]):
90
+ info_aluno['nome'] = row[1].strip() if len(row) > 1 else ''
91
+ elif 'RA' in str(row[0]):
92
+ info_aluno['ra'] = row[1].strip() if len(row) > 1 else ''
93
+ elif 'Escola' in str(row[0]):
94
+ info_aluno['escola'] = row[1].strip() if len(row) > 1 else ''
95
+ elif 'Turma' in str(row[0]):
96
+ info_aluno['turma'] = row[1].strip() if len(row) > 1 else ''
97
+
98
+ # Encontrar a tabela de notas
99
+ for i, table in enumerate(tables):
100
+ df_temp = table.df
101
+ if 'Disciplina' in str(df_temp.iloc[0,0]) or 'Bimestre' in str(df_temp.iloc[0,1]):
102
+ df = df_temp
103
+ break
104
+
105
  if df.empty:
106
  raise ValueError("A tabela extraída está vazia.")
107
+
108
+ # Adicionar informações do aluno ao DataFrame
109
+ for key, value in info_aluno.items():
110
+ df.attrs[key] = value
 
 
 
111
 
112
  return df
113
+
114
  except Exception as e:
115
  print(f"Erro na extração das tabelas: {str(e)}")
116
  raise
117
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
118
  def obter_disciplinas_validas(df):
119
+ """Identifica disciplinas válidas no boletim com seus dados."""
 
120
  colunas_notas = ['Nota B1', 'Nota B2', 'Nota B3', 'Nota B4']
121
  colunas_freq = ['%Freq B1', '%Freq B2', '%Freq B3', '%Freq B4']
122
 
123
+ disciplinas_dados = []
 
 
 
 
 
 
 
 
 
124
 
 
 
125
  for _, row in df.iterrows():
126
  disciplina = row['Disciplina']
127
  if pd.isna(disciplina) or disciplina == '':
128
  continue
129
+
130
+ # Coletar notas e frequências
131
+ notas = []
132
+ freqs = []
133
+ bimestres_cursados = []
134
+
135
+ for i, (col_nota, col_freq) in enumerate(zip(colunas_notas, colunas_freq), 1):
136
+ nota = converter_nota(row[col_nota])
137
+ freq = row[col_freq] if col_freq in row else None
138
 
139
+ if nota is not None or (freq and freq != '-'):
140
+ bimestres_cursados.append(i)
141
+ notas.append(nota if nota is not None else 0)
142
+ freqs.append(freq)
143
+ else:
144
+ notas.append(None)
145
+ freqs.append(None)
146
 
147
+ # Calcular médias apenas se houver dados válidos
148
+ if bimestres_cursados:
149
+ media_notas = calcular_media_bimestres(notas)
150
+ media_freq = calcular_frequencia_media(freqs)
151
+
152
+ disciplinas_dados.append({
153
+ 'disciplina': disciplina,
154
+ 'notas': notas,
155
+ 'frequencias': freqs,
156
+ 'media_notas': media_notas,
157
+ 'media_freq': media_freq,
158
+ 'bimestres_cursados': bimestres_cursados
159
+ })
160
+
161
+ return disciplinas_dados
162
 
 
 
163
  def gerar_paleta_cores(n_cores):
164
  """Gera uma paleta de cores distintas para o número de disciplinas."""
165
  cores_base = [
166
+ '#1f77b4', '#ff7f0e', '#2ca02c', '#d62728', '#9467bd',
167
+ '#8c564b', '#e377c2', '#7f7f7f', '#bcbd22', '#17becf',
168
+ '#393b79', '#637939', '#8c6d31', '#843c39', '#7b4173'
169
  ]
170
 
171
  if n_cores > len(cores_base):
172
+ HSV_tuples = [(x/n_cores, 0.7, 0.85) for x in range(n_cores)]
173
  cores_extras = ['#%02x%02x%02x' % tuple(int(x*255) for x in colorsys.hsv_to_rgb(*hsv))
174
  for hsv in HSV_tuples]
175
  return cores_extras
176
 
177
  return cores_base[:n_cores]
178
 
179
+ def plotar_evolucao_bimestres(disciplinas_dados, temp_dir):
180
  """Plota gráfico de evolução das notas por bimestre."""
181
+ n_disciplinas = len(disciplinas_dados)
 
 
182
 
183
  if n_disciplinas == 0:
184
  raise ValueError("Nenhuma disciplina válida encontrada para plotar.")
185
 
186
+ # Calcular tamanho da figura para A4 (proporção 1:√2)
187
+ plt.figure(figsize=(11.69, 8.27)) # Tamanho A4 em polegadas
 
188
 
189
  cores = gerar_paleta_cores(n_disciplinas)
190
+ marcadores = ['o', 's', '^', 'D', 'v', '<', '>', 'p', 'h', '*']
191
+ estilos_linha = ['-', '--', '-.', ':', '-', '--', '-.', ':', '-', '--']
192
 
193
  plt.grid(True, linestyle='--', alpha=0.3, zorder=0)
194
 
195
+ for idx, disc_data in enumerate(disciplinas_dados):
196
+ notas = pd.Series(disc_data['notas'])
197
+ bimestres_cursados = disc_data['bimestres_cursados']
198
+
199
+ if bimestres_cursados:
200
+ notas_validas = [nota for i, nota in enumerate(notas, 1) if i in bimestres_cursados and nota is not None]
201
+ bimestres = [bim for bim in bimestres_cursados if notas[bim-1] is not None]
 
202
 
203
+ if notas_validas:
204
+ plt.plot(bimestres, notas_validas,
 
 
 
205
  color=cores[idx % len(cores)],
206
  marker=marcadores[idx % len(marcadores)],
207
  markersize=8,
208
  linewidth=2,
209
+ label=disc_data['disciplina'],
210
  linestyle=estilos_linha[idx % len(estilos_linha)],
211
  alpha=0.8)
212
 
213
+ for x, y in zip(bimestres, notas_validas):
214
+ if y is not None:
215
+ plt.annotate(f"{y:.1f}", (x, y),
216
+ textcoords="offset points",
217
+ xytext=(0, 5),
218
+ ha='center',
219
+ fontsize=8)
220
+
221
+ plt.title('Evolução das Médias por Disciplina ao Longo dos Bimestres',
222
+ pad=20, fontsize=12, fontweight='bold')
223
+ plt.xlabel('Bimestres', fontsize=10)
224
+ plt.ylabel('Notas', fontsize=10)
225
+ plt.xticks([1, 2, 3, 4], ['1º Bim', '2º Bim', '3º Bim', '4º Bim'])
226
+ plt.ylim(0, ESCALA_MAXIMA_NOTAS)
227
+
228
+ # Adicionar linha de aprovação
229
+ plt.axhline(y=LIMITE_APROVACAO_NOTA, color='r', linestyle='--', alpha=0.3)
230
+ plt.text(0.02, LIMITE_APROVACAO_NOTA + 0.1, 'Média mínima para aprovação',
231
+ transform=plt.gca().get_yaxis_transform(), color='r', alpha=0.5)
232
+
233
+ # Ajustar legenda baseado no número de disciplinas
234
+ if n_disciplinas > 8:
235
+ plt.legend(bbox_to_anchor=(1.05, 1), loc='upper left', fontsize=8,
236
+ ncol=max(1, n_disciplinas // 12))
237
  else:
238
+ plt.legend(bbox_to_anchor=(1.05, 1), loc='upper left', ncol=1)
239
 
240
  plt.tight_layout()
241
 
 
244
  plt.close()
245
  return plot_path
246
 
247
+ def plotar_graficos_destacados(disciplinas_dados, temp_dir):
248
  """Plota gráficos de médias e frequências com destaques."""
249
+ n_disciplinas = len(disciplinas_dados)
 
250
 
251
+ if not n_disciplinas:
252
  raise ValueError("Nenhuma disciplina válida encontrada no boletim.")
253
 
254
+ # Calcular tamanho da figura para A4 (proporção 1:√2)
255
+ plt.figure(figsize=(11.69, 8.27)) # Tamanho A4 em polegadas
 
 
 
 
 
 
256
 
257
+ disciplinas = [d['disciplina'] for d in disciplinas_dados]
258
+ medias_notas = [d['media_notas'] for d in disciplinas_dados]
259
+ medias_freq = [d['media_freq'] for d in disciplinas_dados]
 
 
 
 
 
 
260
 
261
+ cores_notas = ['red' if media < LIMITE_APROVACAO_NOTA else 'green'
262
+ for media in medias_notas]
263
+ cores_freq = ['red' if media < LIMITE_APROVACAO_FREQ else 'green'
264
+ for media in medias_freq]
265
 
266
+ media_global = np.mean(medias_notas)
267
+ freq_global = np.mean(medias_freq)
268
 
269
+ # Gráfico de notas
270
+ plt.subplot(2, 1, 1)
271
  barras_notas = plt.bar(disciplinas, medias_notas, color=cores_notas)
272
+ plt.title('Média de Notas por Disciplina', pad=20, fontsize=12, fontweight='bold')
273
+ plt.xticks(rotation=45, ha='right', fontsize=8)
274
+ plt.ylim(0, ESCALA_MAXIMA_NOTAS)
275
 
276
+ # Adicionar linha de média mínima
277
+ plt.axhline(y=LIMITE_APROVACAO_NOTA, color='r', linestyle='--', alpha=0.3)
278
+ plt.text(0.02, LIMITE_APROVACAO_NOTA + 0.1, 'Média mínima',
279
+ transform=plt.gca().get_yaxis_transform(), color='r', alpha=0.5)
280
+
281
+ # Valores nas barras
282
  for barra in barras_notas:
283
  altura = barra.get_height()
284
  plt.text(barra.get_x() + barra.get_width()/2., altura,
285
  f'{altura:.1f}',
286
+ ha='center', va='bottom', fontsize=8)
287
+
288
+ # Gráfico de frequências
289
+ plt.subplot(2, 1, 2)
290
+ barras_freq = plt.bar(disciplinas, medias_freq, color=cores_freq)
291
+ plt.title('Frequência Média por Disciplina', pad=20, fontsize=12, fontweight='bold')
292
+ plt.xticks(rotation=45, ha='right', fontsize=8)
293
+ plt.ylim(0, 110) # Deixar espaço para os valores acima das barras
294
 
295
+ # Adicionar linha de frequência mínima
296
+ plt.axhline(y=LIMITE_APROVACAO_FREQ, color='r', linestyle='--', alpha=0.3)
297
+ plt.text(0.02, LIMITE_APROVACAO_FREQ + 1, 'Frequência mínima',
298
+ transform=plt.gca().get_yaxis_transform(), color='r', alpha=0.5)
 
299
 
300
+ # Valores nas barras
301
  for barra in barras_freq:
302
  altura = barra.get_height()
303
  plt.text(barra.get_x() + barra.get_width()/2., altura,
304
  f'{altura:.1f}%',
305
+ ha='center', va='bottom', fontsize=8)
306
 
307
+ plt.suptitle(
308
+ f'Média Global: {media_global:.1f} | Frequência Global: {freq_global:.1f}%',
309
+ y=0.95, fontsize=12, fontweight='bold'
310
+ )
311
 
312
+ if freq_global < LIMITE_APROVACAO_FREQ:
313
+ plt.figtext(0.5, 0.02,
314
+ "Atenção: Risco de Reprovação por Baixa Frequência",
315
+ ha="center", fontsize=10, color="red")
316
 
317
  plt.tight_layout()
318
 
 
321
  plt.close()
322
  return plot_path
323
 
324
+ def gerar_relatorio_pdf(df, disciplinas_dados, grafico1_path, grafico2_path):
325
  """Gera relatório PDF com os gráficos e análises."""
326
  pdf = FPDF()
327
+ pdf.set_auto_page_break(auto=True, margin=15)
328
  pdf.add_page()
329
 
330
+ # Cabeçalho
331
  pdf.set_font('Helvetica', 'B', 16)
332
  pdf.cell(0, 10, 'Relatório de Desempenho Escolar', 0, new_x=XPos.LMARGIN, new_y=YPos.NEXT, align='C')
333
+ pdf.ln(5)
334
 
335
+ # Informações do aluno
336
+ pdf.set_font('Helvetica', '', 11)
337
+ if hasattr(df, 'attrs'):
338
+ if 'nome' in df.attrs:
339
+ pdf.cell(0, 7, f'Aluno: {df.attrs["nome"]}', 0, new_x=XPos.LMARGIN, new_y=YPos.NEXT, align='L')
340
+ if 'ra' in df.attrs:
341
+ pdf.cell(0, 7, f'RA: {df.attrs["ra"]}', 0, new_x=XPos.LMARGIN, new_y=YPos.NEXT, align='L')
342
+ if 'escola' in df.attrs:
343
+ pdf.cell(0, 7, f'Escola: {df.attrs["escola"]}', 0, new_x=XPos.LMARGIN, new_y=YPos.NEXT, align='L')
344
+ if 'turma' in df.attrs:
345
+ pdf.cell(0, 7, f'Turma: {df.attrs["turma"]}', 0, new_x=XPos.LMARGIN, new_y=YPos.NEXT, align='L')
346
 
347
+ pdf.ln(5)
348
+
349
+ # Data do relatório
350
+ data_atual = datetime.now().strftime('%d/%m/%Y')
351
+ pdf.set_font('Helvetica', 'I', 10)
352
+ pdf.cell(0, 5, f'Relatório gerado em: {data_atual}', 0, new_x=XPos.LMARGIN, new_y=YPos.NEXT, align='R')
353
+ pdf.ln(10)
354
+
355
+ # Gráficos
356
  pdf.image(grafico1_path, x=10, w=190)
357
  pdf.ln(10)
358
  pdf.image(grafico2_path, x=10, w=190)
359
  pdf.ln(10)
360
 
361
+ # Seção de Análise
362
+ pdf.set_font('Helvetica', 'B', 14)
363
+ pdf.cell(0, 10, 'Análise de Desempenho', 0, new_x=XPos.LMARGIN, new_y=YPos.NEXT, align='L')
364
+ pdf.ln(5)
365
+
366
+ # Calcular médias globais
367
+ medias_notas = [d['media_notas'] for d in disciplinas_dados]
368
+ medias_freq = [d['media_freq'] for d in disciplinas_dados]
369
+ media_global = np.mean(medias_notas)
370
+ freq_global = np.mean(medias_freq)
371
+
372
+ # Resumo geral
373
+ pdf.set_font('Helvetica', '', 11)
374
+ pdf.cell(0, 7, f'Média Global: {media_global:.1f}', 0, new_x=XPos.LMARGIN, new_y=YPos.NEXT, align='L')
375
+ pdf.cell(0, 7, f'Frequência Global: {freq_global:.1f}%', 0, new_x=XPos.LMARGIN, new_y=YPos.NEXT, align='L')
376
+ pdf.ln(5)
377
+
378
+ # Avisos Importantes
379
  pdf.set_font('Helvetica', 'B', 12)
380
  pdf.cell(0, 10, 'Avisos Importantes:', 0, new_x=XPos.LMARGIN, new_y=YPos.NEXT, align='L')
381
+ pdf.ln(2)
382
 
383
+ pdf.set_font('Helvetica', '', 10)
 
 
 
 
 
 
384
 
385
+ # Disciplinas com baixo desempenho
386
+ disciplinas_risco = []
387
+ for disc_data in disciplinas_dados:
388
+ avisos = []
389
+ if disc_data['media_notas'] < LIMITE_APROVACAO_NOTA:
390
+ avisos.append(f"Média de notas abaixo de {LIMITE_APROVACAO_NOTA} ({disc_data['media_notas']:.1f})")
391
+ if disc_data['media_freq'] < LIMITE_APROVACAO_FREQ:
392
+ avisos.append(f"Frequência abaixo de {LIMITE_APROVACAO_FREQ}% ({disc_data['media_freq']:.1f}%)")
393
+
394
+ if avisos:
395
+ disciplinas_risco.append((disc_data['disciplina'], avisos))
396
 
397
+ if disciplinas_risco:
398
+ for disc, avisos in disciplinas_risco:
399
+ for aviso in avisos:
400
+ pdf.cell(0, 7, f'- {disc}: {aviso}', 0, new_x=XPos.LMARGIN, new_y=YPos.NEXT, align='L')
401
+ else:
402
+ pdf.cell(0, 7, 'Nenhum problema identificado.', 0, new_x=XPos.LMARGIN, new_y=YPos.NEXT, align='L')
403
 
 
 
404
  pdf.ln(5)
405
 
406
+ # Rodapé
407
+ pdf.set_y(-30)
408
+ pdf.set_font('Helvetica', 'I', 8)
409
+ pdf.cell(0, 10, 'Este relatório é uma análise automática e deve ser validado junto à secretaria da escola.',
410
+ 0, new_x=XPos.LMARGIN, new_y=YPos.NEXT, align='C')
 
 
411
 
412
+ # Salvar PDF
413
  temp_pdf = tempfile.NamedTemporaryFile(delete=False, suffix='.pdf')
414
  pdf_path = temp_pdf.name
415
  pdf.output(pdf_path)
 
419
  """Função principal que processa o boletim e gera o relatório."""
420
  temp_dir = None
421
  try:
 
422
  if file is None:
423
  return None, "Nenhum arquivo foi fornecido."
424
 
 
425
  temp_dir = tempfile.mkdtemp()
426
  print(f"Diretório temporário criado: {temp_dir}")
427
 
 
428
  if not hasattr(file, 'name') or not os.path.exists(file.name):
429
  return None, "Arquivo inválido ou corrompido."
430
 
431
  if os.path.getsize(file.name) == 0:
432
  return None, "O arquivo está vazio."
433
 
 
434
  temp_pdf = os.path.join(temp_dir, 'boletim.pdf')
435
  shutil.copy2(file.name, temp_pdf)
436
  print(f"PDF copiado para: {temp_pdf}")
437
 
 
438
  if not os.path.exists(temp_pdf) or os.path.getsize(temp_pdf) == 0:
439
  return None, "Erro ao copiar o arquivo."
440
 
 
441
  print("Iniciando extração das tabelas...")
442
  df = extrair_tabelas_pdf(temp_pdf)
443
  print("Tabelas extraídas com sucesso")
 
445
  if df is None or df.empty:
446
  return None, "Não foi possível extrair dados do PDF."
447
 
 
448
  try:
449
+ # Processar disciplinas
450
+ disciplinas_dados = obter_disciplinas_validas(df)
451
+ if not disciplinas_dados:
452
+ return None, "Nenhuma disciplina válida encontrada no boletim."
453
+
454
+ # Gerar gráficos
455
+ print("Gerando gráficos...")
456
+ grafico1_path = plotar_evolucao_bimestres(disciplinas_dados, temp_dir)
457
+ grafico2_path = plotar_graficos_destacados(disciplinas_dados, temp_dir)
458
+ print("Gráficos gerados")
459
+
460
+ # Gerar PDF
461
+ print("Gerando relatório PDF...")
462
+ pdf_path = gerar_relatorio_pdf(df, disciplinas_dados, grafico1_path, grafico2_path)
463
+ print("Relatório PDF gerado")
464
+
465
+ # Criar arquivo de retorno
466
+ output_file = tempfile.NamedTemporaryFile(delete=False, suffix='.pdf')
467
+ output_path = output_file.name
468
+ shutil.copy2(pdf_path, output_path)
469
+
470
+ return output_path, "Relatório gerado com sucesso!"
 
 
 
 
 
 
 
 
471
 
472
+ except Exception as e:
473
+ return None, f"Erro ao processar os dados: {str(e)}"
474
 
475
  except Exception as e:
476
  print(f"Erro durante o processamento: {str(e)}")
477
  return None, f"Erro ao processar o boletim: {str(e)}"
478
 
479
  finally:
 
480
  if temp_dir and os.path.exists(temp_dir):
481
  try:
482
  shutil.rmtree(temp_dir)