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 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 | |
| from adjustText import adjust_text | |
| # 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_time(seconds: float) -> str: | |
| """Formata o tempo de forma mais limpa e consistente.""" | |
| minutes = int(seconds // 60) | |
| remaining_seconds = int(seconds % 60) | |
| if minutes == 0: | |
| return f"{remaining_seconds}s" | |
| return f"{minutes}min {remaining_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, eliminando duplicatas e normalizando valores.""" | |
| try: | |
| metrics_df = pd.DataFrame() | |
| # Agrupar por Aluno_Pattern para eliminar duplicatas | |
| grouped_tasks = self.tarefas_df.groupby('Aluno_Pattern').agg({ | |
| 'Aluno': 'first', # Nome original do aluno na tarefa | |
| 'Duração': 'sum', # Soma total do tempo | |
| 'Nota': 'sum', # Soma total dos acertos | |
| }).reset_index() | |
| # Contar número de tarefas únicas por aluno | |
| task_counts = self.tarefas_df.groupby('Aluno_Pattern').size().reset_index(name='total_tarefas') | |
| grouped_tasks = grouped_tasks.merge(task_counts, on='Aluno_Pattern', how='left') | |
| # Processar cada aluno uma única vez usando o DataFrame de alunos para informações corretas | |
| processed_patterns = set() # Conjunto para controlar padrões já processados | |
| for _, aluno in self.alunos_df.iterrows(): | |
| aluno_pattern = aluno['Aluno_Pattern'] | |
| # Evitar duplicatas do mesmo padrão | |
| if aluno_pattern not in processed_patterns: | |
| aluno_data = grouped_tasks[grouped_tasks['Aluno_Pattern'] == aluno_pattern] | |
| if not aluno_data.empty: | |
| metrics = { | |
| 'Nome do Aluno': aluno['Nome do Aluno'], | |
| 'Tarefas Completadas': aluno_data['total_tarefas'].iloc[0], | |
| 'Acertos Absolutos': aluno_data['Nota'].iloc[0], | |
| 'Total Tempo': str(aluno_data['Duração'].iloc[0]), | |
| 'Tempo Médio por Tarefa': str(aluno_data['Duração'].iloc[0] / aluno_data['total_tarefas'].iloc[0] | |
| if aluno_data['total_tarefas'].iloc[0] > 0 else timedelta(0)) | |
| } | |
| metrics_df = pd.concat([metrics_df, pd.DataFrame([metrics])], ignore_index=True) | |
| processed_patterns.add(aluno_pattern) | |
| # Ordenar por acertos e resetar índice | |
| return metrics_df.sort_values('Acertos Absolutos', ascending=False).reset_index(drop=True) | |
| except Exception as e: | |
| logging.error(f"Erro ao calcular métricas: {str(e)}") | |
| raise | |
| 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) | |
| # Adicionar texto com acertos e número total de tarefas | |
| for i, (bar, tarefas) in enumerate(zip(bars, students_data['Tarefas Completadas'])): | |
| plt.text(bar.get_width(), i, | |
| f' {bar.get_width():.0f} acertos / {tarefas:.0f} tarefas', | |
| 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) | |
| # Ajustar margens para acomodar o texto adicional | |
| plt.margins(x=0.2) | |
| return plt.gcf() | |
| def create_time_performance_plot(self) -> plt.Figure: | |
| """Cria o gráfico de relação entre tempo e acertos com visualização otimizada.""" | |
| plt.figure(figsize=(15, 10)) | |
| ax = plt.gca() | |
| # Configuração inicial com estilo limpo | |
| plt.grid(True, alpha=0.2, linestyle='--') | |
| ax.set_facecolor('#f8f9fa') | |
| # Linha de média | |
| media_acertos = self.data['Acertos Absolutos'].mean() | |
| plt.axhline(y=media_acertos, color='gray', linestyle=':', alpha=0.5, | |
| label='Média de Acertos') | |
| # Estruturas para armazenamento | |
| all_points = [] | |
| point_groups = {} | |
| # Plotar pontos e coletar dados | |
| 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 | |
| acertos = self.data[mask]['Acertos Absolutos'] | |
| scatter = plt.scatter(tempo, acertos, c=color, label=nivel, alpha=0.7, s=100) | |
| for t, a, nome in zip(tempo, acertos, self.data[mask]['Nome do Aluno']): | |
| point_key = (round(t, 1), a) | |
| if point_key not in point_groups: | |
| point_groups[point_key] = [] | |
| point_groups[point_key].append((t, a, nome, color)) | |
| all_points.append((t, a)) | |
| # Lista para controle de sobreposições | |
| annotations = [] | |
| # Função para calcular melhor posição | |
| def get_best_position(x, y, existing_annotations): | |
| angles = np.linspace(0, 2*np.pi, 16) # 16 direções possíveis | |
| base_radius = 3.0 | |
| max_radius = 6.0 | |
| best_pos = None | |
| min_overlap = float('inf') | |
| for radius in np.linspace(base_radius, max_radius, 4): | |
| for angle in angles: | |
| new_x = x + radius * np.cos(angle) | |
| new_y = y + radius * np.sin(angle) | |
| # Verificar limites do gráfico | |
| if not (ax.get_xlim()[0] <= new_x <= ax.get_xlim()[1] and | |
| ax.get_ylim()[0] <= new_y <= ax.get_ylim()[1]): | |
| continue | |
| overlaps = sum(1 for ann in existing_annotations if | |
| abs(ann[0] - new_x) < 2 and abs(ann[1] - new_y) < 2) | |
| if overlaps < min_overlap: | |
| min_overlap = overlaps | |
| best_pos = (new_x, new_y) | |
| return best_pos or (x + base_radius, y + base_radius) | |
| # Adicionar anotações | |
| for key, group in point_groups.items(): | |
| if len(group) == 1: | |
| x, y, nome, _ = group[0] | |
| new_pos = get_best_position(x, y, annotations) | |
| plt.annotate( | |
| nome.split()[0], | |
| (x, y), | |
| xytext=new_pos, | |
| textcoords='data', | |
| bbox=dict( | |
| facecolor='white', | |
| edgecolor='lightgray', | |
| alpha=0.95, | |
| pad=1.0, | |
| boxstyle='round,pad=0.8' | |
| ), | |
| fontsize=9, | |
| arrowprops=dict( | |
| arrowstyle='-|>', | |
| connectionstyle='arc3,rad=0.2', | |
| color='gray', | |
| alpha=0.6 | |
| ) | |
| ) | |
| annotations.append(new_pos) | |
| else: | |
| # Calcular centroide do grupo | |
| x_center = sum(p[0] for p in group) / len(group) | |
| y_center = sum(p[1] for p in group) / len(group) | |
| nomes = sorted(set([p[2].split()[0] for p in group])) | |
| new_pos = get_best_position(x_center, y_center, annotations) | |
| plt.annotate( | |
| f"{len(group)} alunos:\n" + "\n".join(nomes), | |
| (x_center, y_center), | |
| xytext=new_pos, | |
| textcoords='data', | |
| bbox=dict( | |
| facecolor='white', | |
| edgecolor='lightgray', | |
| alpha=0.95, | |
| pad=1.0, | |
| boxstyle='round,pad=0.8' | |
| ), | |
| fontsize=9, | |
| arrowprops=dict( | |
| arrowstyle='-|>', | |
| connectionstyle='arc3,rad=0.2', | |
| color='gray', | |
| alpha=0.6 | |
| ) | |
| ) | |
| annotations.append(new_pos) | |
| # Configurações finais | |
| plt.title('Relação entre Tempo e Acertos por Nível', pad=20, fontsize=14) | |
| plt.xlabel('Tempo Total (minutos)', fontsize=12) | |
| plt.ylabel('Número de Acertos', fontsize=12) | |
| # Legenda com todos os elementos | |
| handles, labels = plt.gca().get_legend_handles_labels() | |
| by_label = dict(zip(labels, handles)) | |
| plt.legend(by_label.values(), by_label.keys(), | |
| bbox_to_anchor=(1.05, 1), | |
| loc='upper left', | |
| borderaxespad=0, | |
| frameon=True, | |
| fancybox=True) | |
| 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 visualização otimizada.""" | |
| plt.figure(figsize=(15, 10)) | |
| ax = plt.gca() | |
| # Configuração inicial | |
| plt.grid(True, alpha=0.2, linestyle='--') | |
| ax.set_facecolor('#f8f9fa') | |
| # Forçar ticks inteiros no eixo x | |
| ax.xaxis.set_major_locator(MaxNLocator(integer=True)) | |
| point_groups = {} | |
| all_points = [] | |
| # Plotar pontos e coletar dados | |
| for nivel, color in self.colors.items(): | |
| mask = self.data['Nível'] == nivel | |
| tarefas = self.data[mask]['Tarefas Completadas'] | |
| acertos = self.data[mask]['Acertos Absolutos'] | |
| scatter = plt.scatter(tarefas, acertos, c=color, label=nivel, alpha=0.7, s=100) | |
| for t, a, nome in zip(tarefas, acertos, self.data[mask]['Nome do Aluno']): | |
| point_key = (t, a) | |
| if point_key not in point_groups: | |
| point_groups[point_key] = [] | |
| point_groups[point_key].append((t, a, nome, color)) | |
| all_points.append((t, a)) | |
| # 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() - 0.5, | |
| self.data['Tarefas Completadas'].max() + 0.5, | |
| 100 | |
| ) | |
| plt.plot(x_range, p(x_range), "--", color='#d63031', alpha=0.9, | |
| label='Tendência', linewidth=2.5) | |
| # Linha de média | |
| media_acertos = self.data['Acertos Absolutos'].mean() | |
| plt.axhline(y=media_acertos, color='gray', linestyle=':', alpha=0.5, | |
| label='Média de Acertos') | |
| # Lista para controle de sobreposições | |
| annotations = [] | |
| # Função para calcular melhor posição | |
| def get_best_position(x, y, existing_annotations): | |
| angles = np.linspace(0, 2*np.pi, 16) | |
| base_radius = 2.0 # Menor para tarefas discretas | |
| max_radius = 4.0 | |
| best_pos = None | |
| min_overlap = float('inf') | |
| for radius in np.linspace(base_radius, max_radius, 4): | |
| for angle in angles: | |
| new_x = x + radius * np.cos(angle) | |
| new_y = y + radius * np.sin(angle) | |
| if not (ax.get_xlim()[0] <= new_x <= ax.get_xlim()[1] and | |
| ax.get_ylim()[0] <= new_y <= ax.get_ylim()[1]): | |
| continue | |
| overlaps = sum(1 for ann in existing_annotations if | |
| abs(ann[0] - new_x) < 1.5 and abs(ann[1] - new_y) < 1.5) | |
| if overlaps < min_overlap: | |
| min_overlap = overlaps | |
| best_pos = (new_x, new_y) | |
| return best_pos or (x + base_radius, y + base_radius) | |
| # Adicionar anotações | |
| for key, group in point_groups.items(): | |
| if len(group) == 1: | |
| x, y, nome, _ = group[0] | |
| new_pos = get_best_position(x, y, annotations) | |
| plt.annotate( | |
| nome.split()[0], | |
| (x, y), | |
| xytext=new_pos, | |
| textcoords='data', | |
| bbox=dict( | |
| facecolor='white', | |
| edgecolor='lightgray', | |
| alpha=0.95, | |
| pad=1.0, | |
| boxstyle='round,pad=0.8' | |
| ), | |
| fontsize=9, | |
| arrowprops=dict( | |
| arrowstyle='-|>', | |
| connectionstyle='arc3,rad=0.2', | |
| color='gray', | |
| alpha=0.6 | |
| ) | |
| ) | |
| annotations.append(new_pos) | |
| else: | |
| x_center = sum(p[0] for p in group) / len(group) | |
| y_center = sum(p[1] for p in group) / len(group) | |
| nomes = sorted(set([p[2].split()[0] for p in group])) | |
| new_pos = get_best_position(x_center, y_center, annotations) | |
| plt.annotate( | |
| f"{len(group)} alunos com\n{group[0][0]:.0f} tarefas:\n" + "\n".join(nomes), | |
| (x_center, y_center), | |
| xytext=new_pos, | |
| textcoords='data', | |
| bbox=dict( | |
| facecolor='white', | |
| edgecolor='lightgray', | |
| alpha=0.95, | |
| pad=1.0, | |
| boxstyle='round,pad=0.8' | |
| ), | |
| fontsize=9, | |
| arrowprops=dict( | |
| arrowstyle='-|>', | |
| connectionstyle='arc3,rad=0.2', | |
| color='gray', | |
| alpha=0.6 | |
| ) | |
| ) | |
| annotations.append(new_pos) | |
| # Configurações finais | |
| plt.title('Relação entre Tarefas Completadas e Acertos', pad=20, fontsize=14) | |
| plt.xlabel('Número de Tarefas Completadas', fontsize=12) | |
| plt.ylabel('Número de Acertos', fontsize=12) | |
| # Ajustar limites | |
| plt.xlim( | |
| self.data['Tarefas Completadas'].min() - 1, | |
| self.data['Tarefas Completadas'].max() + 1 | |
| ) | |
| plt.ylim( | |
| max(0, self.data['Acertos Absolutos'].min() - 1), | |
| self.data['Acertos Absolutos'].max() + 2 | |
| ) | |
| # Legenda com todos os elementos | |
| handles, labels = plt.gca().get_legend_handles_labels() | |
| by_label = dict(zip(labels, handles)) | |
| plt.legend(by_label.values(), by_label.keys(), | |
| bbox_to_anchor=(1.05, 1), | |
| loc='upper left', | |
| borderaxespad=0, | |
| frameon=True, | |
| fancybox=True) | |
| 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 e cálculos corrigidos.""" | |
| try: | |
| # Configuração inicial | |
| pdf.set_font('Arial', 'B', 14) | |
| pdf.set_fill_color(240, 240, 240) | |
| # Função auxiliar para calcular taxa de aproveitamento | |
| def calcular_taxa(acertos: float, questoes_total: float) -> float: | |
| """Calcula taxa de aproveitamento considerando total de questões real""" | |
| return (acertos / questoes_total * 100) if questoes_total > 0 else 0 | |
| # Calcular estatísticas do nível | |
| media_acertos = alunos_nivel['Acertos Absolutos'].mean() | |
| media_tarefas = alunos_nivel['Tarefas Completadas'].mean() | |
| # Para a média geral, vamos usar 4 questões por tarefa como base | |
| questoes_por_tarefa = 4 # Média de questões por tarefa | |
| total_questoes_media = media_tarefas * questoes_por_tarefa | |
| taxa_media = calcular_taxa(media_acertos, total_questoes_media) | |
| # Cabeçalho da seção | |
| pdf.cell(0, 10, f'Detalhamento - Nível {nivel}', 0, 1, 'L', True) | |
| pdf.set_font('Arial', '', 10) | |
| pdf.cell(0, 6, (f'Média de Acertos: {media_acertos:.1f} | ' | |
| f'Média de Tarefas: {media_tarefas:.1f} | ' | |
| f'Taxa Média de Aproveitamento: {min(taxa_media, 100):.1f}%'), 0, 1) | |
| pdf.ln(2) | |
| # Configuração da tabela | |
| colunas = [ | |
| ('Nome do Aluno', 70), | |
| ('Acertos', 20), | |
| ('Tarefas', 20), | |
| ('Taxa', 20), | |
| ('Tempo Total', 20) | |
| ] | |
| # Cabeçalho da tabela | |
| 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 dos alunos | |
| pdf.set_font('Arial', '', 10) | |
| for i, (_, row) in enumerate(alunos_nivel.iterrows()): | |
| # Alternar cores das linhas | |
| fill_color = (248, 248, 248) if i % 2 == 0 else (255, 255, 255) | |
| pdf.set_fill_color(*fill_color) | |
| # Calcular taxa de aproveitamento individual | |
| total_questoes = row['Tarefas Completadas'] * questoes_por_tarefa | |
| taxa_aproveitamento = calcular_taxa(row['Acertos Absolutos'], total_questoes) | |
| taxa_aproveitamento = min(taxa_aproveitamento, 100) # Limitar a 100% | |
| # Formatar tempo | |
| tempo = pd.to_timedelta(row['Total Tempo']) | |
| tempo_str = f"{int(tempo.total_seconds() // 60)}min {int(tempo.total_seconds() % 60)}s" | |
| # Adicionar linha na tabela | |
| pdf.cell(70, 7, str(row['Nome do Aluno'])[:35], 1, 0, 'L', True) | |
| pdf.cell(20, 7, f"{row['Acertos Absolutos']:.0f}", 1, 0, 'R', True) | |
| pdf.cell(20, 7, f"{row['Tarefas Completadas']:.0f}", 1, 0, 'R', True) | |
| pdf.cell(20, 7, f"{taxa_aproveitamento:.1f}%", 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 _add_insights_section(self, pdf: FPDF) -> None: | |
| """Adiciona uma nova seção de insights principais com taxa de aproveitamento corrigida.""" | |
| pdf.set_font('Arial', 'B', 14) | |
| pdf.set_fill_color(240, 240, 240) | |
| pdf.cell(0, 10, 'Insights Principais', 0, 1, 'L', True) | |
| pdf.ln(5) | |
| pdf.set_font('Arial', '', 11) | |
| # Calcular métricas avançadas | |
| questoes_por_tarefa = 4 # Média de questões por tarefa | |
| total_questoes = self.data['Tarefas Completadas'].sum() * questoes_por_tarefa | |
| total_acertos = self.data['Acertos Absolutos'].sum() | |
| # Calcular taxa de aproveitamento correta | |
| taxa_aproveitamento = (total_acertos / total_questoes * 100) if total_questoes > 0 else 0 | |
| taxa_aproveitamento = min(taxa_aproveitamento, 100) # Limitar a 100% | |
| # Identificar alunos eficientes | |
| tempo_medio = pd.to_timedelta(self.data['Total Tempo']).dt.total_seconds().mean() | |
| alunos_eficientes = self.data[ | |
| (self.data['Acertos Absolutos'] / (self.data['Tarefas Completadas'] * questoes_por_tarefa) > taxa_aproveitamento/100) & | |
| (pd.to_timedelta(self.data['Total Tempo']).dt.total_seconds() < tempo_medio) | |
| ] | |
| insights_text = f""" | |
| Análise de Eficiência: | |
| - Taxa média de aproveitamento da turma: {taxa_aproveitamento:.1f}% | |
| - {len(alunos_eficientes)} alunos demonstram alta eficiência (acertos acima da média e tempo abaixo da média) | |
| - {len(self.data[self.data['Acertos Absolutos'] == 0])} alunos não registraram acertos | |
| Padrões Identificados: | |
| - Alunos que completaram mais tarefas tendem a ter melhor desempenho | |
| - Tempo dedicado não mostra correlação forte com número de acertos | |
| - {(len(self.data[self.data['Tarefas Completadas'] < 5]) / len(self.data) * 100):.1f}% dos alunos completaram menos de 5 tarefas | |
| Recomendações Baseadas em Dados: | |
| - Focar em aumentar o número de tarefas completadas | |
| - Identificar motivos para baixa participação em alguns alunos | |
| - Avaliar qualidade do tempo dedicado vs. quantidade | |
| """ | |
| pdf.multi_cell(0, 7, insights_text) | |
| 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) | |
| # Adicionar número de página | |
| self.set_font('Arial', 'I', 8) | |
| self.cell(0, 10, f'Página {self.page_no()}', 0, 0, 'R') | |
| 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) | |
| # Nova seção de Insights | |
| pdf.add_page() | |
| self._add_insights_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 | |
| ) |