2005 年諾貝爾經濟學獎得主托馬斯·謝林(Thomas Schelling)在上世紀 70 年代就紐約的人種居住分布得出了著名的 Schelling segregation model,這是一個 ABM(agent-based model),當年謝林只能通過鉛筆在紙上進行模擬,而這次則利用 Python 進行仿真實現這個模型。
在計算機科學中,基於 Agent 的模型(agent-based models)被用來評估獨立(autonomous)agent(諸如個體、群組或物體)在整個系統中的影響。這個強大的分析工具常被用在實驗難以開展或者費用太高的時候。在社會科學,計算機科學,經濟學和商務各領域,這個模型都有着相當廣泛的應用。
在本文中,我會向你介紹用基於 Agent 的模型理解復雜現象的威力。為此,我們會用到一些 Python,社會學的案例分析和 Schelling 模型。
1. 案例分析
如果你觀察多民族(multi-ethnic)混居城市的種族(racial)分布,你會對不可思議的種族隔離感到驚訝。舉個例子,下面是美國人口普查局(US Census)用種族和顏色對應標記的紐約市地圖。種族隔離清晰可見。
許多人會從這個現象中認定人是偏隘的(intolerant),不願與其它種族比鄰而居。然而進一步看,會發現細微的差別。2005 年的諾貝爾經濟學獎得主托馬斯·謝林(Thomas Schelling)在上世紀七十年代,就對這方面非常感興趣,並建立了一個基於 Agent 的模型——“Schelling 隔離模型”的來解釋這種現象。借助這個極其簡單的模型,Schelling 會告訴我們,宏觀所見並非微觀所為(What’s going down)。
我們會借助 Schelling 模型模擬一些仿真來深刻理解隔離現象。
2. Schelling 隔離模型:設置和定義
基於 Agent 的模型需要三個參數:1)Agents,2)行為(規則)和 3)總體層面(aggregate level)的評估。在 Schelling 模型中,Agents 是市民,行為則是基於相似比(similarity ratio )的搬遷,而總體評估評估就是相似比。
假設城市有 n 個種族。我們用唯一的顏色來標識他們,並用網格來代表城市,每個單元格則是一間房。要么是空房子,要么有居民,且數量為 1。如果房子是空的,我們用白色標識。如果有居民,我們用此人的種群顏色來標識。我們把每個人周邊房屋(上下左右、左上右上、左下右下)定義為鄰居。
Schelling 的目的是想測試當居民更傾向於選擇同種族的鄰居(甚至多種族)時會如果發生什么。如果同種族鄰居的比例上升到確定閾值(稱之為相似性閾值(Similarity Threshold)),那么我們認為這個人已經滿意(satisfied)了。如果還沒有,就是不滿意。
Schelling 的仿真如下。首先我們將人隨機分配到城里並留空一些房子。對於每個居民,我們都會檢查他(她)是否滿意。如果滿意,我們什么也不做。但如果不滿意,我們把他分配到空房子。仿真經過幾次迭代后,我們會觀察最終的種族分布。
3. Schelling 模型的 Python 實現
早在上世紀 70 年代,Schelling 就用鉛筆和硬幣在紙上完成了他的仿真。我們現在則用 Python 來完成相同的仿真。
為了模擬仿真,我們首先導入一些必要的庫。除了 Matplotlib
以外,其它庫都是 Python 默認安裝的。
1
2
3
4
5
|
import matplotlib.pyplot as plt
import itertools
import random
import copy
|
接下來,定義名為 Schelling
的類,涉及到 6 個參數:城市的寬和高,空房子的比例,相似性閾值,迭代數和種族數。我們在這個類中定義了 4 個方法:populate
,is_unsatisfied
,update
,move_to_empty
, 還有 plot
)。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
|
class Schelling:
def __init__(self, width, height, empty_ratio, similarity_threshold, n_iterations, races = 2):
self.width = width
self.height = height
self.races = races
self.empty_ratio = empty_ratio
self.similarity_threshold = similarity_threshold
self.n_iterations = n_iterations
self.empty_houses = []
self.agents = {}
def populate(self):
....
def is_unsatisfied(self, x, y):
....
def update(self):
....
def move_to_empty(self, x, y):
....
def plot(self):
....
|
poplate
方法被用在仿真的開頭,這個方法將居民隨機分配在網格上。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
def populate(self):
self.all_houses = list(itertools.product(range(self.width),range(self.height)))
random.shuffle(self.all_houses)
self.n_empty = int( self.empty_ratio * len(self.all_houses) )
self.empty_houses = self.all_houses[:self.n_empty]
self.remaining_houses = self.all_houses[self.n_empty:]
houses_by_race = [self.remaining_houses[i::self.races] for i in range(self.races)]
for i in range(self.races):
# 為每個種族創建 agent
self.agents = dict(
self.agents.items() +
dict(zip(houses_by_race[i], [i+1]*len(houses_by_race[i]))).items()
|
is_unsatisfied
方法把房屋的 (x, y)
坐標作為傳入參數,查看同種群鄰居的比例,如果比理想閾值(happiness threshold)高則返回 True
,否則返回 False
。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
|
def is_unsatisfied(self, x, y):
race = self.agents[(x,y)]
count_similar = 0
count_different = 0
if x > 0 and y > 0 and (x-1, y-1) not in self.empty_houses:
if self.agents[(x-1, y-1)] == race:
count_similar += 1
else:
count_different += 1
if y > 0 and (x,y-1) not in self.empty_houses:
if self.agents[(x,y-1)] == race:
count_similar += 1
else:
count_different += 1
if x < (self.width-1) and y > 0 and (x+1,y-1) not in self.empty_houses:
if self.agents[(x+1,y-1)] == race:
count_similar += 1
else:
count_different += 1
if x > 0 and (x-1,y) not in self.empty_houses:
if self.agents[(x-1,y)] == race:
count_similar += 1
else:
count_different += 1
if x < (self.width-1) and (x+1,y) not in self.empty_houses:
if self.agents[(x+1,y)] == race:
count_similar += 1
else:
count_different += 1
if x > 0 and y < (self.height-1) and (x-1,y+1) not in self.empty_houses:
if self.agents[(x-1,y+1)] == race:
count_similar += 1
else:
count_different += 1
if x > 0 and y < (self.height-1) and (x,y+1) not in self.empty_houses:
if self.agents[(x,y+1)] == race:
count_similar += 1
else:
count_different += 1
if x < (self.width-1) and y < (self.height-1) and (x+1,y+1) not in self.empty_houses:
if self.agents[(x+1,y+1)] == race:
count_similar += 1
else:
count_different += 1
if (count_similar+count_different) == 0:
return False
else:
return float(count_similar)/(count_similar+count_different) < self.happy_threshold
|
update
方法將查看網格上的居民是否尚未滿意,如果尚未滿意,將隨機把此人分配到空房子中。並模擬 n_iterations
次。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
def update(self):
for i in range(self.n_iterations):
self.old_agents = copy.deepcopy(self.agents)
n_changes = 0
for agent in self.old_agents:
if self.is_unhappy(agent[0], agent[1]):
agent_race = self.agents[agent]
empty_house = random.choice(self.empty_houses)
self.agents[empty_house] = agent_race
del self.agents[agent]
self.empty_houses.remove(empty_house)
self.empty_houses.append(agent)
n_changes += 1
print n_changes
if n_changes == 0:
break
|
move_to_empty
方法把房子坐標(x, y)
作為傳入參數,並將 (x, y)
房間內的居民遷入空房子。這個方法被 update
方法調用,會把尚不滿意的人遷入空房子。
1
2
3
4
5
6
7
8
|
def move_to_empty(self, x, y):
race = self.agents[(x,y)]
empty_house = random.choice(self.empty_houses)
self.updated_agents[empty_house] = race
del self.updated_agents[(x, y)]
self.empty_houses.remove(empty_house)
self.empty_houses.append((x, y))
|
plot
方法用來繪制整個城市和居民。我們隨時可以調用這個方法來了解城市的居民分布。這個方法有兩個傳入參數:title
和 file_name
。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
def plot(self, title, file_name):
fig, ax = plt.subplots()
# 如果要進行超過 7 種顏色的仿真,你應該相應地進行設置
agent_colors = {1:'b', 2:'r', 3:'g', 4:'c', 5:'m', 6:'y', 7:'k'}
for agent in self.agents:
ax.scatter(agent[0]+0.5, agent[1]+0.5, color=agent_colors[self.agents[agent]])
ax.set_title(title, fontsize=10, fontweight='bold')
ax.set_xlim([0, self.width])
ax.set_ylim([0, self.height])
ax.set_xticks([])
ax.set_yticks([])
plt.savefig(file_name)
|
4. 仿真
現在我們實現了 Schelling 類,可以模擬仿真並繪制結果。我們會按照下面的需求(characteristics)進行三次仿真:
- 寬 = 50,而高 = 50(包含 2500 間房子)
- 30% 的空房子
- 相似性閾值 = 30%(針對仿真 1),相似性閾值 = 50%(針對仿真 2),相似性閾值 = 80%(針對仿真 3)
- 最大迭代數 = 500
- 種族數量 = 2
創建並“填充”城市。
1
2
3
4
5
6
7
8
9
|
schelling_1 = Schelling(50, 50, 0.3, 0.3, 500, 2)
schelling_1.populate()
schelling_2 = Schelling(50, 50, 0.3, 0.5, 500, 2)
schelling_2.populate()
schelling_3 = Schelling(50, 50, 0.3, 0.8, 500, 2)
schelling_3.populate()
|
接下來,我們繪制初始階段的城市。注意,相似性閾值在城市的初始狀態不起作用。
1
2
|
schelling_1_1.plot('Schelling Model with 2 colors: Initial State', 'schelling_2_initial.png')
|
下面我們運行 update
方法,繪制每個相似性閾值的最終分布。廈門叉車出租
1
2
3
4
5
6
7
8
|
schelling_1.update()
schelling_2.update()
schelling_3.update()
schelling_1.plot('Schelling Model with 2 colors: Final State with Similarity Threshold 30%', 'schelling_2_30_final.png')
schelling_2.plot('Schelling Model with 2 colors: Final State with Similarity Threshold 50%', 'schelling_2_50_final.png')
schelling_3.plot('Schelling Model with 2 colors: Final State with Similarity Threshold 80%', 'schelling_2_80_final.png')
|
觀察以上的繪圖,我們可以發現相似性閾值越高,城市的隔離度就越高。此外,我們還會發現即便相似性閾值很小,城市依舊會產生隔離。換言之,即使居民非常包容(tolerant)(相當於相似性閾值很小),還是會以隔離告終。我們可以總結出:宏觀所見並非微觀所為。
5. 測量隔離
以上仿真,我們只通過可視化來確認隔離發生。然而,我們卻沒有對隔離的計算進行定量評估。本節我們會定義這個評估標准,我們也會模擬一些仿真來確定理想閾值和隔離程度的關系。
首先在 Schelling
類中添加 calculate_similarity
方法。這個方法會計算每個 Agent 的相似性並得出均值。我們會用平均相似比評估隔離程度。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
|
def calculate_similarity(self):
similarity = []
for agent in self.agents:
count_similar = 0
count_different = 0
x = agent[0]
y = agent[1]
race = self.agents[(x,y)]
if x > 0 and y > 0 and (x-1, y-1) not in self.empty_houses:
if self.agents[(x-1, y-1)] == race:
count_similar += 1
else:
count_different += 1
if y > 0 and (x,y-1) not in self.empty_houses:
if self.agents[(x,y-1)] == race:
count_similar += 1
else:
count_different += 1
if x < (self.width-1) and y > 0 and (x+1,y-1) not in self.empty_houses:
if self.agents[(x+1,y-1)] == race:
count_similar += 1
else:
count_different += 1
if x > 0 and (x-1,y) not in self.empty_houses:
if self.agents[(x-1,y)] == race:
count_similar += 1
else:
count_different += 1
if x < (self.width-1) and (x+1,y) not in self.empty_houses:
if self.agents[(x+1,y)] == race:
count_similar += 1
else:
count_different += 1
if x > 0 and y < (self.height-1) and (x-1,y+1) not in self.empty_houses:
if self.agents[(x-1,y+1)] == race:
count_similar += 1
else:
count_different += 1
if x > 0 and y < (self.height-1) and (x,y+1) not in self.empty_houses:
if self.agents[(x,y+1)] == race:
count_similar += 1
else:
count_different += 1
if x < (self.width-1) and y < (self.height-1) and (x+1,y+1) not in self.empty_houses:
if self.agents[(x+1,y+1)] == race:
count_similar += 1
else:
count_different += 1
try:
similarity.append(float(count_similar)/(count_similar+count_different))
except:
similarity.append(1)
return sum(similarity)/len(similarity)
|
接下去,我們算出每個相似性閾值的平均相似比,並繪制出相似性閾值和相似比之間的關系。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
similarity_threshold_ratio = {}
for i in [0, 0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7]:
schelling = Schelling(50, 50, 0.3, i, 500, 2)
schelling.populate()
schelling.update()
similarity_threshold_ratio[i] = schelling.calculate_similarity()
fig, ax = plt.subplots()
plt.plot(similarity_threshold_ratio.keys(), similarity_threshold_ratio.values(), 'ro')
ax.set_title('Similarity Threshold vs. Mean Similarity Ratio', fontsize=15, fontweight='bold')
ax.set_xlim([0, 1])
ax.set_ylim([0, 1.1])
ax.set_xlabel("Similarity Threshold")
ax.set_ylabel("Mean Similarity Ratio")
plt.savefig('schelling_segregation_measure.png')
|
通過上圖,可以發現即便相似性閾值非常小,依然會得到很高的隔離度(由平均相似性評估)。舉個例子,相似閾值為 0.3,會得到 0.75 的平均相似性。我們可以通過量化再次確定宏觀所見並非微觀所為。
6. 總結
在本文中,我們介紹了一個基於 Agent 的模型——“Schelling 隔離模型”,並用 Python 實現。這個十分簡單的模型幫助我們理解了非常復雜的現象:多民族城市中的種族隔離。我們可以發現城市種族的高度隔離不必解讀成個體層面的偏隘。