Visualizing All 116 Chinese 211 Universities with an Animated Python Map
This article explains the background of China's 211 university project, lists the 116 institutions across six tiers, and provides a complete Python script that uses geopandas, matplotlib, and animation to create an interactive map showing the geographic distribution of these top universities.
What Is a 211 University?
The "211" project, launched for the 21st century, aims to build around 100 key higher‑education institutions and disciplines; it became the largest and most prestigious construction effort in Chinese higher education, reflecting the national strategy of "science and education strengthening the country".
Although originally limited to about 100 schools, the list has expanded to 116 universities and is no longer open for new applications.
Six Tiers of 211 Universities
Tier 1: Tsinghua University, Peking University, Fudan University, Zhejiang University, Shanghai Jiao Tong University.
Tier 2: 20 "985" universities including University of Science and Technology of China, Renmin University of China, Nanjing University, Tongji University, and others.
Tier 3: Thirteen universities plus the National University of Defense Technology, such as Northwestern Polytechnical University, Dalian University of Technology, Sichuan University, and others.
Tier 4: Includes Shanghai University of Finance and Economics, Central University of Finance and Economics, University of International Business and Economics, and many more.
Tier 5: Includes China Pharmaceutical University, Donghua University, Hohai University, Beijing Forestry University, and others.
Tier 6: Includes Hunan Normal University, Fuzhou University, Dalian Maritime University, and other regional universities.
Below is an animated map that visualizes the locations of all 116 211 universities using Python.
import numpy as np
import pandas as pd
import geopandas as gpd
import shapely
from shapely import geometry as geo
from shapely import wkt
import matplotlib.pyplot as plt
import matplotlib.animation as animation
import contextily as ctx
import imageio
import os
from PIL import Image
plt.rcParams['font.family'] = 'sans-serif'
plt.rcParams['font.sans-serif'] = ['SimHei']
plt.rcParams['axes.unicode_minus'] = False
plt.rcParams['animation.writer'] = 'html'
plt.rcParams['animation.embed_limit'] = 100
def rgba_to_rgb(img_rgba):
img_rgb = Image.new("RGB", img_rgba.size, (255, 255, 255))
img_rgb.paste(img_rgba, mask=img_rgba.split()[3])
return img_rgb
def html_to_gif(html_file, gif_file, duration=0.5):
path = html_file.replace(".html", "_frames")
images = [os.path.join(path, x) for x in sorted(os.listdir(path))]
frames = [imageio.imread(x) for x in images]
if frames[0].shape[-1] == 4:
frames = [np.array(rgba_to_rgb(Image.fromarray(x))) for x in frames]
imageio.mimsave(gif_file, frames, 'gif', duration=duration)
return gif_file
cmap = ['#2E91E5', '#1CA71C', '#DA16FF', '#B68100', '#EB663B', '#00A08B', '#FC0080', '#6C7C32', '#862A16', '#620042', '#DA60CA', '#0D2A63'] * 100
def getCoords(geom):
if isinstance(geom, geo.MultiPolygon):
return [np.array(g.exterior) for g in geom.geoms]
elif isinstance(geom, geo.Polygon):
return [np.array(geom.exterior)]
elif isinstance(geom, geo.LineString):
return [np.array(geom)]
elif isinstance(geom, geo.MultiLineString):
return [np.array(x) for x in geom.geoms]
else:
raise Exception("geom must be one of [polygon,MultiPolygon,LineString,MultiLineString]!")
# Load base map data
dfprovince = gpd.read_file("./data/dfprovince.geojson").set_crs("epsg:4326").to_crs("epsg:2343")
dfnanhai = gpd.read_file("./data/dfnanhai.geojson").set_crs("epsg:4326").to_crs("epsg:2343")
dfline9 = dfnanhai[(dfnanhai["LENGTH"] > 1.0) & (dfnanhai["LENGTH"] < 2.0)]
# Load university point data
df985 = gpd.read_file("./data/中国985大学.geojson").set_crs("epsg:4326").to_crs("epsg:2343")
df211 = gpd.read_file("./data/中国211大学.geojson").set_crs("epsg:4326").to_crs("epsg:2343")
dfpoints = pd.concat([df985, df211], axis=0)
df = pd.DataFrame({
"x": [pt.x for pt in dfpoints["geometry"]],
"y": [pt.y for pt in dfpoints["geometry"]]
})
df["z"] = 1.0
df.index = dfpoints["name"].values
def bubble_map_dance(df, title="中国116所211高校位置分布", filename=None, figsize=(8,6), dpi=144, duration=0.5,
anotate_points=["北京邮电大学","南昌大学","华中农业大学","东华大学","云南大学","陕西师范大学","内蒙古大学","西藏大学","新疆大学","青海大学","哈尔滨工程大学"]):
fig, ax_base = plt.subplots(figsize=figsize, dpi=dpi)
ax_child = fig.add_axes([0.800, 0.125, 0.10, 0.20])
def plot_frame(i):
ax_base.clear(); ax_child.clear()
# Draw base map
polygons = [getCoords(x) for x in dfprovince["geometry"]]
for coords in polygons:
for x in coords:
poly = plt.Polygon(x, fill=True, ec="gray", fc="white", alpha=0.5, linewidth=.8)
ax_base.add_patch(poly)
ax_child.add_patch(poly)
# Draw nine‑dash line
lines = [np.array(ln) for geom in dfline9["geometry"] for ln in getCoords(geom)]
for ln in lines:
x, y = np.transpose(ln)
line = plt.Line2D(x, y, color="gray", linestyle="-.", linewidth=1.5)
ax_base.add_artist(line)
ax_child.add_artist(line)
# Set limits
bounds = dfprovince.total_bounds
ax_base.set_xlim(bounds[0]-(bounds[2]-bounds[0])/10, bounds[2]+(bounds[2]-bounds[0])/10)
ax_base.set_ylim(bounds[1]+(bounds[3]-bounds[1])/3.5, bounds[3]+(bounds[3]-bounds[1])/100)
ax_child.set_xlim(bounds[2]-(bounds[2]-bounds[0])/2.5, bounds[2]-(bounds[2]-bounds[0])/20)
ax_child.set_ylim(bounds[1]-(bounds[3]-bounds[1])/20, bounds[1]+(bounds[3]-bounds[1])/2)
ax_child.set_xticks([]); ax_child.set_yticks([])
# Scatter points
k = i//3 + 1
m = i%3
dfdata = df.iloc[:k]
dftmp = df.iloc[:k-1]
if not dftmp.empty:
ax_base.scatter(dftmp["x"], dftmp["y"], s=100*dftmp["z"]/df["z"].mean(), c=cmap[:len(dftmp)], alpha=0.3, zorder=3)
ax_child.scatter(dftmp["x"], dftmp["y"], s=100*dftmp["z"]/df["z"].mean(), c=cmap[:len(dftmp)], alpha=0.3, zorder=3)
for i, p in enumerate(dftmp.index):
px, py, pz = dftmp.loc[p, ["x", "y", "z"]]
if p in anotate_points:
ax_base.annotate(p, xy=(px, py), xytext=(-15, 10), textcoords="offset points",
fontsize=10, fontweight="bold", color=cmap[i])
# Title and ranking number
ax_base.text(0.5, 0.95, title, va="center", ha="center", size=12, transform=ax_base.transAxes)
ax_base.text(0.5, 0.5, f"NO.{k}", va="center", ha="center", alpha=0.3, size=50, transform=ax_base.transAxes)
# Highlight newest point
if m == 0:
px, py, pz = dfdata.iloc[-1][["x", "y", "z"]]
p = dfdata.index[-1]
ax_base.scatter(px, py, s=800*pz/df["z"].mean(), c=cmap[len(dfdata)-1:len(dfdata)], alpha=0.5, zorder=4)
ax_base.annotate(p, xy=(px, py), xytext=(-15, 10), textcoords="offset points",
fontsize=20, fontweight="bold", color=cmap[k-1], zorder=5)
elif m == 1:
px, py, pz = dfdata.iloc[-1][["x", "y", "z"]]
p = dfdata.index[-1]
ax_base.scatter(px, py, s=400*pz/df["z"].mean(), c=cmap[len(dfdata)-1:len(dfdata)], alpha=0.5, zorder=4)
ax_base.annotate(p, xy=(px, py), xytext=(-15, 10), textcoords="offset points",
fontsize=15, fontweight="bold", color=cmap[k-1], zorder=5)
else:
px, py, pz = dfdata.iloc[-1][["x", "y", "z"]]
p = dfdata.index[-1]
ax_base.scatter(px, py, s=100*pz/df["z"].mean(), c=cmap[len(dfdata)-1:len(dfdata)], alpha=0.5, zorder=4)
ax_base.annotate(p, xy=(px, py), xytext=(-15, 10), textcoords="offset points",
fontsize=10, fontweight="bold", color=cmap[k-1], zorder=5)
my_animation = animation.FuncAnimation(fig, plot_frame, frames=range(0, 3*len(df)), interval=int(duration*1000))
if filename is None:
try:
from IPython.display import HTML
HTML(my_animation.to_jshtml())
return HTML(my_animation.to_jshtml())
except ImportError:
pass
else:
my_animation.save(filename)
return filename
html_file = "中国116所211高校位置分布.html"
bubble_map_dance(df, filename=html_file)
gif_file = html_file.replace(".html", ".gif")
html_to_gif(html_file, gif_file, duration=0.5)Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
Python Crawling & Data Mining
Life's short, I code in Python. This channel shares Python web crawling, data mining, analysis, processing, visualization, automated testing, DevOps, big data, AI, cloud computing, machine learning tools, resources, news, technical articles, tutorial videos and learning materials. Join us!
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
