Big Data 15 min read

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.

Python Crawling & Data Mining
Python Crawling & Data Mining
Python Crawling & Data Mining
Visualizing All 116 Chinese 211 Universities with an Animated Python Map

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.

Map of 211 Universities
Map of 211 Universities
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)
Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

PythonData visualizationGISgeopandashigher educationAnimated Map
Python Crawling & Data Mining
Written by

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!

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.