Spaces:
Sleeping
Sleeping
Update app.py
Browse files
app.py
CHANGED
@@ -14,32 +14,82 @@ 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 |
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 |
|
@@ -57,12 +107,11 @@ def calcular_frequencia_media(frequencias):
|
|
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:
|
66 |
freq_validas.append(valor)
|
67 |
except:
|
68 |
continue
|
@@ -80,10 +129,8 @@ def extrair_tabelas_pdf(pdf_path):
|
|
80 |
if len(tables) == 0:
|
81 |
raise ValueError("Nenhuma tabela foi extraída do PDF.")
|
82 |
|
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]):
|
@@ -95,14 +142,11 @@ def extrair_tabelas_pdf(pdf_path):
|
|
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 |
-
# Verificar se é a tabela de notas
|
102 |
if any('Disciplina' in str(col) for col in df_temp.iloc[0]) or \
|
103 |
any('Bimestre' in str(col) for col in df_temp.iloc[0]):
|
104 |
df = df_temp
|
105 |
-
# Renomear as colunas corretamente
|
106 |
df = df.rename(columns={
|
107 |
0: 'Disciplina',
|
108 |
1: 'Nota B1', 2: 'Freq B1', 3: '%Freq B1', 4: 'AC B1',
|
@@ -116,17 +160,6 @@ def extrair_tabelas_pdf(pdf_path):
|
|
116 |
if df.empty:
|
117 |
raise ValueError("A tabela extraída está vazia.")
|
118 |
|
119 |
-
# Adicionar informações do aluno ao DataFrame
|
120 |
-
for key, value in info_aluno.items():
|
121 |
-
df.attrs[key] = value
|
122 |
-
|
123 |
-
return df
|
124 |
-
|
125 |
-
except Exception as e:
|
126 |
-
print(f"Erro na extração das tabelas: {str(e)}")
|
127 |
-
raise
|
128 |
-
|
129 |
-
# Adicionar informações do aluno ao DataFrame
|
130 |
for key, value in info_aluno.items():
|
131 |
df.attrs[key] = value
|
132 |
|
@@ -135,7 +168,6 @@ def extrair_tabelas_pdf(pdf_path):
|
|
135 |
except Exception as e:
|
136 |
print(f"Erro na extração das tabelas: {str(e)}")
|
137 |
raise
|
138 |
-
|
139 |
def obter_disciplinas_validas(df):
|
140 |
"""Identifica disciplinas válidas no boletim com seus dados."""
|
141 |
colunas_notas = ['Nota B1', 'Nota B2', 'Nota B3', 'Nota B4']
|
@@ -148,7 +180,6 @@ def obter_disciplinas_validas(df):
|
|
148 |
if pd.isna(disciplina) or disciplina == '':
|
149 |
continue
|
150 |
|
151 |
-
# Coletar notas e frequências
|
152 |
notas = []
|
153 |
freqs = []
|
154 |
bimestres_cursados = []
|
@@ -165,7 +196,6 @@ def obter_disciplinas_validas(df):
|
|
165 |
notas.append(None)
|
166 |
freqs.append(None)
|
167 |
|
168 |
-
# Calcular médias apenas se houver dados válidos
|
169 |
if bimestres_cursados:
|
170 |
media_notas = calcular_media_bimestres(notas)
|
171 |
media_freq = calcular_frequencia_media(freqs)
|
@@ -180,7 +210,7 @@ def obter_disciplinas_validas(df):
|
|
180 |
})
|
181 |
|
182 |
return disciplinas_dados
|
183 |
-
|
184 |
def gerar_paleta_cores(n_cores):
|
185 |
"""Gera uma paleta de cores distintas para o número de disciplinas."""
|
186 |
cores_base = [
|
@@ -197,7 +227,7 @@ def gerar_paleta_cores(n_cores):
|
|
197 |
|
198 |
return cores_base[:n_cores]
|
199 |
|
200 |
-
def plotar_evolucao_bimestres(disciplinas_dados, temp_dir):
|
201 |
"""Plota gráfico de evolução das notas por bimestre com visualização refinada."""
|
202 |
n_disciplinas = len(disciplinas_dados)
|
203 |
|
@@ -252,8 +282,8 @@ def plotar_evolucao_bimestres(disciplinas_dados, temp_dir):
|
|
252 |
plt.plot(bimestres_deslocados, notas_validas,
|
253 |
color=cores[idx % len(cores)],
|
254 |
marker=marcadores[idx % len(marcadores)],
|
255 |
-
markersize=7,
|
256 |
-
linewidth=1.5,
|
257 |
label=disc_data['disciplina'],
|
258 |
linestyle=estilos_linha[idx % len(estilos_linha)],
|
259 |
alpha=0.8)
|
@@ -291,8 +321,10 @@ def plotar_evolucao_bimestres(disciplinas_dados, temp_dir):
|
|
291 |
|
292 |
anotacoes_usadas[bim_orig].append((nota + y_offset/20, texto))
|
293 |
|
294 |
-
|
295 |
-
|
|
|
|
|
296 |
plt.xlabel('Bimestres', fontsize=10)
|
297 |
plt.ylabel('Notas', fontsize=10)
|
298 |
plt.xticks([1, 2, 3, 4], ['1º Bim', '2º Bim', '3º Bim', '4º Bim'])
|
@@ -312,112 +344,28 @@ def plotar_evolucao_bimestres(disciplinas_dados, temp_dir):
|
|
312 |
|
313 |
plt.tight_layout()
|
314 |
|
315 |
-
|
|
|
|
|
316 |
plt.savefig(plot_path, bbox_inches='tight', dpi=300)
|
317 |
plt.close()
|
318 |
return plot_path
|
319 |
|
320 |
-
def
|
321 |
-
"""Plota gráficos de médias e frequências com destaques."""
|
322 |
-
n_disciplinas = len(disciplinas_dados)
|
323 |
-
|
324 |
-
if not n_disciplinas:
|
325 |
-
raise ValueError("Nenhuma disciplina válida encontrada no boletim.")
|
326 |
-
|
327 |
-
# Aumentar a figura para melhor visualização
|
328 |
-
plt.figure(figsize=(12, 10))
|
329 |
-
|
330 |
-
disciplinas = [d['disciplina'] for d in disciplinas_dados]
|
331 |
-
medias_notas = [d['media_notas'] for d in disciplinas_dados]
|
332 |
-
medias_freq = [d['media_freq'] for d in disciplinas_dados]
|
333 |
-
|
334 |
-
# Criar subplot com mais espaço entre os gráficos
|
335 |
-
fig, (ax1, ax2) = plt.subplots(2, 1, figsize=(12, 10), height_ratios=[1, 1])
|
336 |
-
plt.subplots_adjust(hspace=0.5) # Aumentar espaço entre os gráficos
|
337 |
-
|
338 |
-
cores_notas = ['red' if media < LIMITE_APROVACAO_NOTA else '#2ecc71' for media in medias_notas]
|
339 |
-
cores_freq = ['red' if media < LIMITE_APROVACAO_FREQ else '#2ecc71' for media in medias_freq]
|
340 |
-
|
341 |
-
media_global = np.mean(medias_notas)
|
342 |
-
freq_global = np.mean(medias_freq)
|
343 |
-
|
344 |
-
# Gráfico de notas
|
345 |
-
barras_notas = ax1.bar(disciplinas, medias_notas, color=cores_notas)
|
346 |
-
ax1.set_title('Média de Notas por Disciplina', pad=20, fontsize=12, fontweight='bold')
|
347 |
-
ax1.set_ylim(0, ESCALA_MAXIMA_NOTAS)
|
348 |
-
ax1.grid(True, axis='y', alpha=0.3, linestyle='--')
|
349 |
-
|
350 |
-
# Melhorar a apresentação dos rótulos
|
351 |
-
ax1.set_xticklabels(disciplinas, rotation=45, ha='right', va='top')
|
352 |
-
ax1.set_ylabel('Notas', fontsize=10, labelpad=10)
|
353 |
-
|
354 |
-
# Adicionar linha de média mínima
|
355 |
-
ax1.axhline(y=LIMITE_APROVACAO_NOTA, color='r', linestyle='--', alpha=0.3)
|
356 |
-
ax1.text(0.02, LIMITE_APROVACAO_NOTA + 0.1, 'Média mínima (5,0)',
|
357 |
-
transform=ax1.get_yaxis_transform(), color='r', alpha=0.7)
|
358 |
-
|
359 |
-
# Valores nas barras
|
360 |
-
for barra in barras_notas:
|
361 |
-
altura = barra.get_height()
|
362 |
-
ax1.text(barra.get_x() + barra.get_width()/2., altura,
|
363 |
-
f'{altura:.1f}',
|
364 |
-
ha='center', va='bottom', fontsize=8)
|
365 |
-
|
366 |
-
# Gráfico de frequências
|
367 |
-
barras_freq = ax2.bar(disciplinas, medias_freq, color=cores_freq)
|
368 |
-
ax2.set_title('Frequência Média por Disciplina', pad=20, fontsize=12, fontweight='bold')
|
369 |
-
ax2.set_ylim(0, 110)
|
370 |
-
ax2.grid(True, axis='y', alpha=0.3, linestyle='--')
|
371 |
-
|
372 |
-
# Melhorar a apresentação dos rótulos
|
373 |
-
ax2.set_xticklabels(disciplinas, rotation=45, ha='right', va='top')
|
374 |
-
ax2.set_ylabel('Frequência (%)', fontsize=10, labelpad=10)
|
375 |
-
|
376 |
-
# Adicionar linha de frequência mínima
|
377 |
-
ax2.axhline(y=LIMITE_APROVACAO_FREQ, color='r', linestyle='--', alpha=0.3)
|
378 |
-
ax2.text(0.02, LIMITE_APROVACAO_FREQ + 1, 'Frequência mínima (75%)',
|
379 |
-
transform=ax2.get_yaxis_transform(), color='r', alpha=0.7)
|
380 |
-
|
381 |
-
# Valores nas barras
|
382 |
-
for barra in barras_freq:
|
383 |
-
altura = barra.get_height()
|
384 |
-
ax2.text(barra.get_x() + barra.get_width()/2., altura,
|
385 |
-
f'{altura:.1f}%',
|
386 |
-
ha='center', va='bottom', fontsize=8)
|
387 |
-
|
388 |
-
# Título global com informações de média
|
389 |
-
plt.suptitle(
|
390 |
-
f'Desempenho Geral\nMédia Global: {media_global:.1f} | Frequência Global: {freq_global:.1f}%',
|
391 |
-
y=0.98, fontsize=14, fontweight='bold'
|
392 |
-
)
|
393 |
-
|
394 |
-
if freq_global < LIMITE_APROVACAO_FREQ:
|
395 |
-
plt.figtext(0.5, 0.02,
|
396 |
-
"Atenção: Risco de Reprovação por Baixa Frequência",
|
397 |
-
ha="center", fontsize=11, color="red", weight='bold')
|
398 |
-
|
399 |
-
plt.tight_layout()
|
400 |
-
|
401 |
-
plot_path = os.path.join(temp_dir, 'medias_frequencias.png')
|
402 |
-
plt.savefig(plot_path, bbox_inches='tight', dpi=300)
|
403 |
-
plt.close()
|
404 |
-
return plot_path
|
405 |
-
|
406 |
-
def gerar_relatorio_pdf(df, disciplinas_dados, grafico1_path, grafico2_path):
|
407 |
"""Gera relatório PDF com os gráficos e análises."""
|
408 |
pdf = FPDF()
|
409 |
pdf.set_auto_page_break(auto=True, margin=15)
|
410 |
-
pdf.add_page()
|
411 |
|
412 |
-
#
|
|
|
413 |
pdf.set_font('Helvetica', 'B', 18)
|
414 |
pdf.cell(0, 10, 'Relatório de Desempenho Escolar', 0, new_x=XPos.LMARGIN, new_y=YPos.NEXT, align='C')
|
415 |
-
pdf.ln(15)
|
416 |
|
417 |
# Informações do aluno
|
418 |
pdf.set_font('Helvetica', 'B', 12)
|
419 |
pdf.cell(0, 10, 'Informações do Aluno', 0, new_x=XPos.LMARGIN, new_y=YPos.NEXT, align='L')
|
420 |
-
pdf.line(10, pdf.get_y(), 200, pdf.get_y())
|
421 |
pdf.ln(5)
|
422 |
|
423 |
pdf.set_font('Helvetica', '', 11)
|
@@ -439,18 +387,31 @@ def gerar_relatorio_pdf(df, disciplinas_dados, grafico1_path, grafico2_path):
|
|
439 |
pdf.cell(0, 5, f'Data de geração: {data_atual}', 0, new_x=XPos.LMARGIN, new_y=YPos.NEXT, align='R')
|
440 |
pdf.ln(15)
|
441 |
|
442 |
-
#
|
443 |
pdf.set_font('Helvetica', 'B', 14)
|
444 |
-
pdf.cell(0, 10, '
|
445 |
pdf.line(10, pdf.get_y(), 200, pdf.get_y())
|
446 |
pdf.ln(10)
|
|
|
447 |
|
448 |
-
|
449 |
-
pdf.
|
450 |
-
pdf.
|
451 |
-
pdf.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
452 |
|
453 |
-
#
|
|
|
454 |
pdf.set_font('Helvetica', 'B', 14)
|
455 |
pdf.cell(0, 10, 'Análise Detalhada', 0, new_x=XPos.LMARGIN, new_y=YPos.NEXT, align='L')
|
456 |
pdf.line(10, pdf.get_y(), 200, pdf.get_y())
|
@@ -552,15 +513,33 @@ def processar_boletim(file):
|
|
552 |
if not disciplinas_dados:
|
553 |
return None, "Nenhuma disciplina válida encontrada no boletim."
|
554 |
|
|
|
|
|
|
|
|
|
|
|
555 |
# Gerar gráficos
|
556 |
print("Gerando gráficos...")
|
557 |
-
|
558 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
559 |
print("Gráficos gerados")
|
560 |
|
561 |
# Gerar PDF
|
562 |
print("Gerando relatório PDF...")
|
563 |
-
pdf_path = gerar_relatorio_pdf(df, disciplinas_dados,
|
564 |
print("Relatório PDF gerado")
|
565 |
|
566 |
# Criar arquivo de retorno
|
|
|
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 |
+
'INGLES',
|
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 |
|
|
|
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
|
|
|
129 |
if len(tables) == 0:
|
130 |
raise ValueError("Nenhuma tabela foi extraída do PDF.")
|
131 |
|
|
|
132 |
df = tables[0].df
|
133 |
|
|
|
134 |
info_aluno = {}
|
135 |
for i, row in df.iterrows():
|
136 |
if 'Nome do Aluno' in str(row[0]):
|
|
|
142 |
elif 'Turma' in str(row[0]):
|
143 |
info_aluno['turma'] = row[1].strip() if len(row) > 1 else ''
|
144 |
|
|
|
145 |
for i, table in enumerate(tables):
|
146 |
df_temp = table.df
|
|
|
147 |
if any('Disciplina' in str(col) for col in df_temp.iloc[0]) or \
|
148 |
any('Bimestre' in str(col) for col in df_temp.iloc[0]):
|
149 |
df = df_temp
|
|
|
150 |
df = df.rename(columns={
|
151 |
0: 'Disciplina',
|
152 |
1: 'Nota B1', 2: 'Freq B1', 3: '%Freq B1', 4: 'AC B1',
|
|
|
160 |
if df.empty:
|
161 |
raise ValueError("A tabela extraída está vazia.")
|
162 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
163 |
for key, value in info_aluno.items():
|
164 |
df.attrs[key] = value
|
165 |
|
|
|
168 |
except Exception as e:
|
169 |
print(f"Erro na extração das tabelas: {str(e)}")
|
170 |
raise
|
|
|
171 |
def obter_disciplinas_validas(df):
|
172 |
"""Identifica disciplinas válidas no boletim com seus dados."""
|
173 |
colunas_notas = ['Nota B1', 'Nota B2', 'Nota B3', 'Nota B4']
|
|
|
180 |
if pd.isna(disciplina) or disciplina == '':
|
181 |
continue
|
182 |
|
|
|
183 |
notas = []
|
184 |
freqs = []
|
185 |
bimestres_cursados = []
|
|
|
196 |
notas.append(None)
|
197 |
freqs.append(None)
|
198 |
|
|
|
199 |
if bimestres_cursados:
|
200 |
media_notas = calcular_media_bimestres(notas)
|
201 |
media_freq = calcular_frequencia_media(freqs)
|
|
|
210 |
})
|
211 |
|
212 |
return disciplinas_dados
|
213 |
+
|
214 |
def gerar_paleta_cores(n_cores):
|
215 |
"""Gera uma paleta de cores distintas para o número de disciplinas."""
|
216 |
cores_base = [
|
|
|
227 |
|
228 |
return cores_base[:n_cores]
|
229 |
|
230 |
+
def plotar_evolucao_bimestres(disciplinas_dados, temp_dir, titulo=None, nome_arquivo=None):
|
231 |
"""Plota gráfico de evolução das notas por bimestre com visualização refinada."""
|
232 |
n_disciplinas = len(disciplinas_dados)
|
233 |
|
|
|
282 |
plt.plot(bimestres_deslocados, notas_validas,
|
283 |
color=cores[idx % len(cores)],
|
284 |
marker=marcadores[idx % len(marcadores)],
|
285 |
+
markersize=7,
|
286 |
+
linewidth=1.5,
|
287 |
label=disc_data['disciplina'],
|
288 |
linestyle=estilos_linha[idx % len(estilos_linha)],
|
289 |
alpha=0.8)
|
|
|
321 |
|
322 |
anotacoes_usadas[bim_orig].append((nota + y_offset/20, texto))
|
323 |
|
324 |
+
# Usar título personalizado se fornecido
|
325 |
+
titulo_grafico = titulo or 'Evolução das Médias por Disciplina ao Longo dos Bimestres'
|
326 |
+
plt.title(titulo_grafico, pad=20, fontsize=12, fontweight='bold')
|
327 |
+
|
328 |
plt.xlabel('Bimestres', fontsize=10)
|
329 |
plt.ylabel('Notas', fontsize=10)
|
330 |
plt.xticks([1, 2, 3, 4], ['1º Bim', '2º Bim', '3º Bim', '4º Bim'])
|
|
|
344 |
|
345 |
plt.tight_layout()
|
346 |
|
347 |
+
# Usar nome de arquivo personalizado se fornecido
|
348 |
+
nome_arquivo = nome_arquivo or 'evolucao_notas.png'
|
349 |
+
plot_path = os.path.join(temp_dir, nome_arquivo)
|
350 |
plt.savefig(plot_path, bbox_inches='tight', dpi=300)
|
351 |
plt.close()
|
352 |
return plot_path
|
353 |
|
354 |
+
def gerar_relatorio_pdf(df, disciplinas_dados, grafico_basica, grafico_diversificada, grafico_medias):
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
355 |
"""Gera relatório PDF com os gráficos e análises."""
|
356 |
pdf = FPDF()
|
357 |
pdf.set_auto_page_break(auto=True, margin=15)
|
|
|
358 |
|
359 |
+
# Primeira página - Informações e Formação Básica
|
360 |
+
pdf.add_page()
|
361 |
pdf.set_font('Helvetica', 'B', 18)
|
362 |
pdf.cell(0, 10, 'Relatório de Desempenho Escolar', 0, new_x=XPos.LMARGIN, new_y=YPos.NEXT, align='C')
|
363 |
+
pdf.ln(15)
|
364 |
|
365 |
# Informações do aluno
|
366 |
pdf.set_font('Helvetica', 'B', 12)
|
367 |
pdf.cell(0, 10, 'Informações do Aluno', 0, new_x=XPos.LMARGIN, new_y=YPos.NEXT, align='L')
|
368 |
+
pdf.line(10, pdf.get_y(), 200, pdf.get_y())
|
369 |
pdf.ln(5)
|
370 |
|
371 |
pdf.set_font('Helvetica', '', 11)
|
|
|
387 |
pdf.cell(0, 5, f'Data de geração: {data_atual}', 0, new_x=XPos.LMARGIN, new_y=YPos.NEXT, align='R')
|
388 |
pdf.ln(15)
|
389 |
|
390 |
+
# Gráfico de evolução da formação básica
|
391 |
pdf.set_font('Helvetica', 'B', 14)
|
392 |
+
pdf.cell(0, 10, 'Evolução das Notas - Formação Geral Básica', 0, new_x=XPos.LMARGIN, new_y=YPos.NEXT, align='L')
|
393 |
pdf.line(10, pdf.get_y(), 200, pdf.get_y())
|
394 |
pdf.ln(10)
|
395 |
+
pdf.image(grafico_basica, x=10, w=190)
|
396 |
|
397 |
+
# Segunda página - Parte Diversificada
|
398 |
+
pdf.add_page()
|
399 |
+
pdf.set_font('Helvetica', 'B', 14)
|
400 |
+
pdf.cell(0, 10, 'Evolução das Notas - Parte Diversificada', 0, new_x=XPos.LMARGIN, new_y=YPos.NEXT, align='L')
|
401 |
+
pdf.line(10, pdf.get_y(), 200, pdf.get_y())
|
402 |
+
pdf.ln(10)
|
403 |
+
pdf.image(grafico_diversificada, x=10, w=190)
|
404 |
+
|
405 |
+
# Terceira página - Médias e Frequências
|
406 |
+
pdf.add_page()
|
407 |
+
pdf.set_font('Helvetica', 'B', 14)
|
408 |
+
pdf.cell(0, 10, 'Análise de Médias e Frequências', 0, new_x=XPos.LMARGIN, new_y=YPos.NEXT, align='L')
|
409 |
+
pdf.line(10, pdf.get_y(), 200, pdf.get_y())
|
410 |
+
pdf.ln(10)
|
411 |
+
pdf.image(grafico_medias, x=10, w=190)
|
412 |
|
413 |
+
# Quarta página - Análise Detalhada
|
414 |
+
pdf.add_page()
|
415 |
pdf.set_font('Helvetica', 'B', 14)
|
416 |
pdf.cell(0, 10, 'Análise Detalhada', 0, new_x=XPos.LMARGIN, new_y=YPos.NEXT, align='L')
|
417 |
pdf.line(10, pdf.get_y(), 200, pdf.get_y())
|
|
|
513 |
if not disciplinas_dados:
|
514 |
return None, "Nenhuma disciplina válida encontrada no boletim."
|
515 |
|
516 |
+
# Separar disciplinas por categoria
|
517 |
+
categorias = separar_disciplinas_por_categoria(disciplinas_dados)
|
518 |
+
nivel = categorias['nivel']
|
519 |
+
nivel_texto = "Ensino Médio" if nivel == "medio" else "Ensino Fundamental"
|
520 |
+
|
521 |
# Gerar gráficos
|
522 |
print("Gerando gráficos...")
|
523 |
+
grafico_basica = plotar_evolucao_bimestres(
|
524 |
+
categorias['formacao_basica'],
|
525 |
+
temp_dir,
|
526 |
+
titulo=f"Evolução das Médias - Formação Geral Básica ({nivel_texto})",
|
527 |
+
nome_arquivo='evolucao_basica.png'
|
528 |
+
)
|
529 |
+
|
530 |
+
grafico_diversificada = plotar_evolucao_bimestres(
|
531 |
+
categorias['diversificada'],
|
532 |
+
temp_dir,
|
533 |
+
titulo=f"Evolução das Médias - Parte Diversificada ({nivel_texto})",
|
534 |
+
nome_arquivo='evolucao_diversificada.png'
|
535 |
+
)
|
536 |
+
|
537 |
+
grafico_medias = plotar_graficos_destacados(disciplinas_dados, temp_dir)
|
538 |
print("Gráficos gerados")
|
539 |
|
540 |
# Gerar PDF
|
541 |
print("Gerando relatório PDF...")
|
542 |
+
pdf_path = gerar_relatorio_pdf(df, disciplinas_dados, grafico_basica, grafico_diversificada, grafico_medias)
|
543 |
print("Relatório PDF gerado")
|
544 |
|
545 |
# Criar arquivo de retorno
|