Thinking one-dimensionally about how individual preferences can result in global behaviors

Group Members

SOLUTIONS

Learning Goals (Why we're asking you to do this)

We want you to be able to:

  • Consider Thomas Schelling's 1974 research: a complex system where a slight bias in individual preference leads to a large-scale effect
  • Act out Schelling's rules in the form of a game
  • Express the game as a simulation algorithm in pictorial form, text, or pseudo-code
  • Think about when and how simulations terminate
  • Implement the Schelling model in a computer program

Introduction

In 1974, economist Thomas Schelling published what would become a landmark paper called "Dynamic Models of Segregation." In it, Schelling tried to explore how, in a simple toy system, individuals segregate themselves even if they have an expressed preference for having neighbors different from themselves. In other words: Schelling explored how a seemingly sensible individual preference ("I'm OK with up to 50% of the people in my neighborhood being different from me") can lead to larger-scale social segregation, where 'like' people clump together.")

The sensitivity of race and historical artifacts

Now, Schelling wrote this paper in 1974, in a different cultural context. You'll find that,

  • He describes "race" in terms of "blacks" and "whites,"
  • His pronouns are always masculine

Studying this model and the history involved means also accepting the baggage that surrounds them. These models offer us great insight into social systems, but they also have a history and implications that touch on tense social issues. So, as a class, let's:

  • Be aware that we come from many different backgrounds
  • Know that segregation can take many forms, not all of which are strictly based on race
  • Recognize that the model's function is to try to mimic a phenomenon, not to dictate how society is or should be

Schelling's 1-dimensional toy system

His first toy system looked like this: a one-dimensional set of typewriter characters to stand in for individuals who differed on some property ("stars and zeros"):

Schelling describes the figure thusly:

Suppose, now, that everybody wants at least half his neighbors to be like himself, and that everyone defines 'his neighborhood' to include the four nearest neighbors on either side of him. A star wants at least four of his eight nearest neighbors to be stars; a zero wants at least four of his eight nearest neighbors to be zeros. Including himself, this means that he wants a bare majority, five out of the nine. (For those near the end of the line the rule is that, of the four neighbors on the side toward the center plus the one, two or three outboard neighbors, half must be like oneself) I have put a dot over each individual whose neighborhood does not meet his demands.

Your Turn - Calculate an individual's preference assuming a neighborhood size of +/- 4

Let's look at the line again. We'll take two individual characters (two zeroes) as examples. The first zero doesn't want to move, because its neighborhood has a simple majority of zeroes like itself. But what about the zero that lives next door?

// Calculate the number of residents in the second zero's neighborhood to figure out if it wants to move.


The Assumptions of Schelling's model

The results of this section are experimental. They are crude and abstract but have the advantage that anyone can reproduce them using materials that are readily available.

I assume a population exhaustively divided into two groups; everyone's membership is permanent and recognizable. Everybody is assumed to care about the color of the people he lives among and able to observe the number of blacks and whites that occupy a piece of territory. Everybody has a particular location at any moment; and everybody is capable of moving if [they are] dissatisfied with the color mixture where [they are]. The numbers of blacks and whites, their color preferences, and the sizes of 'neighborhoods' will be manipulated....

There is some fascination in the process as it emerges in the linear model; furthermore, the linear experiment can be replicated by any reader in five minutes; variants can readily be devised, and any reader with a spare half hour can change the hypotheses to suit.

The rules of Schelling's model

Here's how Schelling sets up the rules for a 1-dimensional simulation:

Whether a resident wants to move

  • Everybody wants at least half his neighbors to be like himself
  • Everyone defines 'his neighborhood' to include the four nearest neighbors on either side of him
  • A star wants at least four of his eight nearest neighbors to be like stars
  • A zero wants at least four of his eight nearest neighbors to be zeros
  • For those near the end of the line the rule is that, of the four neighbors on the side toward the center plus the one, two or three outboard neighbors, half must be like oneself

How a resident moves

  • A dissatisfied resident moves to the nearest point that meets his minimum demand---the nearest point at which half his neighbors will be like himself when he arrives there
  • 'Nearest' means the point reached by passing the smallest number of neighbors on the way
  • A resident moves by shoving himself between any two other residents. (It's a bit like cutting in a line/queue: if you want to cut in, you just kind of insert yourself between two people already in line and the rest of the line adjusts around you)

The order of moves

As Schelling says, "We also need an order of moving."

  • Arbitrarily let the discontented members (those who have fewer than four neighbors like themselves) move in turn, counting from left to right.

Who gets to move, and when

  • Only discontented members are eligible to move
  • When people become happy, they don't want to move anymore. If an originally discontented member (when the game started) ends up content when their turn comes up, they stay put. (They used to be unhappy, they moved, and now they're content.)
  • Anyone who becomes discontent gets a turn after the originally unhappy people get to move. (This rule is a little tricky, so make sure you feel like you comfortably understand it.)
  • The definition of a neighborhood is the four nearest neighbors on either side at the moment one decides to move or stay; if someone moves in between a resident and their next-door neighbor, they push the fourth neighbor out of the neighborhood. (If a line is 100 people long, the theater only fits 100 people, and you cut somewhere in line, then whoever was last in line isn't getting into the theater. Womp womp.)

What residents know about other residents

  • The residents don't think ahead; if it's their turn to move, they base their decision only on the state of their neighborhood as they see it, on that turn. They don't plan ahead, and they don't try to figure out what the other neighbors might do.

Your Turn - Play Schelling's model

When Schelling did his original simulations in 1974, he wrote "what is reported here has all been done by hand and eye." If he could do it, so can we.

Grab nickels and pennies and find a small group (there probably aren't enough to do it solo or in pairs). We're going to try this in stages.

Set up the game

  • Pick a number of residents who will be in your game, say around 40. You'll probably want to pick a number divisible by 2.
  • Create your line of residents, ensuring a 50/50 mix of nickels and pennies.
  • If you can, take a picture of how your line started.

Play one round of the game

Now that you're set up, play one round of the game.

  • Starting with the left-most discontent resident,
  • move them a distance until they're happy,
  • then move rightward on to the next unhappy resident and repeat

When does/should the round stop? In other words, how would you describe how you know the round is over. Try and be as precise as you can in explaining.

If you can, take a picture of what your board looks like after one round and upload it with your notebook!

Check in with an instructor after your first round

This gives you a chance to talk about whether anything problematic or unexpected happened.

Play more rounds until you feel like you have the hang of the game

Play a second round through. And third, and fourth if you want. Set up a new board and try again, configuring the initial board how you want.

// If you can, take a picture of what your board starts like, looks like after each round and ends like in each game.


Your Turn - Create Instructions that Implement Schelling's Model

You've played the game. You hopefully have a handle on it. Now, let's try to move closer toward describing the model in a way that a computer - not just you - could potentially play/run the game.

You do not yet need to make a working Python program! We're going to start much smaller and try to build up. Using the whiteboards in class, and through words, pictures, flowcharts, or something else, start precisely describing the game so a non-human agent could play it.

One strategy you can use is often called top-down design. You start at the highest level of what you're trying to do, then break each big piece down into smaller, more specific pieces (thinking about specific pieces as functions, loops, or individual important pieces of code). You can also write in a way that looks like code, but doesn't have to be strict python (or any other language). Just something akin to a program sketch: understandable as a well-defined series of steps, even if it isn't Python code.

It takes practice, and you may hit dead-ends. But when you're done, you ought to have a set of instructions that:

  • Sets up the board
  • Determines whether or not an agent is happy at a particular place in a list or array of other agents
  • Tells an agent how to play a round (and when it stops)
  • Keeps playing until some condition is reached. (It could be "play 3 turns, then stop," or it could be some more complex condition, like "play until no one wants to move, or has no place to move to". It's up to you.)

In the cell below, describe the basic things that you want to do. Or, alternately, take pictures of your whiteboard and upload them along with this notebook!

Put your pseudo-code here! (Or upload pictures, but make sure to make a note here.)


Next step - writing some key functions

This problem is going to be based on a few key pieces of code, which we're going to implement as individual components. In the space below, we'll ask you to implement these pieces of code as functions, and to write code that shows these functions are behaving correctly.

Function 1: Write a function that creates a one-dimensional game board composed of agents of two different types (0 and 1, X and O, stars and pluses... whatever you want), where the agents are assigned to spots randomly with a 50% chance of being either type. As arguments to the function, take in (1) the number of spots in the game board (setting the default to 32) and (2) a random seed that you will use to initialize the board (again with some default number), and return your game board. (Hint: which makes more sense to describe the game board, a list or a Numpy array? What are the tradeoffs?) Show that your function is behaving correctly by printing out the returned game board.


In [ ]:
# Put your code here!

import random
import math

def initialize_list(array_size=32, randseed=8675309):
    '''
    This function optionally takes in an array size and random seed
    and returns the initial neighborhood that we're going to start 
    from - a string of zeros and ones.  If no arguments are given, it
    defaults to the values specified.
    '''

    random.seed(randseed)
    
    initial_list = []

    for i in range(array_size):
        initial_list.append(random.randint(0,1))

    return initial_list

def neighborhood_print(neighborhood, note=''):
    '''
    This is a convenience function to take our neighborhood list,
    make a string of stars and zeros out of it, and print the string
    plus optional text at the end.  It's not necessary but it looks pretty.  
    '''
    
    neighborstring=''

    for i in range(len(neighborhood)):
        if(neighborhood[i]) > 0:
            neighborstring += '*'
        else:
            neighborstring += '0'
    
    # make sure optional text is a string
    if type(note)!=str:
        note = str(note)
    
    # add an extra space to make it look nice!
    if note != '':
        note = ' ' + note
        
    neighborstring += note
    
    print(neighborstring)
   

my_board = initialize_list()

neighborhood_print(my_board)

Function 2: Write a function that takes the game board generated by the function you wrote above and determines whether an agent at position i in the game board of a specified type is happy for a game board of any size and a neighborhood of size N (i.e., from position i-N to i+N), and returns that information. Make sure to check that position i is actually inside the game board (i.e., make sure the request makes sense), and ensure that it behaves correctly for agents near the edges of the game board. Show that your function is behaving correctly by giving having it check every position in the game board you generated previously, and decide whether the agent in each spot is happy or not. Verify by eye that it's behaving correctly. (Hint: You're going to use this later, when you're trying to decide where to put an agent. Should you write the function assuming that the agent is already in the board, or that you're testing to see whether or not you've trying to decide whether to put it there?)


In [ ]:
# Put your code here, using additional cells if necessary.

def is_happy(my_list, my_value, my_index):
    '''
    This function assumes that my_list has a value (my_value)
    popped out of it already, and checkes to see if my_value
    would be happy in my_list at index my_index.  It returns
    'True' if happy and 'False' if unhappy under those circumstances.
    '''

    # do some error-checking (is the index within the allowed range?)
    if my_index < 0 or my_index > len(my_list):
        print("you've made an indexing error!", my_index)
        
    start = my_index-4 # start 4 to the left
    end = my_index+4   # end 3 to the right b/c we count the value at my_index too
    
    # if the starting value is out of bounds, fix it
    if start < 0:
        start = 0
    
    # if the ending value is out of bounds, fix it.  note that we want to go to 
    # len(list), not len(list)-1, because range() goes to 1 before the end of 
    # the range!
    if end > len(my_list):
        end = len(my_list)

    # keep track of the neighbors that are like me
    neighbors_like_me = 0
    
    # keep track of total neighbors
    total_neighbors = 0
    
    # loop over the specified range
    for i in range(start,end):
        if my_list[i] == my_value:  # if this neighbor is like me, keep track of that
            neighbors_like_me += 1
        total_neighbors+=1  # also keep track of total neighbors
    
    # happy if at least half are like me, unhappy otherwise
    # note: it's *at least* half because we're not double-counting our
    # own value
    if neighbors_like_me/total_neighbors >= 0.5:
        return True
    else:
        return False

In [ ]:
my_board = initialize_list()

neighborhood_print(my_board)

for i in range(len(my_board)):
    agent = my_board.pop(i)
    
    am_i_happy = is_happy(my_board, agent, i)
    
    my_board.insert(i, agent)
    
    if am_i_happy==True:
        print("agent {} at position {} is HAPPY!  :-)".format(agent,i))
    else:
        print("agent {} at position {} is UNHAPPY!  :-(".format(agent,i))

Final step - putting it all together!

In the space below, create a program that plays the whole game, from start to finish. Make sure to use the two functions that you just created, and for every step in the game print out the position you're looking at and the game board to verify that it is behaving correctly!


In [ ]:
# Put your code here, using additional cells if necessary.

def where_to_move(my_list, my_value, my_index):
    '''
    Given a neighborhood (my_list), a value (my_value), and the index
    that it started at (my_index), figure out where to move my_value
    so that it's happy.  This assumes that my_value is unhappy where it 
    is, by the way!  This function then returns the index where my_value
    should move to in order to be happy.
    '''

    # this block of code steps to the left to see where (if anywhere) it's 
    # happy to the left.  If it continues to be unhappy, it'll stop when it
    # is about to step off the end of the list.
    left_index=my_index-1
    left_happy=False
    
    while left_happy==False and left_index >= 0:
        left_happy = is_happy(my_list,my_value,left_index)
        if left_happy==False:
            left_index -= 1
    
    # as above, but to the right.
    right_index=my_index+1
    right_happy=False

    while right_happy==False and right_index < len(my_list):
        right_happy = is_happy(my_list,my_value,right_index)
        if right_happy==False:
            right_index += 1

    # now we figure out where the new index should be!
    
    if left_index < 0 and right_index < len(my_list):
        # can't be happy to the left; only possible answer is right_index
        new_index = right_index
        
    elif left_index >= 0 and right_index > len(my_list):  
        # can't be happy to the right; only possible answer is left_index
        new_index = left_index
        
    elif left_index >= 0 and right_index <= len(my_list): 
        # we're within bounds, so now check to see which side is closer.
        # if they're the same we move it to the left.  (This was never specified
        # by Schelling, so we have to make a choice on that.)
        if math.fabs(left_index-my_index) > math.fabs(right_index-my_index):
            new_index = right_index
        else:
            new_index = left_index        
    else:
        # this should only ever be called if something goes horribly wrong.
        print("something has gone wrong in where_to_move!")
    
    return new_index;

In [ ]:
# initialize list to defaults
neighborhood = initialize_list(array_size=32)

neighborhood_print(neighborhood, 'initial state')

# do 2 loops over the list
for i in range(2):

    this_index = 0 

    # step through the neighborhood once
    while this_index < len(neighborhood):
        
        this_val = neighborhood.pop(this_index)

        if is_happy(neighborhood,this_val,this_index):
            # if we're happy where we are, don't change anything!
            neighborhood.insert(this_index,this_val)
            
        else:
            # we're unhappy; we need to figure out where to move and then move.
            new_index = where_to_move(neighborhood,this_val,this_index)
            neighborhood.insert(new_index,this_val)

        neighborhood_print(neighborhood, this_index)

        # increment this_index or we'll never stop looping
        this_index += 1

# print out the final state, just to see what it's like.
neighborhood_print(neighborhood, 'final state!')

In [ ]:
%matplotlib inline
import matplotlib.pyplot as plt

plt.plot(neighborhood,'ro')
plt.xlim(-2,len(neighborhood)+1)
plt.ylim(-0.1,1.1)

Assignment wrapup

Please fill out the form that appears when you run the code below. You must completely fill this out in order to receive credit for the assignment!


In [ ]:
from IPython.display import HTML
HTML(
"""
<iframe 
	src="https://goo.gl/forms/c82cM9qmjBfI9DM72?embedded=true" 
	width="80%" 
	height="1200px" 
	frameborder="0" 
	marginheight="0" 
	marginwidth="0">
	Loading...
</iframe>
"""
)

Submitting this assignment

Submit this assignment by uploading it to the course Desire2Learn web page. Go to the "In-Class-Activities" folder, find the dropbox link for Day 14, and upload it there.