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:

  1. Non-substitutability and non-fungibility of real estate assets.

  2. Inefficiency of information processing in real estate markets.

  3. Autoregression in real estate pricing

  4. Cyclicality in real estate pricing

  5. 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.

_images/FaREVuU-figure7.2.png

Fig. 3 Figure 7.2 from [dNG18]#

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:

  1. Market Trend

  2. Volatility (including Autoregression and Mean Reversion)

  3. Cyclicality

  4. Noise

  5. Black Swan

Markets can be generated in two ways:

  1. Deterministically (mostly); where all inputs are specified and have no variability (except for those with explicit randomness like volatility or noise)

  2. 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:

  1. 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.

  2. Growth Rate: This will govern the central tendency of the long-run growth rate trend that will apply over the entire scenario.

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))
_images/eaf52dde94d2610e4500d63673f855e34b2f9b3c0d2cd3baef352815a71913ac.png

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:

  1. Volatility per Period: This is the standard deviation across time (longitudinal dispersion).

  2. Autoregression Parameter: This reflects the inertia in the price movements.

  3. Mean-Reversion Parameter: This determines the strength (or speed) of the mean reversion tendency in the price levels.

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),
        }
    )
_images/7d533e1ed9105fde34aa084e5e9a567d52c2bce70abf14e4915082dab9e3b88a.png

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:

  1. From sine wave parameters (period, phase, amplitude)

  2. 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:

  1. 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)

  2. Space (Rent) Cycle Period: This governs the duration of the Space (Rent) Cycle in time

  3. 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)

  4. Asset (Cap Rate) Cycle Period Difference: This governs the offset/slippage between the Space and Asset Cycles

  5. Asset (Cap Rate) Cycle Phase Difference Proportion: This governs the proportional positioning of the Asset (Cap Rate) Cycle, relative (offset) to the Space Cycle

  6. Asset (Cap Rate) Cycle Amplitude: This specifies the size of the Asset (Cap Rate) Cycle, in absolute values

  7. 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.

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)
        }
    )
_images/a41db239ff37f395f938c4cf33268404deac5f69b8998c68cfd540002356f597.png

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:

  1. 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.

  2. 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:

  1. 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.

  2. 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 Flows, 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 Flows with:

table.plot(
        flows={
            'Market Trend': (0, .1),
            'Space Market': (0, .1),
            'Historical Value': (0, 3)
            }
        )
_images/1e54118f859b4443618df54d62877d9697678a2c149315624e1992eeba9c3239.png

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)
            }
    )
_images/c7f7217be4bbcec4514bbbdb46a4425cd652020299d40cd98edbbf2069f04d08.png