Skip to content

Commit

Permalink
Merge pull request #6 from daniel-fink/develop
Browse files Browse the repository at this point in the history
Merge develop to main
  • Loading branch information
daniel-fink committed Jun 5, 2022
2 parents 02b7640 + f6160a2 commit 29b7775
Show file tree
Hide file tree
Showing 21 changed files with 1,771 additions and 1,033 deletions.
223 changes: 72 additions & 151 deletions distribution.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,21 +3,20 @@
import aenum
import numpy as np
import scipy.stats as ss
from abc import ABC, abstractmethod


class Type(aenum.Enum):
_init_ = 'value __doc__'

uniform = 1
linear = 2
triangular = 3
normal = 4
log_uniform = 5
PERT = 6
exponential = 7
uniform = 'Uniform distribution', 'Continuous uniform distribution or rectangular distribution'
linear = 'Linear distribution', 'Linearly increasing or decreasing distribution between minimum and maximum values'
triangular = 'Triangular distribution', 'Continuous linear distribution with lower limit a, upper limit b and mode c, where a < b and a ≤ c ≤ b.'
normal = 'Normal distribution', 'Continuous probability distribution defined as the limiting case of a discrete binomial distribution.'
PERT = 'PERT distribution', 'Transformation of the four-parameter Beta distribution defined by the minimum, most likely, and maximum values.'


class Distribution:
type: str

def __init__(
self,
generator: Optional[np.random.Generator] = None):
Expand All @@ -38,6 +37,36 @@ def sample(
else:
return variates

@abstractmethod
def interval_density(
self,
parameters: [float]):
"""
Returns the cumulative density (integral of the interpolated curve)
between n parameter pairs as intervals (i.e. returns n-1 results)
:param parameters: Any set of floats between 0 and 1
:return: List of floats representing the cumulative density of that interval.
If the input parameters span 0 to 1, the sum of the interval densities will reach 1.
"""
if (all(parameters) >= 0) & (all(parameters) <= 1):
return [(self.dist.cdf(parameters[i + 1]) - self.dist.cdf(parameters[i]))
for i in (range(0, len(parameters) - 1))]
else:
raise ValueError("Error: Parameter must be between 0 and 1 inclusive")

@abstractmethod
def cumulative_density(
self,
parameters: [float]):
"""
Returns the cumulative distribution at parameters between 0 and 1.
"""
if (all(parameters) >= 0) & (all(parameters) <= 1):
return [self.dist.cdf(parameter) for parameter in parameters]
else:
raise ValueError("Error: Parameter must be between 0 and 1 inclusive")


class Symmetric(Distribution):
def __init__(
Expand All @@ -54,7 +83,6 @@ def __init__(

self.mean = mean
self.residual = residual
print(distribution_type)
if distribution_type is Type.uniform or distribution_type is Type.PERT:
self.distribution_type = distribution_type
else:
Expand All @@ -63,12 +91,24 @@ def __init__(

def distribution(self):
if self.distribution_type == Type.uniform:
return Uniform(lower=self.mean - self.residual,
range=self.residual * 2,
generator=self.generator)
return Uniform(
lower=self.mean - self.residual,
range=self.residual * 2,
generator=self.generator)
elif self.distribution_type == Type.PERT:
return PERT.standard_symmetric(peak=self.mean,
residual=self.residual)
return PERT.standard_symmetric(
peak=self.mean,
residual=self.residual)

def interval_density(
self,
parameters: [float]) -> [float]:
return self.distribution().interval_density(parameters)

def cumulative_density(
self,
parameters: [float]) -> [float]:
return self.distribution().cumulative_density(parameters)


class Uniform(Distribution):
Expand All @@ -89,126 +129,13 @@ def __init__(

def interval_density(
self,
parameters: [float]):
"""
Returns the cumulative density (integral of the distribution curve, or effectively probability)
between n parameter pairs as intervals (i.e. returns n-1 results)
:param parameters: Any set of floats between 0 and 1
:return: List of floats representing the cumulative density of that interval.
If the input parameters span 0 to 1, the sum of the interval densities will reach 1.
"""

if (all(parameters) >= 0) & (all(parameters) <= 1):
return [(self.dist.cdf(parameters[i + 1]) - self.dist.cdf(parameters[i]))
for i in (range(0, len(parameters) - 1))]
else:
raise ValueError("Error: Parameter must be between 0 and 1 inclusive")
parameters: [float]) -> [float]:
return super().interval_density(parameters)

def cumulative_density(
self,
parameters: [float]):
"""
Returns the cumulative distribution at parameters between 0 and 1.
"""
if (all(parameters) >= 0) & (all(parameters) <= 1):
return [self.dist.cdf(parameter) for parameter in parameters]
else:
raise ValueError("Error: Parameter must be between 0 and 1 inclusive")


class Linear(Distribution):
"""
A continuous linearly growing (or decaying) distribution between 0 and 1.
To calculate the density at any point, the distribution is scaled such that the cumulative distribution reaches 1.
To calculate the factor, the distribution is initialized with value 1.
Requires inputs of linear rate of change per period and number of periods.
"""

def __init__(
self,
rate: float,
num_periods: int,
generator: Optional[np.random.Generator] = None):
super().__init__(generator=generator)
self.rate = rate
self.num_periods = num_periods

def factor(self):
"""
Returns the multiplicative factor of the distribution's initial value at each period
"""
# TODO: Check if this is correct
# return [np.power((1 + self.rate), period_index) for period_index in range(self.num_periods)]


class Exponential(Distribution):
"""
A continuous exponentially growing (or decaying) distribution between 0 and 1.
To calculate the density at any point, the distribution is scaled such that the cumulative distribution reaches 1.
To calculate the factor, the distribution is initialized with value 1.
Requires inputs of rate of change per period and number of periods.
"""

def __init__(
self,
rate: float,
num_periods: int,
generator: Optional[np.random.Generator] = None):
super().__init__(generator=generator)
self.rate = rate
self.num_periods = num_periods

# def density(
# self,
# parameters: [float]):
# """
# Returns the value of the density function at a parameter between 0 and 1.
# The form of the function is k*(1+r)^((n-1)x),
# where k is a scaling constant that constrains the cumulative distribution to 1,
# r is the rate, n is the number of periods, and x the parameter.
#
# In this case, k = ((n - 1)*(1 + r)*log(1 + r))/((1 + r)^n - (1 + r)); see https://www.wolframalpha.com/input/?i=integrate+%28%28%28n+-+1%29*%281+%2B+r%29*log%281+%2B+r%29%29%2F%28%281+%2B+r%29%5En+-+%281+%2B+r%29%29%29*%281+%2B+r%29%5E%28%28n-1%29*x%29+dx+from+0+to+1
# """
# if (all(parameters) >= 0) & (all(parameters) <= 1):
# def f(parameter):
# k_num = (self.num_periods - 1) * (1 + self.rate) * np.log(1 + self.rate)
# k_denom = np.power((1 + self.rate), self.num_periods) - (1 + self.rate)
# k = np.divide(k_num, k_denom)
# return k * np.power((1 + self.rate), (self.num_periods - 1) * parameter)
# # return ((self.num_periods * math.log(1 + self.rate)) * math.pow((1 + self.rate), self.num_periods * parameter)) \
# # / (math.pow((1 + self.rate), self.num_periods) - 1)
#
# return [f(parameter) for parameter in parameters]
# else:
# raise ValueError("Error: Parameter must be between 0 and 1 inclusive")

# def cumulative_density(
# self,
# parameters: [
# float]): # #TODO: This is giving incorrect answers for low values of num_periods...
# """
# Returns the cumulative distribution at parameters between 0 and 1.
#
# See https://www.wolframalpha.com/input/?i=integrate+%28%28%28n+-+1%29*%281+%2B+r%29*log%281+%2B+r%29%29%2F%28%281+%2B+r%29%5En+-+%281+%2B+r%29%29%29+*+%28%281+%2B+r%29%5E%28%28n+-+1%29*x%29%29++dx
# """
# if (all(parameters) >= 0) & (all(parameters) <= 1):
# def f(parameter):
# num = np.power((1 + self.rate), (parameter * (self.num_periods - 1)) + 1)
# denom = np.power((1 + self.rate), self.num_periods) - self.rate - 1
# return np.divide(num, denom)
#
# return [f(parameter) for parameter in parameters]
# else:
# raise ValueError("Error: Parameter must be between 0 and 1 inclusive")

def factor(self):
"""
Returns the multiplicative factor of the distribution's initial value at each period
"""
return [np.power((1 + self.rate), period_index) for period_index in range(self.num_periods)]
parameters: [float]) -> [float]:
return super().cumulative_density(parameters)


class PERT(Distribution):
Expand Down Expand Up @@ -250,29 +177,23 @@ def __init__(
else:
raise ValueError("Error: Weighting must be greater than 0 and Peak must be between 0 and 1 inclusive")

@staticmethod
@classmethod
def standard_symmetric(
cls,
peak: float,
residual: float):
return PERT(peak=peak,
weighting=4.,
minimum=peak - residual,
maximum=peak + residual)
return cls(
peak=peak,
weighting=4.,
minimum=peak - residual,
maximum=peak + residual)

def interval_density(
self,
parameters: [float]):
"""
Returns the cumulative density (integral of the distribution curve, or effectively probability)
between n parameter pairs as intervals (i.e. returns n-1 results)
parameters: [float]) -> [float]:
return super().interval_density(parameters)

:param parameters: Any set of floats between 0 and 1
:return: List of floats representing the cumulative density of that interval.
If the input parameters span 0 to 1, the sum of the interval densities will reach 1.
"""

if (all(parameters) >= 0) & (all(parameters) <= 1):
return [(self.dist.cdf(parameters[i + 1]) - self.dist.cdf(parameters[i]))
for i in (range(0, len(parameters) - 1))]
else:
raise ValueError("Error: Parameter must be between 0 and 1 inclusive")
def cumulative_density(
self,
parameters: [float]) -> [float]:
return super().cumulative_density(parameters)
42 changes: 23 additions & 19 deletions dynamics/trend.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
try:
import projection
import distribution
import flux
import periodicity
Expand All @@ -15,7 +16,7 @@
class Trend:
def __init__(self,
phase: phase.Phase,
period_type: periodicity.Periodicity.Type,
period_type: periodicity.Type,
params: dict):
self.current_rent = params['initial_price_factor'] * params['cap_rate']
"""
Expand All @@ -26,12 +27,13 @@ def __init__(self,
The uncertainty is revealed in Year 1. Year 0 is fixed because it is observable already in the present.
"""

initial_rent_dist = distribution.PERT(peak=self.current_rent,
weighting=4.,
minimum=self.current_rent - params['rent_residual'],
maximum=self.current_rent + params['rent_residual'])
initial_rent_dist = distribution.PERT(
peak=self.current_rent,
weighting=4.,
minimum=self.current_rent - params['rent_residual'],
maximum=self.current_rent + params['rent_residual'])
self.initial_rent = initial_rent_dist.sample()
#self.initial_rent = 0.0511437
# self.initial_rent = 0.0511437

"""
Uncertainty Distribution
Expand All @@ -44,21 +46,23 @@ def __init__(self,
This is so for all of the random number generators in this workbook.
"""

trend_dist = distribution.PERT(peak=params['trend_delta'],
weighting=4.,
minimum=params['trend_delta'] - params['trend_residual'],
maximum=params['trend_delta'] + params['trend_residual'])
trend_dist = distribution.PERT(
peak=params['trend_delta'],
weighting=4.,
minimum=params['trend_delta'] - params['trend_residual'],
maximum=params['trend_delta'] + params['trend_residual'])
self.trend_rate = trend_dist.sample()
#self.trend_rate = 0.00698263624
# self.trend_rate = 0.00698263624

trend_dist = distribution.Exponential(rate=self.trend_rate,
num_periods=phase.duration(period_type=period_type,
inclusive=True))
self.trend = flux.Flow.from_initial(name='Trend',
initial=self.initial_rent,
index=phase.to_index(periodicity=period_type),
dist=trend_dist,
units=measure.scalar)
trend_esc = projection.ExponentialExtrapolation(
rate=self.trend_rate)

self.trend = flux.Flow.from_projection(
name='Trend',
value=self.initial_rent,
index=phase.to_index(period_type=period_type),
proj=trend_esc,
units=measure.scalar)
"""
Trend:
Note that the trend is geometric.
Expand Down
1 change: 1 addition & 0 deletions dynamics/volatility.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
from numba import jit

try:
import projection
import distribution
import dynamics.trend
import flux
Expand Down
Loading

0 comments on commit 29b7775

Please sign in to comment.