05 — Auditoria do TEMPOLIMITE nas Integrações Oficiais¶

Cada par da matriz tem um TEMPOLIMITE (min). Passageiros que excedem pagam tarifa dupla mesmo tendo integração oficial ("falsos COM"). Responde: quantas baldeações COM integração violam o limite? Quais pares têm maior taxa (candidatos a ampliar o tempo)? O limite é realista frente à espera observada?

In [1]:
import json
from pathlib import Path
import numpy as np, pandas as pd
import matplotlib.pyplot as plt
from transbordo_utils import (GAP_VALIDACAO_MAX_KM, adicionar_plausibilidade_fisica,
                              resolver_cache, carregar_pares_oficiais)

PARQUET_TB  = resolver_cache("transbordos_2026-01-01_2026-01-31.parquet")
MATRIZ_JSON = resolver_cache("matriz_integracao.json")
OUT_CSV     = str(Path(PARQUET_TB).parent / "violacoes_tempolimite.csv")
print("parquet:", PARQUET_TB)
parquet: cache_urbs/transbordos_2026-01-01_2026-01-31.parquet

1. Carregar dados e matriz¶

In [2]:
df = adicionar_plausibilidade_fisica(pd.read_parquet(PARQUET_TB), GAP_VALIDACAO_MAX_KM)
print(f"transbordos totais : {len(df):,}")

# Apenas baldeações fisicamente plausíveis (linhas diferentes e gap <= 1 km)
df_bald_temporal = df[df["CODLINHA_ORIGEM"] != df["CODLINHA_DESTINO"]].copy()
df_bald = df[df["baldeacao_real"]].copy()
print(f"baldeações temporais (linhas diferentes): {len(df_bald_temporal):,}")
print(f"baldeações fisicamente plausíveis (<= {GAP_VALIDACAO_MAX_KM:.1f} km): {len(df_bald):,}")
transbordos totais : 23,562
baldeações temporais (linhas diferentes): 8,581
baldeações fisicamente plausíveis (<= 1.0 km): 5,084
In [3]:
# Matriz oficial: pares + TEMPOLIMITE (ambos sentidos)
_, tl = carregar_pares_oficiais(MATRIZ_JSON, com_tempolimite=True)
df_matriz = (
    pd.DataFrame([{"orig": o, "dest": d, "tempolimite_min": m} for (o, d), m in tl.items()])
    .drop_duplicates(subset=["orig", "dest"])
)
print(f"pares na matriz (bidirecional): {len(df_matriz):,}")
print("valores únicos de TEMPOLIMITE (min):", sorted(df_matriz["tempolimite_min"].unique()))
pares na matriz (bidirecional): 2,589
valores únicos de TEMPOLIMITE (min): [np.int64(5), np.int64(10), np.int64(30), np.int64(60), np.int64(90), np.int64(120), np.int64(240), np.int64(1440)]

2. Join: baldeações COM integração × TEMPOLIMITE¶

In [4]:
df_com = df_bald.merge(
    df_matriz,
    left_on=["CODLINHA_ORIGEM", "CODLINHA_DESTINO"],
    right_on=["orig", "dest"],
    how="inner"   # apenas pares que estão NA matriz
).drop(columns=["orig", "dest"])

print(f"baldeações COM integração oficial: {len(df_com):,}")

df_com["violou_limite"] = df_com["espera_min"] > df_com["tempolimite_min"]

n_viola = df_com["violou_limite"].sum()
pct = 100 * n_viola / len(df_com)
print(f"\nViolações do TEMPOLIMITE: {n_viola:,} de {len(df_com):,} ({pct:.1f}%)")
baldeações COM integração oficial: 1,189

Violações do TEMPOLIMITE: 0 de 1,189 (0.0%)

3. Ranking de pares com maior taxa de violação¶

In [5]:
por_par = (
    df_com
    .groupby(["CODLINHA_ORIGEM", "NOMELINHA_ORIGEM",
              "CODLINHA_DESTINO", "NOMELINHA_DESTINO",
              "tempolimite_min"])
    .agg(
        total       = ("espera_min", "count"),
        violacoes   = ("violou_limite", "sum"),
        espera_p50  = ("espera_min", "median"),
        espera_p75  = ("espera_min", lambda x: x.quantile(0.75)),
        espera_p95  = ("espera_min", lambda x: x.quantile(0.95)),
    )
    .reset_index()
)
por_par["taxa_violacao_pct"] = 100 * por_par["violacoes"] / por_par["total"]
por_par["par_nome"] = (
    por_par["NOMELINHA_ORIGEM"].str[:20] + " → " +
    por_par["NOMELINHA_DESTINO"].str[:20]
)

# Ordenar por taxa de violação (entre pares com volume mínimo)
por_par_filtrado = por_par[por_par["total"] >= 5].sort_values(
    "taxa_violacao_pct", ascending=False
)

por_par_filtrado.to_csv(OUT_CSV, index=False)
print(f"Salvo → {OUT_CSV}")

print("\n── TOP 20 pares com maior taxa de violação ──")
por_par_filtrado.head(20)[[
    "par_nome", "tempolimite_min", "total", "violacoes",
    "taxa_violacao_pct", "espera_p50", "espera_p95"
]].to_string(index=False, float_format="{:.1f}".format)
Salvo → cache_urbs/violacoes_tempolimite.csv

── TOP 20 pares com maior taxa de violação ──
Out[5]:
'                               par_nome  tempolimite_min  total  violacoes  taxa_violacao_pct  espera_p50  espera_p95\n      INTERBAIRROS I H → RAPOSO TAVARES              120      5          0                0.0        20.8        40.9\n          INTERBAIRROS I H → BRACATINGA              120     14          0                0.0        29.5        42.9\n              INTERBAIRROS I H → SAVÓIA              120     18          0                0.0        27.1        41.6\n     INTERBAIRROS I H → STA. FELICIDADE              120     16          0                0.0        27.1        45.5\n          INTERBAIRROS I H → JÚLIO GRAF              120      5          0                0.0        16.6        35.5\n          INTERBAIRROS I H → JD. ITÁLIA              120      8          0                0.0        21.4        36.2\n             INTERBAIRROS I A → V. NORI               60      5          0                0.0        39.0        48.2\n          INTERBAIRROS I A → BRACATINGA              120     10          0                0.0        29.1        39.6\n     INTERBAIRROS I A → CABRAL / PORTÃO              120      5          0                0.0        41.8        57.1\n              INTERBAIRR II H → V. NORI               60     18          0                0.0        31.2        52.6\n           INTERBAIRR II H → BRACATINGA               60     23          0                0.0        33.9        50.8\n             INTERB II ANTI H → V. NORI               60      8          0                0.0        41.2        50.1\n          INTERB II ANTI H → BRACATINGA               60     26          0                0.0        29.1        49.3\nINTERB II ANTI H → STA. FELICIDADE / ST               60      7          0                0.0        26.0        51.3\n            INTERBAIRROS IV → V. SANDRA              120     32          0                0.0        19.6        43.3\n             V. NORI → INTERBAIRROS I A               60     17          0                0.0        21.6        30.6\n              V. NORI → INTERBAIRR II H               60     36          0                0.0        15.3        25.4\n             V. NORI → INTERB II ANTI H               60     35          0                0.0        16.8        28.8\n           JD. KOSMOS → INTERBAIRR II H               60     16          0                0.0        17.1        22.6\n          JD. KOSMOS → INTERB II ANTI H               60     21          0                0.0        20.0        25.4'

4. Histograma: espera_min vs. TEMPOLIMITE¶

In [6]:
# Agrupamos por TEMPOLIMITE para ver o encaixe com a distribuição
limites_comuns = sorted(df_com["tempolimite_min"].value_counts().nlargest(4).index)

fig, axes = plt.subplots(1, len(limites_comuns), figsize=(5 * len(limites_comuns), 4),
                          sharey=False)
if len(limites_comuns) == 1:
    axes = [axes]

for ax, tl in zip(axes, limites_comuns):
    sub = df_com[df_com["tempolimite_min"] == tl]["espera_min"]
    pct_viola = 100 * (sub > tl).mean()
    ax.hist(sub, bins=40, color="#2980b9", alpha=0.75, edgecolor="white")
    ax.axvline(tl, color="#c0392b", linewidth=2, linestyle="--",
               label=f"Limite = {tl} min")
    ax.set_title(f"TEMPOLIMITE = {tl} min\n{pct_viola:.0f}% ultrapassam", fontsize=10)
    ax.set_xlabel("Espera (min)")
    ax.set_ylabel("Ocorrências" if ax == axes[0] else "")
    ax.legend(fontsize=8)

fig.suptitle("Distribuição de espera_min por TEMPOLIMITE (pares COM integração oficial)",
             fontsize=12, y=1.02)
plt.tight_layout()
plt.savefig("hist_tempolimite.png", dpi=150, bbox_inches="tight")
plt.show()
print("Salvo: hist_tempolimite.png")
No description has been provided for this image
Salvo: hist_tempolimite.png

5. Scatter: TEMPOLIMITE × mediana de espera por par¶

In [7]:
fig, ax = plt.subplots(figsize=(8, 6))

sc = ax.scatter(
    por_par["tempolimite_min"],
    por_par["espera_p50"],
    c=por_par["taxa_violacao_pct"],
    cmap="RdYlGn_r",
    s=por_par["total"] * 3,
    alpha=0.7,
    edgecolors="gray",
    linewidth=0.4
)

# Linha diagonal: espera mediana == limite (pontos acima = limite subestimado)
lim_max = max(por_par["tempolimite_min"].max(), por_par["espera_p50"].max()) + 5
ax.plot([0, lim_max], [0, lim_max], "k--", linewidth=1, alpha=0.4, label="espera = limite")

plt.colorbar(sc, ax=ax, label="Taxa de violação (%)")
ax.set_xlabel("TEMPOLIMITE oficial (min)")
ax.set_ylabel("Espera mediana observada (min)")
ax.set_title("Pares COM integração: limite oficial vs. espera real")
ax.legend()

# Anotar os pares mais críticos (acima da diagonal + alta taxa)
criticos = por_par_filtrado[por_par_filtrado["taxa_violacao_pct"] > 30].head(5)
for _, row in criticos.iterrows():
    ax.annotate(
        row["par_nome"][:25],
        (row["tempolimite_min"], row["espera_p50"]),
        fontsize=6, alpha=0.8,
        xytext=(5, 3), textcoords="offset points"
    )

plt.tight_layout()
plt.savefig("scatter_tempolimite.png", dpi=150, bbox_inches="tight")
plt.show()
print("Salvo: scatter_tempolimite.png")
No description has been provided for this image
Salvo: scatter_tempolimite.png

6. Recomendações derivadas¶

In [8]:
# Pares candidatos a aumento de TEMPOLIMITE (violação > 20% e volume >= 10)
candidatos_aumento = por_par_filtrado[
    (por_par_filtrado["taxa_violacao_pct"] > 20) &
    (por_par_filtrado["total"] >= 10)
][["par_nome", "tempolimite_min", "total", "taxa_violacao_pct",
   "espera_p50", "espera_p75", "espera_p95"]]

print(f"Pares com integração oficial mas TEMPOLIMITE muito curto ({len(candidatos_aumento)} pares):")
print(candidatos_aumento.to_string(index=False, float_format="{:.1f}".format))
print()
print("SUGESTÃO: ajustar TEMPOLIMITE para o P75 da espera observada nestes pares.")
print("Isso cobriria ~75% dos usuários sem custo de criar nova integração.")
Pares com integração oficial mas TEMPOLIMITE muito curto (0 pares):
Empty DataFrame
Columns: [par_nome, tempolimite_min, total, taxa_violacao_pct, espera_p50, espera_p75, espera_p95]
Index: []

SUGESTÃO: ajustar TEMPOLIMITE para o P75 da espera observada nestes pares.
Isso cobriria ~75% dos usuários sem custo de criar nova integração.

7. Interpretação¶

Pares acima da diagonal (scatter): a espera mediana já supera o limite oficial — mais da metade paga tarifa dupla mesmo com integração.

Situação Ação
Violação >20%, espera_p75 < 2×tempolimite Aumentar TEMPOLIMITE para o P75 observado
Violação >20%, espera_p75 ≥ 2×tempolimite Investigar operação (intervalo do ônibus longo? cobertura?)

Diferença da proposta de novas integrações: aqui o par já está na matriz — é ajuste administrativo (baixo custo político), não ausência de política.