# Schelling Segregation Model ## Summary The Schelling segregation model is a classic agent-based model, demonstrating how even a mild preference for similar neighbors can lead to a much higher degree of segregation than we would intuitively expect. The model consists of agents on a square grid, where each grid cell can contain at most one agent. Agents come in two colors: orange and blue. They are happy if a certain number of their eight possible neighbors are of the same color, and unhappy otherwise. Unhappy agents will pick a random empty cell to move to each step, until they are happy. The model keeps running until there are no unhappy agents. By default, the number of similar neighbors the agents need to be happy is set to 3. That means the agents would be perfectly happy with a majority of their neighbors being of a different color (e.g. a Blue agent would be happy with five Orange neighbors and three Blue ones). Despite this, the model consistently leads to a high degree of segregation, with most agents ending up with no neighbors of a different color. ## How to Run To run the model interactively, in this directory, run the following command ``` $ solara run app.py ``` ## Files * ``model.py``: Contains the Schelling model class * ``agents.py``: Contains the Schelling agent class * ``app.py``: Code for the interactive visualization. * ``analysis.ipynb``: Notebook demonstrating how to run experiments and parameter sweeps on the model. ## Further Reading Schelling's original paper describing the model: [Schelling, Thomas C. Dynamic Models of Segregation. Journal of Mathematical Sociology. 1971, Vol. 1, pp 143-186.](https://www.stat.berkeley.edu/~aldous/157/Papers/Schelling_Seg_Models.pdf) An interactive, browser-based explanation and implementation: [Parable of the Polygons](http://ncase.me/polygons/), by Vi Hart and Nicky Case. ## Agents ```python from mesa.discrete_space import CellAgent class SchellingAgent(CellAgent): """Schelling segregation agent.""" def __init__( self, model, cell, agent_type: int, homophily: float = 0.4, radius: int = 1 ) -> None: """Create a new Schelling agent. Args: model: The model instance the agent belongs to agent_type: Indicator for the agent's type (minority=1, majority=0) homophily: Minimum number of similar neighbors needed for happiness radius: Search radius for checking neighbor similarity """ super().__init__(model) self.cell = cell self.type = agent_type self.homophily = homophily self.radius = radius self.happy = False def assign_state(self) -> None: """Determine if agent is happy and move if necessary.""" neighbors = list(self.cell.get_neighborhood(radius=self.radius).agents) # Count similar neighbors similar_neighbors = len([n for n in neighbors if n.type == self.type]) # Calculate the fraction of similar neighbors if (valid_neighbors := len(neighbors)) > 0: similarity_fraction = similar_neighbors / valid_neighbors else: # If there are no neighbors, the similarity fraction is 0 similarity_fraction = 0.0 if similarity_fraction < self.homophily: self.happy = False else: self.happy = True self.model.happy += 1 def step(self) -> None: # Move if unhappy if not self.happy: self.cell = self.model.grid.select_random_empty_cell() ``` ## Model ```python from mesa import Model from mesa.datacollection import DataCollector from mesa.discrete_space import OrthogonalMooreGrid from mesa.examples.basic.schelling.agents import SchellingAgent class Schelling(Model): """Model class for the Schelling segregation model.""" def __init__( self, height: int = 20, width: int = 20, density: float = 0.8, minority_pc: float = 0.5, homophily: float = 0.4, radius: int = 1, seed=None, ): """Create a new Schelling model. Args: width: Width of the grid height: Height of the grid density: Initial chance for a cell to be populated (0-1) minority_pc: Chance for an agent to be in minority class (0-1) homophily: Minimum number of similar neighbors needed for happiness radius: Search radius for checking neighbor similarity seed: Seed for reproducibility """ super().__init__(seed=seed) # Model parameters self.density = density self.minority_pc = minority_pc # Initialize grid self.grid = OrthogonalMooreGrid((width, height), random=self.random, capacity=1) # Track happiness self.happy = 0 # Set up data collection self.datacollector = DataCollector( model_reporters={ "happy": "happy", "pct_happy": lambda m: (m.happy / len(m.agents)) * 100 if len(m.agents) > 0 else 0, "population": lambda m: len(m.agents), "minority_pct": lambda m: ( sum(1 for agent in m.agents if agent.type == 1) / len(m.agents) * 100 if len(m.agents) > 0 else 0 ), }, agent_reporters={"agent_type": "type"}, ) # Create agents and place them on the grid for cell in self.grid.all_cells: if self.random.random() < self.density: agent_type = 1 if self.random.random() < minority_pc else 0 SchellingAgent( self, cell, agent_type, homophily=homophily, radius=radius ) # Collect initial state self.agents.do("assign_state") self.datacollector.collect(self) def step(self): """Run one step of the model.""" self.happy = 0 # Reset counter of happy agents self.agents.shuffle_do("step") # Activate all agents in random order self.agents.do("assign_state") self.datacollector.collect(self) # Collect data self.running = self.happy < len(self.agents) # Continue until everyone is happy ``` ## App ```python import os import solara from mesa.examples.basic.schelling.model import Schelling from mesa.visualization import ( Slider, SolaraViz, SpaceRenderer, make_plot_component, ) from mesa.visualization.components import AgentPortrayalStyle def get_happy_agents(model): """Display a text count of how many happy agents there are.""" return solara.Markdown(f"**Happy agents: {model.happy}**") path = os.path.dirname(os.path.abspath(__file__)) def agent_portrayal(agent): style = AgentPortrayalStyle( x=agent.cell.coordinate[0], y=agent.cell.coordinate[1], marker=os.path.join(path, "resources", "orange_happy.png"), size=75, ) if agent.type == 0: if agent.happy: style.update( ( "marker", os.path.join(path, "resources", "blue_happy.png"), ), ) else: style.update( ( "marker", os.path.join(path, "resources", "blue_unhappy.png"), ), ("size", 50), ("zorder", 2), ) else: if not agent.happy: style.update( ( "marker", os.path.join(path, "resources", "orange_unhappy.png"), ), ("size", 50), ("zorder", 2), ) return style model_params = { "seed": { "type": "InputText", "value": 42, "label": "Random Seed", }, "density": Slider("Agent density", 0.8, 0.1, 1.0, 0.1), "minority_pc": Slider("Fraction minority", 0.2, 0.0, 1.0, 0.05), "homophily": Slider("Homophily", 0.4, 0.0, 1.0, 0.125), "width": 20, "height": 20, } # Note: Models with images as markers are very performance intensive. model1 = Schelling() renderer = SpaceRenderer(model1, backend="matplotlib") # Here we use renderer.render() to render the agents and grid in one go. # This function always renders the grid and then renders the agents or # property layers on top of it if specified. It also supports passing the # post_process function to fine-tune the plot after rendering in itself. renderer.render(agent_portrayal=agent_portrayal) HappyPlot = make_plot_component({"happy": "tab:green"}) page = SolaraViz( model1, renderer, components=[ HappyPlot, get_happy_agents, ], model_params=model_params, ) page # noqa ```