import pandas as pd from pathlib import Path import unicodedata import re from typing import Optional, List, Dict, Union, Any # --- Configurações Globais --- # Configura o Pandas para exibir mais linhas e define caminhos de arquivos e nomes de colunas chave. # Garante que a pasta para cache CSV exista. # Link do arquivo original: https://cdn3.gnarususercontent.com.br/2927-pandas-selecao-agrupamento-dados/1-SEEG10_GERAL-BR_UF_2022.10.27-FINAL-SITE.xlsx pd.set_option('display.max_rows', 1000) EXCEL_FILE_PATH = Path('D:/Python para Data Science/1-SEEG10_GERAL-BR_UF_2022.10.27-FINAL-SITE.xlsx') CSV_DIR = Path('dados_csv') CSV_DIR.mkdir(exist_ok=True) REGION_COLUMN = 'Região' # --- Funções Auxiliares de Entrada e Normalização --- def normalize_name(name: str) -> str: """Normaliza strings (remove acentos, espaços, caracteres especiais) para uso consistente.""" return re.sub(r'_+', '_', unicodedata.normalize('NFKD', name).encode('ASCII', 'ignore').decode('utf-8').replace(' ', '_')).strip('_') def get_user_numeric_input(prompt: str, allow_empty: bool = False, default: Optional[int] = None) -> Optional[int]: """Valida e retorna entrada numérica do usuário, com opção de valor padrão e permitir vazio.""" # Loop de validação para garantir que a entrada seja um número inteiro. while True: user_input = input(prompt).strip() if allow_empty and not user_input: return default try: return int(user_input) except ValueError: print('Entrada inválida. Digite um número inteiro.') def get_user_indices(prompt: str, max_index: int) -> List[int]: """Obtém uma lista de índices numéricos do usuário, validando o formato e o intervalo.""" # Loop para obter e validar índices separados por vírgula. Retorna lista vazia se Enter for pressionado. while True: if not (indices_str := input(prompt).strip()): return [] try: indices = [int(i.strip()) for i in indices_str.split(',') if i.strip()] if all(0 <= i < max_index for i in indices): return indices print(f'Um ou mais índices estão fora do intervalo (0 a {max_index - 1}). Tente novamente.') except ValueError: print('Erro: digite apenas números inteiros separados por vírgula.') # --- Carregamento e Cache de Dados --- def load_or_cache_dataframe(file_path: Path, sheet_name: str) -> pd.DataFrame: """ Carrega dados de uma aba do Excel ou de um CSV cacheado. Prioriza o CSV se ele for mais recente que o arquivo Excel original, otimizando o carregamento. Remove linhas com 'Bunker' == 'Sim' se a coluna existir e salva o DataFrame como CSV para cache. """ csv_path = CSV_DIR / f'{normalize_name(sheet_name)}_df.csv' # Verifica cache CSV. Se existir e for mais recente que o Excel, carrega o CSV. if csv_path.exists() and csv_path.stat().st_mtime > file_path.stat().st_mtime: print(f'Carregando CSV existente para "{sheet_name}": {csv_path}') return pd.read_csv(csv_path) # Se não houver cache ou o Excel for mais novo, lê o Excel. print(f'Lendo aba "{sheet_name}" do Excel...') df = pd.read_excel(file_path, sheet_name=sheet_name) if 'Bunker' in df.columns: df = df[df['Bunker'] != 'Sim'] # Filtra linhas "Bunker". print(f'Salvando CSV para leitura rápida: {csv_path}') df.to_csv(csv_path, index=False) # Salva para futuras leituras rápidas. return df # --- Funções de Visualização e Filtro --- def display_dataframe_head(df: pd.DataFrame, n_lines: Optional[int] = None) -> None: """Exibe as primeiras N linhas de um DataFrame, pedindo N ao usuário se não especificado.""" # Determina N linhas a exibir (padrão 100 se Enter for pressionado). n_lines = n_lines if n_lines is not None else get_user_numeric_input('\nQuantas linhas deseja exibir? (pressione Enter para 100): ', allow_empty=True, default=100) or 100 print(f'\nExibindo as primeiras {n_lines} linhas:') print(df.head(n_lines)) def view_or_select_columns(df: pd.DataFrame) -> None: """ Permite visualizar o DataFrame completo ou selecionar colunas específicas. Se o usuário pressionar ENTER (não selecionar colunas), o DataFrame completo é exibido. """ print(f'\nTamanho do DataFrame: {df.shape[0]} linhas x {df.shape[1]} colunas\nColunas disponíveis:') [print(f'[{i}] {col} ({df[col].dtype})') for i, col in enumerate(df.columns)] # Lista colunas. # Obtém índices das colunas desejadas. Se vazio, exibe o DF completo. if (indices := get_user_indices('Digite os índices das colunas desejadas separadas por vírgula (ou ENTER para ver todas as colunas): ', len(df.columns))): print(f'Colunas selecionadas: {[df.columns[i] for i in indices]}') display_dataframe_head(df[[df.columns[i] for i in indices]]) # Exibe colunas selecionadas. else: print('Nenhuma coluna selecionada. Exibindo o DataFrame completo.') display_dataframe_head(df) # Exibe o DF inteiro. def advanced_filter_data(df: pd.DataFrame) -> None: """ Aplica filtros múltiplos em colunas selecionadas interativamente. O usuário escolhe as colunas e os valores para cada filtro. """ print("\n--- Aplicar Filtros (Básico/Avançado) ---") print("Escolha as colunas para iniciar o filtro (separadas por vírgula):") [print(f"[{i}] {col}") for i, col in enumerate(df.columns)] # Lista colunas para filtrar. if not (base_indices := get_user_indices("Digite os índices das colunas para filtro: ", len(df.columns))): print("Nenhuma coluna selecionada. Voltando ao menu."); return filtered_df = df.copy() # Copia o DF para aplicar filtros sem alterar o original. for col_name in (df.columns[i] for i in base_indices): # Itera sobre as colunas escolhidas. if col_name not in filtered_df.columns: continue # Obtém e exibe valores únicos para a coluna atual. if not (unique_vals := sorted(filtered_df[col_name].dropna().unique())): print(f"Não há valores únicos para a coluna '{col_name}' com os filtros atuais. Pulando."); continue print(f"\nValores disponíveis para '{col_name}':"); [print(f"[{i}] {val}") for i, val in enumerate(unique_vals)] # Obtém índices dos valores para filtrar e aplica o filtro. if (val_indices := get_user_indices(f"Escolha os índices dos valores para filtrar em '{col_name}' (vírgula para múltiplos, ENTER para pular): ", len(unique_vals))): filtered_df = filtered_df[filtered_df[col_name].isin([unique_vals[i] for i in val_indices])] print("\nResultado após filtros:") display_dataframe_head(filtered_df) # Exibe o resultado do filtro. # --- Funções de Análise e Estatísticas --- def calculate_statistics(df: pd.DataFrame) -> None: """ Calcula estatísticas descritivas para colunas de anos, com filtros opcionais por atividade e estado. Os resultados são formatados para melhor legibilidade, removendo notação científica. """ year_cols = [col for col in df.columns if re.fullmatch(r'\d{4}', str(col))] # Identifica colunas de ano. if not year_cols: print("Não foram encontradas colunas de anos para análise."); return # Processo interativo de seleção de anos, atividades e estados para filtrar. print("\nColunas de anos disponíveis:"); [print(f"[{i}] {year}") for i, year in enumerate(year_cols)] if not (selected_year_indices := get_user_indices("Escolha os índices dos anos para análise (vírgula separada): ", len(year_cols))): print("Nenhum ano selecionado para análise."); return selected_year_cols = [year_cols[i] for i in selected_year_indices] # Lógica similar para filtrar por atividade e estado, pedindo ao usuário as seleções. # ... (partes de seleção de atividade e estado, filtragem do DF) # A parte principal da compactação está na chamada final do print das estatísticas. # Aplica filtros de atividade e estado se houver seleções (lógica omitida para brevidade do comentário). # ... # Exemplo completo da lógica de filtro no bloco: print("\nColunas disponíveis para filtro de atividade:"); [print(f"[{i}] {col}") for i, col in enumerate(df.columns)] if (act_idx := get_user_numeric_input("Escolha o índice da coluna para filtrar atividade: ")) is None or not (0 <= act_idx < len(df.columns)): print("Entrada inválida para coluna de atividade."); return activity_col = df.columns[act_idx] act_vals = sorted(df[activity_col].dropna().unique()) print(f"\nValores disponíveis para '{activity_col}':"); [print(f"[{i}] {val}") for i, val in enumerate(act_vals)] selected_activities = [act_vals[i] for i in get_user_indices("Escolha os índices das atividades (vírgula separada, ENTER para pular): ", len(act_vals))] if 'Estado' not in df.columns: print('Coluna "Estado" não encontrada no DataFrame.'); return state_vals = sorted(df['Estado'].dropna().unique()) print("\nEstados disponíveis:"); [print(f"[{i}] {state}") for i, state in enumerate(state_vals)] selected_states = [state_vals[i] for i in get_user_indices("Escolha os índices dos estados (vírgula separada, ENTER para pular): ", len(state_vals))] filtered_df = df.copy() if selected_activities: filtered_df = filtered_df[filtered_df[activity_col].isin(selected_activities)] if selected_states: filtered_df = filtered_df[filtered_df['Estado'].isin(selected_states)] if filtered_df.empty: print("Nenhum dado encontrado com os filtros aplicados."); return print("\n--- Estatísticas para as Colunas de Anos Selecionados ---") try: # Converte para numérico, remove NaNs e calcula estatísticas. stats_df = filtered_df[selected_year_cols].apply(pd.to_numeric, errors='coerce').dropna(how='all') # Formata o DataFrame de estatísticas para melhor legibilidade (sem notação científica). print(stats_df.describe().applymap(lambda x: f"{x:,.2f}" if pd.notna(x) else "") if not stats_df.empty else "Nenhum dado numérico disponível para calcular estatísticas com os filtros aplicados.") except Exception as e: print(f"Erro ao calcular estatísticas: {e}") # --- Função Principal do Programa (Menu Interativo) --- def main() -> None: """ Ponto de entrada do programa. Carrega o DataFrame principal e apresenta um menu interativo. Gerencia a navegação entre as diferentes funcionalidades de análise de dados. """ main_df = load_or_cache_dataframe(EXCEL_FILE_PATH, 'Gee Estados') # Dicionário mapeando opções do menu para funções lambda, tornando o menu conciso e fácil de escalar. menu_options = { '1': ("Visualizar/Selecionar Colunas", lambda: view_or_select_columns(main_df)), '2': ("Aplicar Filtros (Básico/Avançado)", lambda: advanced_filter_data(main_df)), '3': ("Estatísticas por Ano, Atividade e Estado", lambda: calculate_statistics(main_df)), '0': ("Sair", None) } while True: # Loop do menu principal. print('\n--- Menu Principal ---') # Exibe as opções do menu usando uma list comprehension para compactação. [print(f'[{key}] {desc}') for key, (desc, _) in menu_options.items()] # Obtém a opção do usuário usando o operador walrus para atribuir e verificar na mesma linha. if (option := input('Digite sua opção: ').strip()) == '0': print('Encerrando o programa. Até mais!'); break # Sai do programa se '0'. elif action := menu_options.get(option): # Se a opção é válida, executa a função lambda associada. action[1]() else: print('Opção inválida. Tente novamente.') # Trata opção inválida. # --- Execução do Programa --- if __name__ == '__main__': # Garante que 'main()' seja chamado apenas quando o script é executado diretamente. main()