Changes in Ho Chi Minh city map

Summary

Changes in administrative levels in HCM city from 2000 - 2026

  • 2003 - 2 new districts and reorganize communes (ref)

    • Tan Phu (split from Tan Binh)

    • Binh Tan (split from Binh Chanh)

  • 2006 - Reorganize communes in 3 districts (ref)

    • Adjust communes in 3 districts: Go Vap, District 12, Tan Binh
  • 2021 - Establish Thu Duc city and more commune adjustments (ref)

    • Thu Duc city merged from 3 districts: 2, 9 and Thu Duc

    • Adjust communes under Thu Duc city

  • 2023 - early 2025 - Reorganize communes (again) (ref)

    • Adjust communes in several districts: 3,4,5,6,8,10, 11,Phu Nhuan, Binh Thanh, Go Vap
    • Most of these changes were in effect by 2025-01-01
  • July 2025 - Expand city and remove district level (ref)

    • Merge Binh Duong and Vung Tau to Ho Chi Minh city

    • Remove districts level, merge existing communes into 168 communes

Overview

  • 2000 - 2003: 22 districts, 317 communes

  • 2003 - 2021: 24 districts, 322 communes

  • 2021 - 2023: 22 districts, 312 communes

  • 2023 - 2025: 22 districts, 273 communes

  • 2025 - present (April, 2026): 168 communes

Data for R

Preparing data

Since not all changes are captured on open-sourced shapefile data, we can use available datasets for major changes and manually merge polygons for minor ones.

Source for raw data

  • Before 2021 reform:

    • GADM 4.1 (24 districts, 322 communes)
  • 2021 - 2023

    • GISvn (312 HCMC communes, Ba Ria - Vung Tau and Binh Duong communes before merging)
  • After 2025 reform: GIS VN (168 communes)

# Load some dependencies
library(tidyverse)
library(sf)
library(terra)
library(janitor)
library(stringi)
library(downloadthis)
Preparing district data
# Roughly HCMC spatial extent
hcmc_cropvector <- c(106.2, 107.2, 10.2, 11.3)
# hcmc_extent <- ext(cropvector)

district_24 <- read_sf("data/raw/before_reforms/gadm41_VNM_shp/gadm41_VNM_2.shp",
                       options = "ENCODING=UTF-8") %>% 
  clean_names() %>% 
  st_crop(
    xmin = hcmc_cropvector[1],
    xmax = hcmc_cropvector[2],
    ymin = hcmc_cropvector[3],
    ymax = hcmc_cropvector[4]
  ) %>%
  filter(gid_1 == "VNM.25_1")
Preparing commune data
# The 312 communes map fr GISvn
commune_gis <- read_sf(
  "data/raw/before_reforms/gisvn_vnm_shp/Việt Nam (phường xã) - 63.shp",
  options = "ENCODING=UTF-8"
) %>%
  clean_names() %>% 
  mutate(
    ten_xa = str_remove(ten_xa, "^0+(?=[1-9])")
  ) %>% 
  rename(
    name_1 = ten_tinh,
    id_1 = ma_tinh,
    name_2 = ten_huyen,
    id_2 = ma_huyen,
    name_3 = ten_xa,
    id_3 = ma_xa,
    type_3 = loai,
  ) %>% 
  mutate(
    varname_2 = stri_trans_general(name_2, id = "Latin-ASCII"),
    varname_3 = stri_trans_general(name_3, id = "Latin-ASCII")
  ) %>% 
  select(
    name_1, id_1, name_2, id_2, varname_2, name_3, id_3, varname_3, geometry
  )

# Get communes for Binh Duong and Ba Ria - Vung Tau right before merge as well
commune_312 <- commune_gis %>% 
  filter(name_1 == "TP. Hồ Chí Minh")
commune_binhduong <- commune_gis %>% 
  filter(name_1 == "Bình Dương")
commune_br_vt <- commune_gis %>% 
  filter(name_1 == "Bà Rịa - Vũng Tàu")

# The 322 communes map fr GADM
commune_322 <- read_sf("data/raw/before_reforms/gadm41_VNM_shp/gadm41_VNM_3.shp",
                       options = "ENCODING=UTF-8") %>% 
  clean_names() %>% 
  st_crop(
    xmin = hcmc_cropvector[1],
    xmax = hcmc_cropvector[2],
    ymin = hcmc_cropvector[3],
    ymax = hcmc_cropvector[4]
  ) %>%
  filter(gid_1 == "VNM.25_1")


# The 168 communes map fr GIS vn
commune_168 <- read_sf("data/raw/after_2025_reforms/Việt Nam (phường xã) - 34/",
                       options = "ENCODING=UTF-8") %>% 
  filter(ten_tinh == "TP. Hồ Chí Minh") 

# rename GISvn data for consistency
commune_168 <- commune_168 %>% 
  rename(
    name_1 = ten_tinh,
    id_1 = ma_tinh,
    name_2 = ten_xa,
    id_2 = ma_xa,
    type_2 = loai,
    merged_fr = sap_nhap
  ) %>% 
  mutate(
    # make it a bit more consistent
    name_1 = str_remove(name_1, "TP. "),
    varname_2 = stri_trans_general(name_2, id = "Latin-ASCII")
  ) %>% 
  select(
    name_1, id_1, name_2, id_2, varname_2, type_2, merged_fr, geometry
  )
Merge function
# merge function
merge_polygon <- function(sf_dat, merge_map=list(
  "Thu Duc" = c("District 2", "District 9", "Thu Duc")
), colname="varname_2"){
  
  walk(names(merge_map), \(new_shp) {
    old_shps <- merge_map[[new_shp]]
    
    merged <- sf_dat  %>% 
      filter(.data[[colname]] %in% old_shps) %>% 
      st_union()
    
    if (new_shp %in% sf_dat[[colname]]) {
      # new_shp already exists --> update its geometry and drop other old shapes
      rm_shapes <- setdiff(old_shps, new_shp)
      sf_dat <<- sf_dat  %>% 
        mutate(geometry = if_else(.data[[colname]] == new_shp, merged, geometry)) %>%
        filter(!(.data[[colname]] %in% rm_shapes))
    } else {
      # new_shp doesn't exist --> append new row and drop old shapes
      new_row <- tibble("{colname}" := new_shp, geometry = merged) %>% st_sf()
      sf_dat <<- sf_dat  %>% 
        filter(!(.data[[colname]] %in% old_shps)) %>%
        bind_rows(new_row)
    }
  })

  sf_dat
}
Data for download
hcmc_shapefile <- list(
  boundary_pre_reform = district_24 %>% summarize(),
  boundary_post_reform = commune_168 %>% summarize(),
  district_24 = district_24, # sf
  commune_322 = commune_322, # sf
  commune_312 = commune_312, # sf
  commune_binhduong = commune_binhduong, # sf
  commune_br_vt = commune_br_vt, # sf
  commune_168 = commune_168, # sf
  merge_polygon = merge_polygon # function to merge polygons
)

Download data

Data for download is an R list with the following items

  • boundary_pre_reform - HCM city boundary before merging with Vung Tau, Binh Duong

  • boundary_post_reform - HCM city boundary after merging with Vung Tau, Binh Duong

  • district_24 - 24 districts in HCM city (during 2003 - 2021)

  • commune_322 - 322 communes in HCM city (during 2003 - 2021)

  • commune_312 - 312 communes in HCM city (during 2021 - 2023)

  • commune_168 - 168 communes in HCM city (after 2025 reform)

Using data

hcmc_shapefile$merge_polygon() can be used to quickly merge districts or communes

Example: Generate district map during 2021 - 2023, with Thu Duc city

thuduc_city <- hcmc_shapefile$merge_polygon(
  hcmc_shapefile$district_24,
  # specify merging D2, D9, Thu Duc into Thu Duc here
  merge_map = list(
    "Thu Duc city" = c("District 2", "District 9", "Thu Duc")
  ),
  colname="varname_2" # merging by variable varname_2 (i.e., district lvl)
)

ggplot(thuduc_city) +
  geom_sf(aes(fill=varname_2))

Example: Generate district map before 2003, without Tan Phu, Binh Tan

pre_2003 <- hcmc_shapefile$merge_polygon(
  hcmc_shapefile$district_24,
  # generate data before the split 2003
  merge_map = list(
    "Tan Binh" = c("Tan Phu", "Tan Binh"),
    "Binh Chanh" = c("Binh Tan", "Binh Chanh")
  ),
  colname="varname_2" # merging by variable varname_2 (i.e., district lvl)
)

ggplot(pre_2003) +
  geom_sf(aes(fill=varname_2))

Plots

Plot changes in HCM city (at admin lvl 2)

Visualize changes
cropvector_postreform <- c(106, 107.8, 10.2, 12)

walk2(
  list("2000 - 2003", "2003 - 2021", "2021 - 2025", "2025 - present"),
  list(pre_2003, hcmc_shapefile$district_24, thuduc_city, 
       hcmc_shapefile$commune_168 %>% 
        st_crop(
          xmin = cropvector_postreform[1],
          xmax = cropvector_postreform[2],
          ymin = cropvector_postreform[3],
          ymax = cropvector_postreform[4]
        ) ),
  \(period, shape){
    plt <- ggplot(shape) +
      geom_sf(aes(fill = varname_2)) +
      labs(title = paste0("HCMC Administrative Lvl. 2 Boundaries ", period)) +
      guides(fill = "none") +
      theme_minimal()
    
    print(plt)
  }
)