Skip to content

Commit

Permalink
Add basic supply/demand model
Browse files Browse the repository at this point in the history
Right now it's a simple function that works with an LCM, choosers,
and alternatives. It compares summed probabilities from the LCM (demand) to
counts of probabilities (supply) and makes adjustments to prices accordingly.
  • Loading branch information
jiffyclub committed Sep 30, 2014
1 parent c3ab2bd commit fa3e7f8
Show file tree
Hide file tree
Showing 2 changed files with 190 additions and 0 deletions.
104 changes: 104 additions & 0 deletions urbansim/models/supplydemand.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
"""
Tools for modeling how supply and demand affect real estate prices.
"""
import numpy as np
import pandas as pd


def _calculate_adjustment_ratio(
lcm, choosers, alternatives, alt_segmenter,
clip_change_low, clip_change_high):
"""
Calculate adjustments to prices to compensate for
supply and demand effects.
Parameters
----------
lcm : LocationChoiceModel
Used to calculate the probability of agents choosing among
alternatives. Must be fully configured and fitted.
choosers : pandas.DataFrame
alternatives : pandas.DataFrame
alt_segmenter : pandas.Series
Will be used to segment alternatives and probabilities to do
comparisons of supply and demand by submarket.
clip_change_low : float
The minimum amount by which to multiply prices each iteration.
clip_change_high : float
The maximum amount by which to multiply prices each iteration.
Returns
-------
ratio : pandas.Series
Same index as `alternatives`, values clipped to `clip_change_low`
and `clip_change_high`.
"""
# probabilities of agents choosing * number of agents = demand
demand = pd.Series(lcm.summed_probabilities(choosers, alternatives))
# group by submarket
demand = demand.groupby(alt_segmenter.values).sum()

# number of alternatives
supply = alt_segmenter.value_counts()

ratio = (demand / supply).clip(clip_change_low, clip_change_high)

# broadcast ratio back to alternatives index
ratio = ratio.loc[alt_segmenter]
ratio.index = alt_segmenter.index

return ratio


def supply_and_demand(
lcm, choosers, alternatives, alt_segmenter, price_col,
clip_change_low=0.75, clip_change_high=1.25, iterations=5):
"""
Adjust real estate prices to compensate for supply and demand effects.
Parameters
----------
lcm : LocationChoiceModel
Used to calculate the probability of agents choosing among
alternatives. Must be fully configured and fitted.
choosers : pandas.DataFrame
alternatives : pandas.DataFrame
alt_segmenter : str, array, or pandas.Series
Will be used to segment alternatives and probabilities to do
comparisons of supply and demand by submarket.
If a string, it is expected to be the name of a column
in `alternatives`.
price_col : str
The name of the column in `alternatives` that corresponds to price.
This column is what is adjusted by this model.
clip_change_low : float, optional
The minimum amount by which to multiply prices each iteration.
clip_change_high : float, optional
The maximum amount by which to multiply prices each iteration.
iterations : int, optional
Number of times to update prices based on supply/demand comparisons.
Returns
-------
new_prices : pandas.Series
Equivalent of the `price_col` in `alternatives`.
"""
# copy alternatives so we don't modify the user's original
alternatives = alternatives.copy()

# if alt_segmenter is a string, get the actual column for segmenting demand
if isinstance(alt_segmenter, str):
alt_segmenter = alternatives[alt_segmenter]
elif isinstance(alt_segmenter, np.array):
alt_segmenter = pd.Series(alt_segmenter)

for _ in range(iterations):
ratio = _calculate_adjustment_ratio(
lcm, choosers, alternatives, alt_segmenter,
clip_change_low, clip_change_high)
alternatives[price_col] = alternatives[price_col] * ratio

return alternatives[price_col]
86 changes: 86 additions & 0 deletions urbansim/models/tests/test_supplydemand.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
from __future__ import division

import pandas as pd
import pytest
from pandas.util import testing as pdt

from .. import supplydemand as supdem


@pytest.fixture
def choosers():
return pd.DataFrame(
{'var1': range(5, 10),
'thing_id': ['a', 'c', 'e', 'g', 'i']})


@pytest.fixture
def alternatives():
return pd.DataFrame(
{'var2': range(10, 20),
'var3': range(20, 30),
'price_col': [1] * 10,
'zone_id': ['w', 'x', 'y', 'z', 'z', 'x', 'y', 'w', 'y', 'y']},
index=pd.Index([x for x in 'abcdefghij'], name='thing_id'))


@pytest.fixture
def alt_segmenter():
return 'zone_id'


class _TestLCM(object):
def summed_probabilities(self, choosers, alternatives):
return [
1, 0.25, 1, 2, 1, 0.75, 2, 1, 1.5, 0.5]
# w, x, y, z, z, x, y, w, y, y


@pytest.fixture(scope='module')
def lcm():
return _TestLCM()


def test_calculate_adjustment_ratio_clips(
lcm, choosers, alternatives, alt_segmenter):
clip = 1

ratio = supdem._calculate_adjustment_ratio(
lcm, choosers, alternatives, alternatives[alt_segmenter], clip, clip)

pdt.assert_series_equal(
ratio, pd.Series([1] * 10, index=alternatives.index),
check_dtype=False)


def test_calculate_adjustment_ratio(
lcm, choosers, alternatives, alt_segmenter):
clip_low = 0
clip_high = 2

ratio = supdem._calculate_adjustment_ratio(
lcm, choosers, alternatives, alternatives[alt_segmenter],
clip_low, clip_high)

pdt.assert_series_equal(
ratio,
# w, x, y, z, z, x, y, w, y, y
pd.Series([1, 0.5, 1.25, 3 / 2, 3 / 2, 0.5, 1.25, 1, 1.25, 1.25],
index=alternatives.index))


def test_supply_and_demand(
lcm, choosers, alternatives, alt_segmenter):
clip_low = 0
clip_high = 2
price_col = 'price_col'

new_price = supdem.supply_and_demand(
lcm, choosers, alternatives, alt_segmenter, price_col,
clip_low, clip_high)

pdt.assert_series_equal(
new_price,
# w, x, y, z, z, x, y, w, y, y
pd.Series([1, 0.5, 1.25, 3 / 2, 3 / 2, 0.5, 1.25, 1, 1.25, 1.25],
index=alternatives.index) ** 5)

0 comments on commit fa3e7f8

Please sign in to comment.