"""
Tools for modeling how supply and demand affect real estate prices.
"""
from __future__ import division
import logging
import numpy as np
import pandas as pd
logger = logging.getLogger(__name__)
def _calculate_adjustment(
lcm, choosers, alternatives, alt_segmenter,
clip_change_low, clip_change_high, multiplier_func=None):
"""
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.
multiplier_func : function (returns Series, boolean)
A function which takes separate demand and supply Series
and returns a tuple where the first item is a Series with the
ratio of new price to old price (all indexes should be the same) -
by default the ratio of demand to supply is the ratio of the new
price to the old price. The second return value is a
boolean which when True tells this module to stop looping (that
convergence has been satisfied)
Returns
-------
alts_muliplier : pandas.Series
Same index as `alternatives`, values clipped to `clip_change_low`
and `clip_change_high`.
submarkets_multiplier : pandas.Series
Index is unique values from `alt_segmenter`, values are the ratio
of demand / supply for each segment in `alt_segmenter`.
finished : boolean
boolean indicator that this adjustment should be considered the
final adjustment (if True). If false, the iterative algorithm
should continue.
"""
logger.debug('start: calculate supply and demand price adjustment ratio')
# probabilities of agents choosing * number of agents = demand
demand = lcm.summed_probabilities(choosers, alternatives)
# group by submarket
demand = demand.groupby(alt_segmenter.loc[demand.index].values).sum()
# number of alternatives
supply = alt_segmenter.value_counts()
if multiplier_func is not None:
multiplier, finished = multiplier_func(demand, supply)
else:
multiplier, finished = (demand / supply), False
multiplier = multiplier.clip(clip_change_low, clip_change_high)
# broadcast multiplier back to alternatives index
alts_muliplier = multiplier.loc[alt_segmenter]
alts_muliplier.index = alt_segmenter.index
logger.debug(
('finish: calculate supply and demand price adjustment multiplier '
'with mean multiplier {}').format(multiplier.mean()))
return alts_muliplier, multiplier, finished
[docs]def supply_and_demand(
lcm, choosers, alternatives, alt_segmenter, price_col,
base_multiplier=None, clip_change_low=0.75, clip_change_high=1.25,
iterations=5, multiplier_func=None):
"""
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`. If a Series it should have the same index
as `alternatives`.
price_col : str
The name of the column in `alternatives` that corresponds to price.
This column is what is adjusted by this model.
base_multiplier : pandas.Series, optional
A series describing a starting multiplier for submarket prices.
Index should be submarket IDs.
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.
multiplier_func : function (returns Series, boolean)
A function which takes separate demand and supply Series
and returns a tuple where the first item is a Series with the
ratio of new price to old price (all indexes should be the same) -
by default the ratio of demand to supply is the ratio of the new
price to the old price. The second return value is a
boolean which when True tells this module to stop looping (that
convergence has been satisfied)
Returns
-------
new_prices : pandas.Series
Equivalent of the `price_col` in `alternatives`.
submarkets_ratios : pandas.Series
Price adjustment ratio for each submarket. If `base_multiplier` is
given this will be a cummulative multiplier including the
`base_multiplier` and the multipliers calculated for this year.
"""
logger.debug('start: calculating supply and demand price adjustment')
# 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, index=alternatives.index)
choosers, alternatives = lcm.apply_predict_filters(choosers, alternatives)
alt_segmenter = alt_segmenter.loc[alternatives.index]
# check base ratio and apply it to prices if given
if base_multiplier is not None:
bm = base_multiplier.loc[alt_segmenter]
bm.index = alt_segmenter.index
alternatives[price_col] = alternatives[price_col] * bm
base_multiplier = base_multiplier.copy()
for _ in range(iterations):
alts_muliplier, submarkets_multiplier, finished = _calculate_adjustment(
lcm, choosers, alternatives, alt_segmenter,
clip_change_low, clip_change_high, multiplier_func=multiplier_func)
alternatives[price_col] = alternatives[price_col] * alts_muliplier
# might need to initialize this for holding cumulative multiplier
if base_multiplier is None:
base_multiplier = pd.Series(
np.ones(len(submarkets_multiplier)),
index=submarkets_multiplier.index)
base_multiplier *= submarkets_multiplier
if finished:
break
logger.debug('finish: calculating supply and demand price adjustment')
return alternatives[price_col], base_multiplier