Spaces:
Sleeping
Sleeping
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 weasyprint import HTML | |
import seaborn as sns | |
from typing import Tuple, Dict, List | |
import logging | |
import warnings | |
warnings.filterwarnings('ignore') | |
# Configuração de logging | |
logging.basicConfig( | |
level=logging.INFO, | |
format='%(asctime)s - %(levelname)s - %(message)s' | |
) | |
class DataProcessor: | |
def parse_duration(duration_str: str) -> timedelta: | |
"""Converte string de duração em objeto timedelta.""" | |
try: | |
h, m, s = map(int, duration_str.split(':')) | |
return timedelta(hours=h, minutes=m, seconds=s) | |
except: | |
return timedelta(0) | |
def format_timedelta(td: timedelta) -> str: | |
"""Formata timedelta para string no formato HH:MM:SS.""" | |
total_seconds = int(td.total_seconds()) | |
hours, remainder = divmod(total_seconds, 3600) | |
minutes, seconds = divmod(remainder, 60) | |
return f"{hours:02}:{minutes:02}:{seconds:02}" | |
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): | |
self.tarefas_df = tarefas_df | |
self.alunos_df = alunos_df | |
self.processor = DataProcessor() | |
def prepare_data(self) -> pd.DataFrame: | |
"""Prepara e limpa os dados para análise.""" | |
# Limpeza de colunas | |
self.tarefas_df.columns = self.tarefas_df.columns.str.strip() | |
self.alunos_df.columns = self.alunos_df.columns.str.strip() | |
# Verifica colunas necessárias | |
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") | |
# Processamento de duração | |
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: | |
"""Relaciona dados de alunos com suas tarefas.""" | |
def generate_aluno_pattern(ra, dig_ra): | |
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): | |
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: | |
metrics = { | |
'Nome do Aluno': aluno['Nome do Aluno'], | |
'Tarefas Completadas': len(aluno_tarefas), | |
'Acertos Absolutos': aluno_tarefas['Nota'].sum(), | |
'Total Tempo': str(aluno_tarefas['Duração'].sum()), | |
'Média de Acertos': f"{(aluno_tarefas['Nota'].sum() / (len(aluno_tarefas) * 2) * 100):.2f}%", | |
'Tempo Médio por Tarefa': str(aluno_tarefas['Duração'].mean()), | |
'Eficiência': f"{(aluno_tarefas['Nota'].sum() / aluno_tarefas['Duração'].sum().total_seconds() * 3600):.2f}" | |
} | |
metrics_df = pd.concat([metrics_df, pd.DataFrame([metrics])], ignore_index=True) | |
return metrics_df | |
class ReportGenerator: | |
def __init__(self, data: pd.DataFrame): | |
self.data = data | |
self.stats = self.calculate_statistics() | |
# Classificar alunos por níveis | |
self.data['Nível'] = self.data['Média de Acertos'].str.rstrip('%').astype(float).apply(self.classify_performance) | |
self.data = self.data.sort_values('Média de Acertos'.str.rstrip('%').astype(float), ascending=False) | |
def classify_performance(self, score): | |
if score >= 70: | |
return 'Avançado' | |
elif score >= 40: | |
return 'Intermediário' | |
else: | |
return 'Necessita Atenção' | |
def calculate_statistics(self) -> Dict: | |
"""Calcula estatísticas gerais e por grupo.""" | |
basic_stats = { | |
'media_acertos': float(self.data['Média de Acertos'].str.rstrip('%').astype(float).mean()), | |
'desvio_padrao': float(self.data['Média de Acertos'].str.rstrip('%').astype(float).std()), | |
'mediana_acertos': float(self.data['Média de Acertos'].str.rstrip('%').astype(float).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 performers | |
top_students = self.data.nlargest(3, 'Média de Acertos') | |
basic_stats['top_performers'] = top_students[['Nome do Aluno', 'Média de Acertos']].values.tolist() | |
# Mais eficientes (melhor relação acerto/tempo) | |
efficient_students = self.data.nlargest(3, 'Eficiência') | |
basic_stats['most_efficient'] = efficient_students[['Nome do Aluno', 'Eficiência', 'Média de Acertos']].values.tolist() | |
return basic_stats | |
def generate_graphs(self) -> List[plt.Figure]: | |
"""Gera gráficos para o relatório.""" | |
graphs = [] | |
# 1. Distribuição de notas por nível | |
plt.figure(figsize=(12, 6)) | |
nivel_counts = self.data['Nível'].value_counts() | |
colors = {'Avançado': 'green', 'Intermediário': 'yellow', 'Necessita Atenção': 'red'} | |
bars = plt.bar(nivel_counts.index, nivel_counts.values) | |
for i, bar in enumerate(bars): | |
bar.set_color(colors[nivel_counts.index[i]]) | |
plt.text(bar.get_x() + bar.get_width()/2, bar.get_height(), | |
str(nivel_counts.values[i]), | |
ha='center', va='bottom') | |
plt.title('Distribuição dos Alunos por Nível de Desempenho') | |
plt.ylabel('Número de Alunos') | |
graphs.append(plt.gcf()) | |
plt.close() | |
# 2. Top 10 alunos | |
plt.figure(figsize=(12, 6)) | |
top_10 = self.data.head(10) | |
acertos = top_10['Média de Acertos'].str.rstrip('%').astype(float) | |
plt.barh(top_10['Nome do Aluno'], acertos) | |
plt.title('Top 10 Alunos por Desempenho') | |
plt.xlabel('Percentual de Acertos') | |
for i, v in enumerate(acertos): | |
plt.text(v, i, f'{v:.1f}%', va='center') | |
plt.tight_layout() | |
graphs.append(plt.gcf()) | |
plt.close() | |
# 3. Relação tempo x desempenho com níveis | |
plt.figure(figsize=(10, 6)) | |
colors = {'Avançado': 'green', 'Intermediário': 'yellow', 'Necessita Atenção': 'red'} | |
for nivel in colors: | |
mask = self.data['Nível'] == nivel | |
tempo = pd.to_timedelta(self.data[mask]['Total Tempo']).dt.total_seconds() / 60 | |
acertos = self.data[mask]['Média de Acertos'].str.rstrip('%').astype(float) | |
plt.scatter(tempo, acertos, c=colors[nivel], label=nivel, alpha=0.6) | |
plt.title('Relação Tempo x Desempenho por Nível') | |
plt.xlabel('Tempo Total (minutos)') | |
plt.ylabel('Percentual de Acertos') | |
plt.legend() | |
plt.grid(True, alpha=0.3) | |
graphs.append(plt.gcf()) | |
plt.close() | |
return graphs | |
def generate_pdf(self, output_path: str, graphs: List[plt.Figure]) -> None: | |
"""Gera relatório em PDF com análise detalhada.""" | |
class PDF(FPDF): | |
def header(self): | |
self.set_font('Arial', 'B', 15) | |
self.cell(0, 10, 'Relatório de Desempenho - Análise Detalhada', 0, 1, 'C') | |
self.ln(10) | |
pdf = PDF('L', 'mm', 'A4') | |
# Sumário executivo | |
pdf.add_page() | |
pdf.set_font('Arial', 'B', 12) | |
pdf.cell(0, 10, 'Sumário Executivo', 0, 1) | |
pdf.set_font('Arial', '', 10) | |
summary_text = f""" | |
Visão Geral da Turma: | |
- Média de Acertos: {self.stats['media_acertos']:.1f}% | |
- Desvio Padrão: {self.stats['desvio_padrao']:.1f}% | |
- Mediana: {self.stats['mediana_acertos']:.1f}% | |
- Total de Alunos: {self.stats['total_alunos']} | |
Destaques: | |
Top 3 Melhores Desempenhos: | |
""" | |
pdf.multi_cell(0, 10, summary_text) | |
# Adicionar top performers | |
for aluno, nota in self.stats['top_performers']: | |
pdf.cell(0, 10, f"- {aluno}: {nota}", 0, 1) | |
pdf.ln() | |
pdf.cell(0, 10, "Alunos Mais Eficientes:", 0, 1) | |
for aluno, eficiencia, nota in self.stats['most_efficient']: | |
pdf.cell(0, 10, f"- {aluno}: Eficiência {eficiencia:.1f} (Acertos: {nota})", 0, 1) | |
# Gráficos e análises | |
for i, graph in enumerate(graphs): | |
pdf.add_page() | |
graph_path = f'temp_graph_{i}.png' | |
graph.savefig(graph_path) | |
pdf.image(graph_path, x=10, y=30, w=270) | |
os.remove(graph_path) | |
# Tabela detalhada por nível | |
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() | |
pdf.set_font('Arial', 'B', 12) | |
pdf.cell(0, 10, f'Alunos - Nível {nivel}', 0, 1) | |
# Cabeçalhos | |
columns = ['Nome do Aluno', 'Média de Acertos', 'Tarefas', 'Tempo Total', 'Eficiência'] | |
widths = [80, 30, 30, 30, 30] | |
pdf.set_font('Arial', 'B', 8) | |
for i, col in enumerate(columns): | |
pdf.cell(widths[i], 7, col, 1) | |
pdf.ln() | |
# Dados | |
pdf.set_font('Arial', '', 8) | |
for _, row in alunos_nivel.iterrows(): | |
pdf.cell(widths[0], 6, str(row['Nome do Aluno'])[:40], 1) | |
pdf.cell(widths[1], 6, str(row['Média de Acertos']), 1) | |
pdf.cell(widths[2], 6, str(row['Tarefas Completadas']), 1) | |
pdf.cell(widths[3], 6, str(row['Total Tempo']), 1) | |
pdf.cell(widths[4], 6, str(row['Eficiência']), 1) | |
pdf.ln() | |
pdf.output(output_path) | |
def process_files(html_file, excel_files) -> Tuple[str, str, str]: | |
"""Processa arquivos e gera relatório.""" | |
try: | |
# Criar diretório temporário | |
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) | |
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) | |
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 | |
alunos_df = pd.read_csv(alunos_csv_path) | |
analyzer = StudentAnalyzer(tarefas_df, alunos_df) | |
results_df = analyzer.prepare_data() | |
# Gerar relatório | |
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 | |
# 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") | |
with gr.Row(): | |
with gr.Column(): | |
gr.Markdown("## Lista de Alunos (arquivo .htm)") | |
html_file = gr.File(label="Upload do arquivo HTML", type="binary") | |
with gr.Column(): | |
gr.Markdown("## Relatórios de Tarefas (arquivos .xlsx)") | |
excel_files = gr.Files(label="Upload dos arquivos Excel", type="binary", file_count="multiple") | |
generate_btn = gr.Button("Gerar Relatório", variant="primary") | |
output_html = gr.HTML() | |
download_html_btn = gr.File(label="Download HTML Report") | |
download_pdf_btn = gr.File(label="Download PDF Report") | |
generate_btn.click( | |
fn=process_files, | |
inputs=[html_file, excel_files], | |
outputs=[output_html, download_html_btn, download_pdf_btn] | |
) | |
interface.launch() |