22. 种族隔离#

22.1. 大纲#

1969年,托马斯谢林(Thomas Schelling)提出了一个简单但有趣的种族隔离模型 [Schelling, 1969]

他的模型研究了一个社区中不同种族的居民在互动中所产生的动态变化。

与谢林的许多著作一样,该模型展示了局部互动如何导致令人惊讶的总体结果。

它研究了这样一种情况,即个体(可以认为是家庭)对同一种族的邻居具有相对温和的偏好。

例如,这些个体可能对混合种族的社区感到舒适,但当他们感觉被不同种族的人“包围”时会感到不舒服。

谢林说明了以下令人惊讶的结果:在这种情况中,混合种族的社区很可能是不稳定的,随着时间的推移会趋于崩溃。

事实上,该模型预测出的社区分化严重,隔离程度高。

换句话说,即使人们的偏好不是特别极端,也会出现极端的隔离结果。

之所以会出现这些极端结果,是因为模型中的个体(例如,城市中的家庭)之间的互动,推动了模型中的自我强化机制。

随着讲座的展开,这些想法将变得更加清晰。

为了表彰谢林在种族隔离和其他研究方面的工作,他获得了2005年诺贝尔经济学奖(与罗伯特·奥曼共同获得)。

让我们从一些代码的导入开始:

import matplotlib.pyplot as plt
from random import uniform, seed
from math import sqrt
import numpy as np
import matplotlib as mpl
FONTPATH = "fonts/SourceHanSerifSC-SemiBold.otf"
mpl.font_manager.fontManager.addfont(FONTPATH)
plt.rcParams['font.family'] = ['Source Han Serif SC']

22.2. 模型#

在这一节中,我们将构建谢林模型的一种版本。

22.2.1. 设置#

我们将介绍一个与原始谢林模型不同的变种,但它同样易于编程,也能捕捉到谢林的主要思想。

假设我们有两种类型的人:橙色人和绿色人。

假设每种类型都有n个人。

这些个体都居住在一个单位正方形上。

因此,一个个体的位置(例如,地址)只是一个点(x,y),其中0<x,y<1

  • 所有点(x,y)满足0<x,y<1 的集合称为单位正方形

  • 下面我们用S表示单位正方形

22.2.2. 偏好#

我们将说一个个体是 满意(快乐) 的,如果她最近的10个邻居中有5个或以上是同类型的。

而一个不快乐的个体被称为不满意(不快乐)

例如,

  • 如果一个个体是橙色的,她最近的10个邻居中有5个是橙色的,那么她是满意(快乐)的。

  • 如果一个个体是绿色的,她最近的10个邻居中有8个是橙色的,那么她是不满意(不快乐)的。

“最近”是指欧几里得度量(欧几里得距离)

要注意的是,个体反对居住在混合区域。

如果他们有一半的邻居是另一种颜色,他们也会完全满意(快乐)。

22.2.3. 行为#

最初,个体们混居在一起。

换作这个模型的语言,即我们假设每个个体的初始位置是从单位正方形 S 上的一个双变量均匀分布中独立抽取的。

  • 首先,他们的 x 坐标从 (0,1) 上的均匀分布中抽取

  • 然后,他们的 y 坐标从同一分布中独立地抽取。

现在,遍历集合中的所有个体,每个个体都有机会留下或移动。

每个个体如果满意(快乐)就留下,不满意(不快乐)就移动。

移动的算法如下:

Algorithm 22.1 (跳转链算法)

  1. S 中随机抽取一个位置

  2. 如果在新位置上感到满意(快乐),就移动到那里

  3. 否则,回到步骤 1

我们在个体中不断循环,每次都允许一个不满意(不快乐)的个体移动。

我们继续循环,直到没有人愿意移动为止。

22.3. 结果#

让我们现在实现和运行这个模拟。

在下文中,个体被模型化为对象

以下是它们的结构指示:

* 数据:

    * 类型(绿色或橙色)
    * 位置

* 方法:

    * 根据其他个体的位置确定是否满意(快乐)
    * 如果不满意(不快乐),移动
        * 找到一个满意(快乐)的新位置

让我们构建这个结构。

class Agent:

    def __init__(self, type):
        self.type = type
        self.draw_location()

    def draw_location(self):
        self.location = uniform(0, 1), uniform(0, 1)

    def get_distance(self, other):
        "计算自己与另一个体之间的欧几里得距离。"
        a = (self.location[0] - other.location[0])**2
        b = (self.location[1] - other.location[1])**2
        return sqrt(a + b)

    def happy(self,
                agents,                # 其他个体的列表
                num_neighbors=10,      # 视为邻居的个体数量
                require_same_type=5):  # 必须是同一类型的邻居数量
        """
            如果有足够多的最近的邻居是同一类型,则返回True。
        """

        distances = []

        # distances是一个包含(d, agent)的列表,其中d是agent到self的距离
        for agent in agents:
            if self != agent:
                distance = self.get_distance(agent)
                distances.append((distance, agent))

        # 根据距离从小到大排序
        distances.sort()

        # 提取相邻的个体
        neighbors = [agent for d, agent in distances[:num_neighbors]]

        # 计算有多少邻居与自己类型相同
        num_same_type = sum(self.type == agent.type for agent in neighbors)
        return num_same_type >= require_same_type

    def update(self, agents):
        "如果不满意(不快乐),随机选择新位置直到满意(快乐)。"
        while not self.happy(agents):
            self.draw_location()

运用以下的代码,我们可以获取个体们的列表,并绘制出他们在单位正方形上的位置图。

橙色个体用橙点表示,绿色个体用绿点表示。

def plot_distribution(agents, cycle_num):
    "绘制经过cycle_num轮循环后的个体分布图。"
    x_values_0, y_values_0 = [], []
    x_values_1, y_values_1 = [], []
    # == 获取每种类型的位置 == #
    for agent in agents:
        x, y = agent.location
        if agent.type == 0:
            x_values_0.append(x)
            y_values_0.append(y)
        else:
            x_values_1.append(x)
            y_values_1.append(y)
    fig, ax = plt.subplots()
    plot_args = {'markersize': 8, 'alpha': 0.8}
    ax.set_facecolor('azure')
    ax.plot(x_values_0, y_values_0,
        'o', markerfacecolor='orange', **plot_args)
    ax.plot(x_values_1, y_values_1,
        'o', markerfacecolor='green', **plot_args)
    ax.set_title(f'周期 {cycle_num-1}')
    plt.show()

这里有一段伪代码,它描述了主循环的过程,我们在这个过程中遍历每个个体,直到没有个体愿意移动为止。

伪代码如下

绘制分布
while 个体还在移动
    for 每个个体 in 个体们
        给予个体机会移动
绘制分布

真实的代码如下

def run_simulation(num_of_type_0=600,
                   num_of_type_1=600,
                   max_iter=100_000,       # 最大迭代次数
                   set_seed=1234):

    # 设置种子以确保可重复性
    seed(set_seed)

    # 创建类型0的个体列表
    agents = [Agent(0) for i in range(num_of_type_0)]
    # 添加类型1的个体列表
    agents.extend(Agent(1) for i in range(num_of_type_1))

    # 初始化计数器
    count = 1

    # 绘制初始分布
    plot_distribution(agents, count)

    # 循环直到没有个体愿意移动
    while count < max_iter:
        print('进入循环 ', count)
        count += 1
        no_one_moved = True
        for agent in agents:
            old_location = agent.location
            agent.update(agents)
            if agent.location != old_location:
                no_one_moved = False
        if no_one_moved:
            break

    # 绘制最终分布
    plot_distribution(agents, count)

    if count < max_iter:
        print(f'在 {count} 次迭代后收敛。')
    else:
        print('达到迭代上限并终止。')

让我们看一下结果。

run_simulation()
_images/84b08c15b904d7c1140c8a98385b4d29dc08d67f51f3712a863f0edb6d6c9595.png
进入循环  1
进入循环  2
进入循环  3
进入循环  4
进入循环  5
进入循环  6
进入循环  7
_images/a084374af7811bd029eae9536868a44759c767021f659f0f957c8db0757f0097.png
在 8 次迭代后收敛。

如上所述,个体们最初是随机混合在一起的。

但经过几轮循环后,它们会被隔离到不同的区域。

在这个例子中,程序在一组个体中循环了几个周期后就终止了,这表明所有个体都达到了幸福的状态。

这些图片的惊人之处在于种族融合的瓦解速度是如此之快。

尽管实际上模型中的人并不介意和其他类型的人混居。

即使是有这些偏好,结果依旧是高度隔离。

22.4. 练习#

Exercise 22.1

我们之前用到的面向对象式编程虽然整洁,但相比于过程式编程(即,围绕函数而非对象和方法的代码)更难优化。

尝试编写一个新版本的模型,它能够存储:

  • 所有个体的位置,作为一个二维的NumPy浮点数数组。

  • 所有个体的类型,作为一个平面的NumPy整数数组。

编写对这些数据进行操作的函数,并根据上述逻辑更新模型。

不过,要实现以下两个变化:

  1. 个体们被随机提供移动机会(即,被随机选中并给予移动的机会)。

  2. 个体们移动后,会有0.01的概率翻转其类型。

第二个变化为模型引入了额外的随机性。

(我们可以想象,每隔一段时间,就会有一个个体迁移到不同的城市,并以很小的概率被另一种类型的个体替换。)

当我们运行这个程序时,我们再次发现混合社区会瓦解,隔离现象会出现。

这里是一个运行的样例。

sim_random_select(max_iter=50_000, flip_prob=0.01, test_freq=10_000)
_images/e044d618857208abf19037b52c2600e756c2f94a7ef07b552e47fefeaba67415.png _images/102c1de230db9b4a958bb87128661f6e9dfa03a86d2ce10eaa0fdf5b0721e6c9.png _images/e5d6400691c5189433fb87e12baf7326809d8197495c5e406ecab9c6f06cff58.png _images/94e11c35afd3ec9e218492780d04a4f873301840f948b369043bfbf7f79936f2.png _images/0f81309afea5b4cfcee2964bb48a7b6e260fb709bb1724eba09250862145afc4.png _images/5746ed008839c63e46d2568d5c890cb69b1cb56383b3afd515619b9a69c30186.png
在迭代 50001 时终止