Spaces:
Sleeping
Sleeping
Update app.py
Browse files
app.py
CHANGED
@@ -9,14 +9,9 @@ import tempfile
|
|
9 |
import os
|
10 |
import matplotlib
|
11 |
import shutil
|
|
|
12 |
matplotlib.use('Agg')
|
13 |
|
14 |
-
# Lista global de disciplinas básicas
|
15 |
-
DISCIPLINAS_BASICAS = [
|
16 |
-
'LINGUA PORTUGUESA', 'ARTE', 'LINGUA ESTRANGEIRA INGLES',
|
17 |
-
'GEOGRAFIA', 'CIENCIAS', 'HISTORIA', 'MATEMATICA'
|
18 |
-
]
|
19 |
-
|
20 |
def extrair_tabelas_pdf(pdf_path):
|
21 |
"""Extrai tabelas do PDF e retorna um DataFrame processado."""
|
22 |
try:
|
@@ -59,28 +54,66 @@ def converter_nota(valor):
|
|
59 |
except:
|
60 |
return 0
|
61 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
62 |
def plotar_evolucao_bimestres(df_filtrado, temp_dir):
|
63 |
"""Plota gráfico de evolução das notas por bimestre."""
|
64 |
-
|
|
|
|
|
65 |
|
66 |
-
#
|
67 |
-
|
|
|
68 |
|
69 |
-
|
70 |
-
|
71 |
-
|
72 |
-
|
73 |
-
'GEOGRAFIA': {'cor': '#32CD32', 'marcador': '^', 'zorder': 4, 'linestyle': ':', 'desloc': 0.04},
|
74 |
-
'CIENCIAS': {'cor': '#FF8C00', 'marcador': 's', 'zorder': 5, 'linestyle': '-', 'desloc': 0.02},
|
75 |
-
'HISTORIA': {'cor': '#00CED1', 'marcador': '*', 'zorder': 6, 'linestyle': '--', 'desloc': -0.02},
|
76 |
-
'MATEMATICA': {'cor': '#FF69B4', 'marcador': 'o', 'zorder': 7, 'linestyle': '-.', 'desloc': -0.04}
|
77 |
-
}
|
78 |
|
79 |
plt.grid(True, linestyle='--', alpha=0.3, zorder=0)
|
80 |
|
81 |
colunas_notas = ['Nota B1', 'Nota B2', 'Nota B3', 'Nota B4']
|
82 |
|
83 |
-
for disciplina in
|
84 |
dados_disciplina = df_filtrado[df_filtrado['Disciplina'] == disciplina]
|
85 |
if not dados_disciplina.empty:
|
86 |
notas = dados_disciplina[colunas_notas].values[0]
|
@@ -90,28 +123,34 @@ def plotar_evolucao_bimestres(df_filtrado, temp_dir):
|
|
90 |
bimestres = np.arange(1, len(colunas_notas) + 1)[notas_validas]
|
91 |
notas_filtradas = notas[notas_validas]
|
92 |
|
93 |
-
|
94 |
-
|
95 |
-
|
96 |
-
|
97 |
-
|
98 |
-
marker=estilo['marcador'],
|
99 |
-
markersize=10,
|
100 |
-
linewidth=2.5,
|
101 |
label=disciplina,
|
102 |
-
|
103 |
-
linestyle=estilo['linestyle'],
|
104 |
alpha=0.8)
|
105 |
|
106 |
for x, y in zip(bimestres, notas_filtradas):
|
107 |
-
plt.annotate(f"{y:.1f}", (x, y),
|
|
|
|
|
|
|
|
|
108 |
|
109 |
plt.title('Evolução das Médias por Disciplina ao Longo dos Bimestres')
|
110 |
plt.xlabel('Bimestres')
|
111 |
plt.ylabel('Média de Notas')
|
112 |
plt.xticks([1, 2, 3, 4], ['B1', 'B2', 'B3', 'B4'])
|
113 |
plt.ylim(0, 10)
|
114 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
115 |
plt.tight_layout()
|
116 |
|
117 |
plot_path = os.path.join(temp_dir, 'evolucao_notas.png')
|
@@ -121,10 +160,15 @@ def plotar_evolucao_bimestres(df_filtrado, temp_dir):
|
|
121 |
|
122 |
def plotar_graficos_destacados(df_boletim_clean, temp_dir):
|
123 |
"""Plota gráficos de médias e frequências com destaques."""
|
124 |
-
|
|
|
|
|
125 |
|
126 |
-
#
|
127 |
-
|
|
|
|
|
|
|
128 |
disciplinas = df_filtrado['Disciplina'].astype(str)
|
129 |
|
130 |
# Processar frequências (remover % e converter para número)
|
@@ -140,21 +184,36 @@ def plotar_graficos_destacados(df_boletim_clean, temp_dir):
|
|
140 |
frequencia_global_media = medias_frequencia.mean()
|
141 |
|
142 |
plt.subplot(1, 2, 1)
|
143 |
-
plt.bar(disciplinas, medias_notas, color=cores_notas)
|
144 |
plt.title('Média de Notas por Disciplina (Vermelho: < 5)')
|
145 |
-
plt.xticks(rotation=
|
146 |
plt.ylim(0, 10)
|
147 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
148 |
plt.subplot(1, 2, 2)
|
149 |
-
plt.bar(disciplinas, medias_frequencia, color=cores_frequencias)
|
150 |
plt.title('Média de Frequência por Disciplina (Vermelho: < 75%)')
|
151 |
-
plt.xticks(rotation=
|
152 |
plt.ylim(0, 100)
|
153 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
154 |
plt.suptitle(f"Frequência Global Média: {frequencia_global_media:.2f}%")
|
155 |
|
156 |
if frequencia_global_media < 75:
|
157 |
-
plt.figtext(0.5, 0.
|
|
|
158 |
|
159 |
plt.tight_layout()
|
160 |
|
@@ -172,6 +231,11 @@ def gerar_relatorio_pdf(df, grafico1_path, grafico2_path):
|
|
172 |
pdf.cell(0, 10, 'Relatório de Desempenho Escolar', 0, new_x=XPos.LMARGIN, new_y=YPos.NEXT, align='C')
|
173 |
pdf.ln(10)
|
174 |
|
|
|
|
|
|
|
|
|
|
|
175 |
pdf.image(grafico1_path, x=10, w=190)
|
176 |
pdf.ln(10)
|
177 |
pdf.image(grafico2_path, x=10, w=190)
|
@@ -181,8 +245,9 @@ def gerar_relatorio_pdf(df, grafico1_path, grafico2_path):
|
|
181 |
pdf.cell(0, 10, 'Avisos Importantes:', 0, new_x=XPos.LMARGIN, new_y=YPos.NEXT, align='L')
|
182 |
pdf.set_font('Helvetica', '', 10)
|
183 |
|
184 |
-
#
|
185 |
-
|
|
|
186 |
|
187 |
# Calcular médias
|
188 |
medias_notas = df_filtrado[['Nota B1', 'Nota B2', 'Nota B3', 'Nota B4']].apply(pd.to_numeric, errors='coerce').mean(axis=1)
|
@@ -192,6 +257,14 @@ def gerar_relatorio_pdf(df, grafico1_path, grafico2_path):
|
|
192 |
freq_data = df_filtrado[colunas_freq].replace('%', '', regex=True)
|
193 |
medias_freq = freq_data.apply(pd.to_numeric, errors='coerce').mean(axis=1)
|
194 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
195 |
for idx, (disciplina, media_nota, media_freq) in enumerate(zip(df_filtrado['Disciplina'], medias_notas, medias_freq)):
|
196 |
if media_nota < 5:
|
197 |
pdf.cell(0, 10, f'- {disciplina}: Média de notas abaixo de 5 ({media_nota:.1f})', 0,
|
@@ -267,7 +340,7 @@ def processar_boletim(file):
|
|
267 |
print("Gerando relatório PDF...")
|
268 |
pdf_path = gerar_relatorio_pdf(df, grafico1_path, grafico2_path)
|
269 |
print("Relatório PDF gerado")
|
270 |
-
|
271 |
# Criar arquivo temporário para retorno
|
272 |
output_file = tempfile.NamedTemporaryFile(delete=False, suffix='.pdf')
|
273 |
output_path = output_file.name
|
|
|
9 |
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:
|
|
|
54 |
except:
|
55 |
return 0
|
56 |
|
57 |
+
def obter_disciplinas_validas(df):
|
58 |
+
"""Identifica disciplinas válidas no boletim."""
|
59 |
+
# Colunas de notas e frequências
|
60 |
+
colunas_notas = ['Nota B1', 'Nota B2', 'Nota B3', 'Nota B4']
|
61 |
+
colunas_freq = ['%Freq B1', '%Freq B2', '%Freq B3', '%Freq B4']
|
62 |
+
|
63 |
+
# Converter notas para numérico
|
64 |
+
for col in colunas_notas:
|
65 |
+
if col in df.columns:
|
66 |
+
df[col] = df[col].apply(converter_nota)
|
67 |
+
|
68 |
+
# Identificar disciplinas que têm pelo menos uma nota ou frequência
|
69 |
+
disciplinas_validas = []
|
70 |
+
for _, row in df.iterrows():
|
71 |
+
disciplina = row['Disciplina']
|
72 |
+
notas = row[colunas_notas].astype(float)
|
73 |
+
freq = row[colunas_freq].replace('%', '', regex=True).astype(float)
|
74 |
+
|
75 |
+
if (notas > 0).any() or (freq > 0).any():
|
76 |
+
disciplinas_validas.append(disciplina)
|
77 |
+
|
78 |
+
return disciplinas_validas
|
79 |
+
|
80 |
+
def gerar_paleta_cores(n_cores):
|
81 |
+
"""Gera uma paleta de cores distintas para o número de disciplinas."""
|
82 |
+
cores_base = [
|
83 |
+
'#DC143C', '#4169E1', '#9370DB', '#32CD32', '#FF8C00',
|
84 |
+
'#00CED1', '#FF69B4', '#8B4513', '#4B0082', '#556B2F',
|
85 |
+
'#B8860B', '#483D8B', '#008B8B', '#8B008B', '#8B0000'
|
86 |
+
]
|
87 |
+
|
88 |
+
# Se precisar de mais cores, gerar automaticamente
|
89 |
+
if n_cores > len(cores_base):
|
90 |
+
HSV_tuples = [(x/n_cores, 0.8, 0.9) for x in range(n_cores)]
|
91 |
+
cores_extras = ['#%02x%02x%02x' % tuple(int(x*255) for x in colorsys.hsv_to_rgb(*hsv))
|
92 |
+
for hsv in HSV_tuples]
|
93 |
+
return cores_extras
|
94 |
+
|
95 |
+
return cores_base[:n_cores]
|
96 |
+
|
97 |
def plotar_evolucao_bimestres(df_filtrado, temp_dir):
|
98 |
"""Plota gráfico de evolução das notas por bimestre."""
|
99 |
+
# Obter disciplinas válidas
|
100 |
+
disciplinas_validas = obter_disciplinas_validas(df_filtrado)
|
101 |
+
n_disciplinas = len(disciplinas_validas)
|
102 |
|
103 |
+
# Calcular tamanho da figura baseado no número de disciplinas
|
104 |
+
altura_figura = max(6, n_disciplinas * 0.4)
|
105 |
+
plt.figure(figsize=(14, altura_figura))
|
106 |
|
107 |
+
# Gerar cores para as disciplinas
|
108 |
+
cores = gerar_paleta_cores(n_disciplinas)
|
109 |
+
marcadores = ['o', 's', '^', 'D', 'v', '<', '>', 'p', 'h', '8', '*', 'H', '+', 'x', 'd']
|
110 |
+
estilos_linha = ['-', '--', '-.', ':', '-', '--', '-.', ':', '-', '--', '-.', ':', '-', '--', '-.']
|
|
|
|
|
|
|
|
|
|
|
111 |
|
112 |
plt.grid(True, linestyle='--', alpha=0.3, zorder=0)
|
113 |
|
114 |
colunas_notas = ['Nota B1', 'Nota B2', 'Nota B3', 'Nota B4']
|
115 |
|
116 |
+
for idx, disciplina in enumerate(disciplinas_validas):
|
117 |
dados_disciplina = df_filtrado[df_filtrado['Disciplina'] == disciplina]
|
118 |
if not dados_disciplina.empty:
|
119 |
notas = dados_disciplina[colunas_notas].values[0]
|
|
|
123 |
bimestres = np.arange(1, len(colunas_notas) + 1)[notas_validas]
|
124 |
notas_filtradas = notas[notas_validas]
|
125 |
|
126 |
+
plt.plot(bimestres, notas_filtradas,
|
127 |
+
color=cores[idx % len(cores)],
|
128 |
+
marker=marcadores[idx % len(marcadores)],
|
129 |
+
markersize=8,
|
130 |
+
linewidth=2,
|
|
|
|
|
|
|
131 |
label=disciplina,
|
132 |
+
linestyle=estilos_linha[idx % len(estilos_linha)],
|
|
|
133 |
alpha=0.8)
|
134 |
|
135 |
for x, y in zip(bimestres, notas_filtradas):
|
136 |
+
plt.annotate(f"{y:.1f}", (x, y),
|
137 |
+
textcoords="offset points",
|
138 |
+
xytext=(0, 5),
|
139 |
+
ha='center',
|
140 |
+
fontsize=8)
|
141 |
|
142 |
plt.title('Evolução das Médias por Disciplina ao Longo dos Bimestres')
|
143 |
plt.xlabel('Bimestres')
|
144 |
plt.ylabel('Média de Notas')
|
145 |
plt.xticks([1, 2, 3, 4], ['B1', 'B2', 'B3', 'B4'])
|
146 |
plt.ylim(0, 10)
|
147 |
+
|
148 |
+
# Ajustar legenda baseado no número de disciplinas
|
149 |
+
if n_disciplinas > 10:
|
150 |
+
plt.legend(bbox_to_anchor=(1.05, 1), loc='upper left', fontsize=8)
|
151 |
+
else:
|
152 |
+
plt.legend(bbox_to_anchor=(1.05, 1), loc='upper left')
|
153 |
+
|
154 |
plt.tight_layout()
|
155 |
|
156 |
plot_path = os.path.join(temp_dir, 'evolucao_notas.png')
|
|
|
160 |
|
161 |
def plotar_graficos_destacados(df_boletim_clean, temp_dir):
|
162 |
"""Plota gráficos de médias e frequências com destaques."""
|
163 |
+
# Obter disciplinas válidas
|
164 |
+
disciplinas_validas = obter_disciplinas_validas(df_boletim_clean)
|
165 |
+
n_disciplinas = len(disciplinas_validas)
|
166 |
|
167 |
+
# Calcular tamanho da figura baseado no número de disciplinas
|
168 |
+
altura_figura = max(6, n_disciplinas * 0.4)
|
169 |
+
plt.figure(figsize=(14, altura_figura))
|
170 |
+
|
171 |
+
df_filtrado = df_boletim_clean[df_boletim_clean['Disciplina'].isin(disciplinas_validas)]
|
172 |
disciplinas = df_filtrado['Disciplina'].astype(str)
|
173 |
|
174 |
# Processar frequências (remover % e converter para número)
|
|
|
184 |
frequencia_global_media = medias_frequencia.mean()
|
185 |
|
186 |
plt.subplot(1, 2, 1)
|
187 |
+
barras_notas = plt.bar(disciplinas, medias_notas, color=cores_notas)
|
188 |
plt.title('Média de Notas por Disciplina (Vermelho: < 5)')
|
189 |
+
plt.xticks(rotation=45, ha='right')
|
190 |
plt.ylim(0, 10)
|
191 |
|
192 |
+
# Adicionar valores nas barras
|
193 |
+
for barra in barras_notas:
|
194 |
+
altura = barra.get_height()
|
195 |
+
plt.text(barra.get_x() + barra.get_width()/2., altura,
|
196 |
+
f'{altura:.1f}',
|
197 |
+
ha='center', va='bottom')
|
198 |
+
|
199 |
plt.subplot(1, 2, 2)
|
200 |
+
barras_freq = plt.bar(disciplinas, medias_frequencia, color=cores_frequencias)
|
201 |
plt.title('Média de Frequência por Disciplina (Vermelho: < 75%)')
|
202 |
+
plt.xticks(rotation=45, ha='right')
|
203 |
plt.ylim(0, 100)
|
204 |
|
205 |
+
# Adicionar valores nas barras
|
206 |
+
for barra in barras_freq:
|
207 |
+
altura = barra.get_height()
|
208 |
+
plt.text(barra.get_x() + barra.get_width()/2., altura,
|
209 |
+
f'{altura:.1f}%',
|
210 |
+
ha='center', va='bottom')
|
211 |
+
|
212 |
plt.suptitle(f"Frequência Global Média: {frequencia_global_media:.2f}%")
|
213 |
|
214 |
if frequencia_global_media < 75:
|
215 |
+
plt.figtext(0.5, 0.02, "Cuidado: Risco de Reprovação por Baixa Frequência",
|
216 |
+
ha="center", fontsize=12, color="red")
|
217 |
|
218 |
plt.tight_layout()
|
219 |
|
|
|
231 |
pdf.cell(0, 10, 'Relatório de Desempenho Escolar', 0, new_x=XPos.LMARGIN, new_y=YPos.NEXT, align='C')
|
232 |
pdf.ln(10)
|
233 |
|
234 |
+
# Informações do aluno se disponíveis
|
235 |
+
if 'Nome do Aluno' in df.columns:
|
236 |
+
pdf.set_font('Helvetica', '', 12)
|
237 |
+
pdf.cell(0, 10, f'Aluno: {df["Nome do Aluno"].iloc[0]}', 0, new_x=XPos.LMARGIN, new_y=YPos.NEXT, align='L')
|
238 |
+
|
239 |
pdf.image(grafico1_path, x=10, w=190)
|
240 |
pdf.ln(10)
|
241 |
pdf.image(grafico2_path, x=10, w=190)
|
|
|
245 |
pdf.cell(0, 10, 'Avisos Importantes:', 0, new_x=XPos.LMARGIN, new_y=YPos.NEXT, align='L')
|
246 |
pdf.set_font('Helvetica', '', 10)
|
247 |
|
248 |
+
# Obter disciplinas válidas
|
249 |
+
disciplinas_validas = obter_disciplinas_validas(df)
|
250 |
+
df_filtrado = df[df['Disciplina'].isin(disciplinas_validas)]
|
251 |
|
252 |
# Calcular médias
|
253 |
medias_notas = df_filtrado[['Nota B1', 'Nota B2', 'Nota B3', 'Nota B4']].apply(pd.to_numeric, errors='coerce').mean(axis=1)
|
|
|
257 |
freq_data = df_filtrado[colunas_freq].replace('%', '', regex=True)
|
258 |
medias_freq = freq_data.apply(pd.to_numeric, errors='coerce').mean(axis=1)
|
259 |
|
260 |
+
# Adicionar média global
|
261 |
+
media_global = medias_notas.mean()
|
262 |
+
freq_global = medias_freq.mean()
|
263 |
+
|
264 |
+
pdf.cell(0, 10, f'Média Global: {media_global:.1f}', 0, new_x=XPos.LMARGIN, new_y=YPos.NEXT, align='L')
|
265 |
+
pdf.cell(0, 10, f'Frequência Global: {freq_global:.1f}%', 0, new_x=XPos.LMARGIN, new_y=YPos.NEXT, align='L')
|
266 |
+
pdf.ln(5)
|
267 |
+
|
268 |
for idx, (disciplina, media_nota, media_freq) in enumerate(zip(df_filtrado['Disciplina'], medias_notas, medias_freq)):
|
269 |
if media_nota < 5:
|
270 |
pdf.cell(0, 10, f'- {disciplina}: Média de notas abaixo de 5 ({media_nota:.1f})', 0,
|
|
|
340 |
print("Gerando relatório PDF...")
|
341 |
pdf_path = gerar_relatorio_pdf(df, grafico1_path, grafico2_path)
|
342 |
print("Relatório PDF gerado")
|
343 |
+
|
344 |
# Criar arquivo temporário para retorno
|
345 |
output_file = tempfile.NamedTemporaryFile(delete=False, suffix='.pdf')
|
346 |
output_path = output_file.name
|