#30DayMapChallenge - Minimal Map
Mimimal Theme for the #30DayMapChallenge 2025. A simple visualization of how many people are served by the average train station in Japanese prefectures, kept as minimal as possible.
Japanese Prefectures by Average # of People Served by Each Train Station

Japanese Prefectures by Average # of People Served by Each Train Station
Click markers or scroll to explore
Box Size Denotes # of People
Notice the shere volume of passengers served in Tokyo region prefectures. A comprehensive rail network services significantly more passengers on average than the rest of Japna's prefectures.
Allows for Quick Visual Inspection to Confirm Patterns
Japan's urban core outside of Tokyo (Nagoya, Kyoto, Osaka, Fukayama, Hiroshima) have similar levels of service density, conforming to relatively similar urban patterns across this urban agglomeration.
Can Easily Spot Outliers
Outliers are obvious, particularly Okinawa which sees Japan's highest number of people served per train station. Additionally, you can see how much more rail density exists per capita in Hokkaido than Honshu's Northern regions.
Code
Day11_clean.pyimport unicodedata
import numpy as np
import pandas as pd
import geopandas as gpd
import osmnx as ox
import matplotlib.pyplot as plt
from shapely.geometry import Point
# --- Paths ---
TRANSIT_SHP = '/Users/mauricefarber/Documents/Personal Projects/30-Day Map Challenge/Data/japan_transit.shp'
PREF_SHP = '/Users/mauricefarber/Documents/Personal Projects/30-Day Map Challenge/Data/jpn_adm_2019_shp/jpn_admbnda_adm1_2019.shp'
POP_CSV = '/Users/mauricefarber/Documents/Personal Projects/30-Day Map Challenge/Data/jap_pop.csv'
OUTPUT_PNG = '/Users/mauricefarber/Documents/Personal Projects/30-Day Map Challenge/Images/japan_simple.png'
# --- Fetch Japan transit stations from OSM & save checkpoint ---
jpn = ox.features_from_place("Japan", tags={"public_transport": "station"})
points = jpn[jpn.geometry.geom_type == "Point"]
polygons = jpn[jpn.geometry.geom_type.isin(["Polygon", "MultiPolygon"])].copy()
polygons.geometry = polygons.geometry.centroid
japan_stations = pd.concat([points, polygons])
japan_stations.to_file(TRANSIT_SHP, driver="ESRI Shapefile", index=False)
# --- Filter to railway stations only ---
train_stations = japan_stations[
japan_stations['railway'].str.contains("station", case=False, na=False)
]
# --- Load prefectures ---
pref = gpd.read_file(PREF_SHP)
# --- Load & clean population data ---
def clean_text(s):
if pd.isna(s):
return None
return unicodedata.normalize("NFKC", str(s)).strip().lower()
pop = pd.read_csv(POP_CSV)
pop24 = pop[pop['YEAR'] == 2024].drop(index=0).copy()
pop24 = pop24.rename(columns={"A1101_Total population (Both sexes)[person]": "pop"})
pop24['name'] = pop24['AREA'].str.strip().str.replace(r"-(ken|to|fu)$", "", regex=True)
pop24['name'] = pop24['name'].map(clean_text).replace({
"gumma": "gunma",
"hyogo": "hyōgo",
"kochi": "kōchi",
"oita": "ōita",
})
pref['ADM1_EN'] = pref['ADM1_EN'].map(clean_text)
# --- Merge population into prefectures ---
pref_pop = gpd.GeoDataFrame(
pop24.merge(pref, left_on='name', right_on='ADM1_EN', how='left'),
geometry='geometry',
crs=pref.crs,
)
# --- Count stations per prefecture ---
pref_transit = gpd.sjoin(train_stations, pref_pop, how="left", predicate="within")
pref_counts = pref_transit.groupby("ADM1_EN").size().reset_index(name="count")
prefectures = pref_pop.merge(pref_counts, on='ADM1_EN', how='left')
# --- Compute people-per-station ratio ---
prefectures['pop'] = (
prefectures['pop'].astype(str)
.str.replace(",", "", regex=False)
.astype("int64")
)
prefectures['ratio2'] = (prefectures['pop'] / prefectures['count']).round(2)
# --- Plot & save ---
centroids = prefectures.copy()
centroids.geometry = centroids.geometry.centroid
fig, ax = plt.subplots(figsize=(20, 20))
centroids.plot(
ax=ax,
markersize=centroids['ratio2'] / 80,
edgecolor='#BC002D',
marker='s',
color='white',
)
ax.set_facecolor('white')
ax.margins(x=0.12, y=0.12)
plt.axis("equal")
plt.axis("off")
plt.savefig(OUTPUT_PNG, dpi=380, bbox_inches="tight")
Conclusion
This sort of map demonstrates the power simple, minimal visualizations can have for conveying surprisingly deep analytical insights. It also just looks incredible.