Spaces:
Running
Running
import gradio as gr | |
import pandas as pd | |
import numpy as np | |
import re | |
import os | |
import matplotlib.pyplot as plt | |
from datetime import timedelta | |
from fpdf import FPDF | |
from typing import Tuple, Dict, List | |
import logging | |
import warnings | |
import seaborn as sns | |
from matplotlib.colors import LinearSegmentedColormap | |
from matplotlib.ticker import MaxNLocator | |
from math import ceil | |
# Configura o estilo seaborn e suprime warnings | |
warnings.filterwarnings('ignore') | |
sns.set_style("whitegrid") | |
# Configuração de logging | |
logging.basicConfig( | |
level=logging.INFO, | |
format='%(asctime)s - %(levelname)s - %(message)s', | |
handlers=[ | |
logging.FileHandler("app.log"), | |
logging.StreamHandler() | |
] | |
) | |
class DataProcessor: | |
def parse_duration(duration_str: str) -> timedelta: | |
"""Converte string de duração em objeto timedelta.""" | |
try: | |
if isinstance(duration_str, str) and ':' in duration_str: | |
h, m, s = map(int, duration_str.split(':')) | |
return timedelta(hours=h, minutes=m, seconds=s) | |
except Exception as e: | |
logging.warning(f"Erro ao processar duração '{duration_str}': {str(e)}") | |
return timedelta(0) | |
def format_timedelta(td: timedelta) -> str: | |
"""Formata timedelta em string legível.""" | |
total_seconds = int(td.total_seconds()) | |
hours, remainder = divmod(total_seconds, 3600) | |
minutes, seconds = divmod(remainder, 60) | |
if hours > 0: | |
return f"{hours}h {minutes}min {seconds}s" | |
elif minutes > 0: | |
return f"{minutes}min {seconds}s" | |
return f"{seconds}s" | |
def normalize_html_to_csv(input_html_path: str, output_csv_path: str) -> None: | |
"""Converte arquivo HTML para CSV.""" | |
try: | |
html_data = pd.read_html(input_html_path) | |
data = html_data[0] | |
data.to_csv(output_csv_path, index=False, encoding='utf-8-sig') | |
logging.info(f"HTML normalizado com sucesso: {output_csv_path}") | |
except Exception as e: | |
logging.error(f"Erro ao normalizar HTML: {str(e)}") | |
raise | |
def normalize_excel_to_csv(input_excel_path: str, output_csv_path: str) -> None: | |
"""Converte arquivo Excel para CSV.""" | |
try: | |
excel_data = pd.read_excel(input_excel_path) | |
unnecessary_columns = [col for col in excel_data.columns if 'Unnamed' in str(col)] | |
if unnecessary_columns: | |
excel_data = excel_data.drop(columns=unnecessary_columns) | |
excel_data.to_csv(output_csv_path, index=False, encoding='utf-8-sig') | |
logging.info(f"Excel normalizado com sucesso: {output_csv_path}") | |
except Exception as e: | |
logging.error(f"Erro ao normalizar Excel: {str(e)}") | |
raise | |
class StudentAnalyzer: | |
def __init__(self, tarefas_df: pd.DataFrame, alunos_df: pd.DataFrame): | |
"""Inicializa o analisador com DataFrames de tarefas e alunos.""" | |
self.tarefas_df = tarefas_df | |
self.alunos_df = alunos_df | |
self.processor = DataProcessor() | |
def prepare_data(self) -> pd.DataFrame: | |
"""Prepara os dados para análise.""" | |
self.tarefas_df.columns = self.tarefas_df.columns.str.strip() | |
self.alunos_df.columns = self.alunos_df.columns.str.strip() | |
required_columns = ['Aluno', 'Nota', 'Duração'] | |
if not all(col in self.tarefas_df.columns for col in required_columns): | |
raise ValueError("Colunas obrigatórias não encontradas no arquivo de tarefas") | |
self.tarefas_df['Duração'] = self.tarefas_df['Duração'].apply(self.processor.parse_duration) | |
return self.match_students() | |
def match_students(self) -> pd.DataFrame: | |
"""Realiza o match entre alunos e tarefas.""" | |
def generate_aluno_pattern(ra: str, dig_ra: str) -> str: | |
ra_str = str(ra).zfill(9) | |
return f"{ra_str[1]}{ra_str[2:]}{dig_ra}-sp".lower() | |
self.alunos_df['Aluno_Pattern'] = self.alunos_df.apply( | |
lambda row: generate_aluno_pattern(row['RA'], row['Dig. RA']), axis=1 | |
) | |
def extract_pattern(nome: str) -> str: | |
if isinstance(nome, str): | |
match = re.search(r'\d+.*', nome.lower()) | |
return match.group(0) if match else None | |
return None | |
self.tarefas_df['Aluno_Pattern'] = self.tarefas_df['Aluno'].apply(extract_pattern) | |
return self.calculate_metrics() | |
def calculate_metrics(self) -> pd.DataFrame: | |
"""Calcula métricas de desempenho dos alunos.""" | |
metrics_df = pd.DataFrame() | |
for _, aluno in self.alunos_df.iterrows(): | |
aluno_pattern = aluno['Aluno_Pattern'] | |
aluno_tarefas = self.tarefas_df[self.tarefas_df['Aluno_Pattern'] == aluno_pattern] | |
if not aluno_tarefas.empty: | |
duracao_total = aluno_tarefas['Duração'].sum() | |
acertos_total = aluno_tarefas['Nota'].sum() | |
metrics = { | |
'Nome do Aluno': aluno['Nome do Aluno'], | |
'Tarefas Completadas': len(aluno_tarefas), | |
'Acertos Absolutos': acertos_total, | |
'Total Tempo': str(duracao_total), | |
'Tempo Médio por Tarefa': str(duracao_total / len(aluno_tarefas)) | |
} | |
metrics_df = pd.concat([metrics_df, pd.DataFrame([metrics])], ignore_index=True) | |
return metrics_df.sort_values('Acertos Absolutos', ascending=False) | |
class ReportGenerator: | |
"""Classe responsável pela geração de relatórios e visualizações.""" | |
def __init__(self, data: pd.DataFrame): | |
self.data = data | |
self.stats = self.calculate_statistics() | |
self.data['Nível'] = self.data['Acertos Absolutos'].apply(self.classify_performance) | |
self.colors = { | |
'Avançado': '#2ecc71', | |
'Intermediário': '#f1c40f', | |
'Necessita Atenção': '#e74c3c' | |
} | |
self.setup_plot_style() | |
def setup_plot_style(self): | |
"""Configura o estilo padrão dos gráficos.""" | |
plt.rcParams['figure.figsize'] = [15, 10] | |
plt.rcParams['font.size'] = 11 | |
plt.rcParams['axes.titlesize'] = 14 | |
plt.rcParams['axes.labelsize'] = 12 | |
plt.rcParams['axes.grid'] = True | |
plt.rcParams['grid.alpha'] = 0.3 | |
plt.rcParams['grid.linestyle'] = '--' | |
def classify_performance(self, acertos: float) -> str: | |
"""Classifica o desempenho do aluno baseado no número de acertos.""" | |
if acertos >= 10: | |
return 'Avançado' | |
elif acertos >= 5: | |
return 'Intermediário' | |
else: | |
return 'Necessita Atenção' | |
def calculate_statistics(self) -> Dict: | |
"""Calcula estatísticas básicas do desempenho dos alunos.""" | |
try: | |
basic_stats = { | |
'media_acertos': float(self.data['Acertos Absolutos'].mean()), | |
'desvio_padrao': float(self.data['Acertos Absolutos'].std()), | |
'mediana_acertos': float(self.data['Acertos Absolutos'].median()), | |
'total_alunos': len(self.data), | |
'media_tarefas': float(self.data['Tarefas Completadas'].mean()), | |
'media_tempo': str(pd.to_timedelta(self.data['Total Tempo']).mean()) | |
} | |
top_students = self.data.nlargest(3, 'Acertos Absolutos')[ | |
['Nome do Aluno', 'Acertos Absolutos'] | |
].values.tolist() | |
basic_stats['top_performers'] = top_students | |
return basic_stats | |
except Exception as e: | |
logging.error(f"Erro ao calcular estatísticas: {str(e)}") | |
raise | |
def create_distribution_plot(self) -> plt.Figure: | |
"""Cria o gráfico de distribuição por nível.""" | |
plt.figure(figsize=(15, 8)) | |
nivel_counts = self.data['Nível'].value_counts() | |
total_alunos = len(self.data) | |
bars = plt.bar(nivel_counts.index, nivel_counts.values, width=0.6) | |
for i, bar in enumerate(bars): | |
bar.set_color(self.colors[nivel_counts.index[i]]) | |
percentage = (nivel_counts.values[i] / total_alunos) * 100 | |
plt.text(bar.get_x() + bar.get_width()/2, bar.get_height(), | |
f'{nivel_counts.values[i]}\n({percentage:.1f}%)', | |
ha='center', va='bottom', fontsize=12, fontweight='bold') | |
plt.title('Distribuição dos Alunos por Nível de Desempenho', pad=20) | |
plt.ylabel('Número de Alunos') | |
plt.grid(True, axis='y', alpha=0.3) | |
return plt.gcf() | |
def create_ranking_plot(self) -> plt.Figure: | |
"""Cria o gráfico de ranking completo dos alunos.""" | |
plt.figure(figsize=(15, max(10, len(self.data) * 0.4))) | |
students_data = self.data.sort_values('Acertos Absolutos', ascending=True) | |
colors = [self.colors[nivel] for nivel in students_data['Nível']] | |
bars = plt.barh(range(len(students_data)), students_data['Acertos Absolutos']) | |
for bar, color in zip(bars, colors): | |
bar.set_color(color) | |
bar.set_alpha(0.8) | |
plt.yticks(range(len(students_data)), students_data['Nome do Aluno'], | |
fontsize=10) | |
for i, bar in enumerate(bars): | |
plt.text(bar.get_width(), i, f' {bar.get_width():.0f}', | |
va='center', fontsize=10, fontweight='bold') | |
plt.title('Ranking Completo - Acertos Absolutos', pad=20) | |
plt.xlabel('Número de Acertos') | |
plt.grid(True, axis='x', alpha=0.3) | |
return plt.gcf() | |
def create_time_performance_plot(self) -> plt.Figure: | |
"""Cria o gráfico de relação entre tempo e acertos com melhor legibilidade.""" | |
plt.figure(figsize=(15, 10)) | |
# Configurar fundo e grade | |
plt.grid(True, alpha=0.2, linestyle='--') | |
plt.gca().set_facecolor('#f8f9fa') | |
# Scatter plot com cores por nível | |
for nivel, color in self.colors.items(): | |
mask = self.data['Nível'] == nivel | |
tempo = pd.to_timedelta(self.data[mask]['Total Tempo']).dt.total_seconds() / 60 | |
plt.scatter(tempo, self.data[mask]['Acertos Absolutos'], | |
c=color, label=nivel, alpha=0.7, s=150) | |
# Melhorar posicionamento dos rótulos | |
for x, y, nome in zip(tempo, self.data[mask]['Acertos Absolutos'], | |
self.data[mask]['Nome do Aluno']): | |
# Calcular a posição do texto para evitar sobreposição | |
if x > np.median(tempo.values): | |
ha = 'right' | |
offset = (-10, 0) | |
else: | |
ha = 'left' | |
offset = (10, 0) | |
# Adicionar pequena linha conectora | |
plt.annotate(nome, | |
xy=(x, y), | |
xytext=(x + offset[0]/5, y + 0.3), | |
ha=ha, | |
va='bottom', | |
fontsize=8, | |
bbox=dict( | |
facecolor='white', | |
edgecolor='none', | |
alpha=0.8, | |
pad=0.5 | |
), | |
arrowprops=dict( | |
arrowstyle='-', | |
color='gray', | |
alpha=0.3, | |
connectionstyle='arc3,rad=0' | |
)) | |
plt.title('Relação entre Tempo e Acertos por Nível', pad=20) | |
plt.xlabel('Tempo Total (minutos)') | |
plt.ylabel('Número de Acertos') | |
# Melhorar posição e aparência da legenda | |
plt.legend(bbox_to_anchor=(1.05, 1), | |
loc='upper left', | |
borderaxespad=0, | |
frameon=True, | |
facecolor='white', | |
edgecolor='none') | |
# Ajustar margens para acomodar a legenda | |
plt.tight_layout() | |
return plt.gcf() | |
def create_tasks_performance_plot(self) -> plt.Figure: | |
"""Cria o gráfico de relação entre tarefas e acertos com melhor legibilidade.""" | |
plt.figure(figsize=(15, 10)) | |
# Configurar fundo e grade | |
plt.grid(True, alpha=0.2, linestyle='--') | |
plt.gca().set_facecolor('#f8f9fa') | |
# Scatter plot com cores por nível | |
for nivel, color in self.colors.items(): | |
mask = self.data['Nível'] == nivel | |
plt.scatter(self.data[mask]['Tarefas Completadas'], | |
self.data[mask]['Acertos Absolutos'], | |
c=color, alpha=0.7, s=150, label=nivel) | |
# Melhorar posicionamento dos rótulos | |
for _, row in self.data[mask].iterrows(): | |
x = row['Tarefas Completadas'] | |
y = row['Acertos Absolutos'] | |
# Calcular posição do texto para evitar sobreposição | |
if x > np.median(self.data['Tarefas Completadas']): | |
ha = 'right' | |
offset = (-10, 0) | |
else: | |
ha = 'left' | |
offset = (10, 0) | |
# Adicionar pequena linha conectora | |
plt.annotate(row['Nome do Aluno'], | |
xy=(x, y), | |
xytext=(x + offset[0]/5, y + 0.3), | |
ha=ha, | |
va='bottom', | |
fontsize=8, | |
bbox=dict( | |
facecolor='white', | |
edgecolor='none', | |
alpha=0.8, | |
pad=0.5 | |
), | |
arrowprops=dict( | |
arrowstyle='-', | |
color='gray', | |
alpha=0.3, | |
connectionstyle='arc3,rad=0' | |
)) | |
# Linha de tendência | |
z = np.polyfit(self.data['Tarefas Completadas'], | |
self.data['Acertos Absolutos'], 1) | |
p = np.poly1d(z) | |
x_range = np.linspace(self.data['Tarefas Completadas'].min(), | |
self.data['Tarefas Completadas'].max(), 100) | |
plt.plot(x_range, p(x_range), "--", color='#e74c3c', alpha=0.8, | |
label='Tendência', linewidth=2) | |
plt.title('Relação entre Tarefas Completadas e Acertos', pad=20) | |
plt.xlabel('Número de Tarefas Completadas') | |
plt.ylabel('Número de Acertos') | |
# Melhorar posição e aparência da legenda | |
plt.legend(bbox_to_anchor=(1.05, 1), | |
loc='upper left', | |
borderaxespad=0, | |
frameon=True, | |
facecolor='white', | |
edgecolor='none') | |
# Ajustar margens para acomodar a legenda | |
plt.tight_layout() | |
return plt.gcf() | |
def generate_graphs(self) -> List[plt.Figure]: | |
"""Gera todos os gráficos para o relatório.""" | |
try: | |
graphs = [] | |
# 1. Distribuição por nível | |
graphs.append(self.create_distribution_plot()) | |
plt.close() | |
# 2. Ranking completo | |
graphs.append(self.create_ranking_plot()) | |
plt.close() | |
# 3. Relação tempo x acertos | |
graphs.append(self.create_time_performance_plot()) | |
plt.close() | |
# 4. Relação tarefas x acertos | |
graphs.append(self.create_tasks_performance_plot()) | |
plt.close() | |
return graphs | |
except Exception as e: | |
logging.error(f"Erro ao gerar gráficos: {str(e)}") | |
raise | |
def generate_table_section(self, pdf: FPDF, nivel: str, alunos_nivel: pd.DataFrame): | |
"""Gera uma seção de tabela com formatação melhorada.""" | |
try: | |
pdf.set_font('Arial', 'B', 14) | |
pdf.set_fill_color(240, 240, 240) | |
pdf.cell(0, 10, f'Detalhamento - Nível {nivel}', 0, 1, 'L', True) | |
pdf.ln(5) | |
# Configuração da tabela | |
colunas = [ | |
('Nome do Aluno', 80), | |
('Acertos', 25), | |
('Tarefas', 25), | |
('Tempo Total', 35) | |
] | |
# Cabeçalho | |
pdf.set_font('Arial', 'B', 10) | |
pdf.set_fill_color(230, 230, 230) | |
for titulo, largura in colunas: | |
pdf.cell(largura, 8, titulo, 1, 0, 'C', True) | |
pdf.ln() | |
# Dados com cores alternadas | |
pdf.set_font('Arial', '', 10) | |
for i, (_, row) in enumerate(alunos_nivel.iterrows()): | |
# Cor de fundo alternada | |
fill_color = (245, 245, 245) if i % 2 == 0 else (255, 255, 255) | |
pdf.set_fill_color(*fill_color) | |
tempo = pd.to_timedelta(row['Total Tempo']) | |
tempo_str = f"{int(tempo.total_seconds() // 60)}min {int(tempo.total_seconds() % 60)}s" | |
pdf.cell(80, 7, str(row['Nome do Aluno'])[:40], 1, 0, 'L', True) | |
pdf.cell(25, 7, f"{row['Acertos Absolutos']:.0f}", 1, 0, 'R', True) | |
pdf.cell(25, 7, str(row['Tarefas Completadas']), 1, 0, 'R', True) | |
pdf.cell(35, 7, tempo_str, 1, 0, 'R', True) | |
pdf.ln() | |
except Exception as e: | |
logging.error(f"Erro ao gerar seção de tabela: {str(e)}") | |
raise | |
def generate_pdf(self, output_path: str, graphs: List[plt.Figure]) -> None: | |
"""Gera relatório em PDF com análise detalhada.""" | |
try: | |
class PDF(FPDF): | |
def header(self): | |
"""Define o cabeçalho padrão do PDF.""" | |
self.set_font('Arial', 'B', 15) | |
self.set_fill_color(240, 240, 240) | |
self.cell(0, 15, 'Relatório de Desempenho - Análise Detalhada', 0, 1, 'C', True) | |
self.ln(10) | |
pdf = PDF('L', 'mm', 'A4') | |
# Introdução | |
pdf.add_page() | |
self._add_introduction_section(pdf) | |
# Visão Geral | |
pdf.add_page() | |
self._add_overview_section(pdf) | |
# Destaques | |
self._add_highlights_section(pdf) | |
# Gráficos e Análises | |
self._add_graphs_section(pdf, graphs) | |
# Detalhamento por Nível | |
self._add_detailed_sections(pdf) | |
# Recomendações Finais | |
self._add_recommendations_section(pdf) | |
pdf.output(output_path) | |
except Exception as e: | |
logging.error(f"Erro ao gerar PDF: {str(e)}") | |
raise | |
def _add_introduction_section(self, pdf: FPDF) -> None: | |
"""Adiciona a seção de introdução ao PDF.""" | |
pdf.set_font('Arial', 'B', 14) | |
pdf.set_fill_color(240, 240, 240) | |
pdf.cell(0, 10, 'Introdução', 0, 1, 'L', True) | |
pdf.ln(5) | |
pdf.set_font('Arial', '', 11) | |
intro_text = """ | |
Este relatório apresenta uma análise abrangente do desempenho dos alunos nas atividades realizadas. | |
Os dados são analisados considerando três aspectos principais: | |
- Acertos: Total de questões respondidas corretamente | |
- Engajamento: Número de tarefas completadas | |
- Dedicação: Tempo investido nas atividades | |
Os alunos são classificados em três níveis de acordo com seu desempenho: | |
- Avançado: 10 ou mais acertos - Excelente domínio do conteúdo | |
- Intermediário: 5 a 9 acertos - Bom entendimento, com espaço para melhorias | |
- Necessita Atenção: Menos de 5 acertos - Requer suporte adicional | |
""" | |
pdf.multi_cell(0, 7, intro_text) | |
def _add_overview_section(self, pdf: FPDF) -> None: | |
"""Adiciona a seção de visão geral ao PDF.""" | |
pdf.set_font('Arial', 'B', 14) | |
pdf.cell(0, 10, 'Visão Geral da Turma', 0, 1, 'L', True) | |
pdf.ln(5) | |
tempo_medio = pd.to_timedelta(self.stats['media_tempo']) | |
minutos = int(tempo_medio.total_seconds() // 60) | |
segundos = int(tempo_medio.total_seconds() % 60) | |
pdf.set_font('Arial', '', 11) | |
stats_text = f""" | |
Participação e Resultados: | |
- Total de Alunos Participantes: {self.stats['total_alunos']} | |
- Média de Tarefas por Aluno: {self.stats['media_tarefas']:.1f} | |
- Média de Acertos: {self.stats['media_acertos']:.1f} | |
- Tempo Médio de Dedicação: {minutos} minutos e {segundos} segundos | |
Distribuição de Desempenho: | |
- Desvio Padrão: {self.stats['desvio_padrao']:.1f} acertos | |
- Mediana: {self.stats['mediana_acertos']:.1f} acertos | |
""" | |
pdf.multi_cell(0, 7, stats_text) | |
def _add_highlights_section(self, pdf: FPDF) -> None: | |
"""Adiciona a seção de destaques ao PDF.""" | |
pdf.ln(5) | |
pdf.set_font('Arial', 'B', 12) | |
pdf.cell(0, 10, 'Destaques de Desempenho', 0, 1) | |
pdf.set_font('Arial', '', 11) | |
pdf.ln(3) | |
pdf.cell(0, 7, "Melhores Desempenhos:", 0, 1) | |
for aluno, acertos in self.stats['top_performers']: | |
pdf.cell(0, 7, f"- {aluno}: {acertos:.0f} acertos", 0, 1) | |
def _add_graphs_section(self, pdf: FPDF, graphs: List[plt.Figure]) -> None: | |
"""Adiciona a seção de gráficos ao PDF.""" | |
for i, graph in enumerate(graphs): | |
pdf.add_page() | |
graph_path = f'temp_graph_{i}.png' | |
graph.savefig(graph_path, dpi=300, bbox_inches='tight') | |
pdf.image(graph_path, x=10, y=30, w=270) | |
os.remove(graph_path) | |
pdf.ln(150) | |
pdf.set_font('Arial', 'B', 12) | |
if i == 0: | |
pdf.cell(0, 10, 'Análise da Distribuição por Nível', 0, 1, 'L', True) | |
pdf.set_font('Arial', '', 11) | |
pdf.multi_cell(0, 6, """ | |
Este gráfico ilustra como os alunos estão distribuídos entre os três níveis de desempenho. | |
- Verde: Alunos no nível Avançado - demonstram excelente compreensão | |
- Amarelo: Alunos no nível Intermediário - bom progresso com espaço para melhorias | |
- Vermelho: Alunos que Necessitam Atenção - requerem suporte adicional | |
""") | |
elif i == 1: | |
pdf.cell(0, 10, 'Ranking Completo dos Alunos', 0, 1, 'L', True) | |
pdf.set_font('Arial', '', 11) | |
pdf.multi_cell(0, 6, """ | |
Apresenta o ranking completo dos alunos por número de acertos. | |
Este ranking permite: | |
- Visualizar o desempenho individual de cada aluno | |
- Identificar diferentes níveis de aproveitamento | |
- Estabelecer metas realistas para melhorias | |
""") | |
elif i == 2: | |
pdf.cell(0, 10, 'Relação Tempo x Desempenho', 0, 1, 'L', True) | |
pdf.set_font('Arial', '', 11) | |
pdf.multi_cell(0, 6, """ | |
Mostra a relação entre tempo dedicado e número de acertos. | |
Pontos importantes: | |
- Cores indicam o nível de cada aluno | |
- Posição vertical mostra o número de acertos | |
- Posição horizontal indica o tempo total dedicado | |
- Dispersão dos pontos revela diferentes padrões de estudo | |
""") | |
elif i == 3: | |
pdf.cell(0, 10, 'Progresso por Número de Tarefas', 0, 1, 'L', True) | |
pdf.set_font('Arial', '', 11) | |
pdf.multi_cell(0, 6, """ | |
Analisa se mais tarefas realizadas resultam em melhor desempenho. | |
A linha de tendência (tracejada) indica: | |
- Correlação entre quantidade de tarefas e acertos | |
- Expectativa média de progresso | |
- Alunos acima da linha superam a expectativa da turma | |
""") | |
def _add_detailed_sections(self, pdf: FPDF) -> None: | |
"""Adiciona as seções detalhadas por nível ao PDF.""" | |
for nivel in ['Avançado', 'Intermediário', 'Necessita Atenção']: | |
alunos_nivel = self.data[self.data['Nível'] == nivel] | |
if not alunos_nivel.empty: | |
pdf.add_page() | |
self.generate_table_section(pdf, nivel, alunos_nivel) | |
def _add_recommendations_section(self, pdf: FPDF) -> None: | |
"""Adiciona a seção de recomendações ao PDF.""" | |
pdf.add_page() | |
pdf.set_font('Arial', 'B', 14) | |
pdf.cell(0, 10, 'Recomendações e Próximos Passos', 0, 1, 'L', True) | |
pdf.ln(5) | |
pdf.set_font('Arial', '', 11) | |
percent_necessita_atencao = len(self.data[self.data['Nível'] == 'Necessita Atenção']) / len(self.data) * 100 | |
recom_text = f""" | |
Com base na análise dos dados, recomenda-se: | |
1. Ações Imediatas: | |
- Implementar monitoria com alunos do nível Avançado | |
- Realizar reforço focado nos {percent_necessita_atencao:.1f}% que necessitam atenção | |
- Desenvolver planos de estudo personalizados | |
2. Melhorias no Processo: | |
- Acompanhamento individualizado dos alunos com baixo desempenho | |
- Feedback regular sobre o progresso | |
- Atividades extras para alunos com bom desempenho | |
3. Próximos Passos: | |
- Compartilhar resultados individuais | |
- Agendar sessões de reforço | |
- Reconhecer publicamente bons desempenhos | |
- Estabelecer metas claras de melhoria | |
""" | |
pdf.multi_cell(0, 7, recom_text) | |
def process_files(html_file, excel_files) -> Tuple[str, str, str]: | |
"""Processa arquivos e gera relatório.""" | |
try: | |
temp_dir = "temp_files" | |
os.makedirs(temp_dir, exist_ok=True) | |
# Limpar diretório temporário | |
for file in os.listdir(temp_dir): | |
os.remove(os.path.join(temp_dir, file)) | |
# Salvar arquivos | |
html_path = os.path.join(temp_dir, "alunos.htm") | |
with open(html_path, "wb") as f: | |
f.write(html_file) | |
# Processar arquivos Excel | |
excel_paths = [] | |
for i, excel_file in enumerate(excel_files): | |
excel_path = os.path.join(temp_dir, f"tarefa_{i}.xlsx") | |
with open(excel_path, "wb") as f: | |
f.write(excel_file) | |
excel_paths.append(excel_path) | |
# Processar arquivos | |
processor = DataProcessor() | |
alunos_csv_path = os.path.join(temp_dir, "alunos.csv") | |
processor.normalize_html_to_csv(html_path, alunos_csv_path) | |
# Concatenar dados das tarefas | |
tarefas_df = pd.DataFrame() | |
for excel_path in excel_paths: | |
csv_path = excel_path.replace('.xlsx', '.csv') | |
processor.normalize_excel_to_csv(excel_path, csv_path) | |
df = pd.read_csv(csv_path) | |
tarefas_df = pd.concat([tarefas_df, df], ignore_index=True) | |
# Análise e geração de relatório | |
alunos_df = pd.read_csv(alunos_csv_path) | |
analyzer = StudentAnalyzer(tarefas_df, alunos_df) | |
results_df = analyzer.prepare_data() | |
report_generator = ReportGenerator(results_df) | |
graphs = report_generator.generate_graphs() | |
# Salvar outputs | |
output_html = os.path.join(temp_dir, "relatorio.html") | |
output_pdf = os.path.join(temp_dir, "relatorio.pdf") | |
results_df.to_html(output_html, index=False) | |
report_generator.generate_pdf(output_pdf, graphs) | |
return results_df.to_html(index=False), output_html, output_pdf | |
except Exception as e: | |
logging.error(f"Erro no processamento: {str(e)}") | |
raise | |
def create_interface(): | |
"""Cria a interface Gradio.""" | |
theme = gr.themes.Default( | |
primary_hue="blue", | |
secondary_hue="gray", | |
font=["Arial", "sans-serif"], | |
font_mono=["Courier New", "monospace"], | |
) | |
with gr.Blocks(theme=theme) as interface: | |
gr.Markdown(""" | |
# Sistema de Análise de Desempenho Acadêmico | |
Este sistema analisa o desempenho dos alunos e gera um relatório detalhado com: | |
- Análise estatística completa | |
- Visualizações gráficas | |
- Recomendações personalizadas | |
""") | |
with gr.Row(): | |
with gr.Column(): | |
gr.Markdown("## Lista de Alunos") | |
html_file = gr.File( | |
label="Arquivo HTML com lista de alunos (.htm)", | |
type="binary", | |
file_types=[".htm", ".html"] | |
) | |
with gr.Column(): | |
gr.Markdown("## Relatórios de Tarefas") | |
excel_files = gr.Files( | |
label="Arquivos Excel com dados das tarefas (.xlsx)", | |
type="binary", | |
file_count="multiple", | |
file_types=[".xlsx"] | |
) | |
with gr.Row(): | |
generate_btn = gr.Button( | |
"Gerar Relatório", | |
variant="primary", | |
size="lg" | |
) | |
with gr.Row(): | |
output_html = gr.HTML() | |
with gr.Row(): | |
with gr.Column(): | |
download_html_btn = gr.File( | |
label="Download Relatório HTML", | |
type="filepath", | |
interactive=False | |
) | |
with gr.Column(): | |
download_pdf_btn = gr.File( | |
label="Download Relatório PDF", | |
type="filepath", | |
interactive=False | |
) | |
generate_btn.click( | |
fn=process_files, | |
inputs=[html_file, excel_files], | |
outputs=[output_html, download_html_btn, download_pdf_btn] | |
) | |
return interface | |
if __name__ == "__main__": | |
interface = create_interface() | |
interface.launch( | |
share=False, | |
server_name="0.0.0.0", | |
server_port=7860, | |
show_error=True | |
) |