Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix: Schelling Model Neighbor Similarity Calculation #2518

Open
wants to merge 7 commits into
base: main
Choose a base branch
from

Conversation

Sahil-Chhoker
Copy link

Summary

Fixed #2515 Schelling segregation model to calculate agent happiness using neighbor similarity fraction, aligning with Wikipedia and NetLogo standards.

Bug / Issue

Current model incorrectly counts empty spaces as neighbors, leading to inaccurate agent happiness determination.

Implementation

Modified SchellingAgent.step() to:

  • Filter out empty cells
  • Calculate similar neighbor fraction
  • Scale homophily threshold
similar_neighbors = [
    neighbor for neighbor in neighbors 
    if hasattr(neighbor, 'type') and neighbor.type == self.type
]
total_neighbors = [
    neighbor for neighbor in neighbors 
    if hasattr(neighbor, 'type')
]

if len(total_neighbors) > 0:
    similarity_fraction = len(similar_neighbors) / len(total_neighbors)
    
    if similarity_fraction < self.model.homophily / 8.0:
        self.model.grid.move_to_empty(self)
    else:
        self.model.happy += 1

@EwoutH EwoutH added the example Changes the examples or adds to them. label Nov 25, 2024
Copy link

Performance benchmarks:

Model Size Init time [95% CI] Run time [95% CI]
BoltzmannWealth small 🔴 +5.4% [+3.5%, +7.2%] 🔵 +0.4% [+0.3%, +0.5%]
BoltzmannWealth large 🔵 -0.3% [-0.9%, +0.2%] 🔵 -3.5% [-6.0%, -1.0%]
Schelling small 🔵 +0.6% [+0.3%, +0.9%] 🟢 -16.1% [-16.3%, -15.8%]
Schelling large 🔵 +0.4% [-0.0%, +0.9%] 🟢 -6.5% [-7.6%, -5.4%]
WolfSheep small 🔵 +0.2% [-0.1%, +0.5%] 🔵 -0.0% [-0.2%, +0.1%]
WolfSheep large 🔵 -1.4% [-2.3%, -0.5%] 🔵 -3.2% [-5.9%, +0.1%]
BoidFlockers small 🔵 +0.9% [+0.4%, +1.5%] 🔵 +0.7% [-0.2%, +1.7%]
BoidFlockers large 🔵 +0.4% [-0.4%, +1.2%] 🔵 +0.1% [-0.3%, +0.5%]

@EwoutH EwoutH requested a review from quaquel November 25, 2024 14:12
else:
self.model.happy += 1
# If unhappy, move to a random empty cell
if similarity_fraction < self.model.homophily / 8.0:
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why not change homopily at the model level to be a fraction? That makes the model independent of the neighborhood size.

if hasattr(neighbor, "type") and neighbor.type == self.type
]
total_neighbors = [
neighbor for neighbor in neighbors if hasattr(neighbor, "type")
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

you iterate twice, with one check being done in both. You can make this more efficient by looping only once.

@EwoutH
Copy link
Member

EwoutH commented Nov 28, 2024

@Sahil-Chhoker Thanks for your PR. When do you expect to be able to incorporate @quaquel's feedback?

If you have any question about it feel free to ask!

@Sahil-Chhoker
Copy link
Author

Thank you, @quaquel, for your review! Could you please provide more details about the model and how can I can make changes to it?

@quaquel
Copy link
Member

quaquel commented Nov 29, 2024

My feedback is quite clear, and there is a link to a more detailed description of the model in the original issue. I requested 2 well-defined changes to this PR: change homophily to a fraction and rewrite so you only loop once.

From your reaction, I therefore deduce that you are not particularly familiar with agent-based modeling.

similar_neighbors = 0

for neighbor in neighbors:
if hasattr(neighbor, "type"): # Exclude empty cells
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is not needed, iter_neighbors returns a list of agents. In this model all agents have the type attribute.

Comment on lines 19 to 39
self.pos, moore=True, radius=self.model.radius
)

# Filter out empty cells
similar_neighbors = [
neighbor
for neighbor in neighbors
if hasattr(neighbor, "type") and neighbor.type == self.type
]
total_neighbors = [
neighbor for neighbor in neighbors if hasattr(neighbor, "type")
]

# Calculate fraction of similar neighbors
if len(total_neighbors) > 0:
similarity_fraction = len(similar_neighbors) / len(total_neighbors)
valid_neighbors = 0
similar_neighbors = 0

for neighbor in neighbors:
if hasattr(neighbor, "type"): # Exclude empty cells
valid_neighbors += 1
if neighbor.type == self.type: # Count similar neighbors
similar_neighbors += 1

# Calculate the fraction of similar neighbors
if valid_neighbors > 0:
similarity_fraction = similar_neighbors / valid_neighbors

# If unhappy, move to a random empty cell
if similarity_fraction < self.model.homophily / 8.0:
if similarity_fraction < self.model.homophily:
self.model.grid.move_to_empty(self)
else:
self.model.happy += 1
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

        neighbors = self.model.grid.get_neighbors(
            self.pos, moore=True, radius=self.model.radius
        )

        # 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

            # If unhappy, move to a random empty cell
            if similarity_fraction < self.model.homophily:
                self.model.grid.move_to_empty(self)
            else:
                self.model.happy += 1

Copy link
Member

@quaquel quaquel left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

see my second round of feedback

@quaquel
Copy link
Member

quaquel commented Nov 29, 2024

Please switch from iter_neighbors to get_neighbors.

        neighbors = self.model.grid.get_neighbors(
            self.pos, moore=True, radius=self.model.radius
        )

currently, as you can see the tests fail because the model won't run. The list expression exhausts the iterator, so getting the number of valid neighbors fails.

@EwoutH EwoutH added trigger-benchmarks Special label that triggers the benchmarking CI and removed trigger-benchmarks Special label that triggers the benchmarking CI labels Nov 29, 2024
Copy link

Performance benchmarks:

Model Size Init time [95% CI] Run time [95% CI]
BoltzmannWealth small 🔵 -1.5% [-2.1%, -0.9%] 🔵 -1.6% [-1.8%, -1.3%]
BoltzmannWealth large 🔵 -2.8% [-5.8%, -1.1%] 🔵 -2.8% [-3.4%, -2.4%]
Schelling small 🔵 -1.9% [-2.4%, -1.5%] 🔴 +82.8% [+81.9%, +83.7%]
Schelling large 🔵 -1.5% [-1.9%, -1.2%] 🔴 +85.2% [+83.9%, +86.6%]
WolfSheep small 🔵 -0.3% [-0.5%, -0.1%] 🔵 -0.5% [-0.7%, -0.2%]
WolfSheep large 🔵 +0.6% [-0.1%, +1.4%] 🔵 +5.2% [+2.4%, +8.2%]
BoidFlockers small 🔵 -0.6% [-1.1%, -0.0%] 🔵 -0.2% [-1.0%, +0.5%]
BoidFlockers large 🔵 -0.6% [-0.9%, -0.3%] 🔵 -0.0% [-0.6%, +0.4%]

self.pos, moore=True, radius=self.model.radius
)

# Count similar neighbors
similar = sum(neighbor.type == self.type for neighbor in neighbors)
similar_neighbors = len([n for n in neighbors if n.type == self.type])
Copy link
Member

@EwoutH EwoutH Nov 29, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The model looks significantly slower. I’m curious if we could minimize that performance degradation, I suspect the main problem is with this line doing additional work.

(But not sure, haven’t profiled it)

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The slowdown is not due to this line of code but because the behavior changes a lot. Prior to this PR, the model would reach steady state in a few ticks. Now, it can take many ticks and frequently the model never reaches stable state.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
example Changes the examples or adds to them.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

homophily parameter in basic schelling example
3 participants