rangekeeper 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- rangekeeper/__init__.py +40 -0
- rangekeeper/api.py +88 -0
- rangekeeper/distribution.py +269 -0
- rangekeeper/dynamics/__init__.py +6 -0
- rangekeeper/dynamics/black_swan.py +77 -0
- rangekeeper/dynamics/cyclicality.py +414 -0
- rangekeeper/dynamics/market.py +328 -0
- rangekeeper/dynamics/noise.py +34 -0
- rangekeeper/dynamics/trend.py +79 -0
- rangekeeper/dynamics/volatility.py +140 -0
- rangekeeper/extrapolation.py +100 -0
- rangekeeper/flux.py +733 -0
- rangekeeper/graph.py +144 -0
- rangekeeper/measure.py +138 -0
- rangekeeper/models/__init__.py +5 -0
- rangekeeper/models/deterministic.py +143 -0
- rangekeeper/models/flexible.py +177 -0
- rangekeeper/models/linear.py +144 -0
- rangekeeper/models/linear_graph.py +171 -0
- rangekeeper/models/probabilistic.py +157 -0
- rangekeeper/periodicity.py +236 -0
- rangekeeper/policy.py +20 -0
- rangekeeper/projection.py +248 -0
- rangekeeper/segmentation.py +220 -0
- rangekeeper/setup.py +14 -0
- rangekeeper/space.py +42 -0
- rangekeeper/span.py +226 -0
- rangekeeper-0.1.0.dist-info/LICENSE +373 -0
- rangekeeper-0.1.0.dist-info/METADATA +32 -0
- rangekeeper-0.1.0.dist-info/RECORD +31 -0
- rangekeeper-0.1.0.dist-info/WHEEL +4 -0
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import enum
|
|
4
|
+
import math
|
|
5
|
+
from typing import List, Dict, Union, Tuple
|
|
6
|
+
from collections import namedtuple
|
|
7
|
+
|
|
8
|
+
import pandas as pd
|
|
9
|
+
import numpy as np
|
|
10
|
+
from aenum import Enum
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class Characteristic(Enum):
|
|
14
|
+
_init_ = 'value'
|
|
15
|
+
|
|
16
|
+
use = 'Use'
|
|
17
|
+
tenure = 'Tenure'
|
|
18
|
+
span = 'Span'
|
|
19
|
+
type = 'Type'
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class Type:
|
|
23
|
+
name: str
|
|
24
|
+
code: str
|
|
25
|
+
supertype: Type
|
|
26
|
+
subtypes: [Type]
|
|
27
|
+
|
|
28
|
+
def __init__(
|
|
29
|
+
self,
|
|
30
|
+
name: str,
|
|
31
|
+
code: str = None,
|
|
32
|
+
supertype: Type = None):
|
|
33
|
+
self.name = name
|
|
34
|
+
self.code = code
|
|
35
|
+
self.supertype = supertype
|
|
36
|
+
self.subtypes = []
|
|
37
|
+
|
|
38
|
+
if supertype is not None:
|
|
39
|
+
supertype.subtypes.append(self)
|
|
40
|
+
|
|
41
|
+
def __str__(self):
|
|
42
|
+
code = ' :' + self.code if self.code is not None else ''
|
|
43
|
+
return self.name + code
|
|
44
|
+
|
|
45
|
+
def add_subtypes(
|
|
46
|
+
self,
|
|
47
|
+
subtypes: [Type]):
|
|
48
|
+
for subtype in subtypes:
|
|
49
|
+
self.subtypes.append(subtype)
|
|
50
|
+
subtype.supertype = self
|
|
51
|
+
|
|
52
|
+
def ancestors(self) -> [Type]:
|
|
53
|
+
ancestors = []
|
|
54
|
+
supertype = self.supertype
|
|
55
|
+
while supertype is not None:
|
|
56
|
+
ancestors.append(supertype)
|
|
57
|
+
supertype = supertype.supertype
|
|
58
|
+
return ancestors
|
|
59
|
+
|
|
60
|
+
def primogenitor(self) -> Type:
|
|
61
|
+
return self.ancestors()[-1]
|
|
62
|
+
|
|
63
|
+
def display(self):
|
|
64
|
+
print(' > '.join([ancestor.__str__() for ancestor in self.ancestors()]))
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
class Interval(pd.Interval):
|
|
68
|
+
def __init__(
|
|
69
|
+
self,
|
|
70
|
+
right: float,
|
|
71
|
+
left: float = 0.):
|
|
72
|
+
super().__init__(
|
|
73
|
+
left=left,
|
|
74
|
+
right=right)
|
|
75
|
+
|
|
76
|
+
def __str__(self):
|
|
77
|
+
return f'({self.left}, {self.right})'
|
|
78
|
+
|
|
79
|
+
def split(
|
|
80
|
+
self,
|
|
81
|
+
proportion: float) -> tuple[Interval, Interval]:
|
|
82
|
+
if proportion < 0 or proportion > 1:
|
|
83
|
+
raise ValueError('Error: proportion must be between 0 and 1')
|
|
84
|
+
|
|
85
|
+
split_point = self.left + (proportion * self.length)
|
|
86
|
+
left_interval = self.__class__(
|
|
87
|
+
right=split_point,
|
|
88
|
+
left=self.left)
|
|
89
|
+
right_interval = self.__class__(
|
|
90
|
+
right=self.right,
|
|
91
|
+
left=split_point)
|
|
92
|
+
return left_interval, right_interval
|
|
93
|
+
|
|
94
|
+
def subdivide(
|
|
95
|
+
self,
|
|
96
|
+
values: Union[int, List[float]]) -> List[Interval]:
|
|
97
|
+
if isinstance(values, int):
|
|
98
|
+
if values < 0:
|
|
99
|
+
raise ValueError('Error: divisors must be greater than 0')
|
|
100
|
+
lengths = [self.length / values for i in range(values)]
|
|
101
|
+
elif all(isinstance(fraction, float) for fraction in values):
|
|
102
|
+
if math.isclose(sum(values), 1) and all(0 < fraction < 1 for fraction in values):
|
|
103
|
+
lengths = [self.length * fraction for fraction in values]
|
|
104
|
+
else:
|
|
105
|
+
raise ValueError('Error: Sum of divisors must complete unit interval')
|
|
106
|
+
else:
|
|
107
|
+
raise TypeError('Error: values must be either number of divisors or split proportions')
|
|
108
|
+
|
|
109
|
+
parameters = [self.left + sum(lengths[:i]) for i in range(1, len(lengths) + 1)]
|
|
110
|
+
parameters.insert(0, self.left)
|
|
111
|
+
results = []
|
|
112
|
+
for i in range(len(parameters) - 1):
|
|
113
|
+
results.append(
|
|
114
|
+
self.__class__(
|
|
115
|
+
left=parameters[i],
|
|
116
|
+
right=parameters[i + 1]))
|
|
117
|
+
return results
|
|
118
|
+
|
|
119
|
+
def interval_value(self):
|
|
120
|
+
return self.right - self.left
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
class Segment:
|
|
124
|
+
name: str
|
|
125
|
+
bounds: Interval
|
|
126
|
+
parent: Segment
|
|
127
|
+
children: List[Segment]
|
|
128
|
+
characteristics: Dict[Characteristic, str]
|
|
129
|
+
|
|
130
|
+
def __init__(
|
|
131
|
+
self,
|
|
132
|
+
bounds: Interval,
|
|
133
|
+
characteristics: Dict[Characteristic, str] = None,
|
|
134
|
+
parent: Segment = None,
|
|
135
|
+
children: List[Segment] = None,
|
|
136
|
+
name: str = None):
|
|
137
|
+
self.name = name
|
|
138
|
+
self.bounds = bounds
|
|
139
|
+
self.characteristics = {} if characteristics is None else characteristics
|
|
140
|
+
|
|
141
|
+
if parent is not None:
|
|
142
|
+
parent.children.append(self)
|
|
143
|
+
self.parent = parent
|
|
144
|
+
|
|
145
|
+
if children is not None: # Need to run tests on this to check this works.
|
|
146
|
+
for child in children:
|
|
147
|
+
child.parent = self
|
|
148
|
+
self.children = [] if children is None else children
|
|
149
|
+
|
|
150
|
+
def to_frame(self):
|
|
151
|
+
frame = pd.DataFrame(
|
|
152
|
+
data=[list(self.characteristics.values())],
|
|
153
|
+
index=pd.IntervalIndex(data=[self.bounds]),
|
|
154
|
+
columns=[characteristic.value for characteristic in self.characteristics.keys()])
|
|
155
|
+
frame['Amount'] = self.bounds.length
|
|
156
|
+
|
|
157
|
+
if self.parent is not None:
|
|
158
|
+
frame['Proportion (of Parent)'] = "{:.0%}".format(self.bounds.length / self.parent.bounds.length)
|
|
159
|
+
|
|
160
|
+
return frame
|
|
161
|
+
|
|
162
|
+
def display(self):
|
|
163
|
+
print('\n')
|
|
164
|
+
floatfmt = "." + str(2) + "f"
|
|
165
|
+
print(self.to_frame().to_markdown(
|
|
166
|
+
tablefmt='github',
|
|
167
|
+
floatfmt=floatfmt))
|
|
168
|
+
|
|
169
|
+
def display_children(
|
|
170
|
+
self,
|
|
171
|
+
pivot: Characteristic = None,
|
|
172
|
+
decimals: int = 2,
|
|
173
|
+
):
|
|
174
|
+
if len(self.children) == 0:
|
|
175
|
+
raise ValueError("Error: Segment has no children")
|
|
176
|
+
|
|
177
|
+
frame = pd.concat([child.to_frame() for child in self.children])
|
|
178
|
+
pivot_name = ''
|
|
179
|
+
if pivot is not None:
|
|
180
|
+
frame = pd.pivot_table(
|
|
181
|
+
data=frame,
|
|
182
|
+
index=[pivot.value],
|
|
183
|
+
aggfunc=np.sum
|
|
184
|
+
# columns=['Amount', 'Use', 'Proportion (of Parent)']
|
|
185
|
+
)
|
|
186
|
+
frame['Proportion (of Parent)'] = frame['Amount'] / self.bounds.length
|
|
187
|
+
frame['Proportion (of Parent)'] = frame['Proportion (of Parent)'].map("{:.0%}".format)
|
|
188
|
+
pivot_name = ' by ' + pivot.value
|
|
189
|
+
|
|
190
|
+
floatfmt = "." + str(decimals) + "f"
|
|
191
|
+
|
|
192
|
+
print('Children' + pivot_name)
|
|
193
|
+
print(frame.to_markdown(
|
|
194
|
+
tablefmt='github',
|
|
195
|
+
floatfmt=floatfmt))
|
|
196
|
+
|
|
197
|
+
# def display_hierarchy(self):
|
|
198
|
+
|
|
199
|
+
def split(self, proportion: float) -> tuple[Segment, Segment]:
|
|
200
|
+
split_lower, split_upper = self.bounds.split(proportion)
|
|
201
|
+
lower = Segment(
|
|
202
|
+
bounds=split_lower,
|
|
203
|
+
parent=self)
|
|
204
|
+
upper = Segment(
|
|
205
|
+
bounds=split_upper,
|
|
206
|
+
parent=self)
|
|
207
|
+
return (lower, upper)
|
|
208
|
+
|
|
209
|
+
def subdivide(
|
|
210
|
+
self,
|
|
211
|
+
divisions: List[Tuple[str, Dict[Characteristic, str], float]]) -> List[Segment]:
|
|
212
|
+
result = []
|
|
213
|
+
intervals = self.bounds.subdivide([division[2] for division in divisions])
|
|
214
|
+
for name, division, interval in zip([division[0] for division in divisions],[division[1] for division in divisions], intervals):
|
|
215
|
+
result.append(Segment(
|
|
216
|
+
name=name,
|
|
217
|
+
bounds=interval,
|
|
218
|
+
characteristics=division,
|
|
219
|
+
parent=self))
|
|
220
|
+
return result
|
rangekeeper/setup.py
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
from setuptools import setup
|
|
2
|
+
|
|
3
|
+
setup(
|
|
4
|
+
name='Rangekeeper',
|
|
5
|
+
version='0.2.0-alpha',
|
|
6
|
+
packages=['models', 'dynamics'],
|
|
7
|
+
package_dir={'': 'rangekeeper'},
|
|
8
|
+
url='https://github.com/daniel-fink/rangekeeper',
|
|
9
|
+
license='MPL-2.0',
|
|
10
|
+
author='Daniel Fink',
|
|
11
|
+
author_email='danfink@mit.edu',
|
|
12
|
+
description='Rangekeeper is a library assisting financial modelling in real estate scenario planning, decision-making, cashflow forecasting, and the like.'
|
|
13
|
+
)
|
|
14
|
+
|
rangekeeper/space.py
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
from typing import Dict, List, Union, Optional
|
|
3
|
+
from decimal import Decimal
|
|
4
|
+
from pint import Quantity
|
|
5
|
+
|
|
6
|
+
import rangekeeper as rk
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class Space(rk.graph.Element):
|
|
10
|
+
def __init__(
|
|
11
|
+
self,
|
|
12
|
+
name: str,
|
|
13
|
+
type: str,
|
|
14
|
+
measurements: Dict[rk.measure.Measure, Quantity] = None,
|
|
15
|
+
events: List[rk.graph.Event] = None,
|
|
16
|
+
attributes: Dict = None):
|
|
17
|
+
super().__init__(
|
|
18
|
+
name=name,
|
|
19
|
+
type=type,
|
|
20
|
+
measurements=measurements,
|
|
21
|
+
events=events,
|
|
22
|
+
attributes=attributes)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class Apartment(Space):
|
|
26
|
+
num_bed: int
|
|
27
|
+
num_bath: Decimal
|
|
28
|
+
num_balcony: int
|
|
29
|
+
|
|
30
|
+
def __init__(
|
|
31
|
+
self,
|
|
32
|
+
name: str,
|
|
33
|
+
type: str,
|
|
34
|
+
num_bed: int,
|
|
35
|
+
num_bath: Union[int, Decimal],
|
|
36
|
+
num_balcony: Optional[int] = None,
|
|
37
|
+
measurements: Dict[rk.measure.Measure, Quantity] = None,
|
|
38
|
+
attributes: Dict = None):
|
|
39
|
+
super().__init__(name, type, measurements, attributes)
|
|
40
|
+
self.num_bed = num_bed
|
|
41
|
+
self.num_bath = num_bath
|
|
42
|
+
self.num_balcony = num_balcony
|
rangekeeper/span.py
ADDED
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import numpy as np
|
|
4
|
+
import pandas as pd
|
|
5
|
+
|
|
6
|
+
import rangekeeper as rk
|
|
7
|
+
|
|
8
|
+
class Span:
|
|
9
|
+
start_date: pd.Timestamp
|
|
10
|
+
end_date: pd.Timestamp
|
|
11
|
+
name: str = None
|
|
12
|
+
"""
|
|
13
|
+
A `Span` is a pd.Interval of pd.Timestamps that bound its start and end dates
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
def __init__(
|
|
17
|
+
self,
|
|
18
|
+
start_date: pd.Timestamp,
|
|
19
|
+
end_date: pd.Timestamp = None,
|
|
20
|
+
name: str = None):
|
|
21
|
+
"""
|
|
22
|
+
Define a Span using dates and a name. If no end date is provided, it is
|
|
23
|
+
assumed to be a one-day Span (ie, the start and end dates are the same)
|
|
24
|
+
:param start_date:
|
|
25
|
+
:param end_date:
|
|
26
|
+
:param name:
|
|
27
|
+
"""
|
|
28
|
+
if end_date is None:
|
|
29
|
+
end_date = start_date
|
|
30
|
+
elif end_date < start_date:
|
|
31
|
+
raise Exception('Error: end_date cannot be before start_date')
|
|
32
|
+
|
|
33
|
+
if name is None:
|
|
34
|
+
self.name = ''
|
|
35
|
+
else:
|
|
36
|
+
self.name = name
|
|
37
|
+
|
|
38
|
+
self._interval = pd.Interval(left=start_date, right=end_date)
|
|
39
|
+
self.start_date = self._interval.left
|
|
40
|
+
self.end_date = self._interval.right
|
|
41
|
+
|
|
42
|
+
def __str__(self):
|
|
43
|
+
return 'Span: {}\n' \
|
|
44
|
+
'Start Date: {}\n' \
|
|
45
|
+
'End Date: {}'.format(self.name, self.start_date.date(), self.end_date.date())
|
|
46
|
+
|
|
47
|
+
def __repr__(self):
|
|
48
|
+
return self.__str__()
|
|
49
|
+
|
|
50
|
+
@classmethod
|
|
51
|
+
def merge(
|
|
52
|
+
cls,
|
|
53
|
+
name: str,
|
|
54
|
+
spans: [Span]) -> Span:
|
|
55
|
+
"""
|
|
56
|
+
Merge a set of Spans into a single Span
|
|
57
|
+
"""
|
|
58
|
+
start_date = spans[0].start_date
|
|
59
|
+
end_date = spans[0].end_date
|
|
60
|
+
for span in spans:
|
|
61
|
+
if span.start_date < start_date:
|
|
62
|
+
start_date = span.start_date
|
|
63
|
+
if span.end_date > end_date:
|
|
64
|
+
end_date = span.end_date
|
|
65
|
+
return cls(name=name,
|
|
66
|
+
start_date=start_date,
|
|
67
|
+
end_date=end_date)
|
|
68
|
+
|
|
69
|
+
def shift(self,
|
|
70
|
+
num_periods: int,
|
|
71
|
+
period_type: rk.periodicity.Type,
|
|
72
|
+
bound: str = 'end',
|
|
73
|
+
name: str = None) -> Span:
|
|
74
|
+
"""
|
|
75
|
+
Shift the Span's bounds by a specified period increment
|
|
76
|
+
|
|
77
|
+
:param num_periods: The number of periods to shift the bounds
|
|
78
|
+
:param period_type: The period type of the shift
|
|
79
|
+
:param bound: The bound to shift. Options are 'start', 'end', or 'both'
|
|
80
|
+
"""
|
|
81
|
+
name = self.name if name is None else name
|
|
82
|
+
if bound == 'start':
|
|
83
|
+
return Span(
|
|
84
|
+
name=name,
|
|
85
|
+
start_date=rk.periodicity.offset_date(
|
|
86
|
+
date=self.start_date,
|
|
87
|
+
period_type=period_type,
|
|
88
|
+
num_periods=num_periods),
|
|
89
|
+
end_date=self.end_date)
|
|
90
|
+
elif bound == 'end':
|
|
91
|
+
return Span(
|
|
92
|
+
name=name,
|
|
93
|
+
start_date=self.start_date,
|
|
94
|
+
end_date=rk.periodicity.offset_date(
|
|
95
|
+
date=self.end_date,
|
|
96
|
+
period_type=period_type,
|
|
97
|
+
num_periods=num_periods))
|
|
98
|
+
elif bound == 'both':
|
|
99
|
+
return Span(
|
|
100
|
+
name=name,
|
|
101
|
+
start_date=rk.periodicity.offset_date(
|
|
102
|
+
date=self.start_date,
|
|
103
|
+
period_type=period_type,
|
|
104
|
+
num_periods=num_periods),
|
|
105
|
+
end_date=rk.periodicity.offset_date(
|
|
106
|
+
date=self.end_date,
|
|
107
|
+
period_type=period_type,
|
|
108
|
+
num_periods=num_periods))
|
|
109
|
+
else:
|
|
110
|
+
raise Exception('Error: invalid bound specified')
|
|
111
|
+
|
|
112
|
+
def to_index(
|
|
113
|
+
self,
|
|
114
|
+
period_type: rk.periodicity.Type) -> pd.PeriodIndex:
|
|
115
|
+
"""
|
|
116
|
+
Return a pd.PeriodIndex of the Span at the specified periodicity
|
|
117
|
+
"""
|
|
118
|
+
return rk.periodicity.period_index(
|
|
119
|
+
include_start=self.start_date,
|
|
120
|
+
period_type=period_type,
|
|
121
|
+
bound=self.end_date)
|
|
122
|
+
|
|
123
|
+
def duration(
|
|
124
|
+
self,
|
|
125
|
+
period_type: rk.periodicity.Type,
|
|
126
|
+
inclusive: bool = False) -> int:
|
|
127
|
+
"""
|
|
128
|
+
Return the whole-period duration of the Span in the specified period_type
|
|
129
|
+
"""
|
|
130
|
+
return rk.periodicity.duration(
|
|
131
|
+
start_date=self.start_date,
|
|
132
|
+
end_date=self.end_date,
|
|
133
|
+
period_type=period_type,
|
|
134
|
+
inclusive=inclusive)
|
|
135
|
+
|
|
136
|
+
@classmethod
|
|
137
|
+
def from_num_periods(
|
|
138
|
+
cls,
|
|
139
|
+
name: str,
|
|
140
|
+
date: pd.Timestamp,
|
|
141
|
+
period_type: rk.periodicity.Type,
|
|
142
|
+
num_periods: int) -> Span:
|
|
143
|
+
"""
|
|
144
|
+
Create a Span from a date with a set number of periods of specified type.
|
|
145
|
+
If the number of periods is negative, the Span will end on the date specified.
|
|
146
|
+
If the number of periods is positive, the Span will start on the date specified.
|
|
147
|
+
"""
|
|
148
|
+
if num_periods < 0:
|
|
149
|
+
end_date = date
|
|
150
|
+
start_date = rk.periodicity.offset_date(
|
|
151
|
+
date=date,
|
|
152
|
+
period_type=period_type,
|
|
153
|
+
num_periods=num_periods)
|
|
154
|
+
start_date = rk.periodicity.offset_date(
|
|
155
|
+
date=start_date,
|
|
156
|
+
period_type=rk.periodicity.Type.DAY,
|
|
157
|
+
num_periods=1)
|
|
158
|
+
return cls(name=name, start_date=start_date, end_date=end_date)
|
|
159
|
+
else:
|
|
160
|
+
start_date = date
|
|
161
|
+
end_date = rk.periodicity.offset_date(
|
|
162
|
+
date=date,
|
|
163
|
+
period_type=period_type,
|
|
164
|
+
num_periods=num_periods)
|
|
165
|
+
end_date = rk.periodicity.offset_date(
|
|
166
|
+
date=end_date,
|
|
167
|
+
period_type=rk.periodicity.Type.DAY,
|
|
168
|
+
num_periods=-1)
|
|
169
|
+
return cls(name=name, start_date=start_date, end_date=end_date)
|
|
170
|
+
|
|
171
|
+
@classmethod
|
|
172
|
+
def from_date_sequence(
|
|
173
|
+
cls,
|
|
174
|
+
names: [str],
|
|
175
|
+
dates: [pd.Timestamp]) -> [Span]:
|
|
176
|
+
"""
|
|
177
|
+
Create a set of Spans from a sequence of dates.
|
|
178
|
+
Note: dates list length must be 1 longer than names
|
|
179
|
+
:param names:
|
|
180
|
+
:type names:
|
|
181
|
+
:param dates:
|
|
182
|
+
:type dates:
|
|
183
|
+
:return:
|
|
184
|
+
:rtype:
|
|
185
|
+
"""
|
|
186
|
+
if len(names) != len(dates) - 1:
|
|
187
|
+
raise Exception('Error: number of Span names must equal number of Spans created'
|
|
188
|
+
' (i.e. one less than number of dates)')
|
|
189
|
+
|
|
190
|
+
date_pairs = list(zip(dates, dates[1:]))
|
|
191
|
+
date_pairs = [(start, rk.periodicity.offset_date(end, rk.periodicity.Type.DAY, -1)) for (start, end) in date_pairs]
|
|
192
|
+
|
|
193
|
+
spans = []
|
|
194
|
+
for i in range(len(names)):
|
|
195
|
+
spans.append(cls(name=names[i], start_date=date_pairs[i][0], end_date=date_pairs[i][1]))
|
|
196
|
+
return spans
|
|
197
|
+
|
|
198
|
+
@classmethod
|
|
199
|
+
def from_num_periods_sequence(
|
|
200
|
+
cls,
|
|
201
|
+
names: [str],
|
|
202
|
+
period_type: rk.periodicity.Type,
|
|
203
|
+
durations: [int],
|
|
204
|
+
start_date: pd.Timestamp) -> [Span]:
|
|
205
|
+
"""
|
|
206
|
+
Create a set of Spans from a sequence of durations of specified period type
|
|
207
|
+
:param names:
|
|
208
|
+
:type names:
|
|
209
|
+
:param period_type:
|
|
210
|
+
:type period_type:
|
|
211
|
+
:param durations:
|
|
212
|
+
:type durations:
|
|
213
|
+
:param start_date:
|
|
214
|
+
:type start_date:
|
|
215
|
+
:return:
|
|
216
|
+
:rtype:
|
|
217
|
+
"""
|
|
218
|
+
if len(names) != len(durations):
|
|
219
|
+
raise Exception('Error: number of Span names must equal number of Spans')
|
|
220
|
+
|
|
221
|
+
dates = []
|
|
222
|
+
cumulative_durations = np.cumsum(durations)
|
|
223
|
+
for duration in cumulative_durations:
|
|
224
|
+
dates.append(rk.periodicity.offset_date(date=start_date, period_type=period_type, num_periods=duration))
|
|
225
|
+
|
|
226
|
+
return cls.from_date_sequence(names=names, dates=dates)
|