{ "cells": [ { "cell_type": "markdown", "id": "ed2eb115", "metadata": {}, "source": [ "\n", "<a id='schelling'></a>" ] }, { "cell_type": "markdown", "id": "a83d5237", "metadata": {}, "source": [ "# Racial Segregation\n", "\n", "\n", "<a id='index-1'></a>" ] }, { "cell_type": "markdown", "id": "7448c1e4", "metadata": {}, "source": [ "## Outline\n", "\n", "In 1969, Thomas C. Schelling developed a simple but striking model of racial\n", "segregation [[Schelling, 1969](https://intro.quantecon.org/zreferences.html#id231)].\n", "\n", "His model studies the dynamics of racially mixed neighborhoods.\n", "\n", "Like much of Schelling’s work, the model shows how local interactions can lead\n", "to surprising aggregate outcomes.\n", "\n", "It studies a setting where agents (think of households) have relatively mild\n", "preference for neighbors of the same race.\n", "\n", "For example, these agents might be comfortable with a mixed race neighborhood\n", "but uncomfortable when they feel “surrounded” by people from a different race.\n", "\n", "Schelling illustrated the follow surprising result: in such a setting, mixed\n", "race neighborhoods are likely to be unstable, tending to collapse over time.\n", "\n", "In fact the model predicts strongly divided neighborhoods, with high levels of\n", "segregation.\n", "\n", "In other words, extreme segregation outcomes arise even though people’s\n", "preferences are not particularly extreme.\n", "\n", "These extreme outcomes happen because of *interactions* between agents in the\n", "model (e.g., households in a city) that drive self-reinforcing dynamics in the\n", "model.\n", "\n", "These ideas will become clearer as the lecture unfolds.\n", "\n", "In recognition of his work on segregation and other research, Schelling was\n", "awarded the 2005 Nobel Prize in Economic Sciences (joint with Robert Aumann).\n", "\n", "Let’s start with some imports:" ] }, { "cell_type": "code", "execution_count": null, "id": "3b16893c", "metadata": { "hide-output": false }, "outputs": [], "source": [ "import matplotlib.pyplot as plt\n", "from random import uniform, seed\n", "from math import sqrt\n", "import numpy as np" ] }, { "cell_type": "markdown", "id": "da57a1cb", "metadata": {}, "source": [ "## The model\n", "\n", "In this section we will build a version of Schelling’s model." ] }, { "cell_type": "markdown", "id": "c273514f", "metadata": {}, "source": [ "### Set-Up\n", "\n", "We will cover a variation of Schelling’s model that is different from the\n", "original but also easy to program and, at the same time, captures his main\n", "idea.\n", "\n", "Suppose we have two types of people: orange people and green people.\n", "\n", "Assume there are $ n $ of each type.\n", "\n", "These agents all live on a single unit square.\n", "\n", "Thus, the location (e.g, address) of an agent is just a point $ (x, y) $, where\n", "$ 0 < x, y < 1 $.\n", "\n", "- The set of all points $ (x,y) $ satisfying $ 0 < x, y < 1 $ is called the **unit square** \n", "- Below we denote the unit square by $ S $ " ] }, { "cell_type": "markdown", "id": "8357091a", "metadata": {}, "source": [ "### Preferences\n", "\n", "We will say that an agent is *happy* if 5 or more of her 10 nearest neighbors are of the same type.\n", "\n", "An agent who is not happy is called *unhappy*.\n", "\n", "For example,\n", "\n", "- if an agent is orange and 5 of her 10 nearest neighbors are orange, then she is happy. \n", "- if an agent is green and 8 of her 10 nearest neighbors are orange, then she is unhappy. \n", "\n", "\n", "‘Nearest’ is in terms of [Euclidean distance](https://en.wikipedia.org/wiki/Euclidean_distance).\n", "\n", "An important point to note is that agents are **not** averse to living in mixed areas.\n", "\n", "They are perfectly happy if half of their neighbors are of the other color." ] }, { "cell_type": "markdown", "id": "cc5e70b2", "metadata": {}, "source": [ "### Behavior\n", "\n", "Initially, agents are mixed together (integrated).\n", "\n", "In particular, we assume that the initial location of each agent is an\n", "independent draw from a bivariate uniform distribution on the unit square $ S $.\n", "\n", "- First their $ x $ coordinate is drawn from a uniform distribution on $ (0,1) $ \n", "- Then, independently, their $ y $ coordinate is drawn from the same distribution. \n", "\n", "\n", "Now, cycling through the set of all agents, each agent is now given the chance to stay or move.\n", "\n", "Each agent stays if they are happy and moves if they are unhappy.\n", "\n", "The algorithm for moving is as follows" ] }, { "cell_type": "markdown", "id": "26377619", "metadata": {}, "source": [ "### (Jump Chain Algorithm)\n", "\n", "1. Draw a random location in $ S $ \n", "1. If happy at new location, move there \n", "1. Otherwise, go to step 1 \n", "\n", "\n", "We cycle continuously through the agents, each time allowing an unhappy agent\n", "to move.\n", "\n", "We continue to cycle until no one wishes to move." ] }, { "cell_type": "markdown", "id": "5185bcca", "metadata": {}, "source": [ "## Results\n", "\n", "Let’s now implement and run this simulation.\n", "\n", "In what follows, agents are modeled as [objects](https://python-programming.quantecon.org/python_oop.html).\n", "\n", "Here’s an indication of their structure:" ] }, { "cell_type": "markdown", "id": "8cdbda0c", "metadata": { "hide-output": false }, "source": [ "```text\n", "* Data:\n", "\n", " * type (green or orange)\n", " * location\n", "\n", "* Methods:\n", "\n", " * determine whether happy or not given locations of other agents\n", " * If not happy, move\n", " * find a new location where happy\n", "```\n" ] }, { "cell_type": "markdown", "id": "34a0b1ad", "metadata": {}, "source": [ "Let’s build them." ] }, { "cell_type": "code", "execution_count": null, "id": "9755c80d", "metadata": { "hide-output": false }, "outputs": [], "source": [ "class Agent:\n", "\n", " def __init__(self, type):\n", " self.type = type\n", " self.draw_location()\n", "\n", " def draw_location(self):\n", " self.location = uniform(0, 1), uniform(0, 1)\n", "\n", " def get_distance(self, other):\n", " \"Computes the euclidean distance between self and other agent.\"\n", " a = (self.location[0] - other.location[0])**2\n", " b = (self.location[1] - other.location[1])**2\n", " return sqrt(a + b)\n", "\n", " def happy(self,\n", " agents, # List of other agents\n", " num_neighbors=10, # No. of agents viewed as neighbors\n", " require_same_type=5): # How many neighbors must be same type\n", " \"\"\"\n", " True if a sufficient number of nearest neighbors are of the same\n", " type.\n", " \"\"\"\n", "\n", " distances = []\n", "\n", " # Distances is a list of pairs (d, agent), where d is distance from\n", " # agent to self\n", " for agent in agents:\n", " if self != agent:\n", " distance = self.get_distance(agent)\n", " distances.append((distance, agent))\n", "\n", " # Sort from smallest to largest, according to distance\n", " distances.sort()\n", "\n", " # Extract the neighboring agents\n", " neighbors = [agent for d, agent in distances[:num_neighbors]]\n", "\n", " # Count how many neighbors have the same type as self\n", " num_same_type = sum(self.type == agent.type for agent in neighbors)\n", " return num_same_type >= require_same_type\n", "\n", " def update(self, agents):\n", " \"If not happy, then randomly choose new locations until happy.\"\n", " while not self.happy(agents):\n", " self.draw_location()" ] }, { "cell_type": "markdown", "id": "7686acd7", "metadata": {}, "source": [ "Here’s some code that takes a list of agents and produces a plot showing their\n", "locations on the unit square.\n", "\n", "Orange agents are represented by orange dots and green ones are represented by\n", "green dots." ] }, { "cell_type": "code", "execution_count": null, "id": "629ed535", "metadata": { "hide-output": false }, "outputs": [], "source": [ "def plot_distribution(agents, cycle_num):\n", " \"Plot the distribution of agents after cycle_num rounds of the loop.\"\n", " x_values_0, y_values_0 = [], []\n", " x_values_1, y_values_1 = [], []\n", " # == Obtain locations of each type == #\n", " for agent in agents:\n", " x, y = agent.location\n", " if agent.type == 0:\n", " x_values_0.append(x)\n", " y_values_0.append(y)\n", " else:\n", " x_values_1.append(x)\n", " y_values_1.append(y)\n", " fig, ax = plt.subplots()\n", " plot_args = {'markersize': 8, 'alpha': 0.8}\n", " ax.set_facecolor('azure')\n", " ax.plot(x_values_0, y_values_0,\n", " 'o', markerfacecolor='orange', **plot_args)\n", " ax.plot(x_values_1, y_values_1,\n", " 'o', markerfacecolor='green', **plot_args)\n", " ax.set_title(f'Cycle {cycle_num-1}')\n", " plt.show()" ] }, { "cell_type": "markdown", "id": "d8c559ba", "metadata": {}, "source": [ "And here’s some pseudocode for the main loop, where we cycle through the\n", "agents until no one wishes to move.\n", "\n", "The pseudocode is" ] }, { "cell_type": "markdown", "id": "4eb01a57", "metadata": { "hide-output": false }, "source": [ "```text\n", "plot the distribution\n", "while agents are still moving\n", " for agent in agents\n", " give agent the opportunity to move\n", "plot the distribution\n", "```\n" ] }, { "cell_type": "markdown", "id": "f8dc1a54", "metadata": {}, "source": [ "The real code is below" ] }, { "cell_type": "code", "execution_count": null, "id": "7d1aa5c5", "metadata": { "hide-output": false }, "outputs": [], "source": [ "def run_simulation(num_of_type_0=600,\n", " num_of_type_1=600,\n", " max_iter=100_000, # Maximum number of iterations\n", " set_seed=1234):\n", "\n", " # Set the seed for reproducibility\n", " seed(set_seed)\n", "\n", " # Create a list of agents of type 0\n", " agents = [Agent(0) for i in range(num_of_type_0)]\n", " # Append a list of agents of type 1\n", " agents.extend(Agent(1) for i in range(num_of_type_1))\n", "\n", " # Initialize a counter\n", " count = 1\n", "\n", " # Plot the initial distribution\n", " plot_distribution(agents, count)\n", "\n", " # Loop until no agent wishes to move\n", " while count < max_iter:\n", " print('Entering loop ', count)\n", " count += 1\n", " no_one_moved = True\n", " for agent in agents:\n", " old_location = agent.location\n", " agent.update(agents)\n", " if agent.location != old_location:\n", " no_one_moved = False\n", " if no_one_moved:\n", " break\n", "\n", " # Plot final distribution\n", " plot_distribution(agents, count)\n", "\n", " if count < max_iter:\n", " print(f'Converged after {count} iterations.')\n", " else:\n", " print('Hit iteration bound and terminated.')" ] }, { "cell_type": "markdown", "id": "a0c5ebb3", "metadata": {}, "source": [ "Let’s have a look at the results." ] }, { "cell_type": "code", "execution_count": null, "id": "6a863991", "metadata": { "hide-output": false }, "outputs": [], "source": [ "run_simulation()" ] }, { "cell_type": "markdown", "id": "d3200cb1", "metadata": {}, "source": [ "As discussed above, agents are initially mixed randomly together.\n", "\n", "But after several cycles, they become segregated into distinct regions.\n", "\n", "In this instance, the program terminated after a small number of cycles\n", "through the set of agents, indicating that all agents had reached a state of\n", "happiness.\n", "\n", "What is striking about the pictures is how rapidly racial integration breaks down.\n", "\n", "This is despite the fact that people in the model don’t actually mind living mixed with the other type.\n", "\n", "Even with these preferences, the outcome is a high degree of segregation." ] }, { "cell_type": "markdown", "id": "5664e928", "metadata": {}, "source": [ "## Exercises" ] }, { "cell_type": "markdown", "id": "4330d4a0", "metadata": {}, "source": [ "## Exercise 23.1\n", "\n", "The object oriented style that we used for coding above is neat but harder to\n", "optimize than procedural code (i.e., code based around functions rather than\n", "objects and methods).\n", "\n", "Try writing a new version of the model that stores\n", "\n", "- the locations of all agents as a 2D NumPy array of floats. \n", "- the types of all agents as a flat NumPy array of integers. \n", "\n", "\n", "Write functions that act on this data to update the model using the logic\n", "similar to that described above.\n", "\n", "However, implement the following two changes:\n", "\n", "1. Agents are offered a move at random (i.e., selected randomly and given the\n", " opportunity to move). \n", "1. After an agent has moved, flip their type with probability 0.01 \n", "\n", "\n", "The second change introduces extra randomness into the model.\n", "\n", "(We can imagine that, every so often, an agent moves to a different city and,\n", "with small probability, is replaced by an agent of the other type.)" ] }, { "cell_type": "markdown", "id": "664f3b0c", "metadata": {}, "source": [ "## Solution to[ Exercise 23.1](https://intro.quantecon.org/#schelling_ex1)\n", "\n", "solution here" ] }, { "cell_type": "code", "execution_count": null, "id": "0d935035", "metadata": { "hide-output": false }, "outputs": [], "source": [ "from numpy.random import uniform, randint\n", "\n", "n = 1000 # number of agents (agents = 0, ..., n-1)\n", "k = 10 # number of agents regarded as neighbors\n", "require_same_type = 5 # want >= require_same_type neighbors of the same type\n", "\n", "def initialize_state():\n", " locations = uniform(size=(n, 2))\n", " types = randint(0, high=2, size=n) # label zero or one\n", " return locations, types\n", "\n", "\n", "def compute_distances_from_loc(loc, locations):\n", " \"\"\" Compute distance from location loc to all other points. \"\"\"\n", " return np.linalg.norm(loc - locations, axis=1)\n", "\n", "def get_neighbors(loc, locations):\n", " \" Get all neighbors of a given location. \"\n", " all_distances = compute_distances_from_loc(loc, locations)\n", " indices = np.argsort(all_distances) # sort agents by distance to loc\n", " neighbors = indices[:k] # keep the k closest ones\n", " return neighbors\n", "\n", "def is_happy(i, locations, types):\n", " happy = True\n", " agent_loc = locations[i, :]\n", " agent_type = types[i]\n", " neighbors = get_neighbors(agent_loc, locations)\n", " neighbor_types = types[neighbors]\n", " if sum(neighbor_types == agent_type) < require_same_type:\n", " happy = False\n", " return happy\n", "\n", "def count_happy(locations, types):\n", " \" Count the number of happy agents. \"\n", " happy_sum = 0\n", " for i in range(n):\n", " happy_sum += is_happy(i, locations, types)\n", " return happy_sum\n", "\n", "def update_agent(i, locations, types):\n", " \" Move agent if unhappy. \"\n", " moved = False\n", " while not is_happy(i, locations, types):\n", " moved = True\n", " locations[i, :] = uniform(), uniform()\n", " return moved\n", "\n", "def plot_distribution(locations, types, title, savepdf=False):\n", " \" Plot the distribution of agents after cycle_num rounds of the loop.\"\n", " fig, ax = plt.subplots()\n", " colors = 'orange', 'green'\n", " for agent_type, color in zip((0, 1), colors):\n", " idx = (types == agent_type)\n", " ax.plot(locations[idx, 0],\n", " locations[idx, 1],\n", " 'o',\n", " markersize=8,\n", " markerfacecolor=color,\n", " alpha=0.8)\n", " ax.set_title(title)\n", " plt.show()\n", "\n", "def sim_random_select(max_iter=100_000, flip_prob=0.01, test_freq=10_000):\n", " \"\"\"\n", " Simulate by randomly selecting one household at each update.\n", "\n", " Flip the color of the household with probability `flip_prob`.\n", "\n", " \"\"\"\n", "\n", " locations, types = initialize_state()\n", " current_iter = 0\n", "\n", " while current_iter <= max_iter:\n", "\n", " # Choose a random agent and update them\n", " i = randint(0, n)\n", " moved = update_agent(i, locations, types)\n", "\n", " if flip_prob > 0:\n", " # flip agent i's type with probability epsilon\n", " U = uniform()\n", " if U < flip_prob:\n", " current_type = types[i]\n", " types[i] = 0 if current_type == 1 else 1\n", "\n", " # Every so many updates, plot and test for convergence\n", " if current_iter % test_freq == 0:\n", " cycle = current_iter / n\n", " plot_distribution(locations, types, f'iteration {current_iter}')\n", " if count_happy(locations, types) == n:\n", " print(f\"Converged at iteration {current_iter}\")\n", " break\n", "\n", " current_iter += 1\n", "\n", " if current_iter > max_iter:\n", " print(f\"Terminating at iteration {current_iter}\")" ] }, { "cell_type": "markdown", "id": "fff3c599", "metadata": {}, "source": [ "When we run this we again find that mixed neighborhoods break down and segregation emerges.\n", "\n", "Here’s a sample run." ] }, { "cell_type": "code", "execution_count": null, "id": "64b030b3", "metadata": { "hide-output": false }, "outputs": [], "source": [ "sim_random_select(max_iter=50_000, flip_prob=0.01, test_freq=10_000)" ] } ], "metadata": { "date": 1750038457.647007, "filename": "schelling.md", "kernelspec": { "display_name": "Python", "language": "python3", "name": "python3" }, "title": "Racial Segregation" }, "nbformat": 4, "nbformat_minor": 5 }