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 12 commits into
base: main
Choose a base branch
from

Conversation

Sahil-Chhoker
Copy link
Contributor

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 requested review from EwoutH and wang-boyu November 25, 2024 14:10
@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
Contributor 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%]

Comment on lines 26 to 33
if (valid_neighbors := len(neighbors)) > 0:
similarity_fraction = similar_neighbors / valid_neighbors

# If unhappy, move to a random empty cell:
if similar < self.model.homophily:
self.model.grid.move_to_empty(self)
else:
self.model.happy += 1
# 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

Choose a reason for hiding this comment

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

what happens if len(neighbors) is 0? Currently, nothing happens, which most definitely is not correct.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Please correct me if I’m wrong, but if there are no neighbors surrounding the agent, it should be considered unhappy as it does not meet the criteria for happiness. Should I proceed with this approach?

Copy link
Member

Choose a reason for hiding this comment

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

I have checked some literature, including the original article. It's not entirely clear. However, your reading is defendable, so I am fine with it.

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

github-actions bot commented Dec 5, 2024

Performance benchmarks:

Model Size Init time [95% CI] Run time [95% CI]
BoltzmannWealth small 🔵 +0.1% [-0.7%, +0.9%] 🔵 +0.3% [-0.4%, +0.9%]
BoltzmannWealth large 🔵 +0.8% [-0.5%, +2.2%] 🔵 +0.7% [-0.3%, +1.6%]
Schelling small 🔵 +0.1% [-0.6%, +0.9%] 🔴 +87.7% [+86.7%, +88.7%]
Schelling large 🔵 +0.0% [-0.3%, +0.3%] 🔴 +84.7% [+83.9%, +85.5%]
WolfSheep small 🟢 -4.1% [-4.8%, -3.3%] 🟢 -8.9% [-9.3%, -8.4%]
WolfSheep large 🟢 -3.7% [-4.0%, -3.4%] 🟢 -10.4% [-10.9%, -9.8%]
BoidFlockers small 🔵 +0.5% [-0.4%, +1.5%] 🔵 -1.3% [-2.0%, -0.6%]
BoidFlockers large 🔵 +0.7% [-0.1%, +1.7%] 🔵 -1.2% [-1.5%, -0.8%]

@EwoutH
Copy link
Member

EwoutH commented Dec 5, 2024

Thanks for bringing this PR this far. Is the performance regression expected from the code changes, or is it bigger than expected?

@quaquel
Copy link
Member

quaquel commented Dec 5, 2024

I am not surprised as indicated before, but I still want to check before merging.

@quaquel
Copy link
Member

quaquel commented Dec 10, 2024

I have checked the model, and something is not quite right. Within a few steps, all agents are supposedly happy but if you check the grid manually, you see that this cannot be true. I am not yet sure what's causing this. It might be merely the order of agent activation (so an agent goes first and is happy, but some of its neighbors move afterwards, meaning it is unhappy if checked again). I need time to look more closely at this and reason true what is going on.

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