-
Notifications
You must be signed in to change notification settings - Fork 130
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
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
Showing
2 changed files
with
190 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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] |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) |