#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

Description of the map image for accessibility
Description of the map image for accessibility

Japanese Prefectures by Average # of People Served by Each Train Station

Click markers or scroll to explore

1

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.

2

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.

3

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.py
python
import 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.