Alocação Proporcional de Jefferson

Autor

Carlos Gomes

Data de Publicação

quinta-feira, outubro 31, 2024

O método de Jefferson

\(\text{\Large O}\) Método de Jefferson foi criado em 1784 por Thomas Jefferson (um dos pais fundadores dos EUA e seu terceiro presidente) e foi utilizado para distribuir assentos na Câmara dos Representantes dos EUA de 1792 a 1842, proporcionalmente à população de cada estado. Mais tarde, tornou-se a base para o Método de Hondt, muito popular na Europa. Em vez de procurar um divisor ajustado (como Jefferson fazia), Hondt simplesmente apresentou uma tabela de divisões sucessivas \({(1,\ 2,\ 3,\ 4,\ \cdots)}\). Esta apresentação mais intuitiva tornou o método mais acessível para uso eleitoral.
O método de Jefferson veio antes do método de Hamilton. No entanto, este foi adoptado mais tarde (1794) e chegou a ser usado nos EUA até 1842, altura em foi substituído por outros, mas não mais pelo de Jefferson! (O método de Hamilton gerava paradoxos matemáticos estando, por isso, sujeito a distorções.)
Thomas Jefferson quis manter o espírito proporcional do problema e entendia que se deveria ajustar o divisor padrão para que a soma das quotas fosse igual ao número de cadeiras a serem alocadas. Veja abaixo uma exemplificação do método.

Madeira 2023

Copie os dados abaixo (canto superior direito da caixa) e cole-os na caixa de entrada da aplicação (Partidos e votos). Modifique o divisor padrão e recalcule até que a distribuição dos lugares esteja concluída.

PSD,49104
PS,28981
JPP,22959
CH,12562
CDS-PP,5374
IL,3481
PAN,2531
PCP,2217
BE,1912
PTP,1222
Livre,905
ADN,772
MPT,577
RIR,527
#| '!! shinylive warning !!': |
#|   shinylive does not work in self-contained HTML documents.
#|   Please set `embed-resources: false` in your metadata.
#| standalone: true
#| viewerHeight: 680 

from shiny import App, reactive, render, ui
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import matplotlib.patches as patches
import matplotlib.colors as mcolors

def calcular_alocacoes(votos, total_cadeiras, divisor):
    quotas_padrao = votos / divisor
    quotas_inferiores = np.floor(quotas_padrao)
    return quotas_padrao, quotas_inferiores

app_ui = ui.page_fluid(
    ui.row(
        ui.column(2,
            ui.card(
                ui.card_header("Dados"),
                ui.input_numeric("total_cadeiras", "Número de lugares:", value=75, min=1),
                ui.input_text_area("partidos_votos", "Partidos e votos (formato: Partido,Votos):", 
                                   rows=5,
                                   placeholder="Ex:\nPartido A,4404\nPartido B,1672\nPartido C,2087"),
                ui.input_text("divisor_manual", "Divisor modificado:", value=""),
                ui.input_action_button("calcular", "Calcular Alocação", class_="btn-primary"),
            ),
        ),
        ui.column(6,
            ui.card(
                ui.card_header("Resultados"),
                ui.output_table("resultados"),
                ui.output_text("total_alocado"),
                ui.output_text("divisor_padrao"),
            ),
        ),
        ui.column(4,
            ui.card(
                ui.card_header("Lugares Alocados"),
                ui.output_plot("grafico_cadeiras"),
            ),
        ),
    ),
    title="Alocação de Cadeiras usando o Método Jefferson"
)

def server(input, output, session):
    dados_base_rv = reactive.value(None)
    resultados_rv = reactive.value(None)
    divisor_padrao_rv = reactive.value(None)
    cores_partidos_rv = reactive.value(None)
    divisor_manual_rv = reactive.value(None)
    primeira_execucao_rv = reactive.value(True)

    @reactive.effect
    @reactive.event(input.calcular)
    def _():
        # Process the initial data
        linhas = input.partidos_votos().strip().split("\n")
        dados = [linha.split(",") for linha in linhas]

        if len(dados) < 2 or any(len(linha) != 2 for linha in dados):
            ui.notification_show("Por favor, insira pelo menos dois partidos no formato correto: Partido,Votos", type="error")
            return None

        try:
            partidos = [linha[0].strip() for linha in dados]
            votos = [float(linha[1].strip()) for linha in dados]
        except ValueError:
            ui.notification_show("Todos os votos devem ser números válidos.", type="error")
            return None

        if any(np.isnan(voto) for voto in votos):
            ui.notification_show("Todos os votos devem ser números válidos.", type="error")
            return None

        # Calculate and set the initial divisor
        total_votos = sum(votos)
        divisor_inicial = total_votos / input.total_cadeiras()
        divisor_padrao_rv.set(divisor_inicial)

        # Na primeira execução, use o divisor padrão
        if primeira_execucao_rv.get():
            ui.update_text("divisor_manual", value=str(round(divisor_inicial, 3)))
            divisor_manual_rv.set(divisor_inicial)
            primeira_execucao_rv.set(False)
        else:
            try:
                divisor_manual = float(input.divisor_manual().replace(",", "."))
                if divisor_manual <= 0:
                    raise ValueError("Divisor deve ser positivo")
                divisor_manual_rv.set(divisor_manual)
            except ValueError:
                ui.notification_show("O divisor manual deve ser um número válido e positivo.", type="error")
                return None

        dados_base_rv.set((partidos, votos))
        
        # Set up colors
        cores = plt.cm.get_cmap('tab20')(np.linspace(0, 1, len(partidos)))
        cores_hex = [mcolors.rgb2hex(cor[:3]) for cor in cores]
        cores_partidos_rv.set(dict(zip(partidos, cores_hex)))

    @reactive.effect
    def check_total_seats():
        df = calcular_alocacao()
        if df is not None:
            total_alocado = int(df["QIM"].iloc[:-1].sum())
            if total_alocado == 47:
                ui.notification_show(
                    f"Divisor encontrado! Total de cadeiras alocadas: {total_alocado}",
                    duration=5,
                    type="message"
                )
            else:
                ui.notification_show(
                    f"O número de lugares ainda não é 47 (atual: {total_alocado})",
                    duration=5,
                    type="error"
                )

    @reactive.calc
    def calcular_alocacao():
        dados_base = dados_base_rv.get()
        if dados_base is None:
            return None

        partidos, votos = dados_base
        votos_array = np.array(votos)
        
        divisor_padrao = divisor_padrao_rv.get()
        divisor_manual = divisor_manual_rv.get()
        if divisor_padrao is None or divisor_manual is None:
            return None

        alocacoes_originais = calcular_alocacoes(votos_array, input.total_cadeiras(), divisor_padrao)
        alocacoes_modificadas = calcular_alocacoes(votos_array, input.total_cadeiras(), divisor_manual)

        df = pd.DataFrame({
            "Partidos": partidos + ["Total"],
            "Votos": votos + [sum(votos)],
            "QP": np.round(np.append(alocacoes_originais[0], sum(alocacoes_originais[0])), 2),
            "QI": np.append(alocacoes_originais[1], sum(alocacoes_originais[1])),
            "QPM": np.round(np.append(alocacoes_modificadas[0], sum(alocacoes_modificadas[0])), 2),
            "QIM": np.append(alocacoes_modificadas[1], sum(alocacoes_modificadas[1]))
        })

        return df

    @output
    @render.table
    def resultados():
        df = calcular_alocacao()
        cores_partidos = cores_partidos_rv.get()
        if df is None or cores_partidos is None:
            return None
        
        def estilo_linha(row):
            estilo = ['text-align: left'] * len(row)
            if row.name == df.index[-1]:
                estilo = ['font-weight: bold; text-align: left'] * len(row)
            partido = row['Partidos']
            cor = cores_partidos.get(partido, '')
            return [f'background-color: {cor}; color: black; {s}' for s in estilo]

        return (df.style
                .apply(estilo_linha, axis=1)
                .format(precision=2)
                .set_properties(**{'font-size': '14px'})
                .hide(axis="index"))

    @output
    @render.text
    def total_alocado():
        df = calcular_alocacao()
        if df is None:
            return ""
        total_alocado = int(df["QIM"].iloc[:-1].sum())
        return f"Cadeiras alocadas: {total_alocado}"

    @output
    @render.text
    def divisor_padrao():
        div_padrao = divisor_padrao_rv.get()
        if div_padrao is None:
            return ""
        return f"Divisor padrão original: {round(div_padrao, 3)}"

    @output
    @render.plot
    def grafico_cadeiras():
        df = calcular_alocacao()
        cores_partidos = cores_partidos_rv.get()
        if df is None or cores_partidos is None:
            return None

        cadeiras_alocadas = df["QIM"].iloc[:-1].astype(int)
        partidos = df["Partidos"].iloc[:-1]

        total_cadeiras = input.total_cadeiras()
        num_rows = int(np.ceil(np.sqrt(total_cadeiras)))
        num_cols = int(np.ceil(total_cadeiras / num_rows))

        fig, ax = plt.subplots(figsize=(8, 4))
        ax.set_xlim(0, num_cols)
        ax.set_ylim(0, num_rows)

        cadeira_atual = 0

        for partido, cadeiras in zip(partidos, cadeiras_alocadas):
            cor = cores_partidos[partido]
            for _ in range(cadeiras):
                row = cadeira_atual // num_cols
                col = cadeira_atual % num_cols
                rect = patches.Rectangle((col, num_rows - 1 - row), 0.9, 0.9,
                                         facecolor=cor, edgecolor='white')
                ax.add_patch(rect)
                ax.text(col + 0.45, num_rows - 1 - row + 0.45, partido, 
                        ha='center', va='center', fontsize=8, wrap=True)
                cadeira_atual += 1

        for _ in range(cadeira_atual, total_cadeiras):
            row = cadeira_atual // num_cols
            col = cadeira_atual % num_cols
            rect = patches.Rectangle((col, num_rows - 1 - row), 0.9, 0.9,
                                     facecolor='white', edgecolor='black')
            ax.add_patch(rect)
            cadeira_atual += 1

        ax.set_xticks([])
        ax.set_yticks([])

        handles = [patches.Patch(color=cores_partidos[partido], label=partido) 
                   for partido in partidos]
        ax.legend(handles=handles, title="Partidos", loc='upper center', 
                  bbox_to_anchor=(0.5, -0.05), ncol=7, fontsize='x-small')

        plt.tight_layout()
        return fig

app = App(app_ui, server)