Spaces:
Running
Running
| from pathlib import Path | |
| import streamlit as st | |
| import pandas as pd | |
| import numpy as np | |
| import matplotlib.pyplot as plt | |
| import seaborn as sns | |
| import pickle | |
| import time | |
| try: | |
| from .carregar_dados import carregar_uci_dados, carregar_oulad_dados | |
| except ImportError: | |
| # Fallback para quando executado diretamente | |
| from carregar_dados import carregar_uci_dados, carregar_oulad_dados | |
| def leitura_oulad_data(): | |
| """Função para leitura dos dados OULAD - mantida para compatibilidade""" | |
| datasets_path = Path(__file__).parent.parents[1] / 'datasets' / 'oulad_data' | |
| return datasets_path | |
| # Cache por 1 hora | |
| def carregar_dados_uci_cached(): | |
| """Carrega dados UCI com cache""" | |
| return carregar_uci_dados() | |
| # Cache por 1 hora | |
| def carregar_dados_oulad_cached(): | |
| """Carrega dados OULAD com cache""" | |
| return carregar_oulad_dados() | |
| def carregar_dados_dashboard(): | |
| """Carrega os dados processados para o painel analítico com cache""" | |
| try: | |
| # Carregar dados UCI com cache | |
| df_uci = carregar_dados_uci_cached() | |
| st.session_state['df_uci'] = df_uci | |
| except Exception as e: | |
| st.warning(f"Erro ao carregar dados UCI: {e}") | |
| df_uci = pd.DataFrame() | |
| st.session_state['df_uci'] = df_uci | |
| try: | |
| # Carregar dados OULAD com cache | |
| df_oulad = carregar_dados_oulad_cached() | |
| st.session_state['df_oulad'] = df_oulad | |
| except Exception as e: | |
| st.warning(f"Erro ao carregar dados OULAD: {e}") | |
| df_oulad = pd.DataFrame() | |
| st.session_state['df_oulad'] = df_oulad | |
| return df_uci, df_oulad | |
| def obter_metricas_principais_uci(): | |
| """Retorna métricas principais do dataset UCI calculadas dinamicamente""" | |
| try: | |
| df_uci = carregar_dados_uci_cached() | |
| if df_uci.empty: | |
| return { | |
| 'total_estudantes': 0, | |
| 'media_nota_final': 0, | |
| 'taxa_aprovacao': 0, | |
| 'media_faltas': 0, | |
| 'distribuicao_genero': {}, | |
| 'media_tempo_estudo': 0, | |
| 'correlacao_g1_g3': 0, | |
| 'correlacao_g2_g3': 0, | |
| 'estudantes_alcool_baixo': 0, | |
| 'estudantes_alcool_alto': 0 | |
| } | |
| # Calcular métricas reais - contar estudantes únicos baseado em características demográficas | |
| # Usar combinação de colunas que identificam unicamente cada estudante | |
| colunas_id = ['school', 'sex', 'age', 'address', 'famsize', 'Pstatus', 'Medu', 'Fedu', 'Mjob', 'Fjob', 'reason', 'guardian'] | |
| total_estudantes = df_uci[colunas_id].drop_duplicates().shape[0] | |
| media_nota_final = df_uci['G3'].mean() if 'G3' in df_uci.columns else 0 | |
| taxa_aprovacao = (df_uci['G3'] >= 10).mean() * 100 if 'G3' in df_uci.columns else 0 | |
| media_faltas = df_uci['absences'].mean() if 'absences' in df_uci.columns else 0 | |
| # Distribuição de gênero | |
| if 'sex' in df_uci.columns: | |
| dist_genero = df_uci['sex'].value_counts(normalize=True) * 100 | |
| distribuicao_genero = {k: round(v, 1) for k, v in dist_genero.to_dict().items()} | |
| else: | |
| distribuicao_genero = {} | |
| # Tempo de estudo médio - converter strings para números | |
| if 'studytime' in df_uci.columns: | |
| # Mapear strings para números para calcular média | |
| studytime_map = {'<2h': 1, '2-5h': 2, '5-10h': 3, '>10h': 4} | |
| studytime_numeric = df_uci['studytime'].map(studytime_map) | |
| media_tempo_estudo = studytime_numeric.mean() | |
| else: | |
| media_tempo_estudo = 0 | |
| # Correlações | |
| correlacao_g1_g3 = df_uci[['G1', 'G3']].corr().iloc[0, 1] if all(col in df_uci.columns for col in ['G1', 'G3']) else 0 | |
| correlacao_g2_g3 = df_uci[['G2', 'G3']].corr().iloc[0, 1] if all(col in df_uci.columns for col in ['G2', 'G3']) else 0 | |
| # Consumo de álcool | |
| if 'Dalc' in df_uci.columns: | |
| alcool_baixo = (df_uci['Dalc'] <= 2).mean() * 100 | |
| alcool_alto = (df_uci['Dalc'] >= 4).mean() * 100 | |
| else: | |
| alcool_baixo = 0 | |
| alcool_alto = 0 | |
| return { | |
| 'total_estudantes': total_estudantes, | |
| 'media_nota_final': round(media_nota_final, 2), | |
| 'taxa_aprovacao': round(taxa_aprovacao, 1), | |
| 'media_faltas': round(media_faltas, 1), | |
| 'distribuicao_genero': distribuicao_genero, | |
| 'media_tempo_estudo': round(media_tempo_estudo, 1), | |
| 'correlacao_g1_g3': round(correlacao_g1_g3, 2), | |
| 'correlacao_g2_g3': round(correlacao_g2_g3, 2), | |
| 'estudantes_alcool_baixo': round(alcool_baixo, 1), | |
| 'estudantes_alcool_alto': round(alcool_alto, 1) | |
| } | |
| except Exception as e: | |
| st.warning(f"Erro ao calcular métricas UCI: {e}") | |
| return { | |
| 'total_estudantes': 0, | |
| 'media_nota_final': 0, | |
| 'taxa_aprovacao': 0, | |
| 'media_faltas': 0, | |
| 'distribuicao_genero': {}, | |
| 'media_tempo_estudo': 0, | |
| 'correlacao_g1_g3': 0, | |
| 'correlacao_g2_g3': 0, | |
| 'estudantes_alcool_baixo': 0, | |
| 'estudantes_alcool_alto': 0 | |
| } | |
| def obter_metricas_principais_oulad(): | |
| """Retorna métricas principais do dataset OULAD calculadas dinamicamente""" | |
| try: | |
| df_oulad = carregar_dados_oulad_cached() | |
| if df_oulad.empty: | |
| return { | |
| 'total_estudantes': 0, | |
| 'taxa_aprovacao': 0, | |
| 'media_cliques': 0, | |
| 'distribuicao_genero': {}, | |
| 'faixa_etaria_principal': 'N/A', | |
| 'atividade_mais_comum': 'N/A', | |
| 'regiao_principal': 'N/A', | |
| 'estudantes_aprovados': 0, | |
| 'estudantes_distincao': 0, | |
| 'estudantes_reprovados': 0 | |
| } | |
| # Calcular métricas reais | |
| # Usar nunique() para contar estudantes únicos, não registros | |
| if 'id_student' in df_oulad.columns: | |
| total_estudantes = df_oulad['id_student'].nunique() | |
| else: | |
| total_estudantes = len(df_oulad) # Fallback se não houver coluna id_student | |
| # Calcular média de cliques - verificar tanto 'clicks' quanto 'sum_click' | |
| if 'clicks' in df_oulad.columns: | |
| media_cliques = df_oulad['clicks'].mean() | |
| elif 'sum_click' in df_oulad.columns: | |
| media_cliques = df_oulad['sum_click'].mean() | |
| else: | |
| media_cliques = 0 | |
| # Taxa de aprovação | |
| if 'final_result' in df_oulad.columns: | |
| taxa_aprovacao = (df_oulad['final_result'] == 'Pass').mean() * 100 | |
| estudantes_aprovados = taxa_aprovacao | |
| estudantes_distincao = (df_oulad['final_result'] == 'Distinction').mean() * 100 | |
| estudantes_reprovados = (df_oulad['final_result'] == 'Fail').mean() * 100 | |
| else: | |
| taxa_aprovacao = 0 | |
| estudantes_aprovados = 0 | |
| estudantes_distincao = 0 | |
| estudantes_reprovados = 0 | |
| # Distribuição de gênero | |
| if 'gender' in df_oulad.columns and 'id_student' in df_oulad.columns: | |
| dist_genero = df_oulad.groupby('gender', observed=False)['id_student'].nunique() | |
| total_estudantes = df_oulad['id_student'].nunique() | |
| dist_genero_pct = (dist_genero / total_estudantes * 100) | |
| distribuicao_genero = {k: round(v, 1) for k, v in dist_genero_pct.to_dict().items()} | |
| else: | |
| distribuicao_genero = {} | |
| # Faixa etária principal | |
| if 'age_band' in df_oulad.columns and 'id_student' in df_oulad.columns: | |
| # Encontrar a faixa etária com mais estudantes únicos | |
| idade_counts = df_oulad.groupby('age_band', observed=False)['id_student'].nunique() | |
| faixa_etaria_principal = idade_counts.idxmax() if not idade_counts.empty else 'N/A' | |
| else: | |
| faixa_etaria_principal = 'N/A' | |
| # Atividade mais comum | |
| if 'activity_type' in df_oulad.columns: | |
| atividade_mais_comum = df_oulad['activity_type'].mode().iloc[0] if not df_oulad['activity_type'].mode().empty else 'N/A' | |
| else: | |
| atividade_mais_comum = 'N/A' | |
| # Região principal | |
| if 'region' in df_oulad.columns and 'id_student' in df_oulad.columns: | |
| # Encontrar a região com mais estudantes únicos | |
| regiao_counts = df_oulad.groupby('region', observed=False)['id_student'].nunique() | |
| regiao_principal = regiao_counts.idxmax() if not regiao_counts.empty else 'N/A' | |
| else: | |
| regiao_principal = 'N/A' | |
| return { | |
| 'total_estudantes': total_estudantes, | |
| 'taxa_aprovacao': round(taxa_aprovacao, 1), | |
| 'media_cliques': round(media_cliques, 2), | |
| 'distribuicao_genero': distribuicao_genero, | |
| 'faixa_etaria_principal': faixa_etaria_principal, | |
| 'atividade_mais_comum': atividade_mais_comum, | |
| 'regiao_principal': regiao_principal, | |
| 'estudantes_aprovados': round(estudantes_aprovados, 1), | |
| 'estudantes_distincao': round(estudantes_distincao, 1), | |
| 'estudantes_reprovados': round(estudantes_reprovados, 1) | |
| } | |
| except Exception as e: | |
| st.warning(f"Erro ao calcular métricas OULAD: {e}") | |
| return { | |
| 'total_estudantes': 0, | |
| 'taxa_aprovacao': 0, | |
| 'media_cliques': 0, | |
| 'distribuicao_genero': {}, | |
| 'faixa_etaria_principal': 'N/A', | |
| 'atividade_mais_comum': 'N/A', | |
| 'regiao_principal': 'N/A', | |
| 'estudantes_aprovados': 0, | |
| 'estudantes_distincao': 0, | |
| 'estudantes_reprovados': 0 | |
| } | |
| def calcular_metricas_uci(df_uci): | |
| """Calcula métricas principais para o dataset UCI""" | |
| if df_uci.empty: | |
| return {} | |
| # Contar estudantes únicos baseado em características demográficas | |
| colunas_id = ['school', 'sex', 'age', 'address', 'famsize', 'Pstatus', 'Medu', 'Fedu', 'Mjob', 'Fjob', 'reason', 'guardian'] | |
| total_estudantes_unicos = df_uci[colunas_id].drop_duplicates().shape[0] | |
| metricas = { | |
| 'total_alunos': total_estudantes_unicos, | |
| 'media_nota_final': df_uci['G3'].mean() if 'G3' in df_uci.columns else 0, | |
| 'taxa_aprovacao': (df_uci['G3'] >= 10).mean() * 100 if 'G3' in df_uci.columns else 0, | |
| 'media_faltas': df_uci['absences'].mean() if 'absences' in df_uci.columns else 0, | |
| 'media_tempo_estudo': df_uci['studytime'].map({'<2h': 1, '2-5h': 2, '5-10h': 3, '>10h': 4}).mean() if 'studytime' in df_uci.columns else 0, | |
| 'distribuicao_genero': df_uci['sex'].value_counts().to_dict() if 'sex' in df_uci.columns else {}, | |
| 'correlacao_notas': df_uci[['G1', 'G2', 'G3']].corr().to_dict() if all(col in df_uci.columns for col in ['G1', 'G2', 'G3']) else {} | |
| } | |
| return metricas | |
| def calcular_metricas_oulad(df_oulad): | |
| """Calcula métricas principais para o dataset OULAD""" | |
| if df_oulad.empty: | |
| return {} | |
| metricas = { | |
| 'total_estudantes': df_oulad['id_student'].nunique() if 'id_student' in df_oulad.columns else len(df_oulad), | |
| 'media_cliques': ( | |
| df_oulad['clicks'].mean() if 'clicks' in df_oulad.columns | |
| else (df_oulad['sum_click'].mean() if 'sum_click' in df_oulad.columns else 0) | |
| ), | |
| 'taxa_aprovacao': (df_oulad['final_result'] == 'Pass').mean() * 100 if 'final_result' in df_oulad.columns else 0, | |
| 'distribuicao_genero': df_oulad.groupby('gender', observed=False)['id_student'].nunique().to_dict() if 'gender' in df_oulad.columns and 'id_student' in df_oulad.columns else {}, | |
| 'distribuicao_idade': df_oulad.groupby('age_band', observed=False)['id_student'].nunique().to_dict() if 'age_band' in df_oulad.columns and 'id_student' in df_oulad.columns else {}, | |
| 'atividade_mais_comum': df_oulad['activity_type'].mode().iloc[0] if 'activity_type' in df_oulad.columns else 'N/A', | |
| 'regiao_mais_comum': df_oulad['region'].mode().iloc[0] if 'region' in df_oulad.columns else 'N/A' | |
| } | |
| return metricas | |
| def gerar_metricas_consolidadas(df_uci, df_oulad): | |
| """Gera métricas consolidadas para o painel analítico""" | |
| metricas_uci = calcular_metricas_uci(df_uci) | |
| metricas_oulad = calcular_metricas_oulad(df_oulad) | |
| # Métricas consolidadas | |
| total_estudantes = metricas_uci.get('total_alunos', 0) + metricas_oulad.get('total_estudantes', 0) | |
| taxa_aprovacao_geral = np.mean([ | |
| metricas_uci.get('taxa_aprovacao', 0), | |
| metricas_oulad.get('taxa_aprovacao', 0) | |
| ]) | |
| return { | |
| 'total_estudantes_geral': total_estudantes, | |
| 'taxa_aprovacao_geral': taxa_aprovacao_geral, | |
| 'metricas_uci': metricas_uci, | |
| 'metricas_oulad': metricas_oulad | |
| } | |
| def criar_sidebar_dashboard(): | |
| """Cria a barra lateral do painel analítico""" | |
| with st.sidebar: | |
| st.markdown("### 📊 Painel Analítico") | |
| # Carregar métricas dinâmicas | |
| metricas_uci = obter_metricas_principais_uci() | |
| metricas_oulad = obter_metricas_principais_oulad() | |
| st.markdown("### 📚 Sobre os Datasets") | |
| st.markdown(f""" | |
| **📚 UCI Dataset:** | |
| - Escolas públicas portuguesas | |
| - {metricas_uci['total_estudantes']:,} estudantes | |
| - Dados demográficos e acadêmicos | |
| - Análise de fatores de sucesso | |
| """) | |
| st.markdown(f""" | |
| **🌐 OULAD Dataset:** | |
| - Plataforma de aprendizado online | |
| - {metricas_oulad['total_estudantes']:,} estudantes | |
| - Dados de engajamento digital | |
| - Análise de atividades online | |
| """) | |
| st.markdown("---") | |
| st.markdown("### 📈 Métricas Rápidas") | |
| # Métricas UCI | |
| st.metric( | |
| "🎓 UCI - Aprovação", | |
| f"{metricas_uci['taxa_aprovacao']:.1f}%", | |
| help="Taxa de aprovação nas escolas públicas" | |
| ) | |
| st.metric( | |
| "📊 UCI - Média Notas", | |
| f"{metricas_uci['media_nota_final']:.1f}", | |
| help="Média das notas finais" | |
| ) | |
| # Métricas OULAD | |
| st.metric( | |
| "🌐 OULAD - Aprovação", | |
| f"{metricas_oulad['taxa_aprovacao']:.1f}%", | |
| help="Taxa de aprovação na plataforma online" | |
| ) | |
| st.metric( | |
| "🖱️ OULAD - Engajamento", | |
| f"{metricas_oulad['media_cliques']:.1f}", | |
| help="Média de cliques por estudante" | |
| ) | |
| st.markdown("---") | |
| st.markdown("### 💡 Principais Insights") | |
| # Insights dinâmicos baseados nos dados reais | |
| insights_text = [] | |
| if metricas_uci['correlacao_g1_g3'] > 0.7: | |
| insights_text.append(f"**Correlação forte** entre notas bimestrais e finais ({metricas_uci['correlacao_g1_g3']:.2f})") | |
| if metricas_uci['distribuicao_genero']: | |
| genero_maioria = max(metricas_uci['distribuicao_genero'], key=metricas_uci['distribuicao_genero'].get) | |
| insights_text.append(f"**Gênero predominante**: {genero_maioria} ({metricas_uci['distribuicao_genero'][genero_maioria]:.1f}%)") | |
| if metricas_uci['media_faltas'] > 0: | |
| insights_text.append(f"**Média de faltas**: {metricas_uci['media_faltas']:.1f} por estudante") | |
| if metricas_uci['media_tempo_estudo'] > 0: | |
| insights_text.append(f"**Tempo de estudo médio**: {metricas_uci['media_tempo_estudo']:.1f}h/semana") | |
| if metricas_oulad['atividade_mais_comum'] != 'N/A': | |
| insights_text.append(f"**Atividade mais comum**: {metricas_oulad['atividade_mais_comum']}") | |
| if insights_text: | |
| for insight in insights_text: | |
| st.markdown(f"- {insight}") | |
| else: | |
| st.markdown(""" | |
| - **Correlação forte** entre notas bimestrais e finais | |
| - **Gênero influencia** desempenho acadêmico | |
| - **Faltas impactam** negativamente o desempenho | |
| - **Tempo de estudo** ideal: 5-10h/semana | |
| - **Atividades online** mais efetivas: outcontent, forumng | |
| """) | |
| st.markdown("---") | |
| st.markdown("### ℹ️ Informações") | |
| st.markdown("**Mestrado em Tecnologia Educacional - UFC**") | |
| return None, None # Retorna None para manter compatibilidade | |
| def exibir_cartoes_informativos(): | |
| """Exibe cartões informativos com métricas principais""" | |
| metricas_uci = obter_metricas_principais_uci() | |
| metricas_oulad = obter_metricas_principais_oulad() | |
| # Cartões principais | |
| st.markdown("## 📊 Métricas Principais") | |
| col1, col2, col3, col4 = st.columns(4) | |
| with col1: | |
| st.metric( | |
| "🎓 Total de Estudantes", | |
| f"{metricas_uci['total_estudantes'] + metricas_oulad['total_estudantes']:,}", | |
| help="Soma dos estudantes dos datasets UCI e OULAD" | |
| ) | |
| with col2: | |
| taxa_geral = (metricas_uci['taxa_aprovacao'] + metricas_oulad['taxa_aprovacao']) / 2 | |
| st.metric( | |
| "✅ Taxa de Aprovação Geral", | |
| f"{taxa_geral:.1f}%", | |
| help="Média das taxas de aprovação dos dois datasets" | |
| ) | |
| with col3: | |
| st.metric( | |
| "📚 Média de Notas (UCI)", | |
| f"{metricas_uci['media_nota_final']:.1f}", | |
| help="Média das notas finais no dataset UCI" | |
| ) | |
| with col4: | |
| st.metric( | |
| "🖱️ Média de Cliques (OULAD)", | |
| f"{metricas_oulad['media_cliques']:.1f}", | |
| help="Média de cliques por estudante no dataset OULAD" | |
| ) | |
| def exibir_cartoes_detalhados(): | |
| """Exibe cartões detalhados para cada dataset""" | |
| metricas_uci = obter_metricas_principais_uci() | |
| metricas_oulad = obter_metricas_principais_oulad() | |
| # Cartões UCI | |
| st.markdown("### 📚 Dataset UCI - Escolas Públicas Portuguesas") | |
| col1, col2, col3, col4 = st.columns(4) | |
| with col1: | |
| st.metric( | |
| "👥 Total de Estudantes", | |
| f"{metricas_uci['total_estudantes']:,}", | |
| help="Estudantes de escolas públicas portuguesas" | |
| ) | |
| with col2: | |
| st.metric( | |
| "✅ Taxa de Aprovação", | |
| f"{metricas_uci['taxa_aprovacao']:.1f}%", | |
| help="Percentual de estudantes aprovados" | |
| ) | |
| with col3: | |
| st.metric( | |
| "📊 Média de Faltas", | |
| f"{metricas_uci['media_faltas']:.1f}", | |
| help="Número médio de faltas por estudante" | |
| ) | |
| with col4: | |
| st.metric( | |
| "⏰ Tempo de Estudo", | |
| f"{metricas_uci['media_tempo_estudo']:.1f}h/semana", | |
| help="Tempo médio de estudo semanal" | |
| ) | |
| # Cartões OULAD | |
| st.markdown("### 🌐 Dataset OULAD - Plataforma de Aprendizado Online") | |
| col1, col2, col3, col4 = st.columns(4) | |
| with col1: | |
| st.metric( | |
| "👥 Total de Estudantes", | |
| f"{metricas_oulad['total_estudantes']:,}", | |
| help="Estudantes da plataforma online" | |
| ) | |
| with col2: | |
| st.metric( | |
| "✅ Taxa de Aprovação", | |
| f"{metricas_oulad['taxa_aprovacao']:.1f}%", | |
| help="Percentual de estudantes aprovados" | |
| ) | |
| with col3: | |
| st.metric( | |
| "🏆 Distinção", | |
| f"{metricas_oulad['estudantes_distincao']:.1f}%", | |
| help="Percentual de estudantes com distinção" | |
| ) | |
| with col4: | |
| st.metric( | |
| "🖱️ Engajamento", | |
| f"{metricas_oulad['media_cliques']:.1f} cliques", | |
| help="Média de cliques por estudante" | |
| ) | |
| def obter_insights_uci(): | |
| """Retorna insights principais do dataset UCI baseados em dados reais""" | |
| metricas = obter_metricas_principais_uci() | |
| insights = [] | |
| # Correlação forte | |
| if metricas['correlacao_g1_g3'] > 0.7 and metricas['correlacao_g2_g3'] > 0.7: | |
| insights.append(f"🎯 **Correlação Forte**: Notas do 1º e 2º bimestre têm correlação de {metricas['correlacao_g1_g3']:.2f} e {metricas['correlacao_g2_g3']:.2f} com a nota final") | |
| # Gênero | |
| if metricas['distribuicao_genero']: | |
| genero_maioria = max(metricas['distribuicao_genero'], key=metricas['distribuicao_genero'].get) | |
| genero_menor = min(metricas['distribuicao_genero'], key=metricas['distribuicao_genero'].get) | |
| insights.append(f"👥 **Gênero**: Estudantes do sexo {genero_maioria} representam {metricas['distribuicao_genero'][genero_maioria]:.1f}% vs {genero_menor} com {metricas['distribuicao_genero'][genero_menor]:.1f}%") | |
| # Consumo de álcool | |
| if metricas['estudantes_alcool_baixo'] > 0: | |
| insights.append(f"🍷 **Consumo de Álcool**: {metricas['estudantes_alcool_baixo']:.1f}% dos estudantes têm baixo consumo, com melhor desempenho acadêmico") | |
| # Tempo de estudo | |
| if metricas['media_tempo_estudo'] > 0: | |
| insights.append(f"📚 **Tempo de Estudo**: Média de {metricas['media_tempo_estudo']:.1f}h/semana por estudante") | |
| # Faltas | |
| if metricas['media_faltas'] > 0: | |
| insights.append(f"❌ **Faltas**: Média de {metricas['media_faltas']:.1f} faltas por estudante") | |
| # Taxa de aprovação | |
| if metricas['taxa_aprovacao'] > 0: | |
| insights.append(f"✅ **Aprovação**: Taxa de aprovação de {metricas['taxa_aprovacao']:.1f}%") | |
| # Média de notas | |
| if metricas['media_nota_final'] > 0: | |
| insights.append(f"📊 **Desempenho**: Média de notas finais de {metricas['media_nota_final']:.1f}") | |
| return { | |
| 'titulo': '📚 Principais Insights - Dataset UCI', | |
| 'insights': insights if insights else [ | |
| "🎯 **Correlação Forte**: Notas do 1º e 2º bimestre têm correlação forte com a nota final", | |
| "👥 **Gênero**: Distribuição equilibrada entre gêneros", | |
| "📚 **Tempo de Estudo**: Fator importante para o desempenho acadêmico", | |
| "❌ **Faltas**: Impactam negativamente o desempenho", | |
| "👨👩👧👦 **Família**: Escolaridade dos pais influencia o desempenho dos filhos" | |
| ] | |
| } | |
| def obter_insights_oulad(): | |
| """Retorna insights principais do dataset OULAD baseados em dados reais""" | |
| metricas = obter_metricas_principais_oulad() | |
| insights = [] | |
| # Demografia | |
| if metricas['distribuicao_genero']: | |
| genero_maioria = max(metricas['distribuicao_genero'], key=metricas['distribuicao_genero'].get) | |
| insights.append(f"👥 **Demografia**: {metricas['distribuicao_genero'][genero_maioria]:.1f}% são do sexo {genero_maioria}") | |
| if metricas['faixa_etaria_principal'] != 'N/A': | |
| insights.append(f"👥 **Faixa Etária**: Faixa etária predominante de {metricas['faixa_etaria_principal']}") | |
| # Desempenho | |
| if metricas['taxa_aprovacao'] > 0: | |
| insights.append(f"🏆 **Alto Desempenho**: {metricas['taxa_aprovacao']:.1f}% de aprovação") | |
| if metricas['estudantes_distincao'] > 0: | |
| insights.append(f"🏆 **Distinção**: {metricas['estudantes_distincao']:.1f}% obtendo distinção") | |
| # Engajamento | |
| if metricas['media_cliques'] > 0: | |
| insights.append(f"🖱️ **Engajamento**: Média de {metricas['media_cliques']:.1f} cliques por estudante, indicando engajamento moderado") | |
| # Atividades | |
| if metricas['atividade_mais_comum'] != 'N/A': | |
| insights.append(f"📚 **Atividades**: '{metricas['atividade_mais_comum']}' é a atividade mais realizada") | |
| # Região | |
| if metricas['regiao_principal'] != 'N/A': | |
| insights.append(f"🌍 **Região**: {metricas['regiao_principal']} concentra a maior parte dos estudantes") | |
| # Distribuição de resultados | |
| if metricas['estudantes_reprovados'] > 0: | |
| insights.append(f"📊 **Distribuição**: Aprovação supera largamente outras categorias (reprovação: {metricas['estudantes_reprovados']:.1f}%)") | |
| # Total de estudantes | |
| if metricas['total_estudantes'] > 0: | |
| insights.append(f"👥 **Total**: {metricas['total_estudantes']:,} estudantes analisados") | |
| return { | |
| 'titulo': '🌐 Principais Insights - Dataset OULAD', | |
| 'insights': insights if insights else [ | |
| "👥 **Demografia**: Distribuição equilibrada entre gêneros", | |
| "🏆 **Alto Desempenho**: Boa taxa de aprovação geral", | |
| "🖱️ **Engajamento**: Nível moderado de engajamento na plataforma", | |
| "📚 **Atividades**: Diversas atividades disponíveis", | |
| "🌍 **Região**: Distribuição geográfica variada", | |
| "📊 **Distribuição**: Resultados positivos predominam" | |
| ] | |
| } | |
| # ============================================================================= | |
| # FUNÇÕES DE TREINAMENTO SOB DEMANDA | |
| # ============================================================================= | |
| def treinar_modelo_uci_on_demand(): | |
| """Treina modelo UCI sob demanda com progresso e salvamento automático""" | |
| try: | |
| from sklearn.ensemble import RandomForestRegressor | |
| from sklearn.preprocessing import OneHotEncoder | |
| from sklearn.compose import ColumnTransformer | |
| from sklearn.pipeline import Pipeline | |
| from sklearn.model_selection import train_test_split | |
| import pickle | |
| # Indicador de progresso (compatível com e sem Streamlit) | |
| try: | |
| progress_bar = st.progress(0) | |
| status_text = st.empty() | |
| use_streamlit = True | |
| except: | |
| progress_bar = None | |
| status_text = None | |
| use_streamlit = False | |
| print("🔄 Carregando dados UCI...") | |
| if use_streamlit: | |
| status_text.text("🔄 Carregando dados UCI...") | |
| progress_bar.progress(20) | |
| else: | |
| print("🔄 Carregando dados UCI...") | |
| # Carregar dados UCI | |
| df_uci = carregar_uci_dados() | |
| if use_streamlit: | |
| status_text.text("🔄 Preparando dados...") | |
| progress_bar.progress(40) | |
| else: | |
| print("🔄 Preparando dados...") | |
| # Preparar dados como na página 1_UCI.py | |
| Y = df_uci['G3'] | |
| X = df_uci.drop('G3', axis=1) | |
| # Dividir dados | |
| X_train, X_test, y_train, y_test = train_test_split(X, Y, test_size=0.2, random_state=42) | |
| if use_streamlit: | |
| status_text.text("🔄 Treinando modelo RandomForest...") | |
| progress_bar.progress(60) | |
| else: | |
| print("🔄 Treinando modelo RandomForest...") | |
| # Identificar colunas categóricas | |
| categorical_features = X_train.select_dtypes(include=['object']).columns | |
| # Criar preprocessor | |
| preprocessor = ColumnTransformer( | |
| transformers=[ | |
| ('cat', OneHotEncoder(handle_unknown='ignore'), categorical_features) | |
| ], | |
| remainder='passthrough' | |
| ) | |
| # Criar pipeline | |
| model = Pipeline(steps=[ | |
| ('preprocessor', preprocessor), | |
| ('regressor', RandomForestRegressor(n_estimators=100, random_state=42)) | |
| ]) | |
| # Treinar modelo | |
| y_train_encoded = y_train.astype(float) | |
| model.fit(X_train, y_train_encoded) | |
| if use_streamlit: | |
| status_text.text("🔄 Salvando modelo...") | |
| progress_bar.progress(80) | |
| else: | |
| print("🔄 Salvando modelo...") | |
| # Salvar modelo | |
| with open('uci.pkl', 'wb') as f: | |
| pickle.dump(model, f) | |
| if use_streamlit: | |
| status_text.text("✅ Modelo UCI treinado e salvo!") | |
| progress_bar.progress(100) | |
| # Limpar indicadores | |
| progress_bar.empty() | |
| status_text.empty() | |
| else: | |
| print("✅ Modelo UCI treinado e salvo!") | |
| return model | |
| except Exception as e: | |
| try: | |
| st.error(f"Erro ao treinar modelo UCI: {e}") | |
| except: | |
| print(f"❌ Erro ao treinar modelo UCI: {e}") | |
| return None | |
| def treinar_modelo_oulad_on_demand(): | |
| """Treina modelo OULAD sob demanda com progresso e salvamento automático""" | |
| try: | |
| from sklearn.ensemble import RandomForestClassifier | |
| from sklearn.preprocessing import OneHotEncoder, LabelEncoder | |
| from sklearn.compose import ColumnTransformer | |
| from sklearn.pipeline import Pipeline | |
| from sklearn.model_selection import train_test_split | |
| from sklearn.impute import SimpleImputer | |
| import pickle | |
| import numpy as np | |
| # Indicador de progresso (compatível com e sem Streamlit) | |
| try: | |
| progress_bar = st.progress(0) | |
| status_text = st.empty() | |
| use_streamlit = True | |
| except: | |
| progress_bar = None | |
| status_text = None | |
| use_streamlit = False | |
| print("🔄 Carregando dados OULAD...") | |
| if use_streamlit: | |
| status_text.text("🔄 Carregando dados OULAD...") | |
| progress_bar.progress(10) | |
| else: | |
| print("🔄 Carregando dados OULAD...") | |
| # Carregar dados OULAD | |
| df_oulad = carregar_oulad_dados() | |
| # Usar amostra para treinamento mais rápido | |
| if len(df_oulad) > 50000: | |
| df_oulad = df_oulad.sample(n=50000, random_state=42) | |
| if use_streamlit: | |
| st.info("📊 Usando amostra de 50k registros para treinamento mais rápido") | |
| else: | |
| print("📊 Usando amostra de 50k registros para treinamento mais rápido") | |
| if use_streamlit: | |
| status_text.text("🔄 Preparando dados...") | |
| progress_bar.progress(30) | |
| else: | |
| print("🔄 Preparando dados...") | |
| # Preparar dados como na página 2_OULAD.py | |
| Y = df_oulad['final_result'] | |
| X = df_oulad.loc[:, df_oulad.columns != 'final_result'] | |
| # Remover colunas irrelevantes | |
| X = X.drop(['id_student', 'id_site', 'id_assessment', 'code_module', 'code_presentation', 'code_module_y', 'code_module_x'], axis=1, errors='ignore') | |
| if use_streamlit: | |
| status_text.text("🔄 Dividindo dados...") | |
| progress_bar.progress(50) | |
| else: | |
| print("🔄 Dividindo dados...") | |
| # Dividir dados | |
| X_train, X_test, y_train, y_test = train_test_split(X, Y, test_size=0.2, random_state=42) | |
| if use_streamlit: | |
| status_text.text("🔄 Limpando dados de treino...") | |
| progress_bar.progress(60) | |
| else: | |
| print("🔄 Limpando dados de treino...") | |
| # Limpar dados de treino | |
| nan_rows_train = y_train.isnull() | |
| X_train_cleaned = X_train[~nan_rows_train].copy() | |
| y_train_cleaned = y_train[~nan_rows_train].copy() | |
| if use_streamlit: | |
| status_text.text("🔄 Treinando modelo RandomForest...") | |
| progress_bar.progress(70) | |
| else: | |
| print("🔄 Treinando modelo RandomForest...") | |
| # Identificar colunas categóricas e numéricas | |
| categorical_cols = X_train_cleaned.select_dtypes(include='object').columns | |
| numerical_cols = X_train_cleaned.select_dtypes(include=[np.number]).columns | |
| # Verificar se há colunas que não são nem categóricas nem numéricas | |
| all_cols = set(X_train_cleaned.columns) | |
| processed_cols = set(categorical_cols) | set(numerical_cols) | |
| remaining_cols = all_cols - processed_cols | |
| if len(remaining_cols) > 0: | |
| # Adicionar colunas restantes como categóricas | |
| categorical_cols = list(categorical_cols) + list(remaining_cols) | |
| # Criar preprocessor | |
| transformers = [] | |
| # Adicionar transformador numérico apenas se houver colunas numéricas | |
| if len(numerical_cols) > 0: | |
| transformers.append(('num', SimpleImputer(strategy='mean'), numerical_cols)) | |
| # Adicionar transformador categórico apenas se houver colunas categóricas | |
| if len(categorical_cols) > 0: | |
| transformers.append(('cat', Pipeline(steps=[ | |
| ('imputer', SimpleImputer(strategy='most_frequent')), | |
| ('onehot', OneHotEncoder(handle_unknown='ignore'))]), categorical_cols)) | |
| preprocessor = ColumnTransformer( | |
| transformers=transformers, | |
| remainder='passthrough' | |
| ) | |
| # Criar pipeline | |
| model = Pipeline(steps=[ | |
| ('preprocessor', preprocessor), | |
| ('classifier', RandomForestClassifier(n_estimators=50, n_jobs=2, max_depth=4, random_state=42)) | |
| ]) | |
| # Treinar modelo | |
| model.fit(X_train_cleaned, y_train_cleaned) | |
| if use_streamlit: | |
| status_text.text("🔄 Salvando modelo...") | |
| progress_bar.progress(90) | |
| else: | |
| print("🔄 Salvando modelo...") | |
| # Salvar modelo | |
| with open('oulad.pkl', 'wb') as f: | |
| pickle.dump(model, f) | |
| if use_streamlit: | |
| status_text.text("✅ Modelo OULAD treinado e salvo!") | |
| progress_bar.progress(100) | |
| # Limpar indicadores | |
| progress_bar.empty() | |
| status_text.empty() | |
| else: | |
| print("✅ Modelo OULAD treinado e salvo!") | |
| return model | |
| except Exception as e: | |
| try: | |
| st.error(f"Erro ao treinar modelo OULAD: {e}") | |
| import traceback | |
| st.error(f"Traceback: {traceback.format_exc()}") | |
| except: | |
| print(f"❌ Erro ao treinar modelo OULAD: {e}") | |
| import traceback | |
| print(f"Traceback: {traceback.format_exc()}") | |
| return None | |
| # Cache por 2 horas | |
| def carregar_modelo_uci(): | |
| """Carrega o modelo UCI com cache ou treina sob demanda""" | |
| try: | |
| # Tentar diferentes caminhos para o arquivo pickle | |
| possible_paths = [ | |
| '../uci.pkl', | |
| '../../uci.pkl', | |
| Path(__file__).parent.parents[1] / "uci.pkl", | |
| 'uci.pkl' | |
| ] | |
| model = None | |
| for path in possible_paths: | |
| p = Path(path) | |
| if p.is_file(): | |
| try: | |
| with p.open("rb") as f: | |
| model = pickle.load(f) | |
| break | |
| except Exception as e: | |
| continue | |
| if model is None: | |
| st.info("📦 Modelo UCI não encontrado. Treinando modelo automaticamente...") | |
| return treinar_modelo_uci_on_demand() | |
| return model | |
| except Exception as e: | |
| st.warning(f"Erro ao carregar modelo UCI: {e}") | |
| return None | |
| # Cache por 2 horas | |
| def carregar_modelo_oulad(): | |
| """Carrega o modelo OULAD com cache ou treina sob demanda""" | |
| try: | |
| # Tentar diferentes caminhos para o arquivo pickle | |
| possible_paths = [ | |
| '../oulad.pkl', | |
| '../../oulad.pkl', | |
| Path(__file__).parent.parents[1] / "oulad.pkl", | |
| 'oulad.pkl' | |
| ] | |
| model = None | |
| for path in possible_paths: | |
| p = Path(path) | |
| if p.is_file(): | |
| try: | |
| with p.open("rb") as f: | |
| model = pickle.load(f) | |
| break | |
| except Exception as e: | |
| continue | |
| if model is None: | |
| st.info("📦 Modelo OULAD não encontrado. Treinando modelo automaticamente (pode levar alguns minutos)...") | |
| return treinar_modelo_oulad_on_demand() | |
| return model | |
| except Exception as e: | |
| st.warning(f"Erro ao carregar modelo OULAD: {e}") | |
| return None | |
| # Cache por 1 hora (UCI é menor) | |
| def calcular_feature_importance_uci(): | |
| """Calcula feature importance real para UCI com otimizações""" | |
| try: | |
| from sklearn.inspection import permutation_importance | |
| from sklearn.model_selection import train_test_split | |
| # Indicador de progresso | |
| progress_bar = st.progress(0) | |
| status_text = st.empty() | |
| status_text.text("🔄 Carregando dados UCI...") | |
| progress_bar.progress(20) | |
| # Carregar dados UCI | |
| df_uci = carregar_uci_dados() | |
| status_text.text("🔄 Preparando dados...") | |
| progress_bar.progress(40) | |
| # Preparar dados como nas páginas individuais | |
| Y = df_uci['G3'] | |
| X = df_uci.drop('G3', axis=1) | |
| # Dividir dados | |
| X_train, X_test, y_train, y_test = train_test_split(X, Y, test_size=0.2, random_state=42) | |
| status_text.text("🔄 Carregando modelo...") | |
| progress_bar.progress(60) | |
| # Carregar modelo treinado | |
| model = carregar_modelo_uci() | |
| if model is None: | |
| progress_bar.empty() | |
| status_text.empty() | |
| return pd.DataFrame() | |
| # Garantir que todas as colunas tenham tipos corretos | |
| # Converter colunas não numéricas para object (categóricas) | |
| for col in X_test.columns: | |
| if X_test[col].dtype not in [np.number, 'object']: | |
| # Tentar converter para numérico primeiro | |
| try: | |
| X_test[col] = pd.to_numeric(X_test[col], errors='coerce') | |
| # Se ainda não for numérico, converter para string/object | |
| if X_test[col].dtype not in [np.number]: | |
| X_test[col] = X_test[col].astype(str).astype('object') | |
| except: | |
| X_test[col] = X_test[col].astype(str).astype('object') | |
| # Garantir que colunas numéricas não tenham valores infinitos | |
| numeric_cols = X_test.select_dtypes(include=[np.number]).columns | |
| for col in numeric_cols: | |
| X_test[col] = pd.to_numeric(X_test[col], errors='coerce').fillna(0) | |
| X_test[col] = X_test[col].replace([np.inf, -np.inf], 0) | |
| # Garantir que colunas categóricas sejam do tipo object | |
| categorical_cols = X_test.select_dtypes(include=['object']).columns | |
| for col in categorical_cols: | |
| X_test[col] = X_test[col].astype(str).replace('nan', np.nan).astype('object') | |
| status_text.text("🔄 Calculando feature importance...") | |
| progress_bar.progress(80) | |
| # OTIMIZAÇÃO: Usar todos os cores disponíveis | |
| try: | |
| result = permutation_importance( | |
| model, X_test, y_test, | |
| n_repeats=10, # Manter 10 para UCI (é pequeno) | |
| random_state=42, | |
| n_jobs=-1 # Usar todos os cores disponíveis | |
| ) | |
| sorted_idx = result.importances_mean.argsort() | |
| status_text.text("✅ Finalizando...") | |
| progress_bar.progress(95) | |
| # Criar DataFrame com resultados reais | |
| features = X_test.columns[sorted_idx] | |
| importance = result.importances_mean[sorted_idx] | |
| except Exception as e: | |
| # Se permutation_importance falhar, retornar DataFrame vazio | |
| st.warning(f"Erro ao calcular feature importance: {e}") | |
| progress_bar.empty() | |
| status_text.empty() | |
| return pd.DataFrame() | |
| df_result = pd.DataFrame({ | |
| 'feature': features, | |
| 'importance': importance | |
| }).sort_values('importance', ascending=True) | |
| # Limpar indicadores | |
| progress_bar.empty() | |
| status_text.empty() | |
| return df_result | |
| except Exception as e: | |
| st.warning(f"Erro ao calcular feature importance UCI: {e}") | |
| return pd.DataFrame() | |
| # Cache por 2 horas (OULAD é pesado) | |
| def calcular_feature_importance_oulad(): | |
| """Calcula feature importance real para OULAD com otimizações""" | |
| try: | |
| from sklearn.inspection import permutation_importance | |
| from sklearn.model_selection import train_test_split | |
| # Indicador de progresso | |
| progress_bar = st.progress(0) | |
| status_text = st.empty() | |
| status_text.text("🔄 Carregando dados OULAD...") | |
| progress_bar.progress(10) | |
| # Carregar dados OULAD | |
| df_oulad = carregar_oulad_dados() | |
| # AMOSTRAGEM: Usar apenas 50k registros para OULAD (muito mais rápido) | |
| if len(df_oulad) > 50000: | |
| df_oulad = df_oulad.sample(n=50000, random_state=42) | |
| st.info("📊 Usando amostra de 50k registros para análise mais rápida") | |
| status_text.text("🔄 Preparando dados...") | |
| progress_bar.progress(30) | |
| # Preparar dados como nas páginas individuais | |
| Y = df_oulad['final_result'] | |
| X = df_oulad.loc[:, df_oulad.columns != 'final_result'] | |
| # Remover colunas irrelevantes | |
| X = X.drop(['id_student', 'id_site', 'id_assessment', 'code_module', 'code_presentation', 'code_module_y', 'code_module_x'], axis=1, errors='ignore') | |
| status_text.text("🔄 Carregando modelo...") | |
| progress_bar.progress(40) | |
| # Carregar modelo treinado primeiro para saber quais colunas ele espera | |
| model = carregar_modelo_oulad() | |
| if model is None: | |
| progress_bar.empty() | |
| status_text.empty() | |
| return pd.DataFrame() | |
| status_text.text("🔄 Dividindo dados...") | |
| progress_bar.progress(50) | |
| # Dividir dados | |
| X_train, X_test, y_train, y_test = train_test_split(X, Y, test_size=0.2, random_state=42) | |
| status_text.text("🔄 Limpando dados de teste...") | |
| progress_bar.progress(70) | |
| # Limpar dados de teste | |
| nan_rows_test = y_test.isnull() | |
| X_test_cleaned = X_test[~nan_rows_test].copy() | |
| y_test_cleaned = y_test[~nan_rows_test].copy() | |
| # Garantir que todas as colunas tenham tipos corretos | |
| # Primeiro, identificar colunas numéricas e categóricas | |
| numeric_cols = X_test_cleaned.select_dtypes(include=[np.number]).columns | |
| categorical_cols = X_test_cleaned.select_dtypes(include=['object', 'category']).columns | |
| # Garantir que colunas numéricas não tenham valores infinitos ou NaN problemáticos | |
| for col in numeric_cols: | |
| X_test_cleaned[col] = pd.to_numeric(X_test_cleaned[col], errors='coerce').fillna(0) | |
| X_test_cleaned[col] = X_test_cleaned[col].replace([np.inf, -np.inf], 0) | |
| # Garantir que TODAS as colunas categóricas sejam explicitamente convertidas para string | |
| # Isso é crítico para evitar erros de conversão | |
| # Especialmente importante para colunas como 'activity_type' que contêm valores como 'homepage' | |
| for col in categorical_cols: | |
| # Converter todos os valores para string explicitamente | |
| X_test_cleaned[col] = X_test_cleaned[col].apply( | |
| lambda x: str(x) if pd.notna(x) and x != '' else 'missing' | |
| ) | |
| # Garantir que seja do tipo object (categórica) | |
| X_test_cleaned[col] = X_test_cleaned[col].astype('object') | |
| # Garantir que colunas conhecidas como categóricas sejam explicitamente tratadas | |
| known_categorical_cols = ['activity_type', 'gender', 'region', 'highest_education', | |
| 'imd_band', 'age_band', 'disability', 'final_result', | |
| 'assessment_type', 'code_module', 'code_presentation'] | |
| for col in known_categorical_cols: | |
| if col in X_test_cleaned.columns: | |
| # Forçar conversão para string e depois object | |
| X_test_cleaned[col] = X_test_cleaned[col].apply( | |
| lambda x: str(x) if pd.notna(x) and x != '' else 'missing' | |
| ).astype('object') | |
| # Tratar colunas que não são nem numéricas nem categóricas | |
| other_cols = set(X_test_cleaned.columns) - set(numeric_cols) - set(categorical_cols) | |
| for col in other_cols: | |
| # Tentar converter para numérico primeiro | |
| try: | |
| X_test_cleaned[col] = pd.to_numeric(X_test_cleaned[col], errors='coerce') | |
| if X_test_cleaned[col].isna().all(): | |
| # Se todas as conversões falharam, tratar como categórica | |
| X_test_cleaned[col] = X_test_cleaned[col].astype(str).astype('object') | |
| else: | |
| # Preencher NaN com 0 | |
| X_test_cleaned[col] = X_test_cleaned[col].fillna(0) | |
| except: | |
| # Se falhar, tratar como categórica | |
| X_test_cleaned[col] = X_test_cleaned[col].astype(str).astype('object') | |
| status_text.text("🔄 Testando modelo com dados de exemplo...") | |
| progress_bar.progress(80) | |
| # Testar se o modelo consegue processar os dados antes de calcular feature importance | |
| # Identificar e remover colunas problemáticas | |
| problematic_cols = [] | |
| test_sample = X_test_cleaned.head(1).copy() | |
| for col in test_sample.columns: | |
| try: | |
| # Tentar fazer predição apenas com esta coluna para identificar problemas | |
| test_df = pd.DataFrame({col: test_sample[col]}) | |
| # Não vamos testar coluna por coluna, mas sim identificar tipos problemáticos | |
| if test_sample[col].dtype == 'object': | |
| # Verificar se há valores que não são strings válidas | |
| non_string_values = test_sample[col].apply(lambda x: not isinstance(x, (str, type(None)))) | |
| if non_string_values.any(): | |
| problematic_cols.append(col) | |
| except: | |
| problematic_cols.append(col) | |
| # Remover colunas problemáticas se houver | |
| if problematic_cols: | |
| st.warning(f"⚠️ Removendo colunas problemáticas: {problematic_cols}") | |
| X_test_cleaned = X_test_cleaned.drop(columns=problematic_cols, errors='ignore') | |
| # Testar se o modelo consegue processar os dados | |
| try: | |
| # Tentar fazer uma predição de exemplo para verificar se os dados estão corretos | |
| _ = model.predict(X_test_cleaned.head(1)) | |
| except Exception as test_error: | |
| st.warning(f"⚠️ Erro ao testar modelo com dados: {test_error}") | |
| st.info("💡 Tentando ajustar tipos de dados...") | |
| # Tentar converter todas as colunas categóricas explicitamente | |
| for col in X_test_cleaned.columns: | |
| if X_test_cleaned[col].dtype == 'object': | |
| # Garantir que todos os valores sejam strings válidas | |
| X_test_cleaned[col] = X_test_cleaned[col].apply(lambda x: str(x) if pd.notna(x) else 'missing') | |
| X_test_cleaned[col] = X_test_cleaned[col].astype('object') | |
| # Tentar novamente | |
| try: | |
| _ = model.predict(X_test_cleaned.head(1)) | |
| except Exception as test_error2: | |
| # Se ainda falhar, tentar identificar a coluna específica do erro | |
| error_msg = str(test_error2) | |
| if "'homepage'" in error_msg or "homepage" in error_msg: | |
| # Remover coluna 'homepage' ou similar se existir | |
| homepage_cols = [col for col in X_test_cleaned.columns if 'homepage' in col.lower() or 'activity_type' in col.lower()] | |
| if homepage_cols: | |
| st.warning(f"⚠️ Removendo colunas relacionadas a 'homepage': {homepage_cols}") | |
| X_test_cleaned = X_test_cleaned.drop(columns=homepage_cols, errors='ignore') | |
| try: | |
| _ = model.predict(X_test_cleaned.head(1)) | |
| except: | |
| st.error(f"❌ Não foi possível processar os dados mesmo após remover colunas problemáticas: {test_error2}") | |
| progress_bar.empty() | |
| status_text.empty() | |
| return pd.DataFrame() | |
| else: | |
| st.error(f"❌ Não foi possível processar os dados mesmo após ajustes: {test_error2}") | |
| progress_bar.empty() | |
| status_text.empty() | |
| return pd.DataFrame() | |
| else: | |
| st.error(f"❌ Não foi possível processar os dados mesmo após ajustes: {test_error2}") | |
| progress_bar.empty() | |
| status_text.empty() | |
| return pd.DataFrame() | |
| status_text.text("🔄 Calculando feature importance...") | |
| progress_bar.progress(85) | |
| # OTIMIZAÇÃO: Menos repetições e mais jobs | |
| # O modelo Pipeline fará o preprocessing automaticamente | |
| try: | |
| # Verificar se o modelo é um Pipeline antes de chamar permutation_importance | |
| if hasattr(model, 'named_steps') and 'preprocessor' in model.named_steps: | |
| # O modelo é um Pipeline, pode passar dados brutos | |
| result = permutation_importance( | |
| model, X_test_cleaned, y_test_cleaned, | |
| n_repeats=5, # Reduzido de 10 para 5 | |
| random_state=42, | |
| n_jobs=-1 # Usar todos os cores disponíveis | |
| ) | |
| else: | |
| # Modelo não é Pipeline, precisa pré-processar manualmente | |
| raise ValueError("Modelo não é um Pipeline com preprocessor") | |
| sorted_idx = result.importances_mean.argsort() | |
| status_text.text("✅ Finalizando...") | |
| progress_bar.progress(95) | |
| # Criar DataFrame com resultados reais | |
| # Nota: após o preprocessing, as features podem ter mudado (OneHotEncoder cria múltiplas colunas) | |
| # Vamos usar os nomes das colunas originais | |
| features = X_test_cleaned.columns[sorted_idx] | |
| importance = result.importances_mean[sorted_idx] | |
| except Exception as e: | |
| # Se permutation_importance falhar, retornar DataFrame vazio | |
| st.warning(f"Erro ao calcular feature importance: {e}") | |
| progress_bar.empty() | |
| status_text.empty() | |
| return pd.DataFrame() | |
| df_result = pd.DataFrame({ | |
| 'feature': features, | |
| 'importance': importance | |
| }).sort_values('importance', ascending=True) | |
| # Limpar indicadores | |
| progress_bar.empty() | |
| status_text.empty() | |
| return df_result | |
| except Exception as e: | |
| st.warning(f"Erro ao calcular feature importance OULAD: {e}") | |
| return pd.DataFrame() | |
| def criar_grafico_feature_importance_uci(): | |
| """Cria gráfico de feature importance para UCI""" | |
| df_importance = calcular_feature_importance_uci() | |
| if df_importance.empty: | |
| return None | |
| fig, ax = plt.subplots(figsize=(10, 8)) | |
| bars = ax.barh(df_importance['feature'], df_importance['importance'], color='skyblue') | |
| ax.set_title('Importância das Features - Dataset UCI', fontsize=14, fontweight='bold') | |
| ax.set_xlabel('Importância') | |
| ax.set_ylabel('Features') | |
| # Adicionar valores nas barras | |
| for i, (bar, importance) in enumerate(zip(bars, df_importance['importance'])): | |
| ax.text(bar.get_width() + 0.01, bar.get_y() + bar.get_height()/2, | |
| f'{importance:.3f}', va='center', fontsize=10) | |
| plt.tight_layout() | |
| return fig | |
| def criar_grafico_feature_importance_oulad(): | |
| """Cria gráfico de feature importance para OULAD""" | |
| df_importance = calcular_feature_importance_oulad() | |
| if df_importance.empty: | |
| return None | |
| # Tradução amigável das variáveis para exibição | |
| feature_translation = { | |
| 'date_unregistration': 'Data de cancelamento', | |
| 'date_registration': 'Data de registro', | |
| 'age_band': 'Faixa etária', | |
| 'studied_credits': 'Créditos cursados', | |
| 'studied_credits_x': 'Créditos cursados', | |
| 'studied_credits_y': 'Créditos cursados', | |
| 'score': 'Nota', | |
| 'score_x': 'Nota', | |
| 'score_y': 'Nota', | |
| 'activity_type': 'Tipo de atividade', | |
| 'clicks': 'Cliques', | |
| 'gender': 'Gênero', | |
| 'region': 'Região', | |
| 'disability': 'Deficiência', | |
| 'highest_education': 'Escolaridade', | |
| 'imd_band': 'Faixa IMD', | |
| 'num_of_prev_attempts': 'Tentativas anteriores', | |
| 'module_presentation_length': 'Duração do módulo', | |
| 'cancelou': 'Cancelou', | |
| } | |
| df_importance['feature_pt'] = df_importance['feature'].map(feature_translation).fillna(df_importance['feature']) | |
| fig, ax = plt.subplots(figsize=(10, 8)) | |
| bars = ax.barh(df_importance['feature_pt'], df_importance['importance'], color='lightcoral') | |
| ax.set_title('Importância das Features - Dataset OULAD', fontsize=14, fontweight='bold') | |
| ax.set_xlabel('Importância') | |
| ax.set_ylabel('Variáveis') | |
| # Adicionar valores nas barras | |
| for i, (bar, importance) in enumerate(zip(bars, df_importance['importance'])): | |
| ax.text(bar.get_width() + 0.01, bar.get_y() + bar.get_height()/2, | |
| f'{importance:.3f}', va='center', fontsize=10) | |
| plt.tight_layout() | |
| return fig | |
| def criar_secao_pygwalker(): | |
| """Cria seção opcional para PyGWalker com seleção de dataset""" | |
| st.markdown("---") | |
| st.markdown("### 🔍 Análise Interativa com PyGWalker") | |
| col1, col2 = st.columns([2, 1]) | |
| with col1: | |
| dataset_selecionado = st.selectbox( | |
| "Selecione o dataset para análise:", | |
| ["UCI", "OULAD"], | |
| help="Escolha qual dataset analisar interativamente" | |
| ) | |
| with col2: | |
| usar_pygwalker_uci = st.checkbox( | |
| "Ativar PyGWalker UCI", | |
| value=False, | |
| help="Permite análise interativa dos dados UCI" | |
| ) | |
| usar_pygwalker_oulad = st.checkbox( | |
| "Ativar PyGWalker OULAD", | |
| value=False, | |
| help="Permite análise interativa dos dados OULAD" | |
| ) | |
| if usar_pygwalker_uci: | |
| try: | |
| import pygwalker as pyg | |
| from pygwalker.api.streamlit import StreamlitRenderer | |
| # Carregar dados baseado na seleção | |
| if dataset_selecionado == "UCI": | |
| if 'df_uci' in st.session_state and not st.session_state['df_uci'].empty: | |
| st.info("📊 Carregando PyGWalker com dados UCI...") | |
| df = st.session_state['df_uci'] | |
| else: | |
| st.info("📊 Carregando dados UCI do arquivo...") | |
| df = carregar_uci_dados() | |
| else: # OULAD | |
| if 'df_oulad' in st.session_state and not st.session_state['df_oulad'].empty: | |
| st.info("📊 Carregando PyGWalker com dados OULAD...") | |
| df = st.session_state['df_oulad'] | |
| else: | |
| st.info("📊 Carregando dados OULAD do arquivo...") | |
| df = carregar_oulad_dados() | |
| # Verificar se os dados foram carregados | |
| if df is not None and not df.empty: | |
| # Criar renderer do PyGWalker | |
| renderer = StreamlitRenderer(df, spec="./gw0.json", debug=False) | |
| renderer.render_explore() | |
| else: | |
| st.warning(f"⚠️ Nenhum dado disponível para {dataset_selecionado}. Verifique se os arquivos de dados existem.") | |
| except ImportError: | |
| st.error("❌ PyGWalker não está instalado. Execute: `pip install pygwalker`") | |
| except Exception as e: | |
| st.error(f"❌ Erro ao carregar PyGWalker: {e}") | |
| else: | |
| st.info(f"💡 Marque a opção acima para ativar a análise interativa com PyGWalker para o dataset {dataset_selecionado}") | |
| if usar_pygwalker_oulad: | |
| try: | |
| import pygwalker as pyg | |
| from pygwalker.api.streamlit import StreamlitRenderer | |
| # Verificar se há dados disponíveis | |
| if 'df_oulad' in st.session_state and not st.session_state['df_oulad'].empty: | |
| st.info("📊 Carregando PyGWalker com dados OULAD...") | |
| df = st.session_state['df_oulad'] | |
| # Criar renderer do PyGWalker | |
| renderer = StreamlitRenderer(df, spec="./gw0.json", debug=False) | |
| renderer.render_explore() | |
| else: | |
| st.warning("⚠️ Nenhum dado disponível para análise interativa. Navegue para as páginas de análise primeiro.") | |
| except ImportError: | |
| st.error("❌ PyGWalker não está instalado. Execute: `pip install pygwalker`") | |
| except Exception as e: | |
| st.error(f"❌ Erro ao carregar PyGWalker: {e}") | |
| else: | |
| st.info("💡 Marque a opção acima para ativar a análise interativa com PyGWalker") | |
| # ============================================================================= | |
| # FUNÇÕES PARA TEMPLATE DE FEATURE IMPORTANCE | |
| # ============================================================================= | |
| def traduzir_nome_feature(feature: str, dataset_origem: str) -> str: | |
| """Traduz nomes de features para português brasileiro""" | |
| # Mapeamento de traduções para features comuns | |
| traducoes = { | |
| # UCI features | |
| 'failures': 'reprovacoes', | |
| 'absences': 'faltas', | |
| 'G1': 'nota_1bim', | |
| 'G2': 'nota_2bim', | |
| 'G3': 'nota_final', | |
| 'studytime': 'tempo_estudo', | |
| 'goout': 'saidas', | |
| 'Dalc': 'alcool_dia', | |
| 'Walc': 'alcool_fds', | |
| 'freetime': 'tempo_livre', | |
| 'health': 'saude', | |
| 'age': 'idade', | |
| 'sex': 'sexo', | |
| 'school': 'escola', | |
| 'address': 'endereco', | |
| 'famsize': 'tamanho_familia', | |
| 'Pstatus': 'status_pais', | |
| 'Medu': 'educacao_mae', | |
| 'Fedu': 'educacao_pai', | |
| 'Mjob': 'trabalho_mae', | |
| 'Fjob': 'trabalho_pai', | |
| 'reason': 'motivo_escola', | |
| 'guardian': 'responsavel', | |
| 'traveltime': 'tempo_viagem', | |
| # OULAD features | |
| 'sum_click': 'cliques', | |
| 'score': 'pontuacao', | |
| 'studied_credits': 'creditos_estudados', | |
| 'num_of_prev_attempts': 'tentativas_anteriores', | |
| 'date': 'data', | |
| 'date_submitted': 'data_submissao', | |
| 'clicks': 'cliques', | |
| 'final_result': 'resultado_final', | |
| 'gender': 'genero', | |
| 'region': 'regiao', | |
| 'highest_education': 'educacao_superior', | |
| 'imd_band': 'banda_imd', | |
| 'age_band': 'faixa_etaria', | |
| 'disability': 'deficiencia', | |
| 'activity_type': 'tipo_atividade', | |
| 'assessment_type': 'tipo_avaliacao', | |
| 'weight': 'peso', | |
| 'module_presentation_length': 'duracao_modulo', | |
| } | |
| # Tentar traduzir, se não existir mapeamento, retornar o original em lowercase | |
| return traducoes.get(feature, feature.lower().replace('_', '_')) | |
| def gerar_template_unificado() -> pd.DataFrame: | |
| """Gera template unificado com TOP 2 features de UCI e OULAD""" | |
| try: | |
| # Get TOP 2 features from UCI (não 3!) | |
| df_importance_uci = calcular_feature_importance_uci() | |
| top_features_uci = df_importance_uci.nlargest(2, 'importance')['feature'].tolist() if not df_importance_uci.empty else [] | |
| # Get TOP features from OULAD, excluding temporal features | |
| df_importance_oulad = calcular_feature_importance_oulad() | |
| if not df_importance_oulad.empty: | |
| # Exclude temporal features that don't make sense for regular school periods | |
| temporal_features = ['date_registration', 'date_unregistration'] | |
| df_filtered = df_importance_oulad[~df_importance_oulad['feature'].isin(temporal_features)] | |
| top_features_oulad = df_filtered.nlargest(2, 'importance')['feature'].tolist() | |
| else: | |
| top_features_oulad = [] | |
| # Build template with name field first | |
| template_data = {'nome_aluno': [''] * 10} | |
| # Store mapping for validation later | |
| feature_mapping = {} | |
| # Add UCI features translated to Portuguese, without prefix | |
| for feature in top_features_uci: | |
| translated = traduzir_nome_feature(feature, 'uci') | |
| # If translation conflicts, add _uci suffix | |
| if translated in template_data or translated in feature_mapping: | |
| translated = f"{translated}_uci" | |
| template_data[translated] = [np.nan] * 10 | |
| feature_mapping[translated] = ('uci', feature) | |
| # Add OULAD features translated to Portuguese, without prefix | |
| for feature in top_features_oulad: | |
| translated = traduzir_nome_feature(feature, 'oulad') | |
| # If translation conflicts, add _oulad suffix | |
| if translated in template_data or translated in feature_mapping: | |
| translated = f"{translated}_oulad" | |
| template_data[translated] = [np.nan] * 10 | |
| feature_mapping[translated] = ('oulad', feature) | |
| # Add result column | |
| template_data['resultado_final'] = [np.nan] * 10 | |
| df_template = pd.DataFrame(template_data) | |
| # Add example row with placeholder values | |
| df_template.loc[0, 'nome_aluno'] = 'João Silva' | |
| # Add example values based on typical feature ranges | |
| for col in df_template.columns: | |
| if col == 'nome_aluno': | |
| continue | |
| elif col == 'resultado_final': | |
| df_template.loc[0, col] = 7.5 # Exemplo de nota 0-10 (padrão brasileiro) | |
| elif 'reprovacoes' in col or 'faltas' in col or 'tentativas' in col: | |
| df_template.loc[0, col] = 0 | |
| elif 'nota' in col or 'pontuacao' in col: | |
| df_template.loc[0, col] = 10.0 | |
| elif 'cliques' in col or 'creditos' in col: | |
| df_template.loc[0, col] = 50 | |
| elif 'tempo' in col: | |
| df_template.loc[0, col] = 2 | |
| else: | |
| df_template.loc[0, col] = 'Exemplo' | |
| # Store mapping as metadata (could be saved separately if needed) | |
| df_template.attrs['feature_mapping'] = feature_mapping | |
| return df_template | |
| except Exception as e: | |
| st.error(f"Erro ao gerar template unificado: {e}") | |
| return pd.DataFrame() | |
| def gerar_template_features(dataset_tipo: str) -> pd.DataFrame: | |
| """Gera template com as 2 features mais importantes do dataset selecionado""" | |
| try: | |
| # Obter feature importance baseado no dataset | |
| if dataset_tipo.lower() == 'uci': | |
| df_importance = calcular_feature_importance_uci() | |
| elif dataset_tipo.lower() == 'oulad': | |
| df_importance = calcular_feature_importance_oulad() | |
| else: | |
| raise ValueError(f"Dataset tipo '{dataset_tipo}' não reconhecido. Use 'uci' ou 'oulad'") | |
| if df_importance.empty: | |
| st.warning(f"Não foi possível obter feature importance para {dataset_tipo}") | |
| return pd.DataFrame() | |
| # Pegar as 2 features mais importantes (maior importance) | |
| top_features = df_importance.nlargest(2, 'importance')['feature'].tolist() | |
| # Criar template DataFrame | |
| template_data = { | |
| feature: [np.nan] * 10 for feature in top_features | |
| } | |
| template_data['resultado_final'] = [np.nan] * 10 | |
| df_template = pd.DataFrame(template_data) | |
| # Adicionar algumas linhas de exemplo com valores placeholder | |
| if dataset_tipo.lower() == 'uci': | |
| # Exemplos baseados no dataset UCI | |
| if len(top_features) >= 1: | |
| df_template.loc[0, top_features[0]] = 1.0 # Exemplo de valor numérico | |
| if len(top_features) >= 2: | |
| df_template.loc[0, top_features[1]] = 2.0 | |
| df_template.loc[0, 'resultado_final'] = 10.0 # Exemplo de nota | |
| else: # OULAD | |
| # Exemplos baseados no dataset OULAD | |
| if len(top_features) >= 1: | |
| df_template.loc[0, top_features[0]] = 'Pass' # Exemplo de categoria | |
| if len(top_features) >= 2: | |
| df_template.loc[0, top_features[1]] = 1.0 | |
| df_template.loc[0, 'resultado_final'] = 'Pass' | |
| return df_template | |
| except Exception as e: | |
| st.error(f"Erro ao gerar template: {e}") | |
| return pd.DataFrame() | |
| def converter_template_para_excel(df_template: pd.DataFrame) -> bytes: | |
| """Converte DataFrame template para formato Excel (bytes)""" | |
| try: | |
| import io | |
| buffer = io.BytesIO() | |
| with pd.ExcelWriter(buffer, engine='openpyxl') as writer: | |
| df_template.to_excel(writer, sheet_name='Template', index=False) | |
| buffer.seek(0) | |
| return buffer.getvalue() | |
| except Exception as e: | |
| st.error(f"Erro ao converter template para Excel: {e}") | |
| return b'' | |
| def validar_template_usuario(df_usuario: pd.DataFrame, df_template: pd.DataFrame = None) -> tuple[bool, str]: | |
| """Valida se o template preenchido pelo usuário está correto""" | |
| try: | |
| # Verificar se tem a coluna resultado_final | |
| if 'resultado_final' not in df_usuario.columns: | |
| return False, "Coluna 'resultado_final' não encontrada no arquivo" | |
| # Verificar se resultado_final está na escala 0-10 (padrão brasileiro) | |
| resultado_values = df_usuario['resultado_final'].dropna() | |
| if len(resultado_values) > 0: | |
| if not pd.api.types.is_numeric_dtype(resultado_values): | |
| return False, "Coluna 'resultado_final' deve conter valores numéricos (0-10)" | |
| if (resultado_values < 0).any() or (resultado_values > 10).any(): | |
| return False, "Valores em 'resultado_final' devem estar entre 0 e 10" | |
| # Verificar se tem a coluna nome_aluno (para template unificado) | |
| if 'nome_aluno' not in df_usuario.columns: | |
| return False, "Coluna 'nome_aluno' não encontrada no arquivo" | |
| # Se df_template foi fornecido, verificar features específicas | |
| if df_template is not None: | |
| expected_features = [col for col in df_template.columns if col not in ['resultado_final', 'nome_aluno']] | |
| missing_features = [col for col in expected_features if col not in df_usuario.columns] | |
| if missing_features: | |
| return False, f"Colunas de features esperadas não encontradas: {missing_features}" | |
| # Verificar se tem dados (não está vazio) | |
| if df_usuario.empty: | |
| return False, "Arquivo está vazio" | |
| # Verificar se tem pelo menos algumas linhas com dados válidos | |
| non_empty_rows = df_usuario.dropna(how='all').shape[0] | |
| if non_empty_rows < 3: | |
| return False, "Arquivo deve ter pelo menos 3 linhas com dados válidos" | |
| # Verificar se tem pelo menos algumas features além de nome e resultado | |
| feature_cols = [col for col in df_usuario.columns if col not in ['nome_aluno', 'resultado_final']] | |
| if len(feature_cols) < 2: | |
| return False, "Template deve ter pelo menos 2 features além de nome_aluno e resultado_final" | |
| return True, "Template válido" | |
| except Exception as e: | |
| return False, f"Erro na validação: {e}" | |
| def realizar_eda_automatica(df_usuario: pd.DataFrame) -> dict: | |
| """Realiza EDA automática no dataset do usuário""" | |
| try: | |
| from sklearn.ensemble import RandomForestRegressor, RandomForestClassifier | |
| from sklearn.preprocessing import OneHotEncoder, LabelEncoder | |
| from sklearn.compose import ColumnTransformer | |
| from sklearn.pipeline import Pipeline | |
| from sklearn.model_selection import train_test_split | |
| from sklearn.metrics import mean_absolute_error, mean_squared_error, r2_score, accuracy_score, classification_report | |
| from sklearn.inspection import permutation_importance | |
| import numpy as np | |
| # Preparar dados - remover nome_aluno se existir | |
| target_col = 'resultado_final' | |
| y = df_usuario[target_col] | |
| X = df_usuario.drop([target_col, 'nome_aluno'], axis=1, errors='ignore') | |
| # Detectar tipo de problema (regressão vs classificação) | |
| # Sempre tratar como regressão se for numérico (escala 0-10) | |
| is_regression = pd.api.types.is_numeric_dtype(y) | |
| # Dividir dados | |
| X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=42) | |
| # Preparar preprocessamento | |
| categorical_features = X_train.select_dtypes(include=['object']).columns | |
| numerical_features = X_train.select_dtypes(include=[np.number]).columns | |
| # Criar preprocessor | |
| preprocessor = ColumnTransformer( | |
| transformers=[ | |
| ('num', 'passthrough', numerical_features), | |
| ('cat', OneHotEncoder(handle_unknown='ignore'), categorical_features) | |
| ] | |
| ) | |
| # Treinar modelo apropriado | |
| if is_regression: | |
| model = Pipeline(steps=[ | |
| ('preprocessor', preprocessor), | |
| ('regressor', RandomForestRegressor(n_estimators=100, random_state=42)) | |
| ]) | |
| y_train_encoded = y_train.astype(float) | |
| y_test_encoded = y_test.astype(float) | |
| else: | |
| model = Pipeline(steps=[ | |
| ('preprocessor', preprocessor), | |
| ('classifier', RandomForestClassifier(n_estimators=100, random_state=42)) | |
| ]) | |
| # Para classificação, usar LabelEncoder se necessário | |
| if not pd.api.types.is_numeric_dtype(y_train): | |
| le = LabelEncoder() | |
| y_train_encoded = le.fit_transform(y_train) | |
| y_test_encoded = le.transform(y_test) | |
| else: | |
| y_train_encoded = y_train | |
| y_test_encoded = y_test | |
| # Treinar modelo | |
| model.fit(X_train, y_train_encoded) | |
| # Fazer predições | |
| predictions = model.predict(X_test) | |
| # Calcular métricas | |
| if is_regression: | |
| mae = mean_absolute_error(y_test_encoded, predictions) | |
| rmse = np.sqrt(mean_squared_error(y_test_encoded, predictions)) | |
| r2 = r2_score(y_test_encoded, predictions) | |
| metrics = { | |
| 'mae': mae, | |
| 'rmse': rmse, | |
| 'r2': r2, | |
| 'type': 'regression' | |
| } | |
| else: | |
| accuracy = accuracy_score(y_test_encoded, predictions) | |
| metrics = { | |
| 'accuracy': accuracy, | |
| 'type': 'classification', | |
| 'classification_report': classification_report(y_test_encoded, predictions, output_dict=True) | |
| } | |
| # Calcular feature importance | |
| try: | |
| # Usar permutation importance | |
| result = permutation_importance( | |
| model, X_test, y_test_encoded, | |
| n_repeats=5, random_state=42, n_jobs=-1 | |
| ) | |
| feature_importance = pd.DataFrame({ | |
| 'feature': X_test.columns, | |
| 'importance': result.importances_mean | |
| }).sort_values('importance', ascending=False) | |
| except: | |
| # Fallback para feature_importances_ do modelo | |
| if hasattr(model.named_steps[list(model.named_steps.keys())[-1]], 'feature_importances_'): | |
| feature_importance = pd.DataFrame({ | |
| 'feature': X_test.columns, | |
| 'importance': model.named_steps[list(model.named_steps.keys())[-1]].feature_importances_ | |
| }).sort_values('importance', ascending=False) | |
| else: | |
| feature_importance = pd.DataFrame() | |
| # Estatísticas descritivas | |
| stats = { | |
| 'shape': df_usuario.shape, | |
| 'missing_values': df_usuario.isnull().sum().to_dict(), | |
| 'dtypes': df_usuario.dtypes.to_dict(), | |
| 'numeric_summary': df_usuario.select_dtypes(include=[np.number]).describe().to_dict() if not df_usuario.select_dtypes(include=[np.number]).empty else {}, | |
| 'categorical_summary': df_usuario.select_dtypes(include=['object']).describe().to_dict() if not df_usuario.select_dtypes(include=['object']).empty else {} | |
| } | |
| return { | |
| 'model': model, | |
| 'metrics': metrics, | |
| 'feature_importance': feature_importance, | |
| 'predictions': predictions, | |
| 'y_test': y_test_encoded, | |
| 'stats': stats, | |
| 'is_regression': is_regression | |
| } | |
| except Exception as e: | |
| st.error(f"Erro na EDA automática: {e}") | |
| return {} | |
| def realizar_analise_completa(df_usuario: pd.DataFrame) -> dict: | |
| """ | |
| Executa análise completa dos dados do usuário | |
| Similar às análises feitas em UCI e OULAD | |
| """ | |
| try: | |
| resultados = { | |
| 'eda': realizar_eda_automatica(df_usuario), | |
| 'graficos': {}, | |
| 'metricas': {} | |
| } | |
| # Estatísticas descritivas | |
| resultados['metricas']['descritivas'] = df_usuario.describe() | |
| # Correlações | |
| numeric_cols = df_usuario.select_dtypes(include=[np.number]).columns | |
| if len(numeric_cols) > 1: | |
| resultados['metricas']['correlacao'] = df_usuario[numeric_cols].corr() | |
| # Distribuições | |
| resultados['graficos']['distribuicoes'] = criar_graficos_distribuicao(df_usuario) | |
| # Gráfico radar (será criado com seleção de aluno) | |
| resultados['graficos']['radar'] = criar_grafico_radar_aluno(df_usuario) | |
| return resultados | |
| except Exception as e: | |
| st.error(f"Erro na análise completa: {e}") | |
| return {} | |
| def criar_graficos_distribuicao(df_usuario: pd.DataFrame) -> dict: | |
| """Cria gráficos de distribuição para análise educacional""" | |
| try: | |
| import matplotlib.pyplot as plt | |
| import seaborn as sns | |
| graficos = {} | |
| # Gráfico de distribuição de resultados | |
| if 'resultado_final' in df_usuario.columns: | |
| fig, ax = plt.subplots(figsize=(12, 8)) # Aumentado o tamanho | |
| # Criar bins para notas numéricas (escala 0-10) | |
| df_traduzido = df_usuario.copy() | |
| df_traduzido['faixa_nota'] = pd.cut( | |
| df_traduzido['resultado_final'], | |
| bins=[0, 5, 7, 10], | |
| labels=['Insuficiente (0-5)', 'Regular (5-7)', 'Bom (7-10)'], | |
| include_lowest=True | |
| ) | |
| # Contar valores por faixa | |
| contagem_faixas = df_traduzido['faixa_nota'].value_counts() | |
| # Criar gráfico de barras | |
| bars = ax.bar(contagem_faixas.index, contagem_faixas.values, | |
| color=['#dc3545', '#ffc107', '#28a745'], alpha=0.8, edgecolor='black', linewidth=1) | |
| # Configurar título e labels | |
| ax.set_title('Distribuição de Resultados da Turma', fontsize=18, fontweight='bold', pad=20) | |
| ax.set_xlabel('Faixa de Nota', fontsize=14, fontweight='bold') | |
| ax.set_ylabel('Quantidade de Alunos', fontsize=14, fontweight='bold') | |
| ax.tick_params(axis='x', rotation=45, labelsize=12) | |
| ax.tick_params(axis='y', labelsize=12) | |
| # Adicionar valores nas barras | |
| for i, (bar, valor) in enumerate(zip(bars, contagem_faixas.values)): | |
| ax.text(bar.get_x() + bar.get_width()/2, bar.get_height() + 0.5, | |
| str(valor), ha='center', va='bottom', fontweight='bold', fontsize=12) | |
| # Adicionar grid para melhor visualização | |
| ax.grid(True, alpha=0.3, axis='y') | |
| ax.set_axisbelow(True) | |
| # Ajustar layout | |
| plt.tight_layout() | |
| graficos['distribuicao_resultados'] = fig | |
| return graficos | |
| except Exception as e: | |
| st.error(f"Erro ao criar gráficos de distribuição: {e}") | |
| return {} | |
| def criar_grafico_radar_aluno(df_usuario: pd.DataFrame, nome_aluno: str = None) -> dict: | |
| """Cria gráfico radar comparando aluno individual com média da turma""" | |
| try: | |
| import matplotlib.pyplot as plt | |
| import numpy as np | |
| from math import pi | |
| graficos = {} | |
| if 'resultado_final' in df_usuario.columns and 'nome_aluno' in df_usuario.columns: | |
| # Se não especificado, usar o primeiro aluno como exemplo | |
| if nome_aluno is None: | |
| nome_aluno = df_usuario['nome_aluno'].iloc[0] | |
| # Verificar se o aluno existe | |
| aluno_data = df_usuario[df_usuario['nome_aluno'] == nome_aluno] | |
| if aluno_data.empty: | |
| st.warning(f"Aluno '{nome_aluno}' não encontrado. Usando primeiro aluno como exemplo.") | |
| nome_aluno = df_usuario['nome_aluno'].iloc[0] | |
| aluno_data = df_usuario.iloc[[0]] | |
| # Obter dados do aluno selecionado | |
| aluno_row = aluno_data.iloc[0] | |
| # Calcular médias da turma (excluindo o aluno selecionado) | |
| turma_media = df_usuario[df_usuario['nome_aluno'] != nome_aluno].mean(numeric_only=True) | |
| # Selecionar colunas numéricas para o radar (incluindo resultado_final) | |
| colunas_numericas = [col for col in df_usuario.select_dtypes(include=[np.number]).columns | |
| if col in aluno_row.index] | |
| if len(colunas_numericas) >= 3: # Mínimo 3 dimensões para radar | |
| # Preparar dados para o radar | |
| valores_aluno = [aluno_row[col] for col in colunas_numericas] | |
| valores_turma = [turma_media[col] for col in colunas_numericas] | |
| # Normalizar valores para escala 0-10 (assumindo que as features já estão nessa escala) | |
| # Se não estiverem, fazer normalização simples | |
| max_val = max(max(valores_aluno), max(valores_turma)) | |
| if max_val > 10: | |
| valores_aluno = [v/max_val*10 for v in valores_aluno] | |
| valores_turma = [v/max_val*10 for v in valores_turma] | |
| # Criar gráfico radar | |
| fig, ax = plt.subplots(figsize=(10, 10), subplot_kw=dict(projection='polar')) | |
| # Ângulos para cada dimensão | |
| angles = [n / float(len(colunas_numericas)) * 2 * pi for n in range(len(colunas_numericas))] | |
| angles += angles[:1] # Fechar o círculo | |
| # Adicionar valores para fechar o círculo | |
| valores_aluno += valores_aluno[:1] | |
| valores_turma += valores_turma[:1] | |
| # Plotar dados | |
| ax.plot(angles, valores_aluno, 'o-', linewidth=2, label=f'{nome_aluno}', color='#2E86AB', markersize=8) | |
| ax.fill(angles, valores_aluno, alpha=0.25, color='#2E86AB') | |
| ax.plot(angles, valores_turma, 'o-', linewidth=2, label='Média da Turma', color='#A23B72', markersize=8) | |
| ax.fill(angles, valores_turma, alpha=0.25, color='#A23B72') | |
| # Configurar eixos com traduções | |
| traducao_rotulos = { | |
| 'nota_2bim': 'Nota 2º Bimestre', | |
| 'faltas': 'Faltas', | |
| 'pontuacao': 'Pontuação', | |
| 'resultado_final': 'Nota Final' | |
| } | |
| ax.set_xticks(angles[:-1]) | |
| ax.set_xticklabels([traducao_rotulos.get(col, col.replace('_', ' ').title()) for col in colunas_numericas]) | |
| ax.set_ylim(0, 10) | |
| ax.set_yticks([2, 4, 6, 8, 10]) | |
| ax.set_yticklabels(['2', '4', '6', '8', '10']) | |
| ax.grid(True) | |
| # Título e legenda | |
| ax.set_title(f'Comparação: {nome_aluno} vs Média da Turma', size=16, fontweight='bold', pad=20) | |
| ax.legend(loc='upper right', bbox_to_anchor=(1.3, 1.0)) | |
| # Adicionar valores nas pontas | |
| for i, (angle, valor_aluno, valor_turma) in enumerate(zip(angles[:-1], valores_aluno[:-1], valores_turma[:-1])): | |
| ax.text(angle, valor_aluno + 0.5, f'{valor_aluno:.1f}', ha='center', va='center', fontweight='bold', color='#2E86AB') | |
| ax.text(angle, valor_turma - 0.5, f'{valor_turma:.1f}', ha='center', va='center', fontweight='bold', color='#A23B72') | |
| plt.tight_layout() | |
| graficos['radar_comparacao_aluno'] = fig | |
| else: | |
| st.warning("Não há colunas numéricas suficientes para criar o gráfico radar.") | |
| return graficos | |
| except Exception as e: | |
| st.error(f"Erro ao criar gráfico radar: {e}") | |
| return {} | |
| def exibir_resultados_com_ia(resultados: dict, df_usuario: pd.DataFrame): | |
| """Exibe resultados com interpretação via OpenAI""" | |
| st.markdown("## 📊 Resultados da Análise") | |
| # 1. Métricas Gerais | |
| st.markdown("### 📈 Métricas Gerais") | |
| col1, col2, col3, col4 = st.columns(4) | |
| with col1: | |
| st.metric("Total de Alunos", len(df_usuario)) | |
| with col2: | |
| taxa_aprovacao = (df_usuario['resultado_final'] >= 5.0).mean() * 100 # Aprovação >= 5.0 | |
| st.metric("Taxa de Aprovação", f"{taxa_aprovacao:.1f}%") | |
| with col3: | |
| # Calcular média das faltas (se a coluna existir) | |
| if 'faltas' in df_usuario.columns: | |
| media_faltas = df_usuario['faltas'].mean() | |
| st.metric("Média das Faltas", f"{media_faltas:.1f}") | |
| else: | |
| st.metric("Média das Faltas", "N/A") | |
| with col4: | |
| # Calcular média das notas finais | |
| media_notas = df_usuario['resultado_final'].mean() | |
| st.metric("Média das Notas Finais", f"{media_notas:.1f}") | |
| # 2. Gráfico de Distribuição + Interpretação IA | |
| st.markdown("### 📊 Distribuição de Resultados") | |
| if 'distribuicoes' in resultados['graficos'] and 'distribuicao_resultados' in resultados['graficos']['distribuicoes']: | |
| fig_dist = resultados['graficos']['distribuicoes']['distribuicao_resultados'] | |
| st.pyplot(fig_dist) | |
| # Interpretação via OpenAI | |
| contexto = { | |
| 'total_alunos': len(df_usuario), | |
| 'aprovados': (df_usuario['resultado_final'] >= 5.0).sum(), | |
| 'reprovados': (df_usuario['resultado_final'] < 5.0).sum(), | |
| 'media_geral': df_usuario['resultado_final'].mean() | |
| } | |
| # Verificar se usuário quer usar IA | |
| usar_ia = st.session_state.get('usar_ia', True) | |
| if usar_ia and 'openai_key' in st.session_state and st.session_state.get('api_valida', False): | |
| # Usar OpenAI se disponível e válida | |
| try: | |
| from .openai_interpreter import interpretar_grafico | |
| interpretacao = interpretar_grafico('distribuicao_resultados', contexto) | |
| st.info(f"💡 **Interpretação IA**: {interpretacao}") | |
| except Exception as e: | |
| # Fallback para interpretação estática | |
| interpretacao = """ | |
| Este gráfico mostra a distribuição de resultados da turma. | |
| Uma boa distribuição tem mais alunos aprovados que reprovados. | |
| Se houver muitos reprovados, considere estratégias de apoio pedagógico. | |
| """ | |
| st.info(f"💡 **Interpretação**: {interpretacao}") | |
| elif usar_ia and 'openai_key' in st.session_state and not st.session_state.get('api_valida', False): | |
| # API configurada mas não testada | |
| st.warning("⚠️ Chave OpenAI configurada mas não testada. Teste a chave na sidebar.") | |
| interpretacao = """ | |
| Este gráfico mostra a distribuição de resultados da turma. | |
| Uma boa distribuição tem mais alunos aprovados que reprovados. | |
| Se houver muitos reprovados, considere estratégias de apoio pedagógico. | |
| """ | |
| st.info(f"💡 **Interpretação**: {interpretacao}") | |
| else: | |
| # Interpretação estática | |
| interpretacao = """ | |
| Este gráfico mostra a distribuição de resultados da turma. | |
| Uma boa distribuição tem mais alunos aprovados que reprovados. | |
| Se houver muitos reprovados, considere estratégias de apoio pedagógico. | |
| """ | |
| st.info(f"💡 **Interpretação**: {interpretacao}") | |
| # Histograma de Distribuição das Notas Finais | |
| st.markdown("### 📊 Histograma de Distribuição das Notas Finais") | |
| if 'resultado_final' in df_usuario.columns: | |
| fig_hist, ax_hist = plt.subplots(figsize=(12, 6)) | |
| # Criar histograma com KDE | |
| sns.histplot(df_usuario['resultado_final'], bins=20, kde=True, ax=ax_hist, | |
| color='#3498db', alpha=0.7, edgecolor='black', linewidth=1) | |
| # Adicionar linha vertical para média | |
| media_notas = df_usuario['resultado_final'].mean() | |
| ax_hist.axvline(media_notas, color='red', linestyle='--', linewidth=2, | |
| label=f'Média: {media_notas:.2f}') | |
| # Adicionar linha vertical para mediana | |
| mediana_notas = df_usuario['resultado_final'].median() | |
| ax_hist.axvline(mediana_notas, color='orange', linestyle='--', linewidth=2, | |
| label=f'Mediana: {mediana_notas:.2f}') | |
| # Adicionar linha vertical para nota de corte (5.0) | |
| ax_hist.axvline(5.0, color='green', linestyle='-', linewidth=2, | |
| label='Nota de Corte (5.0)') | |
| # Configurar gráfico | |
| ax_hist.set_title('Distribuição das Notas Finais - Histograma', | |
| fontsize=16, fontweight='bold', pad=20) | |
| ax_hist.set_xlabel('Nota Final (0-10)', fontsize=14, fontweight='bold') | |
| ax_hist.set_ylabel('Frequência', fontsize=14, fontweight='bold') | |
| ax_hist.legend(fontsize=12) | |
| ax_hist.grid(True, alpha=0.3) | |
| ax_hist.set_xlim(0, 10) | |
| # Adicionar estatísticas no gráfico | |
| stats_text = f""" | |
| 📊 ESTATÍSTICAS: | |
| • Média: {media_notas:.2f} | |
| • Mediana: {mediana_notas:.2f} | |
| • Desvio Padrão: {df_usuario['resultado_final'].std():.2f} | |
| • Mínimo: {df_usuario['resultado_final'].min():.2f} | |
| • Máximo: {df_usuario['resultado_final'].max():.2f} | |
| • Aprovados (≥5.0): {((df_usuario['resultado_final'] >= 5.0).sum() / len(df_usuario) * 100):.1f}% | |
| """ | |
| # Adicionar caixa de estatísticas | |
| ax_hist.text(0.02, 0.98, stats_text, transform=ax_hist.transAxes, | |
| fontsize=10, verticalalignment='top', | |
| bbox=dict(boxstyle="round,pad=0.5", facecolor='lightblue', alpha=0.8)) | |
| plt.tight_layout() | |
| st.pyplot(fig_hist) | |
| # Interpretação do histograma | |
| contexto_hist = { | |
| 'media': media_notas, | |
| 'mediana': mediana_notas, | |
| 'desvio_padrao': df_usuario['resultado_final'].std(), | |
| 'aprovados_pct': (df_usuario['resultado_final'] >= 5.0).mean() * 100, | |
| 'distribuicao': 'normal' if abs(media_notas - mediana_notas) < 0.5 else 'assimétrica' | |
| } | |
| # Verificar se usuário quer usar IA | |
| usar_ia = st.session_state.get('usar_ia', True) | |
| if usar_ia and 'openai_key' in st.session_state and st.session_state.get('api_valida', False): | |
| # Usar OpenAI se disponível e válida | |
| try: | |
| from .openai_interpreter import interpretar_grafico | |
| interpretacao_hist = interpretar_grafico('histograma_notas', contexto_hist) | |
| st.info(f"💡 **Interpretação IA**: {interpretacao_hist}") | |
| except Exception as e: | |
| # Fallback para interpretação estática | |
| interpretacao_hist = f""" | |
| Este histograma mostra a distribuição das notas finais da turma. | |
| A média de {media_notas:.2f} e mediana de {mediana_notas:.2f} indicam o desempenho central. | |
| {((df_usuario['resultado_final'] >= 5.0).sum() / len(df_usuario) * 100):.1f}% dos alunos foram aprovados. | |
| """ | |
| st.info(f"💡 **Interpretação**: {interpretacao_hist}") | |
| elif usar_ia and 'openai_key' in st.session_state and not st.session_state.get('api_valida', False): | |
| # API configurada mas não testada | |
| st.warning("⚠️ Chave OpenAI configurada mas não testada. Teste a chave na sidebar.") | |
| interpretacao_hist = f""" | |
| Este histograma mostra a distribuição das notas finais da turma. | |
| A média de {media_notas:.2f} e mediana de {mediana_notas:.2f} indicam o desempenho central. | |
| {((df_usuario['resultado_final'] >= 5.0).sum() / len(df_usuario) * 100):.1f}% dos alunos foram aprovados. | |
| """ | |
| st.info(f"💡 **Interpretação**: {interpretacao_hist}") | |
| else: | |
| # Interpretação estática | |
| interpretacao_hist = f""" | |
| Este histograma mostra a distribuição das notas finais da turma. | |
| A média de {media_notas:.2f} e mediana de {mediana_notas:.2f} indicam o desempenho central. | |
| {((df_usuario['resultado_final'] >= 5.0).sum() / len(df_usuario) * 100):.1f}% dos alunos foram aprovados. | |
| """ | |
| st.info(f"💡 **Interpretação**: {interpretacao_hist}") | |
| # 3. Gráficos de Distribuição Numérica | |
| st.markdown("### 📊 Distribuições Numéricas") | |
| # Criar gráficos de distribuição | |
| graficos_distribuicao = criar_graficos_distribuicao_numerica(df_usuario) | |
| if graficos_distribuicao: | |
| col1, col2 = st.columns(2) | |
| with col1: | |
| if 'distribuicao_faltas' in graficos_distribuicao: | |
| st.pyplot(graficos_distribuicao['distribuicao_faltas']) | |
| # Interpretação das faltas | |
| if usar_ia and 'openai_key' in st.session_state and st.session_state.get('api_valida', False): | |
| try: | |
| from .openai_interpreter import interpretar_grafico | |
| contexto_faltas = { | |
| 'media_faltas': df_usuario['faltas'].mean() if 'faltas' in df_usuario.columns else 0, | |
| 'total_alunos': len(df_usuario) | |
| } | |
| interpretacao = interpretar_grafico('distribuicao_faltas', contexto_faltas) | |
| st.info(f"💡 **Interpretação IA**: {interpretacao}") | |
| except: | |
| interpretacao = """ | |
| Este gráfico mostra a distribuição de faltas da turma. | |
| Muitas faltas podem indicar problemas de frequência ou engajamento. | |
| Considere estratégias de acompanhamento para alunos com muitas faltas. | |
| """ | |
| st.info(f"💡 **Interpretação**: {interpretacao}") | |
| else: | |
| interpretacao = """ | |
| Este gráfico mostra a distribuição de faltas da turma. | |
| Muitas faltas podem indicar problemas de frequência ou engajamento. | |
| Considere estratégias de acompanhamento para alunos com muitas faltas. | |
| """ | |
| st.info(f"💡 **Interpretação**: {interpretacao}") | |
| with col2: | |
| if 'distribuicao_nota_2bim' in graficos_distribuicao: | |
| st.pyplot(graficos_distribuicao['distribuicao_nota_2bim']) | |
| # Interpretação da nota do 2º bimestre | |
| if usar_ia and 'openai_key' in st.session_state and st.session_state.get('api_valida', False): | |
| try: | |
| from .openai_interpreter import interpretar_grafico | |
| contexto_nota = { | |
| 'media_nota_2bim': df_usuario['nota_2bim'].mean() if 'nota_2bim' in df_usuario.columns else 0, | |
| 'total_alunos': len(df_usuario) | |
| } | |
| interpretacao = interpretar_grafico('distribuicao_nota_2bim', contexto_nota) | |
| st.info(f"💡 **Interpretação IA**: {interpretacao}") | |
| except: | |
| interpretacao = """ | |
| Este gráfico mostra a distribuição das notas do 2º bimestre. | |
| Notas baixas podem indicar necessidade de reforço pedagógico. | |
| Use para identificar alunos que precisam de apoio adicional. | |
| """ | |
| st.info(f"💡 **Interpretação**: {interpretacao}") | |
| else: | |
| interpretacao = """ | |
| Este gráfico mostra a distribuição das notas do 2º bimestre. | |
| Notas baixas podem indicar necessidade de reforço pedagógico. | |
| Use para identificar alunos que precisam de apoio adicional. | |
| """ | |
| st.info(f"💡 **Interpretação**: {interpretacao}") | |
| # 4. Gráfico de Linhas - Análise por Região | |
| st.markdown("### 📊 Análise por Região - Média das Notas Finais") | |
| grafico_linhas = criar_grafico_barras_empilhadas(df_usuario) | |
| if grafico_linhas: | |
| st.pyplot(grafico_linhas) | |
| # Interpretação do gráfico de linhas | |
| if usar_ia and 'openai_key' in st.session_state and st.session_state.get('api_valida', False): | |
| try: | |
| from .openai_interpreter import interpretar_grafico | |
| contexto_linhas = { | |
| 'regioes': df_usuario['regiao'].unique().tolist() if 'regiao' in df_usuario.columns else [], | |
| 'total_alunos': len(df_usuario), | |
| 'media_geral': df_usuario['resultado_final'].mean() | |
| } | |
| interpretacao = interpretar_grafico('grafico_linhas_regiao', contexto_linhas) | |
| st.info(f"💡 **Interpretação IA**: {interpretacao}") | |
| except: | |
| interpretacao = """ | |
| Este gráfico mostra a média das notas finais por região, categorizada por nível de faltas. | |
| Linhas mais altas indicam melhor desempenho. Use para identificar padrões regionais | |
| e a relação entre frequência e desempenho acadêmico. | |
| """ | |
| st.info(f"💡 **Interpretação**: {interpretacao}") | |
| else: | |
| interpretacao = """ | |
| Este gráfico mostra a média das notas finais por região, categorizada por nível de faltas. | |
| Linhas mais altas indicam melhor desempenho. Use para identificar padrões regionais | |
| e a relação entre frequência e desempenho acadêmico. | |
| """ | |
| st.info(f"💡 **Interpretação**: {interpretacao}") | |
| # 5. Gráfico Radar - Comparação Individual | |
| st.markdown("### 🎯 Análise Individual - Gráfico Radar") | |
| # Campo de busca para seleção do aluno | |
| if 'nome_aluno' in df_usuario.columns: | |
| nomes_alunos = sorted(df_usuario['nome_aluno'].unique().tolist()) # Ordem alfabética | |
| nome_selecionado = st.selectbox( | |
| "Selecione o aluno para análise:", | |
| options=nomes_alunos, | |
| index=0, | |
| help="Escolha um aluno para comparar com a média da turma", | |
| key="selectbox_aluno_radar" # Chave única para evitar duplicação | |
| ) | |
| # Criar gráfico radar para o aluno selecionado | |
| grafico_radar = criar_grafico_radar_aluno(df_usuario, nome_selecionado) | |
| if 'radar_comparacao_aluno' in grafico_radar: | |
| st.pyplot(grafico_radar['radar_comparacao_aluno']) | |
| # Interpretação do gráfico radar | |
| if usar_ia and 'openai_key' in st.session_state and st.session_state.get('api_valida', False): | |
| try: | |
| from .openai_interpreter import interpretar_grafico | |
| contexto_radar = { | |
| 'nome_aluno': nome_selecionado, | |
| 'total_alunos': len(df_usuario), | |
| 'media_turma': df_usuario['resultado_final'].mean() | |
| } | |
| interpretacao = interpretar_grafico('radar_comparacao', contexto_radar) | |
| st.info(f"💡 **Interpretação IA**: {interpretacao}") | |
| except Exception as e: | |
| interpretacao = f""" | |
| Este gráfico radar compara o desempenho de {nome_selecionado} com a média da turma. | |
| Áreas onde o aluno está acima da média (linha azul acima da rosa) indicam pontos fortes. | |
| Áreas abaixo da média podem indicar necessidades de apoio pedagógico. | |
| """ | |
| st.info(f"💡 **Interpretação**: {interpretacao}") | |
| elif usar_ia and 'openai_key' in st.session_state and not st.session_state.get('api_valida', False): | |
| st.warning("⚠️ Chave OpenAI configurada mas não testada. Teste a chave na sidebar.") | |
| interpretacao = f""" | |
| Este gráfico radar compara o desempenho de {nome_selecionado} com a média da turma. | |
| Áreas onde o aluno está acima da média (linha azul acima da rosa) indicam pontos fortes. | |
| Áreas abaixo da média podem indicar necessidades de apoio pedagógico. | |
| """ | |
| st.info(f"💡 **Interpretação**: {interpretacao}") | |
| else: | |
| interpretacao = f""" | |
| Este gráfico radar compara o desempenho de {nome_selecionado} com a média da turma. | |
| Áreas onde o aluno está acima da média (linha azul acima da rosa) indicam pontos fortes. | |
| Áreas abaixo da média podem indicar necessidades de apoio pedagógico. | |
| """ | |
| st.info(f"💡 **Interpretação**: {interpretacao}") | |
| else: | |
| st.warning("Não foi possível criar o gráfico radar para este aluno.") | |
| else: | |
| st.warning("Coluna 'nome_aluno' não encontrada nos dados.") | |
| # 5. Tabela de Dados | |
| st.markdown("### 📋 Dados Completos da Turma") | |
| st.dataframe(df_usuario, use_container_width=True) | |
| def criar_grafico_correlacao_traduzido(corr_matrix: pd.DataFrame): | |
| """Cria heatmap de correlação com rótulos traduzidos""" | |
| import matplotlib.pyplot as plt | |
| import seaborn as sns | |
| # Traduzir rótulos das colunas | |
| traducao_colunas = { | |
| 'faltas': 'Faltas', | |
| 'nota_2bim': 'Nota 2º Bimestre', | |
| 'cliques_plataforma': 'Cliques na Plataforma', | |
| 'pontuacao_atividades': 'Pontuação nas Atividades' | |
| } | |
| # Renomear colunas | |
| corr_traduzida = corr_matrix.rename(columns=traducao_colunas, index=traducao_colunas) | |
| # Criar gráfico | |
| fig, ax = plt.subplots(figsize=(10, 8)) | |
| sns.heatmap( | |
| corr_traduzida, | |
| annot=True, | |
| cmap='RdYlBu_r', | |
| center=0, | |
| square=True, | |
| fmt='.2f', | |
| cbar_kws={'label': 'Força da Relação'} | |
| ) | |
| ax.set_title('Relação entre Fatores de Desempenho', fontsize=16, fontweight='bold') | |
| plt.tight_layout() | |
| return fig | |
| def encontrar_top_correlacoes(corr_matrix: pd.DataFrame) -> dict: | |
| """Encontra as top correlações para interpretação""" | |
| try: | |
| # Remover diagonal (correlação de 1.0 com si mesmo) | |
| corr_no_diag = corr_matrix.where(~np.eye(len(corr_matrix), dtype=bool)) | |
| # Encontrar correlações mais altas | |
| top_corr = corr_no_diag.stack().nlargest(3) | |
| return { | |
| 'top_correlacoes': top_corr.to_dict(), | |
| 'num_features': len(corr_matrix.columns) | |
| } | |
| except: | |
| return {'top_correlacoes': {}, 'num_features': 0} | |
| def criar_graficos_distribuicao_numerica(df_usuario: pd.DataFrame) -> dict: | |
| """Cria gráficos de distribuição otimizados para análise educacional""" | |
| try: | |
| import matplotlib.pyplot as plt | |
| import seaborn as sns | |
| import numpy as np | |
| import pandas as pd | |
| graficos = {} | |
| # 1. GRÁFICO DE FALTAS - Linha (Faltas vs Nota Final) + Violin Plot | |
| if 'faltas' in df_usuario.columns and 'resultado_final' in df_usuario.columns: | |
| fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(14, 6)) | |
| # Gráfico de Linha - Relação entre faltas e nota final | |
| # Agrupar por número de faltas e calcular média das notas | |
| df_agrupado = df_usuario.groupby('faltas', observed=False)['resultado_final'].agg(['mean', 'count']).reset_index() | |
| df_agrupado = df_agrupado[df_agrupado['count'] >= 1] # Pelo menos 1 aluno | |
| # Plotar linha principal | |
| ax1.plot(df_agrupado['faltas'], df_agrupado['mean'], | |
| marker='o', linewidth=3, markersize=8, | |
| color='#e74c3c', alpha=0.8, label='Média das Notas') | |
| # Adicionar pontos individuais (transparentes para mostrar densidade) | |
| ax1.scatter(df_usuario['faltas'], df_usuario['resultado_final'], | |
| alpha=0.3, s=30, color='#3498db', label='Alunos individuais') | |
| # Linha de tendência | |
| z = np.polyfit(df_usuario['faltas'], df_usuario['resultado_final'], 1) | |
| p = np.poly1d(z) | |
| x_trend = np.linspace(df_usuario['faltas'].min(), df_usuario['faltas'].max(), 100) | |
| ax1.plot(x_trend, p(x_trend), '--', color='#2c3e50', linewidth=2, alpha=0.7, | |
| label=f'Tendência (R²={np.corrcoef(df_usuario["faltas"], df_usuario["resultado_final"])[0,1]**2:.2f})') | |
| ax1.set_title('Relação: Faltas vs Nota Final', fontsize=14, fontweight='bold') | |
| ax1.set_xlabel('Número de Faltas', fontsize=12) | |
| ax1.set_ylabel('Nota Final', fontsize=12) | |
| ax1.legend() | |
| ax1.grid(True, alpha=0.3) | |
| ax1.set_ylim(0, 10) | |
| # Adicionar valores nos pontos principais | |
| for i, row in df_agrupado.iterrows(): | |
| if row['count'] > 1: # Só mostrar valores onde há múltiplos alunos | |
| ax1.annotate(f'{row["mean"]:.1f}\n(n={int(row["count"])})', | |
| (row['faltas'], row['mean']), | |
| textcoords="offset points", | |
| xytext=(0,15), | |
| ha='center', | |
| fontsize=9, | |
| fontweight='bold', | |
| bbox=dict(boxstyle="round,pad=0.3", facecolor='white', alpha=0.8)) | |
| # Violin Plot - Mostra densidade da distribuição | |
| violin_parts = ax2.violinplot([df_usuario['faltas']], positions=[1], | |
| showmeans=True, showmedians=True) | |
| violin_parts['bodies'][0].set_facecolor('#ff6b6b') | |
| violin_parts['bodies'][0].set_alpha(0.7) | |
| ax2.set_title('Densidade de Faltas - Violin Plot', fontsize=14, fontweight='bold') | |
| ax2.set_ylabel('Número de Faltas', fontsize=12) | |
| ax2.set_xticks([1]) | |
| ax2.set_xticklabels(['Faltas']) | |
| ax2.grid(True, alpha=0.3) | |
| plt.tight_layout() | |
| graficos['distribuicao_faltas'] = fig | |
| # 2. GRÁFICO DE NOTA 2º BIMESTRE - Scatter Plot + Regressão | |
| if 'nota_2bim' in df_usuario.columns and 'resultado_final' in df_usuario.columns: | |
| fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(14, 6)) | |
| # Scatter Plot - Relação entre nota 2º bimestre e nota final | |
| scatter = ax1.scatter(df_usuario['nota_2bim'], df_usuario['resultado_final'], | |
| alpha=0.6, c='#4ecdc4', s=50, edgecolors='black', linewidth=0.5) | |
| # Linha de regressão | |
| z = np.polyfit(df_usuario['nota_2bim'], df_usuario['resultado_final'], 1) | |
| p = np.poly1d(z) | |
| ax1.plot(df_usuario['nota_2bim'], p(df_usuario['nota_2bim']), | |
| "r--", alpha=0.8, linewidth=2, label=f'Tendência (R²={np.corrcoef(df_usuario["nota_2bim"], df_usuario["resultado_final"])[0,1]**2:.2f})') | |
| ax1.set_title('Relação: Nota 2º Bimestre vs Nota Final', fontsize=14, fontweight='bold') | |
| ax1.set_xlabel('Nota do 2º Bimestre', fontsize=12) | |
| ax1.set_ylabel('Nota Final', fontsize=12) | |
| ax1.legend() | |
| ax1.grid(True, alpha=0.3) | |
| # Gráfico de Pizza com Insights Estatísticos | |
| # Criar categorias para notas do 2º bimestre | |
| df_usuario['categoria_2bim'] = pd.cut( | |
| df_usuario['nota_2bim'], | |
| bins=[0, 5, 7, 10], | |
| labels=['Insuficiente (0-5)', 'Regular (5-7)', 'Bom (7-10)'], | |
| include_lowest=True | |
| ) | |
| contagem_categorias = df_usuario['categoria_2bim'].value_counts() | |
| cores_categorias = ['#e74c3c', '#f39c12', '#2ecc71'] # Vermelho, laranja, verde | |
| # Calcular percentuais e estatísticas | |
| total_alunos = len(df_usuario) | |
| percentuais = (contagem_categorias / total_alunos * 100).round(1) | |
| # Criar gráfico de pizza | |
| wedges, texts, autotexts = ax2.pie(contagem_categorias.values, | |
| labels=contagem_categorias.index, | |
| colors=cores_categorias, | |
| autopct='%1.1f%%', | |
| startangle=90, | |
| explode=(0.05, 0.05, 0.05), # Separar fatias | |
| textprops={'fontsize': 10, 'fontweight': 'bold'}) | |
| # Melhorar aparência dos textos | |
| for autotext in autotexts: | |
| autotext.set_color('white') | |
| autotext.set_fontweight('bold') | |
| autotext.set_fontsize(11) | |
| ax2.set_title('Distribuição por Categoria - 2º Bimestre\n(Com Insights Estatísticos)', | |
| fontsize=14, fontweight='bold', pad=20) | |
| # Adicionar caixa de estatísticas | |
| stats_text = f""" | |
| 📊 ESTATÍSTICAS: | |
| • Total de Alunos: {total_alunos} | |
| • Insuficiente: {contagem_categorias.get('Insuficiente (0-5)', 0)} ({percentuais.get('Insuficiente (0-5)', 0):.1f}%) | |
| • Regular: {contagem_categorias.get('Regular (5-7)', 0)} ({percentuais.get('Regular (5-7)', 0):.1f}%) | |
| • Bom: {contagem_categorias.get('Bom (7-10)', 0)} ({percentuais.get('Bom (7-10)', 0):.1f}%) | |
| 🎯 INSIGHTS: | |
| • Taxa de Aprovação: {((contagem_categorias.get('Regular (5-7)', 0) + contagem_categorias.get('Bom (7-10)', 0)) / total_alunos * 100):.1f}% | |
| • Necessita Intervenção: {contagem_categorias.get('Insuficiente (0-5)', 0)} alunos | |
| """ | |
| # Adicionar texto de estatísticas ao lado do gráfico | |
| ax2.text(1.3, 0.5, stats_text, transform=ax2.transAxes, | |
| fontsize=9, verticalalignment='center', | |
| bbox=dict(boxstyle="round,pad=0.5", facecolor='lightblue', alpha=0.8)) | |
| plt.tight_layout() | |
| graficos['distribuicao_nota_2bim'] = fig | |
| return graficos | |
| except Exception as e: | |
| st.error(f"Erro ao criar gráficos de distribuição numérica: {e}") | |
| return {} | |
| def criar_grafico_barras_empilhadas(df_usuario: pd.DataFrame): | |
| """Cria gráfico de linhas mostrando média das notas finais por região e categoria de faltas""" | |
| try: | |
| import matplotlib.pyplot as plt | |
| import pandas as pd | |
| import numpy as np | |
| # Verificar se as colunas necessárias existem | |
| if 'regiao' not in df_usuario.columns or 'resultado_final' not in df_usuario.columns: | |
| return None | |
| # Preparar dados | |
| df_plot = df_usuario.copy() | |
| # Criar categorias para faltas | |
| if 'faltas' in df_usuario.columns: | |
| df_plot['categoria_faltas'] = pd.cut( | |
| df_plot['faltas'], | |
| bins=[0, 2, 5, 10], | |
| labels=['Baixas (0-2)', 'Médias (2-5)', 'Altas (5+)'], | |
| include_lowest=True | |
| ) | |
| else: | |
| df_plot['categoria_faltas'] = 'N/A' | |
| # Calcular média das notas finais por região e categoria de faltas | |
| media_por_categoria = df_plot.groupby(['regiao', 'categoria_faltas'], observed=False)['resultado_final'].mean().unstack(fill_value=0) | |
| # Criar gráfico de linhas | |
| fig, ax = plt.subplots(figsize=(14, 8)) | |
| # Cores e estilos para as linhas | |
| cores = ['#2ecc71', '#f39c12', '#e74c3c'] # Verde, laranja, vermelho | |
| estilos = ['-', '--', '-.'] # Linha sólida, tracejada, ponto-traço | |
| marcadores = ['o', 's', '^'] # Círculo, quadrado, triângulo | |
| # Plotar linhas para cada categoria de faltas | |
| for i, categoria in enumerate(media_por_categoria.columns): | |
| if categoria in media_por_categoria.columns and not media_por_categoria[categoria].isna().all(): | |
| ax.plot(media_por_categoria.index, media_por_categoria[categoria], | |
| color=cores[i % len(cores)], | |
| linestyle=estilos[i % len(estilos)], | |
| marker=marcadores[i % len(marcadores)], | |
| linewidth=3, markersize=8, alpha=0.8, | |
| label=f'{categoria} (Média: {media_por_categoria[categoria].mean():.1f})') | |
| # Configurar gráfico | |
| ax.set_title('Média das Notas Finais por Região e Categoria de Faltas', | |
| fontsize=16, fontweight='bold', pad=20) | |
| ax.set_xlabel('Região', fontsize=14, fontweight='bold') | |
| ax.set_ylabel('Média das Notas Finais (0-10)', fontsize=14, fontweight='bold') | |
| ax.legend(title='Categoria de Faltas', bbox_to_anchor=(1.05, 1), loc='upper left') | |
| ax.grid(True, alpha=0.3) | |
| # Configurar eixo Y para escala 0-10 | |
| ax.set_ylim(0, 10) | |
| ax.set_yticks(range(0, 11, 2)) | |
| # Rotacionar labels do eixo x se necessário | |
| plt.xticks(rotation=45, ha='right') | |
| # Adicionar valores nos pontos | |
| for categoria in media_por_categoria.columns: | |
| if categoria in media_por_categoria.columns: | |
| for i, regiao in enumerate(media_por_categoria.index): | |
| valor = media_por_categoria.loc[regiao, categoria] | |
| if not pd.isna(valor) and valor > 0: | |
| ax.annotate(f'{valor:.1f}', | |
| (i, valor), | |
| textcoords="offset points", | |
| xytext=(0,10), | |
| ha='center', | |
| fontsize=9, | |
| fontweight='bold', | |
| bbox=dict(boxstyle="round,pad=0.3", facecolor='white', alpha=0.8)) | |
| plt.tight_layout() | |
| return fig | |
| except Exception as e: | |
| st.error(f"Erro ao criar gráfico de linhas: {e}") | |
| return None | |