gui-sparim's picture
Update app.py
61265ce verified
raw
history blame
57 kB
# -*- coding: utf-8 -*-
"""
app.py - Interface Gradio para elaboração de modelos estatísticos
Aplicativo para avaliação de imóveis
"""
import gradio as gr
import pandas as pd
import numpy as np
import os
from datetime import datetime
# Importa módulos locais
from core import (
detectar_abas_excel,
carregar_arquivo,
identificar_coluna_y_padrao,
identificar_colunas_coords,
obter_colunas_numericas,
calcular_estatisticas_variaveis,
calcular_metricas_outliers,
sugerir_outliers,
detectar_dicotomicas,
ajustar_modelo,
buscar_melhores_transformacoes,
testar_micronumerosidade,
exportar_modelo_dai,
exportar_base_csv,
TRANSFORMACOES
)
from charts import (
criar_graficos_dispersao,
criar_painel_diagnostico,
criar_matriz_correlacao,
criar_mapa
)
# ============================================================
# CONSTANTES
# ============================================================
TITULO = """
# ELABORAÇÃO DE MODELOS ESTATÍSTICOS
### Divisão de Avaliação de Imóveis
---
"""
# ============================================================
# FUNÇÕES AUXILIARES
# ============================================================
def arredondar_df(df, decimais=4):
"""Arredonda apenas colunas numéricas de um DataFrame."""
if df is None:
return None
df_copy = df.copy()
colunas_numericas = df_copy.select_dtypes(include=[np.number]).columns
df_copy[colunas_numericas] = df_copy[colunas_numericas].round(decimais)
return df_copy
def carregar_css():
"""Carrega CSS externo."""
css_path = os.path.join(os.path.dirname(__file__), "styles.css")
try:
with open(css_path, "r", encoding="utf-8") as f:
return f.read()
except FileNotFoundError:
return ""
def criar_header_secao(numero: int, titulo: str, timestamp: str = "") -> str:
"""Cria um header HTML estilizado para seções, opcionalmente com timestamp."""
timestamp_html = f' <span class="section-timestamp">(Atualizado às {timestamp})</span>' if timestamp else ""
return f'''
<div class="section-header">
<span class="section-number">{numero}</span>
<h2 class="section-title">{titulo}{timestamp_html}</h2>
</div>
'''
def formatar_diagnosticos_html(diagnosticos):
"""Formata diagnósticos como HTML."""
if not diagnosticos:
return "<p>Nenhum modelo ajustado.</p>"
html = '<div class="diagnosticos-container">'
# Estatísticas gerais
html += '<div class="section-title-orange">Estatísticas Gerais</div>'
html += f'''
<div class="stats-grid">
<div class="stat-item"><span class="stat-label">n (observações)</span><span class="stat-value">{diagnosticos["n"]}</span></div>
<div class="stat-item"><span class="stat-label">k (variáveis)</span><span class="stat-value">{diagnosticos["k"]}</span></div>
<div class="stat-item"><span class="stat-label">R²</span><span class="stat-value">{diagnosticos["r2"]:.4f}</span></div>
<div class="stat-item"><span class="stat-label">R² ajustado</span><span class="stat-value">{diagnosticos["r2_ajustado"]:.4f}</span></div>
<div class="stat-item"><span class="stat-label">Correlação Pearson</span><span class="stat-value">{diagnosticos["r_pearson"]:.4f}</span></div>
<div class="stat-item"><span class="stat-label">Desvio Padrão Resíduos</span><span class="stat-value">{diagnosticos["desvio_padrao_residuos"]:.4f}</span></div>
</div>
'''
# Testes
html += '<div class="section-title-orange">Testes Estatísticos</div>'
html += f'''
<div class="teste-item">
<span class="teste-nome">Teste F:</span>
<span class="teste-valor">F = {diagnosticos["Fc"]:.4f}, p = {diagnosticos["p_valor_F"]:.4f}</span>
<span class="teste-interp">{diagnosticos["interp_F"]}</span>
</div>
<div class="teste-item">
<span class="teste-nome">Kolmogorov-Smirnov:</span>
<span class="teste-valor">KS = {diagnosticos["ks_stat"]:.4f}, p = {diagnosticos["ks_p"]:.4f}</span>
<span class="teste-interp">{diagnosticos["interp_KS"]}</span>
</div>
<div class="teste-item">
<span class="teste-nome">Curva Normal (%):</span>
<span class="teste-valor">{diagnosticos["perc_resid"]}</span>
<span class="teste-interp">Ideal: 68%, 90%, 95%</span>
</div>
<div class="teste-item">
<span class="teste-nome">Durbin-Watson:</span>
<span class="teste-valor">DW = {diagnosticos["dw"]:.4f}</span>
<span class="teste-interp">{diagnosticos["interp_DW"]}</span>
</div>
<div class="teste-item">
<span class="teste-nome">Breusch-Pagan:</span>
<span class="teste-valor">LM = {diagnosticos["bp_lm"]:.4f}, p = {diagnosticos["bp_p"]:.4f}</span>
<span class="teste-interp">{diagnosticos["interp_BP"]}</span>
</div>
'''
# Equação
html += '<div class="section-title-orange">Equação do Modelo</div>'
html += f'<div class="equation-box">{diagnosticos["equacao"]}</div>'
html += '</div>'
return html
def formatar_busca_html(resultados_busca):
"""Formata resultados da busca automática como HTML."""
if not resultados_busca:
return "<p>Execute a busca automática para ver resultados.</p>"
html = '<div class="busca-container">'
for r in resultados_busca:
html += f'''
<div class="modelo-card" style="border: 1px solid #ddd; border-radius: 8px; padding: 12px; margin: 8px 0; background: #f9f9f9;">
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 8px;">
<span class="modelo-rank" style="font-weight: bold; font-size: 1.1em;">#{r["rank"]}</span>
<span class="modelo-r2" style="background: #e3f2fd; padding: 4px 8px; border-radius: 4px;">R² = {r["r2"]:.4f}</span>
</div>
<div class="modelo-transf" style="font-size: 0.95em;">
<b>y:</b> {r["transformacao_y"]}<br>
'''
for col, transf in r["transformacoes_x"].items():
html += f"<b>{col}:</b> {transf}<br>"
html += '''
</div>
</div>
'''
html += '</div>'
return html
# ============================================================
# CALLBACKS PRINCIPAIS
# ============================================================
def ao_carregar_arquivo(arquivo):
"""Callback quando arquivo é carregado. Detecta se há múltiplas abas no Excel."""
if arquivo is None:
return (
None, # estado_df
"Nenhum arquivo enviado.", # status
gr.update(choices=[], value=None, visible=False), # dropdown_aba
gr.update(choices=[], value=None), # dropdown_y
None, # tabela_dados
None, # tabela_estatisticas
gr.update(choices=[]), # checkboxes_x
"<p>Carregue um arquivo para ver o mapa.</p>", # mapa
None, # estado_df_filtrado
criar_header_secao(4, "Estatísticas por Variável"), # header_estatisticas
[], # estado_outliers_anteriores
1, # estado_iteracao
gr.update(visible=False), # accordion_outliers_anteriores
"Iteração: 1", # txt_iteracao_atual
"0 outliers excluídos", # txt_n_outliers_anteriores
"", # txt_lista_outliers_anteriores
None, # estado_arquivo_temp
)
# Obtém o caminho do arquivo (para poder reabrir depois)
caminho_arquivo = arquivo.name if hasattr(arquivo, 'name') else str(arquivo)
# Detecta abas do Excel
abas, msg_abas, sucesso_abas = detectar_abas_excel(caminho_arquivo)
# Se há múltiplas abas, mostra dropdown e carrega a primeira aba
if sucesso_abas and len(abas) > 1:
# Carrega a primeira aba automaticamente
resultado = carregar_dados_do_arquivo(caminho_arquivo, abas[0], manter_dropdown=True, lista_abas=abas)
return resultado
# Se não há múltiplas abas, carrega diretamente
return carregar_dados_do_arquivo(caminho_arquivo, None)
def carregar_dados_do_arquivo(caminho_arquivo, nome_aba, manter_dropdown=False, lista_abas=None):
"""Carrega dados de um arquivo, opcionalmente de uma aba específica.
Args:
caminho_arquivo: Caminho do arquivo a carregar
nome_aba: Nome da aba a carregar (apenas para Excel)
manter_dropdown: Se True, mantém o dropdown de abas visível
lista_abas: Lista de abas para popular o dropdown (quando manter_dropdown=True)
"""
df, msg, sucesso = carregar_arquivo(caminho_arquivo, nome_aba)
if not sucesso:
return (
None, # estado_df
msg, # status
gr.update(visible=False), # dropdown_aba (esconde)
gr.update(choices=[], value=None), # dropdown_y
None, # tabela_dados
None, # tabela_estatisticas
gr.update(choices=[]), # checkboxes_x
"<p>Carregue um arquivo para ver o mapa.</p>", # mapa
None, # estado_df_filtrado
criar_header_secao(4, "Estatísticas por Variável"), # header_estatisticas
[], # estado_outliers_anteriores
1, # estado_iteracao
gr.update(visible=False), # accordion_outliers_anteriores
"Iteração: 1", # txt_iteracao_atual
"0 outliers excluídos", # txt_n_outliers_anteriores
"", # txt_lista_outliers_anteriores
None, # estado_arquivo_temp
)
# Identifica colunas
colunas_numericas = obter_colunas_numericas(df)
coluna_y_padrao = identificar_coluna_y_padrao(df)
# Variáveis X padrão: todas exceto Y
colunas_x_padrao = [col for col in colunas_numericas if col != coluna_y_padrao]
# Estatísticas iniciais
estatisticas = calcular_estatisticas_variaveis(df, coluna_y_padrao)
# Mapa
mapa_html = criar_mapa(df)
# Timestamp inicial
timestamp = datetime.now().strftime("%H:%M:%S")
# Configura dropdown de aba
if manter_dropdown and lista_abas:
dropdown_aba_update = gr.update(choices=lista_abas, value=nome_aba, visible=True)
arquivo_temp = caminho_arquivo # Mantém o caminho para trocar de aba depois
else:
dropdown_aba_update = gr.update(visible=False)
arquivo_temp = None
return (
df, # estado_df
msg, # status
dropdown_aba_update, # dropdown_aba
gr.update(choices=colunas_numericas, value=coluna_y_padrao), # dropdown_y
arredondar_df(df), # tabela_dados
estatisticas.round(4), # tabela_estatisticas
gr.update(choices=colunas_numericas, value=colunas_x_padrao), # checkboxes_x (todas marcadas)
mapa_html, # mapa
df, # estado_df_filtrado (inicia com dados completos)
criar_header_secao(4, "Estatísticas por Variável", timestamp), # header_estatisticas
[], # estado_outliers_anteriores
1, # estado_iteracao
gr.update(visible=False), # accordion_outliers_anteriores
"Iteração: 1", # txt_iteracao_atual
"0 outliers excluídos", # txt_n_outliers_anteriores
"", # txt_lista_outliers_anteriores
arquivo_temp, # estado_arquivo_temp
)
def ao_selecionar_aba(caminho_arquivo, nome_aba):
"""Callback quando uma aba é selecionada no dropdown."""
if caminho_arquivo is None or nome_aba is None:
return (
None, "Selecione uma aba.", gr.update(), gr.update(choices=[], value=None),
None, None, gr.update(choices=[]), "<p>Selecione a aba.</p>",
None, "", [], 1, gr.update(visible=False), "Iteração: 1",
"0 outliers excluídos", "", None
)
# Detecta abas novamente para manter o dropdown
abas, _, _ = detectar_abas_excel(caminho_arquivo)
return carregar_dados_do_arquivo(caminho_arquivo, nome_aba, manter_dropdown=True, lista_abas=abas)
def ao_mudar_y(df, coluna_y):
"""Callback quando variável y é alterada."""
if df is None or coluna_y is None:
return None, gr.update(choices=[])
# Lista de X disponíveis (exclui y) - todas marcadas por padrão
colunas_x = [col for col in obter_colunas_numericas(df) if col != coluna_y]
# Calcula estatísticas apenas para colunas selecionadas
colunas_para_stats = [coluna_y] + colunas_x
estatisticas = calcular_estatisticas_variaveis(df, coluna_y, colunas=colunas_para_stats)
return estatisticas.round(4), gr.update(choices=colunas_x, value=colunas_x)
def ajustar_modelo_callback(df, coluna_y, colunas_x, transformacao_y, outliers_anteriores, *valores_dropdowns):
"""Callback para ajustar o modelo e calcular métricas de outliers."""
if df is None or coluna_y is None or not colunas_x:
return (
None, # estado_modelo
criar_header_secao(7, "Diagnóstico de Modelo"), # header_modelo
"<p>Selecione as variáveis para ajustar o modelo.</p>", # diagnosticos_html
None, # tabela_coef
None, # tabela_obs_calc
None, None, None, None, None, # gráficos
criar_header_secao(8, "Gráficos de Diagnóstico"), # header_graficos
None, # tabela_metricas
None, # estado_metricas
criar_header_secao(9, "Analisar Outliers"), # header_outliers
f"Outliers anteriores: {len(outliers_anteriores) if outliers_anteriores else 0} | Novos: 0 | Total após iteração: {len(outliers_anteriores) if outliers_anteriores else 0}" # txt_resumo_outliers
)
# Extrai transformações dos dropdowns
transformacoes_x = obter_transformacoes_dos_dropdowns(colunas_x, *valores_dropdowns)
# Ajusta modelo
resultado = ajustar_modelo(
df, coluna_y, colunas_x,
transformacao_y, transformacoes_x
)
if resultado is None:
return (
None,
criar_header_secao(7, "Diagnóstico de Modelo"),
"<p>Erro ao ajustar modelo. Verifique os dados.</p>",
None, None,
None, None, None, None, None,
criar_header_secao(8, "Gráficos de Diagnóstico"),
None, None, criar_header_secao(9, "Analisar Outliers"),
f"Outliers anteriores: {len(outliers_anteriores) if outliers_anteriores else 0} | Novos: 0 | Total: {len(outliers_anteriores) if outliers_anteriores else 0}"
)
# Formata diagnósticos
diagnosticos_html = formatar_diagnosticos_html(resultado["diagnosticos"])
timestamp = datetime.now().strftime("%H:%M:%S")
# Gráficos
graficos = criar_painel_diagnostico(resultado)
# Correlação
colunas_corr = [coluna_y] + colunas_x
fig_corr = criar_matriz_correlacao(df, colunas_corr)
# Extrai métricas de outliers da tabela_obs_calc (já contém resíduos, Cook, etc.)
tabela_metricas = resultado["tabela_obs_calc"].copy()
# Resumo de outliers
n_anteriores = len(outliers_anteriores) if outliers_anteriores else 0
resumo = f"Outliers anteriores: {n_anteriores} | Novos: 0 | Total após iteração: {n_anteriores}"
return (
resultado,
criar_header_secao(7, "Diagnóstico de Modelo", timestamp), # header_modelo
diagnosticos_html,
resultado["tabela_coef"].round(4),
resultado["tabela_obs_calc"].round(4),
graficos.get("obs_calc"),
graficos.get("residuos"),
graficos.get("histograma"),
graficos.get("cook"),
fig_corr,
criar_header_secao(8, "Gráficos de Diagnóstico", timestamp), # header_graficos
tabela_metricas.round(4), # tabela_metricas
tabela_metricas, # estado_metricas
criar_header_secao(9, "Analisar Outliers", timestamp), # header_outliers
resumo # txt_resumo_outliers
)
def buscar_transformacoes_callback(df, coluna_y, colunas_x):
"""Callback para busca automática de transformações."""
# Retorna: (html, resultados, timestamp, btn1_visible, btn2_visible, btn3_visible, btn4_visible, btn5_visible)
btn_hidden = [gr.update(visible=False)] * 5
if df is None or coluna_y is None or not colunas_x:
return ("<p>Selecione as variáveis primeiro.</p>", [], "", *btn_hidden)
# Verifica se colunas têm dados válidos (não 100% NaN)
colunas_vazias = []
for col in colunas_x:
if col in df.columns and df[col].isna().all():
colunas_vazias.append(col)
if colunas_vazias:
msg = f"<p style='color: red;'><b>Erro:</b> As seguintes colunas estão completamente vazias (sem dados): <b>{', '.join(colunas_vazias)}</b></p>"
msg += "<p>Selecione apenas variáveis que contenham dados válidos.</p>"
return (msg, [], "", *btn_hidden)
# Verifica se Y tem dados válidos
if coluna_y in df.columns and df[coluna_y].isna().all():
msg = f"<p style='color: red;'><b>Erro:</b> A variável dependente <b>{coluna_y}</b> está completamente vazia.</p>"
return (msg, [], "", *btn_hidden)
# Detecta dicotômicas e fixa em (x)
transformacoes_fixas = {}
dicotomicas = detectar_dicotomicas(df, colunas_x)
for col in dicotomicas:
transformacoes_fixas[col] = "(x)"
# Busca melhores transformações
resultados = buscar_melhores_transformacoes(
df, coluna_y, colunas_x,
transformacoes_fixas=transformacoes_fixas,
top_n=5
)
if not resultados:
msg = "<p style='color: orange;'><b>Aviso:</b> Não foi possível encontrar combinações válidas de transformações.</p>"
msg += "<p>Verifique se os dados contêm valores válidos (sem NaN ou Inf após transformações).</p>"
return (msg, [], "", *btn_hidden)
html = formatar_busca_html(resultados)
timestamp = datetime.now().strftime("%H:%M:%S")
# Atualiza visibilidade dos botões "Adotar"
btn_updates = [gr.update(visible=(i < len(resultados))) for i in range(5)]
return (html, resultados, criar_header_secao(5, "Transformações Sugeridas", timestamp), *btn_updates)
def exportar_modelo_callback(resultado_modelo, df, estatisticas, nome_arquivo):
"""Callback para exportar modelo."""
if resultado_modelo is None:
return "Nenhum modelo para exportar. Ajuste o modelo primeiro."
if not nome_arquivo or not nome_arquivo.strip():
return "Informe o nome do arquivo."
# Gera gráficos para incluir no arquivo
graficos = criar_painel_diagnostico(resultado_modelo)
caminho, msg = exportar_modelo_dai(resultado_modelo, df, estatisticas, nome_arquivo.strip(), graficos)
return msg
def ao_clicar_tabela(df, evt: gr.SelectData):
"""Callback quando clica em linha da tabela."""
if df is None or evt is None:
return "<p>Carregue dados para ver o mapa.</p>"
# Pega o índice da linha clicada
indice = evt.index[0] + 1 # Ajusta para índice baseado em 1
return criar_mapa(df, indice_destacado=indice)
def calcular_metricas_callback(df, coluna_y, colunas_x):
"""Callback para calcular métricas de outliers."""
if df is None or coluna_y is None or not colunas_x:
return None, None
# Calcula métricas usando todos os dados
metricas = calcular_metricas_outliers(df, coluna_y, colunas_x)
if metricas is None:
return None, None
return metricas.reset_index().round(4), metricas
def simular_filtro_callback(metricas_estado, coluna, valor):
"""Callback para simular filtro de outliers (coluna <= -valor OU coluna >= +valor)."""
if metricas_estado is None:
return ""
indices = sugerir_outliers(metricas_estado, coluna, valor)
return ", ".join(map(str, indices))
def limpar_filtro_callback():
"""Callback para limpar filtro de outliers."""
return ""
def aplicar_exclusao_callback(df, indices_texto):
"""Callback para criar DataFrame filtrado (sem outliers)."""
if df is None:
return None
if not indices_texto or not indices_texto.strip():
return arredondar_df(df)
try:
indices_excluir = [int(x.strip()) for x in indices_texto.split(",") if x.strip()]
df_filtrado = df.drop(index=indices_excluir, errors='ignore')
return arredondar_df(df_filtrado)
except:
return arredondar_df(df)
def atualizar_estatisticas_callback(df_filtrado, coluna_y):
"""Callback para recalcular estatísticas com dados filtrados."""
if df_filtrado is None or coluna_y is None:
return None
# Usa índices do df_filtrado
indices_usar = df_filtrado.index.tolist() if df_filtrado is not None else None
estatisticas = calcular_estatisticas_variaveis(df_filtrado, coluna_y)
return estatisticas.round(4)
# Máximo de variáveis X suportadas na interface
MAX_VARS_X = 20
def atualizar_campos_transformacoes(df, colunas_x):
"""
Atualiza visibilidade e valores dos campos de transformação.
Retorna: [row_updates (5)] + [column_updates (20)] + [label_updates (20)] + [dropdown_updates (20)]
"""
n_rows = MAX_VARS_X // 4 # 5 rows (4 cards por linha)
if df is None or not colunas_x:
# Esconde todos
row_updates = [gr.update(visible=False)] * n_rows
column_updates = [gr.update(visible=False)] * MAX_VARS_X
label_updates = [gr.update(value="", visible=False)] * MAX_VARS_X
dropdown_updates = [gr.update(value="(x)", interactive=True, visible=False)] * MAX_VARS_X
return row_updates + column_updates + label_updates + dropdown_updates
dicotomicas = detectar_dicotomicas(df, colunas_x)
# Updates para rows (visibilidade) - 4 por linha
row_updates = []
for i in range(n_rows):
idx_base = i * 4
visivel = idx_base < len(colunas_x)
row_updates.append(gr.update(visible=visivel))
# Updates para columns, labels e dropdowns
column_updates = []
label_updates = []
dropdown_updates = []
for i in range(MAX_VARS_X):
if i < len(colunas_x):
col = colunas_x[i]
eh_dicotomica = col in dicotomicas
column_updates.append(gr.update(visible=True))
label_updates.append(gr.update(value=col, visible=True))
dropdown_updates.append(gr.update(
value="(x)",
interactive=not eh_dicotomica,
visible=True
))
else:
column_updates.append(gr.update(visible=False))
label_updates.append(gr.update(value="", visible=False))
dropdown_updates.append(gr.update(value="(x)", interactive=True, visible=False))
return row_updates + column_updates + label_updates + dropdown_updates
def obter_transformacoes_dos_dropdowns(colunas_x, *valores_dropdowns):
"""Coleta transformações dos valores dos dropdowns."""
transformacoes = {}
for i, col in enumerate(colunas_x):
if i < len(valores_dropdowns) and valores_dropdowns[i]:
transformacoes[col] = valores_dropdowns[i]
else:
transformacoes[col] = "(x)"
return transformacoes
def adotar_sugestao(indice, resultados, colunas_x):
"""Preenche os dropdowns com a sugestão selecionada."""
if not resultados or indice >= len(resultados):
return [gr.update()] * (1 + MAX_VARS_X)
sugestao = resultados[indice]
# Update para transformação de Y
updates = [gr.update(value=sugestao["transformacao_y"])]
# Updates para transformações de X
transf_x = sugestao["transformacoes_x"]
for i in range(MAX_VARS_X):
if i < len(colunas_x):
col = colunas_x[i]
updates.append(gr.update(value=transf_x.get(col, "(x)")))
else:
updates.append(gr.update())
return updates
def atualizar_estatisticas_auto(df_filtrado, coluna_y, colunas_x):
"""Recalcula estatísticas automaticamente e retorna header com timestamp."""
if df_filtrado is None or coluna_y is None:
return None, criar_header_secao(4, "Estatísticas das Variáveis Selecionadas")
# Filtra apenas colunas selecionadas + Y
colunas_usar = [coluna_y] + list(colunas_x) if colunas_x else [coluna_y]
colunas_disponiveis = [c for c in colunas_usar if c in df_filtrado.columns]
if not colunas_disponiveis:
return None, criar_header_secao(4, "Estatísticas das Variáveis Selecionadas")
estatisticas = calcular_estatisticas_variaveis(df_filtrado, coluna_y, colunas=colunas_disponiveis)
timestamp = datetime.now().strftime("%H:%M:%S")
return estatisticas.round(4), criar_header_secao(4, "Estatísticas das Variáveis Selecionadas", timestamp)
def reiniciar_iteracao_callback(df_original, outliers_anteriores, outliers_texto, iteracao_atual, coluna_y, colunas_x):
"""
Combina outliers anteriores com novos, atualiza estado e reinicia análise.
"""
# Parse dos novos outliers
novos_outliers = []
if outliers_texto and outliers_texto.strip():
try:
novos_outliers = [int(x.strip()) for x in outliers_texto.split(",") if x.strip()]
except:
pass
# Combina listas (sem duplicatas)
outliers_combinados = list(set((outliers_anteriores or []) + novos_outliers))
outliers_combinados.sort()
# Incrementa iteração
nova_iteracao = (iteracao_atual or 1) + 1
# Cria DataFrame filtrado
df_filtrado = df_original.copy()
if outliers_combinados:
df_filtrado = df_filtrado.drop(index=outliers_combinados, errors='ignore')
# Recalcula estatísticas
estatisticas, header_estatisticas = atualizar_estatisticas_auto(df_filtrado, coluna_y, colunas_x)
# Atualiza textos
txt_iteracao = f"Iteração: {nova_iteracao}"
txt_n_outliers = f"{len(outliers_combinados)} outliers excluídos"
txt_lista = ", ".join(map(str, outliers_combinados)) if outliers_combinados else "Nenhum"
# Visibilidade do accordion
accordion_visivel = len(outliers_combinados) > 0
# Mapa atualizado
mapa_html = criar_mapa(df_filtrado)
return (
outliers_combinados, # estado_outliers_anteriores
nova_iteracao, # estado_iteracao
df_filtrado, # estado_df_filtrado
arredondar_df(df_filtrado), # tabela_dados
estatisticas, # tabela_estatisticas
header_estatisticas, # header_estatisticas
txt_iteracao, # txt_iteracao_atual
txt_n_outliers, # txt_n_outliers_anteriores
txt_lista, # txt_lista_outliers_anteriores
gr.update(visible=accordion_visivel), # accordion_outliers_anteriores
"", # outliers_texto (limpo)
None, # tabela_metricas (limpa)
None, # estado_metricas (limpo)
criar_header_secao(9, "Analisar Outliers"), # header_outliers
f"Outliers anteriores: {len(outliers_combinados)} | Novos: 0 | Total após iteração: {len(outliers_combinados)}", # txt_resumo_outliers
mapa_html, # mapa_html atualizado
)
def limpar_historico_callback(df_original, coluna_y, colunas_x):
"""
Limpa o histórico de outliers e reinicia do zero.
"""
# Recalcula estatísticas com dados originais
estatisticas, header_estatisticas = atualizar_estatisticas_auto(df_original, coluna_y, colunas_x)
# Mapa com dados originais
mapa_html = criar_mapa(df_original)
return (
[], # estado_outliers_anteriores (vazio)
1, # estado_iteracao (reinicia)
df_original, # estado_df_filtrado (dados originais)
arredondar_df(df_original), # tabela_dados
estatisticas, # tabela_estatisticas
header_estatisticas, # header_estatisticas
"Iteração: 1", # txt_iteracao_atual
"0 outliers excluídos", # txt_n_outliers_anteriores
"", # txt_lista_outliers_anteriores
gr.update(visible=False), # accordion_outliers_anteriores (esconde)
"", # outliers_texto (limpo)
None, # tabela_metricas (limpa)
None, # estado_metricas (limpo)
criar_header_secao(9, "Analisar Outliers"), # header_outliers
"Outliers anteriores: 0 | Novos: 0 | Total após iteração: 0", # txt_resumo_outliers
mapa_html, # mapa_html
)
def atualizar_resumo_outliers(outliers_anteriores, outliers_texto):
"""Atualiza o resumo de outliers quando o usuário edita o campo."""
n_anteriores = len(outliers_anteriores) if outliers_anteriores else 0
novos_outliers = []
if outliers_texto and outliers_texto.strip():
try:
novos_outliers = [int(x.strip()) for x in outliers_texto.split(",") if x.strip()]
except:
pass
n_novos = len(novos_outliers)
n_total = len(set((outliers_anteriores or []) + novos_outliers))
return f"Outliers anteriores: {n_anteriores} | Novos: {n_novos} | Total após iteração: {n_total}"
def aplicar_selecao_callback(df, coluna_y, colunas_x, outliers_anteriores):
"""Aplica seleção de variáveis, atualiza estatísticas e busca transformações automaticamente.
NÃO calcula métricas de outliers aqui - isso é feito ao ajustar o modelo.
Filtra dados excluindo outliers de iterações anteriores.
"""
n_rows = MAX_VARS_X // 4 # Agora são 4 por linha
# Valores padrão quando não há dados
campos_vazios = (
[gr.update(visible=False)] * n_rows +
[gr.update(value="", visible=False)] * MAX_VARS_X +
[gr.update(value="(x)", interactive=True, visible=False)] * MAX_VARS_X
)
# Botões de adotar escondidos por padrão
btn_hidden = [gr.update(visible=False)] * 5
if df is None or coluna_y is None:
return (
None, # estado_df_filtrado
gr.update(value=None, visible=False), # tabela_estatisticas
criar_header_secao(4, "Estatísticas das Variáveis Selecionadas"), # header_estatisticas
criar_header_secao(5, "Transformações Sugeridas"), # header_busca
"<p>Selecione as variáveis primeiro.</p>", # busca_html
[], # estado_resultados_busca
*btn_hidden, # botões adotar
*campos_vazios # campos de transformação
)
if not colunas_x:
return (
df,
gr.update(value=None, visible=False),
criar_header_secao(4, "Estatísticas das Variáveis Selecionadas"),
criar_header_secao(5, "Transformações Sugeridas"),
"<p>Selecione pelo menos uma variável independente.</p>",
[],
*btn_hidden,
*campos_vazios
)
# Filtra dados excluindo outliers de iterações anteriores
df_filtrado = df.copy()
if outliers_anteriores:
df_filtrado = df_filtrado.drop(index=outliers_anteriores, errors='ignore')
# Calcula estatísticas com dados filtrados
estatisticas, header_estatisticas = atualizar_estatisticas_auto(df_filtrado, coluna_y, colunas_x)
# Atualiza campos de transformação
campos_updates = atualizar_campos_transformacoes(df, colunas_x)
# Busca automática de transformações
busca_html, resultados_busca, header_busca_html, *btn_updates = buscar_transformacoes_callback(
df_filtrado, coluna_y, colunas_x
)
return (
df_filtrado, # estado_df_filtrado
gr.update(value=estatisticas, visible=True), # tabela_estatisticas (mostra após carregar)
header_estatisticas, # header_estatisticas (já é HTML com timestamp)
header_busca_html, # header_busca (já é HTML com timestamp)
busca_html, # busca_html
resultados_busca, # estado_resultados_busca
*btn_updates, # botões adotar
*campos_updates # campos de transformação
)
# ============================================================
# INTERFACE GRADIO
# ============================================================
def criar_interface():
"""Cria e retorna a interface Gradio."""
# Script JavaScript para forçar o Light Mode removendo a classe 'dark' do body
js_func = """
function refresh() {
const url = new URL(window.location);
if (url.searchParams.get('__theme') !== 'light') {
url.searchParams.set('__theme', 'light');
window.location.href = url.href;
}
}
"""
# Configura o tema base para combinar com o seu CSS (Laranja)
tema = gr.themes.Default(
primary_hue="orange",
secondary_hue="neutral",
).set(
body_background_fill="#f5f5f5",
block_background_fill="#FFFFFF"
)
# Carrega o CSS aqui (passando para o Blocks é mais robusto no Hugging Face)
css_content = carregar_css()
with gr.Blocks(title="Elaboração de Modelos", theme=tema, css=css_content, js=js_func) as app:
gr.Markdown(TITULO)
# Estados
estado_df = gr.State(None)
estado_modelo = gr.State(None)
estado_estatisticas = gr.State(None)
estado_outliers_anteriores = gr.State([]) # Lista de índices excluídos em iterações anteriores
estado_iteracao = gr.State(1) # Contador de iterações
# ========================================
# SEÇÃO 1: IMPORTAR DADOS
# ========================================
# Estado para armazenar o arquivo temporariamente (usado quando há múltiplas abas)
estado_arquivo_temp = gr.State(None)
gr.HTML(criar_header_secao(1, "Importar Dados"))
with gr.Group(elem_classes="section-container"):
with gr.Row():
upload = gr.File(
label="Carregar arquivo (Excel ou CSV)",
file_types=[".xlsx", ".xls", ".csv"],
scale=2
)
dropdown_aba = gr.Dropdown(
label="Selecionar Aba",
choices=[],
interactive=True,
visible=False,
scale=1
)
dropdown_y = gr.Dropdown(
label="Variável Dependente (y)",
choices=[],
interactive=True,
scale=1
)
status = gr.Textbox(
label="Status",
interactive=False,
scale=2
)
with gr.Accordion("Outliers Excluídos (Iterações Anteriores)", open=True, visible=False) as accordion_outliers_anteriores:
with gr.Row():
txt_iteracao_atual = gr.Textbox(
value="Iteração: 1",
label="",
interactive=False,
scale=1
)
txt_n_outliers_anteriores = gr.Textbox(
value="0 outliers excluídos",
label="",
interactive=False,
scale=2
)
txt_lista_outliers_anteriores = gr.Textbox(
label="Índices excluídos em iterações anteriores",
value="",
interactive=False,
max_lines=3
)
btn_limpar_historico = gr.Button("Limpar Histórico e Reiniciar do Zero", variant="secondary")
# ========================================
# SEÇÃO 2: VISUALIZAR DADOS
# ========================================
gr.HTML(criar_header_secao(2, "Visualizar Dados"))
with gr.Group(elem_classes="section-container"):
with gr.Row():
with gr.Column(scale=1):
with gr.Accordion("Dados de Mercado", open=True):
tabela_dados = gr.Dataframe(
label="",
interactive=False,
max_height=400
)
with gr.Column(scale=1):
with gr.Accordion("Mapa", open=True):
mapa_html = gr.HTML(
value="<p>Carregue dados para ver o mapa.</p>",
label=""
)
# ========================================
# SEÇÃO 3: SELECIONAR VARIÁVEIS
# ========================================
gr.HTML(criar_header_secao(3, "Selecionar Variáveis"))
with gr.Group(elem_classes="section-container"):
with gr.Row(elem_classes="checkbox-selecionar-todos"):
checkbox_selecionar_todos = gr.Checkbox(
label="Marcar ou desmarcar todas as variáveis",
value=True,
interactive=True
)
with gr.Row():
checkboxes_x = gr.CheckboxGroup(
label="Variáveis Independentes (X)",
choices=[],
interactive=True
)
with gr.Row():
btn_aplicar_selecao = gr.Button("Aplicar Seleção", variant="primary", scale=1)
# Estados para outliers (usados na seção de Análise de Outliers após o modelo)
estado_metricas = gr.State(None)
estado_df_filtrado = gr.State(None)
# ========================================
# SEÇÃO 4: ESTATÍSTICAS DAS VARIÁVEIS SELECIONADAS
# ========================================
header_estatisticas = gr.HTML(criar_header_secao(4, "Estatísticas das Variáveis Selecionadas"))
with gr.Group(elem_classes="section-container"):
tabela_estatisticas = gr.Dataframe(
label="",
interactive=False,
value=None,
visible=False
)
# ========================================
# SEÇÃO 5: TRANSFORMAÇÕES SUGERIDAS
# ========================================
# Estado para armazenar resultados da busca
estado_resultados_busca = gr.State([])
header_busca = gr.HTML(criar_header_secao(5, "Transformações Sugeridas"))
with gr.Group(elem_classes="section-container") as grupo_busca:
busca_html = gr.HTML(
value="<p>Clique em 'Aplicar Seleção' para buscar as melhores combinações de transformações.</p>"
)
# ========================================
# SEÇÃO 6: APLICAR TRANSFORMAÇÕES E AJUSTAR MODELO
# ========================================
gr.HTML(criar_header_secao(6, "Aplicar Transformações e Ajustar Modelo"))
with gr.Group(elem_classes="section-container"):
# Botões "Adotar" (5 fixos, visibilidade controlada)
gr.Markdown("*Adote uma das sugestões da busca ou configure manualmente:*")
with gr.Row():
btn_adotar_1 = gr.Button("Adotar #1", visible=False, elem_classes="btn-adotar", size="sm")
btn_adotar_2 = gr.Button("Adotar #2", visible=False, elem_classes="btn-adotar", size="sm")
btn_adotar_3 = gr.Button("Adotar #3", visible=False, elem_classes="btn-adotar", size="sm")
btn_adotar_4 = gr.Button("Adotar #4", visible=False, elem_classes="btn-adotar", size="sm")
btn_adotar_5 = gr.Button("Adotar #5", visible=False, elem_classes="btn-adotar", size="sm")
with gr.Row():
transformacao_y = gr.Dropdown(
label="Transformação de y",
choices=TRANSFORMACOES,
value="(x)",
interactive=True,
scale=1
)
gr.Markdown("*Selecione a transformação para cada variável (dicotômicas ficam fixas em (x))*")
# Criar componentes fixos: 20 pares (Label + Dropdown), 4 por linha
transf_x_rows = []
transf_x_columns = []
transf_x_labels = []
transf_x_dropdowns = []
for i in range(MAX_VARS_X // 4): # 5 rows (20 / 4)
row = gr.Row(visible=False)
with row:
for j in range(4):
col = gr.Column(scale=1, min_width=180, elem_classes="transf-card", visible=False)
with col:
label = gr.Textbox(
value="",
label="Variável",
interactive=False,
visible=False,
max_lines=1
)
dropdown = gr.Dropdown(
choices=TRANSFORMACOES,
value="(x)",
label="Transformação",
interactive=True,
visible=False
)
transf_x_columns.append(col)
transf_x_labels.append(label)
transf_x_dropdowns.append(dropdown)
transf_x_rows.append(row)
with gr.Row():
btn_ajustar = gr.Button("Aplicar transformações e ajustar modelo", variant="primary", scale=1)
# ========================================
# SEÇÃO 7: DIAGNÓSTICO DE MODELO
# ========================================
header_modelo = gr.HTML(criar_header_secao(7, "Diagnóstico de Modelo"))
with gr.Group(elem_classes="section-container"):
diagnosticos_html = gr.HTML(
value="<p>Ajuste o modelo para ver os diagnósticos.</p>"
)
with gr.Row():
with gr.Accordion("Tabela de Coeficientes", open=True):
tabela_coef = gr.Dataframe(
label="",
interactive=False,
max_height=300
)
with gr.Accordion("Valores Observados vs Calculados", open=True):
tabela_obs_calc = gr.Dataframe(
label="",
interactive=False,
max_height=400
)
# ========================================
# SEÇÃO 8: GRÁFICOS DE DIAGNÓSTICO
# ========================================
header_graficos = gr.HTML(criar_header_secao(8, "Gráficos de Diagnóstico"))
with gr.Group(elem_classes="section-container"):
with gr.Row():
plot_obs_calc = gr.Plot(label="Observados vs Calculados")
plot_residuos = gr.Plot(label="Resíduos")
with gr.Row():
plot_hist = gr.Plot(label="Histograma dos Resíduos")
plot_cook = gr.Plot(label="Distância de Cook")
with gr.Row():
plot_corr = gr.Plot(label="Matriz de Correlação")
# ========================================
# SEÇÃO 9: ANALISAR OUTLIERS
# ========================================
header_outliers = gr.HTML(criar_header_secao(9, "Analisar Outliers"))
with gr.Group(elem_classes="section-container"):
gr.Markdown("*Métricas calculadas com base no modelo ajustado (resíduos com transformações aplicadas)*")
tabela_metricas = gr.Dataframe(
label="Métricas para identificação de outliers",
interactive=False,
max_height=300
)
gr.Markdown("### Filtrar Outliers")
gr.Markdown("*Filtra valores onde: coluna ≤ -X OU coluna ≥ +X*")
with gr.Row():
col_filtro = gr.Dropdown(
label="Coluna",
choices=["Resíduo Pad.", "Resíduo Stud.", "Cook"],
value="Resíduo Pad.",
scale=1
)
valor_filtro = gr.Number(label="Valor X", value=2.0, scale=1)
btn_simular = gr.Button("Simular Filtro", variant="secondary", scale=1)
btn_limpar = gr.Button("Limpar Filtro", variant="secondary", scale=1)
with gr.Row():
outliers_texto = gr.Textbox(
label="Índices a Excluir nesta Iteração",
placeholder="Ex: 5, 12, 23",
scale=3
)
with gr.Row():
txt_resumo_outliers = gr.Textbox(
label="Resumo",
value="Outliers anteriores: 0 | Novos: 0 | Total após iteração: 0",
interactive=False
)
with gr.Row():
btn_reiniciar_iteracao = gr.Button(
"Aplicar Exclusões e Reiniciar Iteração",
variant="primary",
scale=2
)
btn_download_base = gr.Button("Baixar Base Tratada (CSV)", variant="secondary", scale=1)
download_base_file = gr.File(label="", visible=False)
# ========================================
# SEÇÃO 10: EXPORTAR MODELO
# ========================================
gr.HTML(criar_header_secao(10, "Exportar Modelo"))
with gr.Group(elem_classes="section-container"):
with gr.Row():
nome_arquivo = gr.Textbox(
label="Nome do arquivo",
placeholder="modelo_01",
scale=2
)
btn_exportar = gr.Button("Exportar .dai", variant="primary", scale=1)
status_exportar = gr.Textbox(
label="Status da exportação",
interactive=False
)
# ========================================
# EVENTOS
# ========================================
# Upload de arquivo
upload.upload(
ao_carregar_arquivo,
inputs=[upload],
outputs=[
estado_df,
status,
dropdown_aba,
dropdown_y,
tabela_dados,
tabela_estatisticas,
checkboxes_x,
mapa_html,
estado_df_filtrado,
header_estatisticas,
estado_outliers_anteriores,
estado_iteracao,
accordion_outliers_anteriores,
txt_iteracao_atual,
txt_n_outliers_anteriores,
txt_lista_outliers_anteriores,
estado_arquivo_temp
]
)
# Seleção de aba (quando há múltiplas abas no Excel)
dropdown_aba.change(
ao_selecionar_aba,
inputs=[estado_arquivo_temp, dropdown_aba],
outputs=[
estado_df,
status,
dropdown_aba,
dropdown_y,
tabela_dados,
tabela_estatisticas,
checkboxes_x,
mapa_html,
estado_df_filtrado,
header_estatisticas,
estado_outliers_anteriores,
estado_iteracao,
accordion_outliers_anteriores,
txt_iteracao_atual,
txt_n_outliers_anteriores,
txt_lista_outliers_anteriores,
estado_arquivo_temp
]
)
# Mudança de y
dropdown_y.change(
ao_mudar_y,
inputs=[estado_df, dropdown_y],
outputs=[tabela_estatisticas, checkboxes_x]
)
# Selecionar/Desselecionar todos via checkbox
def toggle_selecionar_todos(selecionar, df, coluna_y):
"""Marca ou desmarca todas as variáveis independentes."""
if selecionar:
# Marcar todos
colunas_x = [col for col in obter_colunas_numericas(df) if col != coluna_y] if df is not None and coluna_y else []
return gr.update(value=colunas_x)
else:
# Desmarcar todos
return gr.update(value=[])
checkbox_selecionar_todos.change(
toggle_selecionar_todos,
inputs=[checkbox_selecionar_todos, estado_df, dropdown_y],
outputs=[checkboxes_x]
)
# Clique na tabela -> atualiza mapa
tabela_dados.select(
ao_clicar_tabela,
inputs=[estado_df],
outputs=[mapa_html]
)
# Seleção de X -> atualiza campos de transformação (preview)
checkboxes_x.change(
atualizar_campos_transformacoes,
inputs=[estado_df, checkboxes_x],
outputs=transf_x_rows + transf_x_columns + transf_x_labels + transf_x_dropdowns
)
# Aplicar seleção de variáveis -> atualiza estatísticas, busca transformações e campos
btn_aplicar_selecao.click(
aplicar_selecao_callback,
inputs=[estado_df, dropdown_y, checkboxes_x, estado_outliers_anteriores],
outputs=[
estado_df_filtrado,
tabela_estatisticas,
header_estatisticas,
header_busca,
busca_html,
estado_resultados_busca,
btn_adotar_1, btn_adotar_2, btn_adotar_3, btn_adotar_4, btn_adotar_5
] + transf_x_rows + transf_x_columns + transf_x_labels + transf_x_dropdowns
)
# Simular filtro de outliers
btn_simular.click(
simular_filtro_callback,
inputs=[estado_metricas, col_filtro, valor_filtro],
outputs=[outliers_texto]
)
# Limpar filtro
btn_limpar.click(
limpar_filtro_callback,
inputs=[],
outputs=[outliers_texto]
)
# Atualizar resumo de outliers quando usuário edita o campo
outliers_texto.change(
atualizar_resumo_outliers,
inputs=[estado_outliers_anteriores, outliers_texto],
outputs=[txt_resumo_outliers]
)
# Simular filtro também atualiza o resumo
btn_simular.click(
atualizar_resumo_outliers,
inputs=[estado_outliers_anteriores, outliers_texto],
outputs=[txt_resumo_outliers]
)
# Download da base tratada (CSV)
def download_base_callback(df_filtrado):
"""Callback para download da base tratada."""
if df_filtrado is None:
return None
caminho = exportar_base_csv(df_filtrado)
return caminho
btn_download_base.click(
download_base_callback,
inputs=[estado_df_filtrado],
outputs=[download_base_file]
)
# Botões "Adotar Sugestão"
btn_adotar_1.click(
lambda res, cols_x: adotar_sugestao(0, res, cols_x),
inputs=[estado_resultados_busca, checkboxes_x],
outputs=[transformacao_y] + transf_x_dropdowns
)
btn_adotar_2.click(
lambda res, cols_x: adotar_sugestao(1, res, cols_x),
inputs=[estado_resultados_busca, checkboxes_x],
outputs=[transformacao_y] + transf_x_dropdowns
)
btn_adotar_3.click(
lambda res, cols_x: adotar_sugestao(2, res, cols_x),
inputs=[estado_resultados_busca, checkboxes_x],
outputs=[transformacao_y] + transf_x_dropdowns
)
btn_adotar_4.click(
lambda res, cols_x: adotar_sugestao(3, res, cols_x),
inputs=[estado_resultados_busca, checkboxes_x],
outputs=[transformacao_y] + transf_x_dropdowns
)
btn_adotar_5.click(
lambda res, cols_x: adotar_sugestao(4, res, cols_x),
inputs=[estado_resultados_busca, checkboxes_x],
outputs=[transformacao_y] + transf_x_dropdowns
)
# Ajustar modelo (usa dados filtrados se disponíveis) e calcula métricas de outliers
btn_ajustar.click(
lambda df_filt, df_orig, col_y, cols_x, transf_y, outliers_ant, *dd_vals: ajustar_modelo_callback(
df_filt if df_filt is not None else df_orig, col_y, cols_x, transf_y, outliers_ant, *dd_vals
),
inputs=[
estado_df_filtrado, estado_df, dropdown_y, checkboxes_x,
transformacao_y, estado_outliers_anteriores
] + transf_x_dropdowns,
outputs=[
estado_modelo,
header_modelo,
diagnosticos_html,
tabela_coef,
tabela_obs_calc,
plot_obs_calc,
plot_residuos,
plot_hist,
plot_cook,
plot_corr,
header_graficos,
tabela_metricas,
estado_metricas,
header_outliers,
txt_resumo_outliers
]
)
# Reiniciar iteração (combina outliers e recomeça) e depois ajusta o modelo
btn_reiniciar_iteracao.click(
reiniciar_iteracao_callback,
inputs=[
estado_df, estado_outliers_anteriores, outliers_texto,
estado_iteracao, dropdown_y, checkboxes_x
],
outputs=[
estado_outliers_anteriores,
estado_iteracao,
estado_df_filtrado,
tabela_dados,
tabela_estatisticas,
header_estatisticas,
txt_iteracao_atual,
txt_n_outliers_anteriores,
txt_lista_outliers_anteriores,
accordion_outliers_anteriores,
outliers_texto,
tabela_metricas,
estado_metricas,
header_outliers,
txt_resumo_outliers,
mapa_html
]
).then(
# Após reiniciar, ajusta modelo automaticamente para calcular métricas de outliers
lambda df_filt, df_orig, col_y, cols_x, transf_y, outliers_ant, *dd_vals: ajustar_modelo_callback(
df_filt if df_filt is not None else df_orig, col_y, cols_x, transf_y, outliers_ant, *dd_vals
),
inputs=[
estado_df_filtrado, estado_df, dropdown_y, checkboxes_x,
transformacao_y, estado_outliers_anteriores
] + transf_x_dropdowns,
outputs=[
estado_modelo,
header_modelo,
diagnosticos_html,
tabela_coef,
tabela_obs_calc,
plot_obs_calc,
plot_residuos,
plot_hist,
plot_cook,
plot_corr,
header_graficos,
tabela_metricas,
estado_metricas,
header_outliers,
txt_resumo_outliers
]
)
# Limpar histórico de outliers
btn_limpar_historico.click(
limpar_historico_callback,
inputs=[estado_df, dropdown_y, checkboxes_x],
outputs=[
estado_outliers_anteriores,
estado_iteracao,
estado_df_filtrado,
tabela_dados,
tabela_estatisticas,
header_estatisticas,
txt_iteracao_atual,
txt_n_outliers_anteriores,
txt_lista_outliers_anteriores,
accordion_outliers_anteriores,
outliers_texto,
tabela_metricas,
estado_metricas,
header_outliers,
txt_resumo_outliers,
mapa_html
]
)
# Exportar (usa dados filtrados se disponíveis)
btn_exportar.click(
lambda res_modelo, df_filt, df_orig, stats, nome: exportar_modelo_callback(
res_modelo, df_filt if df_filt is not None else df_orig, stats, nome
),
inputs=[estado_modelo, estado_df_filtrado, estado_df, tabela_estatisticas, nome_arquivo],
outputs=[status_exportar]
)
return app
# ============================================================
# MAIN
# ============================================================
if __name__ == "__main__":
css = carregar_css()
app = criar_interface()
app.launch()