Python Viz 6 — Bản đồ & Dữ liệu không gian
Phần lớn dữ liệu ngân hàng có một thuộc tính hay bị bỏ quên: vị trí. Mỗi khách hàng có city, mỗi chi nhánh có địa chỉ, mỗi ATM có toạ độ, mỗi khoản vay gắn với một tỉnh. Khi câu hỏi bắt đầu bằng "ở đâu" — dư nợ tập trung tỉnh nào, nên mở chi nhánh mới ở đâu, khu vực nào nợ xấu cao — thì một bảng số hay một biểu đồ cột không trả lời tốt bằng một tấm bản đồ. Bài này bổ sung góc Python cho GIS — Trực quan hoá không gian: thay vì QGIS hay công cụ GIS chuyên dụng, ta vẽ bản đồ bằng code, tái lập được và nhúng thẳng vào dashboard.
Khi nào dùng bản đồ, và loại nào
Bản đồ chỉ đáng dùng khi vị trí thực sự mang thông tin — nghĩa là mẫu hình phân bố theo không gian có ý nghĩa nghiệp vụ. Nếu bạn chỉ muốn so sánh 5 tỉnh có dư nợ cao nhất, một biểu đồ cột sắp xếp rõ ràng hơn bản đồ. Bản đồ thắng khi số vùng lớn (63 tỉnh), khi cụm địa lý là điều cần thấy (dư nợ dồn về Đông Nam Bộ), hoặc khi người xem cần định vị theo trực giác không gian (lãnh đạo nhìn ngay ra "miền Trung mỏng").
Bốn loại bản đồ thường gặp, mỗi loại hợp một dạng dữ liệu khác nhau:
| Loại | Dữ liệu | Dùng khi | Bẫy thường gặp |
|---|---|---|---|
| Choropleth (tô vùng) | Một giá trị cho mỗi vùng hành chính | Chỉ số theo tỉnh/huyện: tỷ lệ nợ xấu, dư nợ bình quân | Tô theo số tuyệt đối → tỉnh đông dân luôn "đỏ" |
| Điểm / bubble | Toạ độ điểm, kích thước theo giá trị | Vị trí chi nhánh/ATM, quy mô mỗi điểm | Quá nhiều điểm chồng lên nhau (overplotting) |
| Heatmap mật độ | Nhiều điểm, quan tâm mật độ | Nơi khách hàng/giao dịch dày đặc | Mật độ điểm ≠ mật độ giá trị nếu chưa chuẩn hoá |
| Cluster | Nhiều điểm cần gom nhóm | Hàng nghìn điểm, gom theo mức zoom | Gom rồi khó đọc giá trị từng điểm |
Nguyên tắc quan trọng nhất, lặp lại xuyên suốt bài: choropleth phải tô theo giá trị đã chuẩn hoá. Tô "tổng dư nợ" theo tỉnh sẽ luôn làm TP.HCM và Hà Nội đỏ rực — không phải vì rủi ro hay cơ hội, mà đơn giản vì ở đó đông người và nhiều tiền. Muốn bản đồ nói điều có ích, hãy tô tỷ lệ (nợ xấu %), bình quân đầu người (dư nợ / dân số), hay mật độ (khách / km²).
Bộ công cụ Python cho không gian
Có ba lớp công cụ, phân vai rõ ràng.
GeoPandas — DataFrame biết về không gian
GeoPandas mở rộng pandas bằng một cột đặc biệt tên geometry — chứa điểm, đường, hoặc đa giác (polygon) thay vì số. Một GeoDataFrame vừa có các cột thường (tên tỉnh, dư nợ) vừa có hình học của tỉnh đó. Nó là nền tảng xử lý trước khi vẽ:
import geopandas as gpd
# Đọc ranh giới 63 tỉnh từ GeoJSON (minh hoạ)
provinces = gpd.read_file("vn_provinces.geojson")
print(provinces.crs) # EPSG:4326 (WGS84 — kinh/vĩ độ)
print(provinces.geometry.head())
# Đọc được cả Shapefile (.shp), GeoPackage (.gpkg), PostGIS...
GeoPandas làm được ba việc mà pandas thường không: (1) đọc/ghi định dạng không gian — GeoJSON, Shapefile, GeoPackage; (2) join không gian (gpd.sjoin) — ghép một điểm vào đúng đa giác chứa nó, ví dụ gán mỗi ATM vào tỉnh của nó dù dữ liệu chỉ có toạ độ; (3) phép không gian — tính diện tích, tâm đa giác (centroid), khoảng cách, giao/hợp vùng. Để vẽ nhanh bản đồ tĩnh, chỉ cần gdf.plot(column="ty_le_no_xau", legend=True) — nó gọi matplotlib bên dưới, hợp cho báo cáo in.
Folium — bản đồ tương tác trên nền Leaflet
Folium bọc thư viện JavaScript Leaflet, sinh ra một file HTML tương tác: người xem kéo, phóng to, bật/tắt lớp, rê chuột xem tooltip — tất cả trên nền bản đồ OpenStreetMap (OSM) miễn phí. Đây là lựa chọn mặc định khi bạn cần bản đồ nhúng vào web nội bộ hoặc gửi cho lãnh đạo tự xem. Folium hỗ trợ đủ bốn loại: Marker/CircleMarker (điểm), Choropleth (tô vùng), HeatMap (mật độ, qua plugin), MarkerCluster (gom cụm).
Plotly, pydeck cho quy mô lớn
Plotly có px.choropleth, px.choropleth_mapbox và px.scatter_mapbox — cùng phong cách figure = data + layout đã quen ở bài 4, tích hợp mượt vào Dash. Khi dữ liệu rất lớn (hàng triệu điểm giao dịch, hàng chục nghìn ATM), Leaflet/Folium bắt đầu giật; lúc đó pydeck (bbinding Python của deck.gl) hoặc kepler.gl vẽ bằng GPU (WebGL), xử lý mượt số điểm mà Folium không kham nổi. Quy tắc chọn nhanh:
- Vài trăm vùng/điểm, cần nhanh và đơn giản → Folium.
- Đã dùng Plotly/Dash cho phần còn lại của dashboard → Plotly mapbox.
- Hàng chục nghìn điểm trở lên, cần layer 3D/hexbin → pydeck / kepler.gl.
- Chỉ cần ảnh tĩnh cho báo cáo PDF → GeoPandas + matplotlib.
Dữ liệu không gian: ranh giới, geocoding, CRS
Ba khái niệm cần nắm trước khi vẽ được bản đồ đúng.
Ranh giới hành chính VN. Choropleth cần một file GeoJSON chứa đa giác của từng tỉnh (hoặc huyện). Điểm mấu chốt là khoá ghép: mỗi feature trong GeoJSON có một thuộc tính định danh (tên tỉnh, hoặc mã tỉnh theo chuẩn của Tổng cục Thống kê), và bảng số liệu của bạn phải khớp đúng khoá đó. Sai lệch nhỏ về cách viết ("TP. Hồ Chí Minh" vs "Hồ Chí Minh" vs "Ho Chi Minh City") là nguyên nhân số một khiến một tỉnh bị trắng trên bản đồ. Hãy chuẩn hoá tên/mã ở cả hai phía trước khi join — dùng mã tỉnh đáng tin hơn tên chữ.
Geocoding. Dữ liệu chi nhánh thường chỉ có địa chỉ dạng chữ, không có toạ độ. Geocoding là bước biến "địa chỉ → (vĩ độ, kinh độ)". Thư viện geopy gọi các dịch vụ như Nominatim (OSM). Lưu ý thực tế: geocoding hàng loạt bị giới hạn tốc độ (rate limit), kết quả có sai số, và với dữ liệu khách hàng thì cần cân nhắc quyền riêng tư — đừng gửi địa chỉ khách ra dịch vụ ngoài nếu chính sách không cho phép; hãy geocode nội bộ hoặc chỉ ở mức tỉnh/quận. Kết quả nên cache lại, đừng geocode lại mỗi lần chạy.
CRS (Coordinate Reference System). Là hệ quy chiếu cho biết một cặp số toạ độ nghĩa là gì. Cần nhớ đúng một điều: WGS84 — EPSG:4326 là hệ kinh/vĩ độ (lat/lon) mà Folium, Leaflet, GeoJSON và GPS đều dùng. Nếu bản đồ của bạn ra trống hoặc điểm rơi giữa đại dương, gần như chắc chắn là lệch CRS — dữ liệu đang ở hệ chiếu mét (ví dụ VN2000/UTM) chưa đổi về 4326. GeoPandas đổi bằng gdf.to_crs(4326). Lưu ý thứ tự: GeoJSON và nhiều công cụ ghi [lon, lat], nhưng Folium nhận [lat, lon] — đảo nhầm là lỗi kinh điển khiến điểm bay sang Trung Quốc.
Nguyên tắc để bản đồ không đánh lừa
Bản đồ dễ gây hiểu sai hơn biểu đồ thường, vì diện tích và màu sắc tác động mạnh vào trực giác. Bốn nguyên tắc, nối tiếp thiết kế dashboard và chọn biểu đồ:
- Chuẩn hoá, luôn luôn. Choropleth số tuyệt đối = bản đồ dân số trá hình. Tô tỷ lệ, bình quân đầu người, hoặc mật độ. Muốn thể hiện quy mô tuyệt đối, dùng bubble (kích thước điểm) chồng lên chứ đừng tô vùng.
- Bảng màu tuần tự (sequential) cho giá trị một chiều tăng dần (dư nợ thấp→cao): một gam màu đậm dần, ví dụ
YlOrRd. Dùng diverging (hai màu tách khỏi một điểm giữa) chỉ khi có mốc trung tâm có nghĩa (tăng trưởng dương/âm). Tránh cầu vồng. - Chú giải và phân lớp (bins). Nói rõ mỗi màu là khoảng giá trị nào. Cách chia lớp (đều nhau, theo phân vị, Jenks) đổi hẳn diện mạo bản đồ — chọn có chủ đích và ghi rõ.
- Bối cảnh vừa đủ. Giữ nhãn tỉnh, thước tỷ lệ, nguồn dữ liệu. Đừng để màu nền OSM quá rực lấn át lớp dữ liệu.
Ví dụ code: choropleth dư nợ + marker chi nhánh
Dưới đây là mã Python minh hoạ (tên file, cột, số liệu là giả định) dựng một bản đồ Folium hai lớp: tô tỉnh theo dư nợ bình quân/khách (đã chuẩn hoá) và chấm marker cho từng chi nhánh.
import folium
import pandas as pd
import geopandas as gpd
# 1) Số liệu theo tỉnh (từ SQL bên dưới) — minh hoạ
loans = pd.read_csv("du_no_theo_tinh.csv") # cols: ma_tinh, du_no, so_kh
loans["du_no_bq"] = loans["du_no"] / loans["so_kh"] # CHUẨN HOÁ
# 2) Ranh giới 63 tỉnh, đảm bảo WGS84
gdf = gpd.read_file("vn_provinces.geojson").to_crs(4326)
# 3) Bản đồ nền OSM, canh giữa VN
m = folium.Map(location=[16.0, 107.5], zoom_start=6, tiles="OpenStreetMap")
# 4) Lớp choropleth — khoá join là ma_tinh
folium.Choropleth(
geo_data=gdf,
data=loans,
columns=["ma_tinh", "du_no_bq"],
key_on="feature.properties.ma_tinh", # phải khớp thuộc tính GeoJSON
fill_color="YlOrRd", # bảng màu tuần tự
fill_opacity=0.7, line_opacity=0.3,
legend_name="Dư nợ bình quân/khách (triệu VND)",
).add_to(m)
# 5) Lớp marker chi nhánh (đã geocode ra lat/lon) — minh hoạ
branches = pd.read_csv("chi_nhanh.csv") # cols: ten, lat, lon, du_no
cluster = folium.plugins.MarkerCluster().add_to(m)
for _, r in branches.iterrows():
folium.CircleMarker(
location=[r["lat"], r["lon"]], # LƯU Ý: [lat, lon]
radius=6, popup=f"{r['ten']}: {r['du_no']:.0f} tỷ",
color="#1f4e79", fill=True,
).add_to(cluster)
folium.LayerControl().add_to(m)
m.save("ban_do_du_no.html") # file HTML tương tác, nhúng vào web nội bộ
Và đây là ví dụ join không gian bằng GeoPandas — gán mỗi ATM (chỉ có toạ độ) vào đúng tỉnh chứa nó, rồi đếm số ATM/tỉnh:
atms = gpd.GeoDataFrame(
atm_df, geometry=gpd.points_from_xy(atm_df.lon, atm_df.lat), crs=4326
)
# sjoin: mỗi điểm ghép vào đa giác tỉnh chứa nó
atm_tinh = gpd.sjoin(atms, gdf, predicate="within")
dem = atm_tinh.groupby("ma_tinh").size() # số ATM mỗi tỉnh
Nguồn số liệu cho bản đồ (SQL chạy được)
Bản đồ chỉ đẹp khi số liệu đúng. Truy vấn dưới đây gộp số khách và tổng số dư theo thành phố từ sandbox — chính là đầu vào (trước bước chuẩn hoá) cho lớp choropleth ở trên. Vì bảng customers có cột city, ta join sang accounts để cộng số dư:
-- ▶ Chạy được
SELECT c.city,
COUNT(DISTINCT c.id) AS so_kh,
COUNT(a.id) AS so_tk,
ROUND(SUM(a.balance)::numeric, 2) AS tong_so_du,
ROUND((SUM(a.balance) / COUNT(DISTINCT c.id))::numeric, 2) AS so_du_bq_kh
FROM customers c
LEFT JOIN accounts a ON a.customer_id = c.id
GROUP BY c.city
ORDER BY tong_so_du DESC NULLS LAST;
Cột so_du_bq_kh (số dư bình quân/khách) chính là loại chỉ số đã chuẩn hoá nên đưa lên choropleth — thay vì tong_so_du tuyệt đối vốn chỉ phản ánh thành phố nào đông khách. Đây là ví dụ trực tiếp cho nguyên tắc lặp lại suốt bài.
Use case thực tế
Bối cảnh (số liệu ước lượng, minh hoạ). Ban lãnh đạo NCB muốn một bản đồ tương tác trả lời hai câu: (1) khách hàng và dư nợ đang phân bố ra sao trên 63 tỉnh; (2) nên ưu tiên mở chi nhánh mới ở đâu.
Cách làm. Đội dữ liệu chạy truy vấn gộp theo tỉnh (dựa trên city như SQL trên, mở rộng thêm dư nợ từ hệ thống core), tính ba chỉ số đã chuẩn hoá: dư nợ bình quân/khách, số khách/vạn dân, và mật độ chi nhánh (số chi nhánh/100 nghìn dân). Ghép với GeoJSON 63 tỉnh (khoá ma_tinh), dựng Folium ba lớp: choropleth dư nợ bình quân, bubble quy mô dư nợ tuyệt đối, và marker chi nhánh hiện có (gom cluster). File HTML nhúng vào cổng BI nội bộ.
Phát hiện (giả định minh hoạ). Bản đồ cho thấy một tỉnh Tây Nguyên có dư nợ bình quân/khách top 10 nhưng mật độ chi nhánh gần thấp nhất — tức nhu cầu cao mà điểm phục vụ mỏng, một ứng viên rõ để mở chi nhánh. Ngược lại vài tỉnh miền Bắc "đỏ" trên bản đồ tổng dư nợ hoá ra chỉ vì đông dân; khi chuyển sang lớp bình quân/khách thì nhạt hẳn — đúng minh chứng vì sao không được tô số tuyệt đối. Lãnh đạo tự bật/tắt lớp để so sánh, thay vì đội dữ liệu phải in 5 phiên bản báo cáo. Kết quả này còn được đối chiếu với phân khúc khách hàng và bước tiếp là mô hình chọn địa điểm chi tiết hơn.
Ghi nhớ
- Dùng bản đồ khi vị trí mang thông tin và số vùng lớn; ít vùng thì biểu đồ cột rõ hơn.
- Bốn loại: choropleth (tô vùng), điểm/bubble, heatmap mật độ, cluster — chọn theo dạng dữ liệu.
- Luật vàng: choropleth phải chuẩn hoá (tỷ lệ, bình quân đầu người, mật độ). Số tuyệt đối = bản đồ dân số trá hình. Muốn thể hiện quy mô tuyệt đối → bubble.
- GeoPandas để xử lý (đọc GeoJSON/Shapefile,
sjoin, đổi CRS), vẽ tĩnh qua matplotlib; Folium cho HTML tương tác trên nền OSM; Plotly khi đã dùng Dash; pydeck/kepler.gl cho dữ liệu rất lớn (WebGL). - CRS: nhớ WGS84 (EPSG:4326) cho web/GeoJSON; bản đồ trống hay điểm rơi ngoài biển thường là lệch CRS. Folium nhận
[lat, lon]— đừng đảo nhầm. - Khoá join chuẩn hoá (ưu tiên mã tỉnh hơn tên chữ) là nguyên nhân số một gây tỉnh trắng.
- Geocoding có rate limit và rủi ro riêng tư — cache kết quả, cân nhắc chỉ geocode ở mức tỉnh/quận với dữ liệu khách.
- Bảng màu tuần tự cho giá trị tăng dần, có chú giải và cách chia lớp rõ ràng — theo thiết kế dashboard.
Nguồn tham khảo
- GeoPandas Documentation — GeoDataFrame, đọc/ghi định dạng không gian,
sjoin,to_crs, vẽ với matplotlib. - Folium Documentation — bản đồ tương tác trên nền Leaflet:
Map,Choropleth,CircleMarker,MarkerCluster,HeatMap. - Shapely Documentation — thư viện hình học nền tảng (điểm, đường, đa giác) mà GeoPandas dùng cho cột
geometry. - GeoPy Documentation — geocoding qua các dịch vụ như Nominatim (OpenStreetMap), kèm lưu ý rate limit.
- Plotly Python — Maps —
choropleth,scatter_mapboxvà các loại bản đồ tích hợp Dash. - Matplotlib Documentation — nền tảng vẽ bản đồ tĩnh cho báo cáo in.
Bài viết liên quan
Vì sao Python là ngôn ngữ số một của data engineer: vai trò trong pipeline (ingest/transform/orchestrate), hệ sinh thái thư viện (pandas/polars/pyarrow/sqlalchemy), quản lý môi trường (venv/uv/poetry), và khi nào dùng Python vs SQL/Spark.
Biến script thành pipeline đáng tin cậy: cấu trúc project & packaging (uv/poetry), type hints & pydantic, kiểm thử với pytest, logging & cấu hình, đóng gói Docker, và tích hợp CI cho code dữ liệu.
Học cách tổ chức code Python: định nghĩa hàm với tham số vị trí/từ khoá/mặc định, *args/**kwargs, lambda và hàm bậc cao, closure, decorator, generator với yield. Đóng gói code thành module và package, cô lập thư viện bằng môi trường ảo venv, quản lý phụ thuộc với pip và requirements.txt để dự án tái lập được trên mọi máy.
Hướng dẫn OOP trong Python từ class/instance, kế thừa và super(), đa hình & duck typing, encapsulation tới dunder methods, @property, classmethod/staticmethod, dataclass và type hints (mypy). Kèm nguyên tắc clean code: đặt tên rõ nghĩa, hàm nhỏ, DRY, SOLID cùng chuẩn PEP8 với công cụ ruff/black.
Cảm nhận của bạn
Bình luận
Chưa có bình luận. Hãy là người đầu tiên chia sẻ!