03 — Propostas priorizadas de novas integrações¶
Consolida a entrega de proposta a partir das baldeações fisicamente plausíveis (até 1 km) SEM integração oficial: ranking por par/região (volume mensal), score multidimensional (volume + distância, com fator de vulnerabilidade social IBGE opcional) e mapas.
Lê transbordos_*.parquet (do 02_analise). Exporta proposta_novas_integracoes.csv/.md, proposta_score_ajustado.csv e mapas .html.
In [1]:
import os, json, math
from pathlib import Path
import numpy as np, pandas as pd
import folium
from folium.plugins import HeatMap
import matplotlib.pyplot as plt
from transbordo_utils import (novo_mapa, GAP_VALIDACAO_MAX_KM, adicionar_plausibilidade_fisica,
resolver_cache, carregar_pares_oficiais,
classificar_regional_oficial, CORES_REGIONAL)
START_DATE, END_DATE = "2026-01-01", "2026-01-31"
TB = resolver_cache(f"transbordos_{START_DATE}_{END_DATE}.parquet") # gerado pelo 02_analise
MATRIZ = resolver_cache("matriz_integracao.json")
df = adicionar_plausibilidade_fisica(pd.read_parquet(TB), GAP_VALIDACAO_MAX_KM)
pares = carregar_pares_oficiais(MATRIZ)
print("transbordos:", len(df), "| pares oficiais:", len(pares))
transbordos: 23562 | pares oficiais: 2589
1. Baldeações plausíveis sem integração¶
In [2]:
# --- Baldeações fisicamente plausíveis (<=1 km) SEM integração oficial ---
df["o"] = df["CODLINHA_ORIGEM"].astype(str)
df["d"] = df["CODLINHA_DESTINO"].astype(str)
df["par"] = list(zip(df["o"], df["d"]))
bald_temporal = df[df["o"] != df["d"]]
bald = df[df["baldeacao_real"]]
sem = bald[[p not in pares for p in bald["par"]]].copy()
print(f"baldeações temporais: {len(bald_temporal)}")
print(f"plausíveis (<= {GAP_VALIDACAO_MAX_KM:.1f} km): {len(bald)} | sem integração: {len(sem)}")
baldeações temporais: 8581 plausíveis (<= 1.0 km): 5084 | sem integração: 3895
2. Proposta priorizada (volume + região)¶
In [3]:
# --- Agrega por PAR de linhas (não-direcionado A<->B) -> proposta priorizada ---
sem["lat_m"] = (sem["LATITUDE_ORIGEM"] + sem["LATITUDE_DESTINO"]) / 2
sem["lon_m"] = (sem["LONGITUDE_ORIGEM"] + sem["LONGITUDE_DESTINO"]) / 2
nome = {}
for _, r in sem.iterrows():
nome.setdefault(r["o"], str(r["NOMELINHA_ORIGEM"]))
nome.setdefault(r["d"], str(r["NOMELINHA_DESTINO"]))
sem["par_und"] = [" <-> ".join(sorted([o, d])) for o, d in zip(sem["o"], sem["d"])]
prop = (sem.groupby("par_und")
.agg(volume=("NUMEROCARTAO", "size"), cartoes=("NUMEROCARTAO", "nunique"),
lat_m=("lat_m", "median"), lon_m=("lon_m", "median"))
.reset_index())
prop["regiao"] = [classificar_regional_oficial(la, lo) for la, lo in zip(prop["lat_m"], prop["lon_m"])]
total = prop["volume"].sum()
MIN_VOLUME = 5 # piso: pares com <5 baldeações/mês são ruído
prop = prop[(prop["volume"] >= MIN_VOLUME) & prop["lat_m"].notna()].copy()
prop["pct_demanda_nao_atendida"] = (100 * prop["volume"] / total).round(2)
prop["score"] = (100 * prop["volume"] / prop["volume"].max()).round(1)
prop = prop.sort_values("volume", ascending=False).reset_index(drop=True)
prop.insert(0, "rank", prop.index + 1)
def nomear(par):
a, b = par.split(" <-> ")
return f"{a} {nome.get(a,'')} <-> {b} {nome.get(b,'')}"
prop["par_linhas"] = prop["par_und"].map(nomear)
cols = ["rank","par_linhas","regiao","volume","cartoes","pct_demanda_nao_atendida","score","lat_m","lon_m"]
proposta = prop[cols]
print(f"pares distintos sem integração: {len(proposta)} | volume total: {total}")
display(proposta.head(25))
pares distintos sem integração: 202 | volume total: 3895
| rank | par_linhas | regiao | volume | cartoes | pct_demanda_nao_atendida | score | lat_m | lon_m | |
|---|---|---|---|---|---|---|---|---|---|
| 0 | 1 | 040 INTERBAIRROS IV <-> 876 SAVÓIA | Regional Santa Felicidade | 54 | 36 | 1.39 | 100.0 | -25.419500 | -49.344543 |
| 1 | 2 | 170 BRACATINGA <-> 171 PRIMAVERA | Regional Matriz | 47 | 46 | 1.21 | 87.0 | -25.406597 | -49.280079 |
| 2 | 3 | 701 FAZENDINHA <-> 703 CAIUÁ | Regional Portão | 42 | 39 | 1.08 | 77.8 | -25.450100 | -49.290314 |
| 3 | 4 | 166 V. NORI <-> 170 BRACATINGA | Regional Santa Felicidade | 34 | 33 | 0.87 | 63.0 | -25.403843 | -49.285419 |
| 4 | 5 | 170 BRACATINGA <-> 901 STA. FELICIDADE | Regional Santa Felicidade | 24 | 13 | 0.62 | 44.4 | -25.405066 | -49.286873 |
| 5 | 6 | 170 BRACATINGA <-> 876 SAVÓIA | Regional Santa Felicidade | 23 | 9 | 0.59 | 42.6 | -25.411976 | -49.286598 |
| 6 | 7 | 040 INTERBAIRROS IV <-> 870 SÃO BRAZ | Regional Santa Felicidade | 20 | 20 | 0.51 | 37.0 | -25.423895 | -49.350636 |
| 7 | 8 | 205 BARREIRINHA <-> 876 SAVÓIA | Regional Matriz | 19 | 15 | 0.49 | 35.2 | -25.429817 | -49.281471 |
| 8 | 9 | 040 INTERBAIRROS IV <-> 822 GABINETO | Regional Cic | 19 | 16 | 0.49 | 35.2 | -25.437876 | -49.353348 |
| 9 | 10 | 528 BOQUEIRÃO/PINHEIRINHO <-> 641 LUIZ NICHELE | Regional Bairro Novo | 18 | 16 | 0.46 | 33.3 | -25.548700 | -49.289421 |
| 10 | 11 | 901 STA. FELICIDADE <-> 967 JÚLIO GRAF | Regional Matriz | 18 | 17 | 0.46 | 33.3 | -25.420534 | -49.288296 |
| 11 | 12 | 030 INTERBAIRROS III <-> 338 CENTENÁRIO/HAUER | Regional Cajuru | 17 | 15 | 0.44 | 31.5 | -25.486351 | -49.219372 |
| 12 | 13 | 030 INTERBAIRROS III <-> 345 B ALTO/ATUBA | Regional Boa Vista | 16 | 14 | 0.41 | 29.6 | -25.403233 | -49.203129 |
| 13 | 14 | 380 DETRAN/V.MACHADO <-> 801 CAMP.SIQ./BATEL | Regional Matriz | 16 | 10 | 0.41 | 29.6 | -25.431225 | -49.266796 |
| 14 | 15 | 040 INTERBAIRROS IV <-> 812 MONTANA | Regional Santa Felicidade | 16 | 14 | 0.41 | 29.6 | -25.418865 | -49.347237 |
| 15 | 16 | 166 V. NORI <-> 168 RAPOSO TAVARES | Regional Santa Felicidade | 16 | 12 | 0.41 | 29.6 | -25.410007 | -49.285828 |
| 16 | 17 | 372 TARUMÃ <-> 801 CAMP.SIQ./BATEL | Regional Matriz | 15 | 14 | 0.39 | 27.8 | -25.436525 | -49.276154 |
| 17 | 18 | 050 INTERBAIRROS V <-> 216 CABRAL / PORTÃO | Regional Portão | 15 | 15 | 0.39 | 27.8 | -25.460695 | -49.273315 |
| 18 | 19 | 040 INTERBAIRROS IV <-> 703 CAIUÁ | Regional Santa Felicidade | 14 | 13 | 0.36 | 25.9 | -25.469894 | -49.329811 |
| 19 | 20 | 020 INTERBAIRR II H <-> 184 V. SUIÇA | Regional Boa Vista | 14 | 12 | 0.36 | 25.9 | -25.386562 | -49.274857 |
| 20 | 21 | 170 BRACATINGA <-> 207 CABRAL / OSÓRIO | Regional Matriz | 14 | 13 | 0.36 | 25.9 | -25.422010 | -49.277866 |
| 21 | 22 | 040 INTERBAIRROS IV <-> 652 V. VERDE | Regional Cic | 13 | 11 | 0.33 | 24.1 | -25.524538 | -49.329079 |
| 22 | 23 | 545 TRABALHADOR <-> 619 STA. RITA / CIC | Regional Cic | 13 | 11 | 0.33 | 24.1 | -25.541122 | -49.320099 |
| 23 | 24 | 545 TRABALHADOR <-> 627 BOSCH | Regional Cic | 13 | 13 | 0.33 | 24.1 | -25.536854 | -49.323296 |
| 24 | 25 | 338 CENTENÁRIO/HAUER <-> 472 UBERABA | Regional Cajuru | 13 | 12 | 0.33 | 24.1 | -25.482044 | -49.225231 |
In [4]:
# --- Exporta CSV + MD (top 30) ---
proposta.to_csv("proposta_novas_integracoes.csv", index=False)
top = proposta.head(30)
linhas_md = ["# Proposta de novas integrações tarifárias — Curitiba (jan/2026)",
"",
f"Baseado em **{int(total)} baldeações fisicamente plausíveis sem integração oficial** (pares de linhas diferentes, "
f"usos consecutivos 3–60 min, gap <= {GAP_VALIDACAO_MAX_KM:.1f} km, fora da matriz oficial liberada).",
"Priorizado por volume mensal. Score = volume normalizado (0–100).",
"",
"| # | Par de linhas (A ↔ B) | Região | Volume/mês | Cartões | % demanda | Score |",
"|---|---|---|---|---|---|---|"]
for _, r in top.iterrows():
linhas_md.append(f"| {r['rank']} | {r['par_linhas']} | {r['regiao']} | {r['volume']} | "
f"{r['cartoes']} | {r['pct_demanda_nao_atendida']}% | {r['score']} |")
porreg = (proposta.groupby("regiao").agg(pares=("rank","size"), volume=("volume","sum"))
.sort_values("volume", ascending=False).reset_index())
linhas_md += ["", "## Volume por região (sem integração)", "",
"| Região | Pares | Volume/mês |", "|---|---|---|"]
for _, r in porreg.iterrows():
linhas_md.append(f"| {r['regiao']} | {r['pares']} | {r['volume']} |")
Path("proposta_novas_integracoes.md").write_text("\n".join(linhas_md), encoding="utf-8")
print("exportado: proposta_novas_integracoes.csv e .md")
display(porreg)
exportado: proposta_novas_integracoes.csv e .md
| regiao | pares | volume | |
|---|---|---|---|
| 0 | Regional Matriz | 83 | 654 |
| 1 | Regional Santa Felicidade | 21 | 308 |
| 2 | Regional Portão | 25 | 218 |
| 3 | Regional Cic | 23 | 201 |
| 4 | Regional Cajuru | 17 | 139 |
| 5 | Regional Boa Vista | 14 | 114 |
| 6 | Regional Bairro Novo | 6 | 49 |
| 7 | Regional Tatuquara | 6 | 44 |
| 8 | Regional Boqueirão | 5 | 31 |
| 9 | Regional Pinheirinho | 2 | 13 |
3. Score multidimensional (volume + distância + vulnerabilidade)¶
In [5]:
# --- Score composto: volume + distância (+ vulnerabilidade social IBGE, opcional) ---
W_VOL, W_DIST, W_VULN = 0.50, 0.25, 0.25
USE_IBGE = False # True após instalar geopandas + shapefile do Censo 2022 (PR)
sem["dist_km"] = sem["dist_linha_km"] # distância de fato percorrida na 1ª linha
sem["par_und_nome"] = [" <-> ".join(sorted([str(o), str(d)]))
for o, d in zip(sem["NOMELINHA_ORIGEM"].astype(str),
sem["NOMELINHA_DESTINO"].astype(str))]
agg = (sem.groupby("par_und_nome")
.agg(volume=("NUMEROCARTAO","count"), cartoes=("NUMEROCARTAO","nunique"),
dist_med=("dist_km","median"),
lat_orig=("LATITUDE_ORIGEM","median"), lon_orig=("LONGITUDE_ORIGEM","median"))
.reset_index())
if USE_IBGE:
import geopandas as gpd
from shapely.geometry import Point
setores = gpd.read_file("/tmp/setores_pr/PR_setores_CD2022.shp")
pts = gpd.GeoDataFrame(agg[["par_und_nome","lat_orig","lon_orig"]],
geometry=[Point(lo, la) for la, lo in zip(agg["lat_orig"], agg["lon_orig"])], crs="EPSG:4326")
joined = gpd.sjoin(pts, setores[["geometry","V009"]], how="left")
agg["renda_setor"] = joined["V009"].values
rmax = agg["renda_setor"].max()
agg["vuln"] = 1.0 - agg["renda_setor"].fillna(rmax/2) / rmax
else:
agg["vuln"] = np.nan
W_VOL, W_DIST, W_VULN = 0.60, 0.40, 0.0
def norm01(s):
mn, mx = s.min(), s.max()
return s * 0.0 if mx == mn else (s - mn) / (mx - mn)
agg["norm_vol"] = norm01(agg["volume"])
agg["norm_dist"] = norm01(agg["dist_med"].fillna(0))
agg["norm_vuln"] = norm01(agg["vuln"].fillna(0)) if USE_IBGE else 0.0
agg["score_ajustado"] = (W_VOL*agg["norm_vol"] + W_DIST*agg["norm_dist"] + W_VULN*agg["norm_vuln"]) * 100
# casa com a proposta (chave por NOMES, sem o código de linha)
proposta_orig = proposta.copy()
def _names_key(p):
return " <-> ".join(sorted(" ".join(s.split(" ")[1:]) for s in str(p).split(" <-> ")))
proposta_orig["par_und_nome"] = proposta_orig["par_linhas"].map(_names_key)
merged = proposta_orig.merge(
agg[["par_und_nome","dist_med","norm_dist","vuln","score_ajustado"]],
on="par_und_nome", how="left")
merged["rank_orig"] = merged["score"].rank(ascending=False, na_option="bottom").astype(int)
merged["rank_novo"] = merged["score_ajustado"].rank(ascending=False, na_option="bottom").astype(int)
merged["delta_rank"] = merged["rank_orig"] - merged["rank_novo"]
merged = merged.sort_values("rank_novo")
merged.to_csv("proposta_score_ajustado.csv", index=False)
print(f"salvo: proposta_score_ajustado.csv (IBGE={'on' if USE_IBGE else 'off'})")
print("\n── TOP 20 por score ajustado ──")
print(merged.head(20)[["par_linhas","regiao","volume","dist_med","score","score_ajustado","delta_rank"]]
.to_string(index=False, float_format="{:.1f}".format))
salvo: proposta_score_ajustado.csv (IBGE=off)
── TOP 20 por score ajustado ──
par_linhas regiao volume dist_med score score_ajustado delta_rank
040 INTERBAIRROS IV <-> 876 SAVÓIA Regional Santa Felicidade 54 2.1 100.0 63.9 0
170 BRACATINGA <-> 171 PRIMAVERA Regional Matriz 47 2.4 87.0 56.9 0
701 FAZENDINHA <-> 703 CAIUÁ Regional Portão 42 1.9 77.8 49.5 0
170 BRACATINGA <-> 901 STA. FELICIDADE Regional Santa Felicidade 24 5.9 44.4 41.0 1
166 V. NORI <-> 170 BRACATINGA Regional Santa Felicidade 34 1.9 63.0 40.6 -1
170 BRACATINGA <-> 876 SAVÓIA Regional Santa Felicidade 23 5.8 42.6 39.5 0
280 NSA.SRA.DE NAZARÉ <-> 876 SAVÓIA Regional Santa Felicidade 12 7.6 22.2 32.3 23
609 MAD. CIC <-> 679 MAD. TATUQUARA Regional Portão 8 8.6 14.8 30.7 66
060 INTERBAIRROS VI <-> 685 RIO BONITO/CIC Regional Tatuquara 11 7.2 20.4 29.9 29
166 V. NORI <-> 168 RAPOSO TAVARES Regional Santa Felicidade 16 4.8 29.6 28.7 4
338 CENTENÁRIO/HAUER <-> 468 JD. CENTAURO Regional Cajuru 10 6.9 18.5 28.1 38
372 TARUMÃ <-> 777 V. VELHA Regional Matriz 11 6.2 20.4 27.1 26
060 INTERBAIRROS VI <-> 619 STA. RITA / CIC Regional Cic 5 8.0 9.3 25.6 165
468 JD. CENTAURO <-> 703 CAIUÁ Regional Cajuru 5 8.0 9.3 25.5 164
468 JD. CENTAURO <-> 474 JD. ITIBERÊ Regional Cajuru 6 7.4 11.1 25.0 115
160 J.MERCES/GUANABA <-> 475 CANAL BELÉM Regional Portão 9 6.1 16.7 24.7 43
380 DETRAN/V.MACHADO <-> 870 SÃO BRAZ Regional Santa Felicidade 5 7.7 9.3 24.6 161
387 PALOTINOS <-> 777 V. VELHA Regional Portão 8 6.4 14.8 24.3 56
468 JD. CENTAURO <-> 777 V. VELHA Regional Matriz 7 6.8 13.0 24.2 76
170 BRACATINGA <-> 205 BARREIRINHA Regional Santa Felicidade 9 5.9 16.7 24.0 39
In [6]:
# Scatter: score original (volume) vs. score ajustado
cores = merged["regiao"].map(CORES_REGIONAL).fillna("#95a5a6")
fig, ax = plt.subplots(figsize=(9, 8))
ax.scatter(merged["score"], merged["score_ajustado"], c=cores,
s=merged["cartoes"].fillna(1)*4, alpha=0.75, edgecolors="gray", linewidth=0.4)
lim = max(merged["score"].max(), merged["score_ajustado"].max()) + 3
ax.plot([0, lim], [0, lim], "k--", alpha=0.3, linewidth=1, label="sem mudança")
for _, row in merged[merged["delta_rank"].abs() >= 5].head(12).iterrows():
ax.annotate(row["par_linhas"][:30], (row["score"], row["score_ajustado"]),
fontsize=6, alpha=0.85, xytext=(4, -3), textcoords="offset points")
from matplotlib.lines import Line2D
handles = [Line2D([0],[0], marker="o", color="w", markerfacecolor=c, markersize=8, label=r)
for r, c in CORES_REGIONAL.items() if r in merged["regiao"].values]
ax.legend(handles=handles, title="Região", fontsize=7, bbox_to_anchor=(1.01, 1), loc="upper left")
ax.set_xlabel("Score original (volume puro)")
ax.set_ylabel("Score ajustado (volume + distância" + (" + IBGE" if USE_IBGE else "") + ")")
ax.set_title("Mudança de prioridade com score multidimensional")
plt.tight_layout(); plt.savefig("scatter_score.png", dpi=150, bbox_inches="tight"); plt.show()
print("Salvo: scatter_score.png")
Salvo: scatter_score.png
In [7]:
# Bump chart: variação de rank (TOP 20)
top20 = merged[merged["rank_orig"] <= 20].copy()
fig, ax = plt.subplots(figsize=(8, 9))
for _, row in top20.iterrows():
cor = CORES_REGIONAL.get(row["regiao"], "#95a5a6")
ax.plot([0, 1], [row["rank_orig"], row["rank_novo"]], color=cor, linewidth=1.5, alpha=0.75)
ax.scatter([0], [row["rank_orig"]], color=cor, s=60, zorder=5)
ax.scatter([1], [row["rank_novo"]], color=cor, s=60, zorder=5)
ax.text(-0.03, row["rank_orig"], row["par_linhas"][:28], ha="right", va="center", fontsize=6)
ax.text(1.03, row["rank_novo"], row["par_linhas"][:28], ha="left", va="center", fontsize=6)
ax.set_xlim(-0.5, 1.5); ax.set_ylim(0.5, 20.5); ax.invert_yaxis()
ax.set_xticks([0, 1])
ax.set_xticklabels(["Score original\n(volume)", "Score ajustado\n(vol + dist" + ("+IBGE" if USE_IBGE else "") + ")"])
ax.set_yticks([])
ax.spines[["top","right","left","bottom"]].set_visible(False)
ax.set_title("Variação de ranking com score multidimensional (TOP 20)", fontsize=11)
plt.tight_layout(); plt.savefig("bump_chart_score.png", dpi=150, bbox_inches="tight"); plt.show()
print("Salvo: bump_chart_score.png")
/tmp/ipykernel_2625478/140039341.py:17: UserWarning: Tight layout not applied. The bottom and top margins cannot be made large enough to accommodate all Axes decorations.
plt.tight_layout(); plt.savefig("bump_chart_score.png", dpi=150, bbox_inches="tight"); plt.show()
Salvo: bump_chart_score.png
4. Mapas¶
In [8]:
# Mapa das integrações PROPOSTAS (top 40, tamanho = volume), cor = região
prop_geo = prop.dropna(subset=["lat_m","lon_m"]).head(40)
mp = novo_mapa(location=[-25.45,-49.29], zoom_start=12)
vmax = prop_geo["volume"].max()
for _, r in prop_geo.iterrows():
folium.CircleMarker([r["lat_m"], r["lon_m"]], radius=5+15*r["volume"]/vmax,
color=CORES_REGIONAL.get(r["regiao"], "#555"), fill=True, fill_opacity=0.6,
popup=folium.Popup(f"<b>{r['par_linhas']}</b><br>região: {r['regiao']}<br>volume/mês: {r['volume']} · cartões: {r['cartoes']}", max_width=300)
).add_to(mp)
mp.save("mapa_proposta_integracoes.html")
print("salvo: mapa_proposta_integracoes.html (top 40 propostas)")
mp
salvo: mapa_proposta_integracoes.html (top 40 propostas)
Out[8]:
Make this Notebook Trusted to load map: File -> Trust Notebook
In [9]:
# TOP 40 pares sem integração (origem→destino, espessura = demanda)
sem_coords = sem.dropna(subset=["LATITUDE_ORIGEM","LONGITUDE_ORIGEM","LATITUDE_DESTINO","LONGITUDE_DESTINO"])
top_od = (sem_coords.groupby(["CODLINHA_ORIGEM","NOMELINHA_ORIGEM","CODLINHA_DESTINO","NOMELINHA_DESTINO"])
.agg(qtd=("NUMEROCARTAO","count"),
lat_o=("LATITUDE_ORIGEM","median"), lon_o=("LONGITUDE_ORIGEM","median"),
lat_d=("LATITUDE_DESTINO","median"), lon_d=("LONGITUDE_DESTINO","median"))
.reset_index().sort_values("qtd", ascending=False).head(40))
mp = novo_mapa(location=[-25.45,-49.29], zoom_start=12)
qmax = top_od["qtd"].max()
for _, r in top_od.iterrows():
folium.PolyLine([[r.lat_o,r.lon_o],[r.lat_d,r.lon_d]], color="#c0392b", weight=2+8*r.qtd/qmax, opacity=0.6,
popup=f"{r.CODLINHA_ORIGEM} {r.NOMELINHA_ORIGEM} → {r.CODLINHA_DESTINO} {r.NOMELINHA_DESTINO}<br>{r.qtd} baldeações").add_to(mp)
folium.CircleMarker([r.lat_o,r.lon_o], radius=4, color="#2980b9", fill=True).add_to(mp)
folium.CircleMarker([r.lat_d,r.lon_d], radius=4, color="#c0392b", fill=True).add_to(mp)
mp.save("mapa_transbordos_sem_integracao.html")
print("salvo: mapa_transbordos_sem_integracao.html")
mp
salvo: mapa_transbordos_sem_integracao.html
Out[9]:
Make this Notebook Trusted to load map: File -> Trust Notebook
In [10]:
# Heatmap das origens/destinos sem integração
hm = novo_mapa(location=[-25.45,-49.29], zoom_start=12)
HeatMap(sem_coords[["LATITUDE_ORIGEM","LONGITUDE_ORIGEM"]].values.tolist(), name="Origens", radius=15,
gradient={"0.4":"yellow","0.65":"orange","1":"red"}).add_to(hm)
HeatMap(sem_coords[["LATITUDE_DESTINO","LONGITUDE_DESTINO"]].values.tolist(), name="Destinos", radius=15,
gradient={"0.4":"cyan","0.65":"blue","1":"navy"}).add_to(hm)
folium.LayerControl().add_to(hm)
hm.save("mapa_heatmap_sem_integracao.html")
print("salvo: mapa_heatmap_sem_integracao.html")
hm
salvo: mapa_heatmap_sem_integracao.html
Out[10]:
Make this Notebook Trusted to load map: File -> Trust Notebook