Real Estate Pricing Factor Dynamics#
[dNG18] continues in Chapter 5 to outline how scenario outcomes are based on the probability of certain situations or circumstances occurring. In Chapter 7, the qualities of real estate market dynamics are described, which are implemented in the Rangekeeper “Dynamics” module.
The following characteristics of real estate markets are identified:
Non-substitutability and non-fungibility of real estate assets.
Inefficiency of information processing in real estate markets.
Autoregression in real estate pricing
Cyclicality in real estate pricing
Mean reversion in real estate pricing
Because of these, the real estate pricing dynamics that are input into simulations should include autoregression, cyclicality, and mean‐reversion, in addition to random-walk process.
Pricing Factors#
The methodology to produce a simulation of a real estate market is to use ‘pricing factors’; a ratio that multiplies the original, single‐stream pro forma cash flow expectation to arrive at a future cash flow outcome for a given scenario.
Pricing factors can capture the historical variations in market prices that are observed in real estate markets, and can be generated from interpretations of available market data.
In this methodology, the pricing factors substantially enhance the traditional random walk process, by recognizing the special features of the dynamics of real estate markets. The resulting output simulation distributions offer a much richer and fuller picture of the future than the traditional, single‐stream DCF.
While [dNG18] does not explicitly describe the formulation and calculations used to produce the Market Dynamics incorporated into later examples and exercises, their accompanying Excel spreadsheets do provide the implementation details.
Rangekeeper replicates the market dynamics calculations (specifically, the
‘MktDynamicsInputs’ tab) by generating a Market
object via five key modules:
Market Trend
Volatility (including Autoregression and Mean Reversion)
Cyclicality
Noise
Black Swan
Market
s can be generated in two ways:
Deterministically (mostly); where all inputs are specified and have no variability (except for those with explicit randomness like volatility or noise)
Stochastically; where inputs are sampled from specified distributions of their likelihoods.
Producing one Scenario (or ‘Trial’) Market
#
First, let’s introduce a Market
by specifiying it as deterministically as
possible:
We need some extra libraries this time:
import pandas as pd
import rangekeeper as rk
Market Span#
Like in [dNG18], we will use a 25-year span. We will also produce the sequence of periods that will be used in following methods:
frequency = rk.duration.Type.YEAR
num_periods = 25
span = rk.span.Span.from_duration(
name="Span",
date=pd.Timestamp(2001, 1, 1),
duration=frequency,
amount=num_periods)
sequence = span.to_sequence(frequency=frequency)
span
Span: Span
Start Date: 2001-01-01
End Date: 2025-12-31
Overall Trend#
Now, we set up the general (rental) market trend (excluding volatility), which requires the following parameters:
Cap Rate: This is the long-run mean cap rate around which the capital market cycle varies, that is, this is the cap rate toward which the market reverts over the cycle. This relates to the reversion (going-out) cap rate that will be applied upon resale.
Note
You can set this so that the inflexible (10yr) reversion will exactly match a pro-forma reversion amount in the Base Case. Just set this as the ratio of the pro-forma Yr11 NOI divided by the pro-forma reversion (gross of selling expenses).
Note that if this long-run cap rate is set to a value much above (below) the going-in cap rate implied by the initial price, then the result will be to tend to favor flexible resale timing later (earlier), so as to mitigate (maximize) the unfavorable (favorable) effect of the yield change between the buy and the sell.
A plausible value for this input would equal the discount rate minus the pro-forma growth rate, plus 100 to 200 basis-points for capital improvements expenditures.
(From [dNG18], accompanying Excel spreadsheets)
Important
If an initial price factor is not set, it is defaulted to 1.00, and thus the intial rent level (net income as a fraction of asset value) is set equal to the cap rate (net income yield). This would also mean that subsequent price factors will be ratios of the pro-forma cash flow expectations. You can perform a sensitivity analysis by varying the initial price factor.
Growth Rate: This will govern the central tendency of the long-run growth rate trend that will apply over the entire scenario.
Note
As Pricing Factors are only RELATIVE TO the traditional pro-forma which should contain any realistic expected growth, the default value input here should normally be zero in principle. However, in the Base Case we make this incremental trend slightly negative to counteract the effect of some minor inconsistencies. (The pro forma does not recognize any Black Swan probability, and has zero selling expenses. The cap rate cycle is symmetric in the cap rate, but that gives resulting capital value levels an upward bias. These factors may interact with other elements of the price dynamics.) Ideally for a more representative sample and better “apples-to-apples” comparisons, you want to set this input so that the t-statistic of the InflxPV-ProFormaPV (cell K13) is generally near zero (and almost always < 1.96 in absolute value). You may need to change the LR Trend Mean input as you change other price dynamics input parameter assumptions away from the Base Case assumptions.
(From [dNG18], accompanying Excel spreadsheets)
cap_rate = .05
growth_rate = -.0005
trend = rk.dynamics.trend.Trend(
sequence=sequence,
cap_rate=cap_rate,
growth_rate=growth_rate)
print('Growth Rate: {:.4%}'.format(trend.growth_rate))
print('Initial Value: {:.4%}'.format(trend.initial_value))
Growth Rate: -0.0500%
Initial Value: 5.0000%
A Trend
object is in fact a Flow
with some extra properties, and so we can
plot it like any other Flow
:
trend.plot(bounds=(0.0475, 0.0525))
Volatility#
Volatility refers to the variation in returns (differences from one period to the next). Rangekeeper includes autoregression and mean-reversion in its “Volatility” module.
To introduce volatility, we require the following parameters:
Volatility per Period: This is the standard deviation across time (longitudinal dispersion).
Note
Volatility “accumulates” in the sense that the realization of the change in one period becomes embedded in the level (of rents) going forward into the next period, whose change is then added on top of the previous level (of rents). If there is autoregression (inertia in price movements) then that will also affect the annual volatility in the rent changes. Cycles will also affect the average volatility observed empirically across the scenario.
Evidence indicates that in mature markets such as the U.S., real estate market volatility (not including individual building idiosyncratic risk) is on the order of 10%. However, this includes the effect of long-term cycles as well as annual accumulating volatility. If you are modeling an individual stabilized building, then you should probably set this in the range of 10% to 15% (which would reflect some idiosyncratic risk as well as market risk).
(From [dNG18], accompanying Excel spreadsheets)
Autoregression Parameter: This reflects the inertia in the price movements.
Note
The autoregression parameter indicates what proportion of the previous period’s return (price change) will automatically become a component of the current period’s return (price change). The greater this parameter, the more inertia or momentum the real estate prices will have. In most real estate markets this would typically be a positive fraction, perhaps in the range +0.1 to +0.5. In more liquid and informationally efficient asset markets such as stock markets you might leave this at zero (no inertia). A “noisy” market would have a negative autoregression parameter, however, we deal with noise separately.
(From [dNG18], accompanying Excel spreadsheets)
Mean-Reversion Parameter: This determines the strength (or speed) of the mean reversion tendency in the price levels.
Note
This parameter is the proportion of the deviation between the current price level and the long-run trend level that will be reduced in each year. This should be between zero and 1, probably not very close to 1. For example, if the previous price level were 1.0, and the long-term trend price level for that period were 1.2, and if the mean reversion parameter were 0.5, then 0.5 * (1.2 - 1.0) = 0.10 will be added to this period’s price level.
This also imparts inertia or momentum into the real estate prices, but in a special way, pulling the prices back towards the long-run trend level (not including any cycle that is in the price dynamics).
Evidence suggests that a mean reversion rate on the order of 0.1 to 0.4 is probably appropriate, especially for individual properties (as distinct from total market aggregates).
(From [dNG18], accompanying Excel spreadsheets)
volatility_per_period = .1
autoregression_param = .2
mean_reversion_param = .3
volatility = rk.dynamics.volatility.Volatility(
sequence=sequence,
trend=trend,
volatility_per_period=volatility_per_period,
autoregression_param=autoregression_param,
mean_reversion_param=mean_reversion_param)
We can now see the volatility with respect to the trend:
rent_market = rk.flux.Stream(
name='Rent Market',
flows=[
volatility,
trend
],
frequency=frequency)
rent_market.plot(
flows={
'Market Trend': (0., .1),
'Cumulative Volatility': (0., .1),
}
)
Cyclicality#
This models a (possibly somewhat) predictable long-term cycle in the pricing. In fact, there are two cycles, not necessarily in sync, one for the space market (rents) and another separate cycle for the asset market (capital flows); the latter reflected by the cap rate. We model each separately, using generalized sine functions governed by the given input period, amplitude, and phase. In addition, there is an asymmetric parameter that governs the degree to which the sine curves are skewed, in order to reflect the sharpness, or quickness, normally noticed in market downturns as opposed to their upturns.
Rangekeeper provides two ways to specify the cyclicality of a Market
:
From sine wave parameters (period, phase, amplitude)
From somewhat-observable or estimate-able tendencies in market data, like phase offsets, or peak-to-trough height
To reflect how [dNG18] constructs market cycles, we will use the
from_estimates()
method with the following inputs:
Space (Rent) Cycle Phase Proportion: This governs the proportional positioning of the Space (Rent) Cycle in time, relative to a base case (starting mid-cycle, heading up)
Note
If you think you know where the market currently is in the cycle, then specify the proportion of the cycle period to shift the cycle by (note, since this is generated from a sine curve, the base (0) is at mid-cycle, heading up. See https://en.wikipedia.org/wiki/Phase_(waves)
(From [dNG18], accompanying Excel spreadsheets)
Space (Rent) Cycle Period: This governs the duration of the Space (Rent) Cycle in time
Note
In the U.S. the real estate market cycle seems to be in the range of 10 to 20 years.
Space (Rent) Cycle Height: The space cycle height is the peak-to-trough full cycle distance as a fraction of the mid-cycle level. (ie, double its amplitude)
Note
Historically in the U.S. such cycles in investment property have been as much as 50% or more in some markets in the rental market (including both rent prices & occupancy effect and considering the leverage that fixed operating expenses have on the bottom-line net cash flow). The Pricing Factors in this model apply to net cash flows, not just top-line potential gross rental revenue.
(From [dNG18], accompanying Excel spreadsheets)
Asset (Cap Rate) Cycle Period Difference: This governs the offset/slippage between the Space and Asset Cycles
Note
This can be randomly different from rent cycle period, but probably not too different, maybe +/- 1 year.
(From [dNG18], accompanying Excel spreadsheets)
Asset (Cap Rate) Cycle Phase Difference Proportion: This governs the proportional positioning of the Asset (Cap Rate) Cycle, relative (offset) to the Space Cycle
Note
The Asset and Space cycles are not generally exactly in sync, but they usually are not too far off from each other (e.g.: a quarter-period)
(From [dNG18], accompanying Excel spreadsheets)
Asset (Cap Rate) Cycle Amplitude: This specifies the size of the Asset (Cap Rate) Cycle, in absolute values
Note
The Asset (Cap Rate) Cycle operates in addition to (but not entirely unrelated to) the Space (Rent) market cycle. Cap rates may cycle +/- 100 to 200 basis-points.
Note that this is in cap rate units, so keep in mind the magnitude of the initial cap rate (in the
Market
Trend`). For example, if the initial (base) cap rate entered there is 5.00%, and you enter 2.00% here, then this will mean a cap rate cycle swinging between 4.00% & 6.00%, which corresponds roughly to a property value swing of +/-20% (other things equal). Note also that because this cycle is symmetric but operates in the denominator of the pricing factors governing the simulated future cash flows, this cycle imparts a positive bias into the project ex post cash flows relative to the proforma expected cash flows, whenever there is reversion.(From [dNG18], accompanying Excel spreadsheets)
Cycle Asymmetric Parameters: The asymmetric parameter governs the degree to which the cycle waveform is skewed from its base (sine) form to resemble more of a ‘sawtooth’. This can be used to generate waveforms that reflect market tendencies where downturns are often sharper and quicker than subsequent recoveries.
Note
This is a parameter between -1 and 1, where 0 indicates no asymmetry and -1/1 are extremes where the upturn/downturn is almost immediate. A value of 0.5 would represent the recovery to last twice as long as the downturn.
cyclicality = rk.dynamics.cyclicality.Cyclicality.from_estimates(
space_cycle_phase_prop=0,
space_cycle_period=13.8,
space_cycle_height=1,
space_cycle_asymmetric_parameter=.5,
asset_cycle_period_diff=0.8,
asset_cycle_phase_diff_prop=-.05,
asset_cycle_amplitude=.02,
asset_cycle_asymmetric_parameter=.5,
sequence=sequence)
We can now visualize the ‘pure’ cycles of the space and asset markets:
Warning
Note the Asset (Cap Rate) Cycle is modelled as the negative of actual cap rate cycle. This makes this cycle directly reflect the asset pricing, as prices are an inverse function of the cap rate. By taking the negative of the actual cap rate, we therefore make it easier to envision the effect on prices.
(From [dNG18], accompanying Excel spreadsheets)
market_waves = rk.flux.Stream(
name='Market Waveforms',
flows=[cyclicality.space_waveform, cyclicality.asset_waveform],
frequency=frequency)
market_waves.plot(
flows={
'Space Cycle Waveform': (0, 1.6),
'Asset Cycle Waveform': (-0.025, 0.025)
}
)
Noise#
Noise models the random realization of “deal noise.” Similar to volatility, only unlike volatility noise does not accumulate over time. It applies directly to the value LEVELs (not returns).
Note
By definition, deal-level noise would not exist at the level of aggregate market prices, so you may want to zero out this parameter here if you’re not using this sheet to represent individual asset or project values.
[dNG18] use a triangular distribution to model noise, though any sampleable distribution is possible with Rangekeeper.
(From [dNG18], accompanying Excel spreadsheets)
Noise uses a “half-range” (or ‘residual’ of a symmetric distribution) around 0.0 as an input.
noise = rk.dynamics.noise.Noise(
sequence=sequence,
noise_dist=rk.distribution.Symmetric(
type=rk.distribution.Type.TRIANGULAR,
residual=.1))
Black Swan#
A Black Swan is an event that comes as a surprise and has a major effect on the
Market
, outside of its other modelled factors. To simplify the nature of a
Black Swan event, [dNG18] model it as a potentially-once-in-a-span
immediate downturn that dissipates over time.
[dNG18] constructs Black Swans with the following input parameters:
Black Swan Probability: This is the likelihood of a “Black Swan” event occurring in any one year, given that it has not occurred yet in the scenario.
Black Swan Effect: This should probably be a negative fraction, as “black swans” are usually negative impacts. Perhaps a fraction in the range of -0.2 to -0.4 could be consistent with historical experience.
Note
[dNG18] dissipate Black Swan events over time, geometrically, at the same mean reversion rate as is applied in general to the rents (in the “Volatility” module)
(From [dNG18], accompanying Excel spreadsheets)
black_swan = rk.dynamics.black_swan.BlackSwan(
sequence=sequence,
likelihood=.05,
dissipation_rate=mean_reversion_param,
probability=rk.distribution.Uniform(),
impact=-.25)
Putting it all Together#
A Rangekeeper Market
integrates the previous modules to produce an object
with two important attributes for use in Proforma DCFs:
Space Market Price Factors: This is the “true value” Pricing Factor for just the space market, not reflecting the asset market cycle (cap rate). We have actually already computed this, and we’re here just making it into a ratio of the initial rent, in order to calibrate it as a Pricing Factor to be applied multiplicatively to the Pro Forma Base Case cash flows. This series of Pricing factors will apply to operating cash flows.
Implied Reversion Cap Rates: These are the forward-looking cap rates implied for each year of the scenario. These will govern the reversion (resale) cash flows in the DCF model of PV.
Note
Rangekeeper constructs these as a Flow
s, that can be used in a Stream
for
multiplication (most presumably, against either Space-based income Cash Flow like
rents, or Asset-based incomes like Reversions (Dispositions))
market = rk.dynamics.market.Market(
sequence=sequence,
trend=trend,
volatility=volatility,
cyclicality=cyclicality,
noise=noise,
black_swan=black_swan)
We can replicate (as good as possible) the table in ‘MktDynamicsInputs’ tab in the accompanying Excel spreadsheets to [dNG18] like so:
table = rk.flux.Stream(
name='Market Dynamics',
flows=[
market.trend,
market.volatility.volatility,
market.volatility.autoregressive_returns,
market.volatility,
market.cyclicality.space_waveform,
market.space_market,
market.cyclicality.asset_waveform,
market.asset_market,
market.asset_true_value,
market.space_market_price_factors,
market.noisy_value,
market.historical_value,
market.implied_rev_cap_rate,
market.returns
],
frequency=frequency)
table
date | Market Trend | Volatility | Autoregressive Returns | Cumulative Volatility | Space Cycle Waveform | Space Market | Asset Cycle Waveform | Asset Market | Asset True Value | Space Market Price Factors | Noisy Value | Historical Value | Implied Cap Rate | Returns |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
2001 | 0.05 | 0.08 | 0.08 | 0.05 | 1.00 | 0.05 | 0.00 | 0.05 | 1.09 | 1.00 | 1.15 | 0.86 | 0.07 | 0.49 |
2002 | 0.05 | 0.07 | 0.09 | 0.05 | 1.15 | 0.06 | 0.01 | 0.04 | 1.55 | 1.25 | 1.55 | 1.28 | 0.06 | 0.59 |
2003 | 0.05 | 0.10 | 0.12 | 0.06 | 1.29 | 0.08 | 0.01 | 0.04 | 2.16 | 1.53 | 2.31 | 2.03 | 0.04 | 0.36 |
2004 | 0.05 | 0.09 | 0.11 | 0.06 | 1.41 | 0.09 | 0.02 | 0.03 | 2.81 | 1.79 | 3.03 | 2.77 | 0.04 | 0.05 |
2005 | 0.05 | 0.07 | 0.09 | 0.07 | 1.49 | 0.10 | 0.02 | 0.03 | 3.24 | 1.94 | 3.10 | 2.91 | 0.03 | -0.22 |
2006 | 0.05 | -0.11 | -0.10 | 0.05 | 1.49 | 0.08 | 0.02 | 0.03 | 2.54 | 1.62 | 2.37 | 2.27 | 0.02 | -0.41 |
2007 | 0.05 | -0.18 | -0.20 | 0.04 | 1.34 | 0.06 | 0.01 | 0.04 | 1.38 | 1.13 | 1.37 | 1.33 | 0.03 | -0.47 |
2008 | 0.05 | -0.07 | -0.11 | 0.04 | 0.95 | 0.04 | -0.01 | 0.06 | 0.66 | 0.76 | 0.72 | 0.71 | 0.03 | -0.51 |
2009 | 0.05 | -0.07 | -0.09 | 0.04 | 0.61 | 0.02 | -0.02 | 0.07 | 0.36 | 0.48 | 0.35 | 0.35 | 0.05 | -0.39 |
2010 | 0.05 | -0.23 | -0.25 | 0.03 | 0.50 | 0.02 | -0.02 | 0.07 | 0.24 | 0.33 | 0.21 | 0.21 | 0.09 | 0.18 |
2011 | 0.05 | -0.04 | -0.09 | 0.03 | 0.52 | 0.02 | -0.02 | 0.07 | 0.27 | 0.37 | 0.25 | 0.25 | 0.10 | 0.48 |
2012 | 0.05 | 0.11 | 0.10 | 0.04 | 0.61 | 0.03 | -0.02 | 0.07 | 0.40 | 0.52 | 0.37 | 0.37 | 0.10 | 0.70 |
2013 | 0.05 | 0.15 | 0.17 | 0.05 | 0.74 | 0.04 | -0.01 | 0.06 | 0.63 | 0.76 | 0.63 | 0.63 | 0.07 | 0.22 |
2014 | 0.05 | -0.12 | -0.08 | 0.05 | 0.88 | 0.04 | -0.00 | 0.05 | 0.75 | 0.82 | 0.77 | 0.77 | 0.06 | 0.09 |
2015 | 0.05 | -0.10 | -0.12 | 0.04 | 1.03 | 0.04 | 0.00 | 0.05 | 0.88 | 0.87 | 0.84 | 0.83 | 0.06 | 0.43 |
2016 | 0.05 | 0.04 | 0.01 | 0.04 | 1.18 | 0.05 | 0.01 | 0.04 | 1.22 | 1.06 | 1.19 | 1.19 | 0.06 | 0.53 |
2017 | 0.05 | 0.18 | 0.19 | 0.05 | 1.32 | 0.07 | 0.01 | 0.04 | 1.88 | 1.44 | 1.82 | 1.82 | 0.04 | 0.29 |
2018 | 0.05 | -0.01 | 0.03 | 0.05 | 1.43 | 0.08 | 0.02 | 0.03 | 2.32 | 1.57 | 2.35 | 2.35 | 0.03 | 0.06 |
2019 | 0.05 | -0.06 | -0.05 | 0.05 | 1.49 | 0.08 | 0.02 | 0.03 | 2.45 | 1.51 | 2.49 | 2.49 | 0.03 | 0.01 |
2020 | 0.05 | -0.04 | -0.05 | 0.05 | 1.47 | 0.07 | 0.02 | 0.03 | 2.33 | 1.41 | 2.51 | 2.50 | 0.03 | -0.25 |
2021 | 0.05 | 0.04 | 0.03 | 0.05 | 1.28 | 0.06 | 0.02 | 0.03 | 1.85 | 1.27 | 1.87 | 1.87 | 0.02 | -0.51 |
2022 | 0.05 | -0.00 | 0.00 | 0.05 | 0.87 | 0.04 | 0.00 | 0.05 | 0.91 | 0.86 | 0.92 | 0.92 | 0.03 | -0.48 |
2023 | 0.05 | 0.03 | 0.03 | 0.05 | 0.57 | 0.03 | -0.01 | 0.06 | 0.47 | 0.59 | 0.48 | 0.48 | 0.04 | -0.39 |
2024 | 0.05 | -0.20 | -0.19 | 0.04 | 0.50 | 0.02 | -0.02 | 0.07 | 0.30 | 0.41 | 0.29 | 0.29 | 0.08 | 0.22 |
2025 | 0.05 | 0.03 | -0.00 | 0.04 | 0.54 | 0.02 | -0.02 | 0.07 | 0.33 | 0.47 | 0.36 | 0.36 | 0.00 | 0.00 |
And plot the key Flow
s with:
table.plot(
flows={
'Market Trend': (0, .1),
'Space Market': (0, .1),
'Historical Value': (0, 3)
}
)
We can replicate the plot of various sources of risk and dynamics shown separately:
table.plot(
flows={
'Market Trend': (0, .1),
'Cumulative Volatility': (0, .1),
'Space Market': (0, .1),
'Asset True Value': (0, 3),
'Noisy Value': (0, 3),
'Historical Value': (0, 3)
}
)