project-highlight-image

Urban Resiliance Model

Developed an Agent-Based Model (ABM) to simulate the complex dynamics between housing availability, economic instability, and homelessness in Seattle, WA. The project focuses on stress-testing urban resilience against "Black Swan" events like natural disasters, economic recessions, and sudden demographic shifts. By simulating individual "agents" (citizens) with unique attributes—such as income progression, aging, and savings behaviors—the model identifies critical thresholds where economic shocks lead to housing loss. The simulation provides a customizable sandbox for policymakers to evaluate which interventions (e.g., job creation vs. housing supply) most effectively reduce long-term homelessness.
Home
Questions?
hero-image

Pradyota Phaneesh

- Current

HighlightS

Agent-Based Architecture: Engineered a multi-agent system where "Buyers" and "Sellers" interact based on realistic economic constraints, including a 20% savings rule and age-based retirement logic.

Scenario Stress-Testing: Conducted a comprehensive sensitivity analysis by simulating three "Unforeseen Circumstances" (Natural Disasters, Recessions, Migrant Influx) to validate model stability.

Policy Optimization: Discovered through iterative simulation that decreasing unemployment rates via job market expansion outperformed other variables in long-term homelessness reduction.

Data Integration: Synthesized disparate datasets from Gusto (income), Mathworks (housing prices), and historical unemployment trends to calibrate agent behaviors.

Technical Scalability: Documented clear computational constraints and established a modular code structure for future scaling to larger population samples.

SKILLS

Python (NumPy, Pandas, Random)
Simulation Framework (Agentpy)
Data Analysis
Domain Knowledge
Technical Writing

The "20-Year" Stochastic Pulse

A unique feature of this model is the implementation of a stochastic shock trigger. Based on historical frequency data (4–5 major events per century), the model introduces a "drastic shift" every 20 years. This forces the agents to adapt to sudden changes—such as a $200,000 drop in property value or a $25,000 income reduction—to measure the "Abyss" or the point of no return for vulnerable populations.

Model Constraints & Future Scope

While current hardware limited the simulation to a sample size of 200 high-fidelity agents, the logic is designed to be linearly scalable. Future iterations aim to utilize cloud-based high-performance computing (HPC) to simulate Seattle's full population and integrate more granular "Quality of House" variables (Levels 1–2) to reflect gentrification and urban decay patterns.

import agentpy as ap
import numpy as np
import random
import pandas as pd


class Citizen(ap.Agent):
    def setup(self):
        if np.random.random() <= .078:
            self.status = 1
        else:
            self.status = 0 # 0 = buyer, 1 = seller
        self.income = np.random.uniform(39000, 170000)  # Random income between $50,000 and $150,000
        self.age = np.random.uniform(20, 70) # age randomized between 20 and 70
        self.savings = (self.age - 20) * .1 * self.income  # initial savings based on age of buyer
        self.budget = self.savings  # Budget is savings
        self.unemployment_prob = 0.031  # Probability of becoming unemployed
        self.quality = np.random.uniform(1, 2)
        self.price = 500000 * self.quality # Median house price with some variability
        self.house = 1 # do you have house
        self.homeless = 0

    def step(self):
        self.age += 1
        self.savings += .2 * self.income
        self.budget = self.savings
        if (self.unemployment_prob >= np.random.random() or self.age > 70) and self.house != 0:
            self.status = 1
            self.income = 0  # Set income to zero if buyer becomes unemployed and dies from old age
            if self.age >= 70 and self.house == 0: # create new citizen after old one dies
                self.status = 0
                self.age = 20
                self.savings = 0
                self.income = np.random.uniform(39000, 170000)
        if self.savings >= 500000:
            self.status = 0
        if self.status == 1:
            self.house = 0

        if self.income == 0 and self.status == 0:
            self.income = np.random.uniform(50000, 150000)
        #if np.random.random() <= .05:
            #self.status = 1
        if self.model.pool and self.status == 0:
            potential_sellers = [s for s in self.model.pool if s.status == 1]
            if potential_sellers:
                chosen_seller = np.random.choice(potential_sellers)
                if chosen_seller.price <= self.budget:
                    self.savings -= chosen_seller.price
                    self.model.pool.remove(chosen_seller)
                    self.model.transactions += 1
                    self.model.total_spent += chosen_seller.price
                    self.house = 1

        if self.house == 0:
            if np.random.random() <= .45:
                self.homeless = 1

class HousingMarketModel(ap.Model):
    def setup(self):
        self.i = 0
        self.pop = 100
        self.pool = ap.AgentList(self, self.pop, Citizen)
        self.transactions = 0
        self.total_spent = 0

    def step(self):
        self.pool.step()
        # self.pool.shuffle()
        self.sellersLeft = [s for s in self.model.pool if s.status == 1]
        self.homeless = [p for p in self.model.pool if p.homeless == 1]
        print(f" {len(self.sellersLeft)}, {len(self.homeless)}, {(self.pop)}\t")
        self.pop = round(self.pop * 1.03)
        self.pool = ap.AgentList(self, self.pop, Citizen)

#define parameters
parameters = {
    'steps': 50,
    'seed': 12
}
# Run the model
model = HousingMarketModel(parameters)
results = model.run()

# Calculate number of houses left on the market
houses_left = len(model.sellersLeft)
print("Number of houses left on the market:", houses_left)

# finding regressions for median income, unemployment, and houseprice
training_data = pd.read_csv('q2_data_v2.csv')

median_income_regression = np.polyfit(training_data['year'], training_data['median_income'], 2)
unemployment_regression  = np.polyfit(training_data['year'], training_data['unemployment_rate'], 2)
house_price_regression   = np.polyfit(training_data['year'], training_data['house_price'], 2)

median_income = lambda year: np.polyval(median_income_regression, year)
unemployment = lambda year: np.polyval(unemployment_regression, year)
house_price = lambda year: np.polyval(house_price_regression, year)

class Citizen(ap.Agent):
    def setup(self):
        self.years = 2022
        if np.random.random() <= .078:
            self.status = 1
        else:
            self.status = 0 # 0 = buyer, 1 = seller
        self.income = np.random.uniform(39000, 170000)  # Random income between $39,000 and $170,000
        self.age = np.random.uniform(20, 70) # age randomized between 20 and 70
        self.savings = (self.age - 20) * .1 * self.income  # initial savings based on age of buyer
        self.budget = self.savings  # Budget is savings
        self.unemployment_prob = .01 * unemployment(self.years)  # Probability of becoming unemployed
        self.quality = np.random.uniform(1, 2)
        self.price = 500000 * self.quality # Median house price with some variability
        self.house = 1 # do you have house
        self.homeless = 0

    def step(self):
        self.age += 1
        self.years += 1
        self.unemployment_prob = .01 * unemployment(self.years)
        self.savings += .2 * self.income
        self.budget = self.savings
        if (self.unemployment_prob >= np.random.random() or self.age > 70) and self.house != 0:
            self.status = 1
            self.income = 0  # Set income to zero if buyer becomes unemployed and dies from old age
            if self.age >= 70 and self.house == 0: # create new citizen after old one dies
                self.status = 0
                self.age = 20
                self.savings = 0
                self.income = np.random.uniform(39000, 170000)
        if self.savings >= 500000:
            self.status = 0
        if self.status == 1:
            self.house = 0

        if self.income == 0 and self.status == 0:
            self.income = np.random.uniform(39000, 170000)
        #if np.random.random() <= .05:
            #self.status = 1
        if self.model.pool and self.status == 0:
            potential_sellers = [s for s in self.model.pool if s.status == 1]
            if potential_sellers:
                chosen_seller = np.random.choice(potential_sellers)
                if chosen_seller.price <= self.budget:
                    self.savings -= chosen_seller.price
                    self.model.pool.remove(chosen_seller)
                    self.house = 1

        if self.house == 0:
            if np.random.random() <= .6:
                self.homeless = 1


class HousingMarketModel(ap.Model):
    def setup(self):
        self.i = 0
        self.pop = 200
        self.pool = ap.AgentList(self, self.pop, Citizen)

    def step(self):
        self.pool.step()
        # self.pool.shuffle()
        self.sellersLeft = [s for s in self.model.pool if s.status == 1]
        self.homeless = [p for p in self.model.pool if p.homeless == 1]
        print(f" {len(self.sellersLeft)}, {len(self.homeless)}, {(self.pop)}\t")
        self.pop = round(self.pop * 1.01024)
        self.pool = ap.AgentList(self, self.pop, Citizen)

#define parameters
parameters = {
    'steps': 50,
    'seed': 12
}
# Run the model
model = HousingMarketModel(parameters)
results = model.run()

# Calculate number of houses left on the market
houses_left = len(model.sellersLeft)
print("Number of houses left on the market:", houses_left)

"""Agent Based Model for natural disaster in Seattle, 20 years after the model starts."""

class Citizen(ap.Agent):
    def setup(self):
        self.years = 2022
        self.priceConstant = 500000
        self.incomeRange = [39000, 170000]
        if np.random.random() <= .078:
            self.status = 1
        else:
            self.status = 0 # 0 = buyer, 1 = seller
        self.income = np.random.uniform(self.incomeRange[0], self.incomeRange[1])  # Random income between $39,000 and $170,000
        self.age = np.random.uniform(20, 70) # age randomized between 20 and 70
        self.savings = (self.age - 20) * .1 * self.income  # initial savings based on age of buyer
        self.budget = self.savings  # Budget is savings
        self.unemployment_prob = .01 * unemployment(self.years)  # Probability of becoming unemployed
        self.quality = np.random.uniform(1, 2)
        self.price = self.priceConstant * self.quality # Median house price with some variability
        self.house = 1 # do you have house
        self.homeless = 0

    def step(self):
        self.priceConstant = self.model.priceConstant
        self.incomeRange = self.model.incomeRange
        self.age += 1
        self.years += 1
        self.unemployment_prob = .01 * unemployment(self.years)
        self.savings += .2 * self.income
        self.budget = self.savings
        if (self.unemployment_prob >= np.random.random() or self.age > 70) and self.house != 0:
            self.status = 1
            self.income = 0  # Set income to zero if buyer becomes unemployed and dies from old age
            if self.age >= 70 and self.house == 0: # create new citizen after old one dies
                self.status = 0
                self.age = 20
                self.savings = 0
                self.income = np.random.uniform(self.incomeRange[0], self.incomeRange[1])
        if self.savings >= 500000:
            self.status = 0
        if self.status == 1:
            self.house = 0

        if self.income == 0 and self.status == 0:
            self.income = np.random.uniform(self.incomeRange[0], self.incomeRange[1])
        #if np.random.random() <= .05:
            #self.status = 1
        if self.model.pool and self.status == 0:
            potential_sellers = [s for s in self.model.pool if s.status == 1]
            if potential_sellers:
                chosen_seller = np.random.choice(potential_sellers)
                if chosen_seller.price <= self.budget:
                    self.savings -= chosen_seller.price
                    self.model.pool.remove(chosen_seller)
                    self.house = 1

        if self.house == 0:
            if np.random.random() <= .6:
                self.homeless = 1


class HousingMarketModel(ap.Model):
    def setup(self):
        self.years = 2022
        self.i = 0
        self.pop = 200
        self.priceConstant = 500000
        self.incomeRange = [39000, 170000]
        self.unemployment_const = .01
        self.pool = ap.AgentList(self, self.pop, Citizen)

    def step(self):
        if self.years == 2042:
            self.priceConstant = 400000 # housing prices decreased from disaster
            self.incomeRange = [30000, 145000] # income decreases from disaster
        elif self.years > 2042:
            self.priceConstant = 100000 * (1 - .8 ** (self.years - 2042)) + 400000 # housing prices recovers over time
            self.incomeRange = [9000 * (1 - .95 ** (self.years - 2042)) + 30000, 25000 * (1 - .9 ** (self.years - 2042)) + 145000] # income recovers over time
        self.years += 1
        self.pool.step()
        # self.pool.shuffle()
        self.sellersLeft = [s for s in self.model.pool if s.status == 1]
        self.homeless = [p for p in self.model.pool if p.homeless == 1]
        print(f" {len(self.sellersLeft)}, {len(self.homeless)}, {(self.pop)}\t")
        self.pop = round(self.pop * .99) # natural disaster makes less people come to the area as the housing is less desirable
        self.pool = ap.AgentList(self, self.pop, Citizen)


#define parameters
parameters = {
    'steps': 50,
    'seed': 12
}
# Run the model
model = HousingMarketModel(parameters)
results = model.run()

# Calculate number of houses left on the market
houses_left = len(model.sellersLeft)
print("Number of houses left on the market:", houses_left)

"""Agent Based Model for economic recessions in Seattle, 20 years after the model starts."""

class Citizen(ap.Agent):
    def setup(self):
        self.years = 2022
        if np.random.random() <= .078: # 7.8% of intial population are selling
            self.status = 1
        else:
            self.status = 0 # 0 = buyer, 1 = seller
        self.income = np.random.uniform(self.model.incomeLow, self.model.incomeHigh)  # Random income between $39,000 and $170,000
        self.age = np.random.uniform(20, 70) # age randomized between 20 and 70
        self.savings = (self.age - 20) * .1 * self.income  # initial savings based on age of buyer
        self.budget = self.savings  # Budget is savings
        self.unemployment_prob = .01 * 3 # unemployment(self.years)  # Probability of becoming unemployed
        self.quality = np.random.uniform(1, 2)
        self.price = self.model.priceConstant * self.quality # Median house price with some variability
        self.house = 1 # do you have house
        self.homeless = 0

    def step(self):
        self.years = self.model.years
        self.age += 1
        self.unemployment_prob = self.model.unemployment_const * unemployment(self.years)
        self.savings += .2 * self.income
        self.budget = self.savings
        if (self.unemployment_prob >= np.random.random() or self.age > 70) and self.house != 0:
            self.status = 1
            self.income = 0  # Set income to zero if buyer becomes unemployed and dies from old age
            if self.age >= 70 and self.house == 0: # create new citizen after old one dies
                self.status = 0
                self.age = 20
                self.savings = 0
                self.income = np.random.uniform(self.model.incomeLow, self.model.incomeHigh)
        if self.savings >= 500000:
            self.status = 0
        if self.status == 1:
            self.house = 0
        if self.income == 0 and self.status == 0:
            self.income = np.random.uniform(self.model.incomeLow, self.model.incomeHigh)
        #if np.random.random() <= .05:
            #self.status = 1
        if self.model.pool and self.status == 0:
            potential_sellers = [s for s in self.model.pool if s.status == 1]
            if potential_sellers:
                chosen_seller = np.random.choice(potential_sellers)
                if chosen_seller.price <= self.budget:
                    self.savings -= chosen_seller.price
                    self.model.pool.remove(chosen_seller)
                    self.house = 1

        if self.house == 0:
            if np.random.random() <= .6:
                self.homeless = 1


class HousingMarketModel(ap.Model):
    def setup(self):
        self.years = 2022
        self.i = 0
        self.pop = 200
        self.priceConstant = 500000
        self.incomeLow = 39000
        self.incomeHigh = 170000
        self.unemployment_const = .01
        self.pool = ap.AgentList(self, self.pop, Citizen)

    def step(self):
        if self.years == 2042:
            self.priceConstant = 550000 # housing prices increases from recession
            self.incomeLow = 30000
            self.incomeHigh = 145000 # income decreases from recession
            self.unemployment_const = .015 # unemployment increases from recession
        elif self.years > 2042:
            self.priceConstant = (-50000) * (1 - .9 ** (self.years - 2042)) + 550000 # housing prices recovers over time
            self.incomeLow = 9000 * (1 - .95 ** (self.years - 2042)) + 30000 # income recovers over time
            self.incomeHigh = 25000 * (1 - .9 ** (self.years - 2042)) + 145000
            self.unemployment_const = (-.003) * (1 - .9 ** (self.years - 2042)) + .013 #unemployment recovers over time
        self.years += 1
        self.pool.step()
        # self.pool.shuffle()
        self.sellersLeft = [s for s in self.model.pool if s.status == 1]
        self.homeless = [p for p in self.model.pool if p.homeless == 1]
        print(f" {len(self.sellersLeft)}, {len(self.homeless)}, {(self.pop)}\t")
        self.pop = round(self.pop * 1.01025)
        self.pool = ap.AgentList(self, self.pop, Citizen)

#define parameters
parameters = {
    'steps': 50,
    'seed': 12
}
# Run the model
model = HousingMarketModel(parameters)
results = model.run()

# Calculate number of houses left on the market
houses_left = len(model.sellersLeft)
print("Number of houses left on the market:", houses_left)


class Citizen(ap.Agent):
    def setup(self):
        self.years = 2022
        if np.random.random() <= .078: # 7.8% of intial population are selling
            self.status = 1
        else:
            self.status = 0 # 0 = buyer, 1 = seller
        self.income = np.random.uniform(self.model.incomeLow, self.model.incomeHigh)  # Random income between $39,000 and $170,000
        self.age = np.random.uniform(20, 70) # age randomized between 20 and 70
        self.savings = (self.age - 20) * .1 * self.income # initial savings based on age of buyer
        self.budget = self.savings  # Budget is savings
        self.unemployment_prob = .01 * 3 # unemployment(self.years)  # Probability of becoming unemployed
        self.quality = np.random.uniform(1, 2)
        self.price = self.model.priceConstant * self.quality # Median house price with some variability
        self.house = 1 # do you have house
        self.homeless = 0

    def step(self):
        self.years = self.model.years
        self.age += 1
        self.unemployment_prob = self.model.unemployment_const * unemployment(self.years)
        self.savings += .2 * self.income
        self.budget = self.savings
        if (self.unemployment_prob >= np.random.random() or self.age > 70) and self.house != 0:
            self.status = 1
            self.income = 0  # Set income to zero if buyer becomes unemployed and dies from old age
            if self.age >= 70 and self.house == 0: # create new citizen after old one dies
                self.status = 0
                self.age = 20
                self.savings = 0
                self.income = np.random.uniform(self.model.incomeLow, self.model.incomeHigh)
        if self.savings >= 500000:
            self.status = 0
        if self.status == 1:
            self.house = 0
        if self.income == 0 and self.status == 0:
            self.income = np.random.uniform(self.model.incomeLow, self.model.incomeHigh)
        #if np.random.random() <= .05:
            #self.status = 1
        if self.model.pool and self.status == 0:
            potential_sellers = [s for s in self.model.pool if s.status == 1]
            if potential_sellers:
                chosen_seller = np.random.choice(potential_sellers)
                if chosen_seller.price <= self.budget:
                    self.savings -= chosen_seller.price
                    self.model.pool.remove(chosen_seller)
                    self.house = 1

        if self.house == 0:
            if np.random.random() <= .8: # less housing available, so more chance of becoming homeless after selling a house
                self.homeless = 1

class HousingMarketModel(ap.Model):
    def setup(self):
        self.years = 2022
        self.i = 0
        self.pop = 100 # decreased initial sample population because my pc was overheating
        self.priceConstant = 500000
        self.incomeLow = 39000
        self.incomeHigh = 170000
        self.unemployment_const = .01
        self.pool = ap.AgentList(self, self.pop, Citizen)

    def step(self):
        if self.years == 2042:
            self.priceConstant = 600000 # housing prices increases from migrant populations
            self.incomeLow = 30000
            self.incomeHigh = 145000 # income decreases from more people wanting jobs
            self.unemployment_const = .02 # unemployment increases from migrant populations
        elif self.years > 2042:
            self.priceConstant = (-50000) * (1 - .9 ** (self.years - 2042)) + 550000 # housing prices recovers over time
            self.incomeLow = 9000 * (1 - .85 ** (self.years - 2042)) + 30000 # income recovers over time
            self.incomeHigh = 25000 * (1 - .85 ** (self.years - 2042)) + 145000
            self.unemployment_const = (-.002) * (1 - .8 ** (self.years - 2042)) + .012 #unemployment recovers over time
        self.years += 1
        self.pool.step()
        # self.pool.shuffle()
        self.sellersLeft = [s for s in self.model.pool if s.status == 1]
        self.homeless = [p for p in self.model.pool if p.homeless == 1]
        print(f" {len(self.sellersLeft)}, {len(self.homeless)}, {(self.pop)}\t")
        self.pop = round(self.pop * 1.03) # migrants greatly increases populations
        self.pool = ap.AgentList(self, self.pop, Citizen)

#define parameters
parameters = {
    'steps': 50,
    'seed': 12
}
# Run the model
model = HousingMarketModel(parameters)
results = model.run()

# Calculate number of houses left on the market
houses_left = len(model.sellersLeft)
print("Number of houses left on the market:", houses_left)


class Citizen(ap.Agent):
    def setup(self):
        self.years = 2022
        if np.random.random() <= .078:
            self.status = 1
        else:
            self.status = 0 # 0 = buyer, 1 = seller
        self.income = np.random.uniform(39000, 170000)  # Random income between $39,000 and $170,000
        self.age = np.random.uniform(20, 70) # age randomized between 20 and 70
        self.savings = (self.age - 20) * .1 * self.income  # initial savings based on age of buyer
        self.budget = self.savings  # Budget is savings
        self.unemployment_prob = .01 * unemployment(self.years)  # Probability of becoming unemployed
        self.quality = np.random.uniform(1, 2)
        self.price = 500000 * self.quality # Median house price with some variability
        self.house = 1 # do you have house
        self.homeless = 0

    def step(self):
        self.age += 1
        self.years += 1
        self.unemployment_prob = .01 * unemployment(self.years)
        self.savings += .2 * self.income
        self.budget = self.savings
        if (self.unemployment_prob >= np.random.random() or self.age > 70) and self.house != 0:
            self.status = 1
            self.income = 0  # Set income to zero if buyer becomes unemployed and dies from old age
            if self.age >= 70 and self.house == 0: # create new citizen after old one dies
                self.status = 0
                self.age = 20
                self.savings = 0
                self.income = np.random.uniform(39000, 170000)
        if self.savings >= 500000:
            self.status = 0
        if self.status == 1:
            self.house = 0

        if self.income == 0 and self.status == 0:
            self.income = np.random.uniform(39000, 170000)
        #if np.random.random() <= .05:
            #self.status = 1
        if self.model.pool and self.status == 0:
            potential_sellers = [s for s in self.model.pool if s.status == 1]
            if potential_sellers:
                chosen_seller = np.random.choice(potential_sellers)
                if chosen_seller.price <= self.budget:
                    self.savings -= chosen_seller.price
                    self.model.pool.remove(chosen_seller)
                    self.house = 1

        if self.house == 0:
            if np.random.random() <= .6:
                self.homeless = 1


class HousingMarketModel(ap.Model):
    def setup(self):
        self.i = 0
        self.pop = 200
        self.pool = ap.AgentList(self, self.pop, Citizen)

    def step(self):
        self.pool.step()
        # self.pool.shuffle()
        self.sellersLeft = [s for s in self.model.pool if s.status == 1]
        self.homeless = [p for p in self.model.pool if p.homeless == 1]
        print(f" {len(self.sellersLeft)}, {len(self.homeless)}, {(self.pop)}\t")
        self.pop = round(self.pop * 1.01024)
        self.pool = ap.AgentList(self, self.pop, Citizen)

#define parameters
parameters = {
    'steps': 50,
    'seed': 12
}
# Run the model
model = HousingMarketModel(parameters)
results = model.run()

# Calculate number of houses left on the market
houses_left = len(model.sellersLeft)
print("Number of houses left on the market:", houses_left)
\end{minted}
| lowinertia |
Engineering Portfolio in 15 minutes
Create Your Portfolio