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
rangekeeper/flux.py
ADDED
|
@@ -0,0 +1,733 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import locale
|
|
4
|
+
import os
|
|
5
|
+
|
|
6
|
+
import itertools
|
|
7
|
+
import math
|
|
8
|
+
from typing import Dict, Union, Optional, Tuple
|
|
9
|
+
|
|
10
|
+
import matplotlib.pyplot as plt
|
|
11
|
+
import numpy as np
|
|
12
|
+
import pandas as pd
|
|
13
|
+
import pint
|
|
14
|
+
import json
|
|
15
|
+
import pyxirr
|
|
16
|
+
|
|
17
|
+
import rangekeeper as rk
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def _format_series(
|
|
21
|
+
series: pd.Series,
|
|
22
|
+
units: pint.Unit,
|
|
23
|
+
decimals: int = 2):
|
|
24
|
+
|
|
25
|
+
if units.dimensionality == '[currency]':
|
|
26
|
+
formatted = pd.Series(
|
|
27
|
+
data=[str(locale.currency(value, grouping=True)) for value in series],
|
|
28
|
+
index=pd.Series(series.index, name='date'),
|
|
29
|
+
name=series.name)
|
|
30
|
+
else:
|
|
31
|
+
floatfmt = "{:." + str(decimals) + "f}"
|
|
32
|
+
formatted = pd.Series(
|
|
33
|
+
data=[str(floatfmt.format(value)) for value in series],
|
|
34
|
+
index=pd.Series(series.index, name='date'),
|
|
35
|
+
name=series.name)
|
|
36
|
+
|
|
37
|
+
return formatted
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class Flow:
|
|
41
|
+
name: str
|
|
42
|
+
movements: pd.Series
|
|
43
|
+
units: pint.Unit
|
|
44
|
+
"""
|
|
45
|
+
A `Flow` is a pd.Series of 'movements' of material (funds, energy, mass, etc) that occur at specified dates.
|
|
46
|
+
Note: the flow.movements Series index is a pd.DatetimeIndex, and its values are floats.
|
|
47
|
+
"""
|
|
48
|
+
|
|
49
|
+
def __init__(
|
|
50
|
+
self,
|
|
51
|
+
movements: pd.Series,
|
|
52
|
+
units: Optional[pint.Unit] = None,
|
|
53
|
+
name: str = None):
|
|
54
|
+
"""
|
|
55
|
+
Initializes a Flow. If units are not provided, a scalar (dimensionless) unit is assumed.
|
|
56
|
+
|
|
57
|
+
:param movements:
|
|
58
|
+
:type movements:
|
|
59
|
+
:param units:
|
|
60
|
+
:type units:
|
|
61
|
+
:param name:
|
|
62
|
+
:type name:
|
|
63
|
+
"""
|
|
64
|
+
|
|
65
|
+
if not isinstance(movements.index, pd.DatetimeIndex):
|
|
66
|
+
raise Exception("Error: Flow's movements' Index is not a pd.DatetimeIndex")
|
|
67
|
+
|
|
68
|
+
if movements.dtype != float:
|
|
69
|
+
try:
|
|
70
|
+
movements = movements.astype(float)
|
|
71
|
+
except Exception as e:
|
|
72
|
+
raise Exception("Error: Flow's movements' dtype cannot be cast to float: {0}".format(str(e)))
|
|
73
|
+
self.movements = movements
|
|
74
|
+
if name:
|
|
75
|
+
self.name = name
|
|
76
|
+
movements.name = name
|
|
77
|
+
else:
|
|
78
|
+
self.name = str(self.movements.name)
|
|
79
|
+
|
|
80
|
+
if units is None:
|
|
81
|
+
self.units = rk.measure.Index.registry.dimensionless
|
|
82
|
+
elif isinstance(units, pint.Unit):
|
|
83
|
+
self.units = units
|
|
84
|
+
else:
|
|
85
|
+
raise ValueError('Error: Units must be of type pint.Unit')
|
|
86
|
+
|
|
87
|
+
def __str__(self):
|
|
88
|
+
return str(_format_series(
|
|
89
|
+
series=self.movements,
|
|
90
|
+
units=self.units))
|
|
91
|
+
|
|
92
|
+
def _repr_html_(self):
|
|
93
|
+
return _format_series(
|
|
94
|
+
series=self.movements,
|
|
95
|
+
units=self.units).to_markdown(
|
|
96
|
+
stralign="right",
|
|
97
|
+
numalign="right",
|
|
98
|
+
tablefmt='html')
|
|
99
|
+
|
|
100
|
+
def duplicate(self) -> Flow:
|
|
101
|
+
return self.__class__(
|
|
102
|
+
movements=self.movements.copy(deep=True),
|
|
103
|
+
units=self.units,
|
|
104
|
+
name=self.name)
|
|
105
|
+
|
|
106
|
+
# def _format(
|
|
107
|
+
# self,
|
|
108
|
+
# decimals: int = 2):
|
|
109
|
+
|
|
110
|
+
def display(
|
|
111
|
+
self,
|
|
112
|
+
tablefmt: str = 'github',
|
|
113
|
+
decimals: int = 2):
|
|
114
|
+
linebreak = os.linesep
|
|
115
|
+
name = 'Name: ' + self.name
|
|
116
|
+
units = 'Units: ' + str(self.units)
|
|
117
|
+
movements = 'Movements: ' + linebreak + _format_series(
|
|
118
|
+
series=self.movements,
|
|
119
|
+
units=self.units,
|
|
120
|
+
decimals=decimals).to_markdown(
|
|
121
|
+
stralign="right",
|
|
122
|
+
numalign="right",
|
|
123
|
+
tablefmt=tablefmt,
|
|
124
|
+
floatfmt='.' + str(decimals) + 'f')
|
|
125
|
+
|
|
126
|
+
print(linebreak.join([name, units, movements, os.linesep]))
|
|
127
|
+
# print(self._format(decimals=decimals) + os.linesep)
|
|
128
|
+
|
|
129
|
+
def plot(
|
|
130
|
+
self,
|
|
131
|
+
bounds: Optional[Tuple[float, float]] = None,
|
|
132
|
+
normalize: bool = False,
|
|
133
|
+
*args, **kwargs):
|
|
134
|
+
self.movements.plot(*args, **kwargs)
|
|
135
|
+
plt.legend(loc='best')
|
|
136
|
+
plt.xlabel('Date')
|
|
137
|
+
plt.ylabel(self.units.__doc__)
|
|
138
|
+
if bounds is not None:
|
|
139
|
+
plt.ylim(bottom=bounds[0], top=bounds[1])
|
|
140
|
+
if normalize:
|
|
141
|
+
plt.ylim(bottom=0)
|
|
142
|
+
plt.show(block=True)
|
|
143
|
+
|
|
144
|
+
@classmethod
|
|
145
|
+
def from_periods(
|
|
146
|
+
cls,
|
|
147
|
+
index: pd.PeriodIndex,
|
|
148
|
+
data: [float],
|
|
149
|
+
units: Optional[pint.Unit] = None,
|
|
150
|
+
name: str = None) -> Flow:
|
|
151
|
+
"""
|
|
152
|
+
Returns a Flow where movement dates are defined by the end-dates of the specified periods
|
|
153
|
+
"""
|
|
154
|
+
|
|
155
|
+
if index.size != len(data):
|
|
156
|
+
raise ValueError("Error: count of periods and data must match")
|
|
157
|
+
dates = [pd.Timestamp(period.to_timestamp(how='end').date()) for period in index]
|
|
158
|
+
series = pd.Series(
|
|
159
|
+
data=data,
|
|
160
|
+
index=pd.Series(data=dates, name='date'),
|
|
161
|
+
name=name,
|
|
162
|
+
dtype=float)
|
|
163
|
+
return cls(
|
|
164
|
+
movements=series,
|
|
165
|
+
units=units,
|
|
166
|
+
name=name)
|
|
167
|
+
|
|
168
|
+
@classmethod
|
|
169
|
+
def from_dict(
|
|
170
|
+
cls,
|
|
171
|
+
movements: Dict[pd.Timestamp, float],
|
|
172
|
+
units: pint.Unit,
|
|
173
|
+
name: str = None) -> Flow:
|
|
174
|
+
"""
|
|
175
|
+
Returns a Flow where movements are defined by key-value pairs of pd.Timestamps and amounts.
|
|
176
|
+
"""
|
|
177
|
+
|
|
178
|
+
dates = movements.keys()
|
|
179
|
+
series = pd.Series(
|
|
180
|
+
data=list(movements.values()),
|
|
181
|
+
index=pd.Series(dates, name='date'),
|
|
182
|
+
name=name,
|
|
183
|
+
dtype=float)
|
|
184
|
+
return cls(
|
|
185
|
+
movements=series,
|
|
186
|
+
units=units,
|
|
187
|
+
name=name)
|
|
188
|
+
|
|
189
|
+
@classmethod
|
|
190
|
+
def from_projection(
|
|
191
|
+
cls,
|
|
192
|
+
value: Union[float, pint.Quantity, rk.distribution.Form],
|
|
193
|
+
proj: rk.projection,
|
|
194
|
+
units: pint.Unit = None,
|
|
195
|
+
name: str = None) -> Flow:
|
|
196
|
+
"""
|
|
197
|
+
Generate a Flow from a projection of a value, with an optional timing
|
|
198
|
+
offset in the application of the projection to the value. (i.e., the
|
|
199
|
+
projection factors/densities begin before or after the index does)
|
|
200
|
+
|
|
201
|
+
Also accepts a Distribution as a value input, which will be randomly
|
|
202
|
+
sampled.
|
|
203
|
+
"""
|
|
204
|
+
|
|
205
|
+
if isinstance(value, float):
|
|
206
|
+
value = value
|
|
207
|
+
elif isinstance(value, pint.Quantity):
|
|
208
|
+
value = value.magnitude
|
|
209
|
+
elif isinstance(value, rk.distribution.Form):
|
|
210
|
+
value = value.sample()[0]
|
|
211
|
+
|
|
212
|
+
if isinstance(proj, rk.projection.Extrapolation):
|
|
213
|
+
if proj.form.type == rk.extrapolation.Type.STRAIGHT_LINE or proj.form.type == rk.extrapolation.Type.RECURRING:
|
|
214
|
+
movements = value + proj.terms()
|
|
215
|
+
elif proj.form.type == rk.extrapolation.Type.COMPOUNDING:
|
|
216
|
+
movements = value * proj.terms()
|
|
217
|
+
else:
|
|
218
|
+
raise ValueError("Unsupported extrapolation form")
|
|
219
|
+
|
|
220
|
+
elif isinstance(proj, rk.projection.Distribution):
|
|
221
|
+
movements = value * proj.interval_density()
|
|
222
|
+
else:
|
|
223
|
+
raise ValueError("Unsupported projection type: {0}".format(type(proj)))
|
|
224
|
+
|
|
225
|
+
return cls(
|
|
226
|
+
movements=movements,
|
|
227
|
+
units=units,
|
|
228
|
+
name=name)
|
|
229
|
+
|
|
230
|
+
def invert(self) -> Flow:
|
|
231
|
+
"""
|
|
232
|
+
Returns a Flow with movement values inverted (multiplied by -1)
|
|
233
|
+
"""
|
|
234
|
+
return self.__class__(
|
|
235
|
+
movements=self.movements.copy(deep=True).multiply(-1),
|
|
236
|
+
units=self.units,
|
|
237
|
+
name=self.name)
|
|
238
|
+
|
|
239
|
+
def collapse(self) -> Flow:
|
|
240
|
+
"""
|
|
241
|
+
Returns a Flow whose movements collapse (are summed) to the last period
|
|
242
|
+
:return:
|
|
243
|
+
"""
|
|
244
|
+
return self.__class__.from_dict(
|
|
245
|
+
name=self.name,
|
|
246
|
+
movements={self.movements.index[-1]: self.movements.sum()},
|
|
247
|
+
units=self.units)
|
|
248
|
+
|
|
249
|
+
def pv(
|
|
250
|
+
self,
|
|
251
|
+
period_type: rk.periodicity.Type,
|
|
252
|
+
discount_rate: float,
|
|
253
|
+
name: str = None) -> Flow:
|
|
254
|
+
"""
|
|
255
|
+
Returns a Flow with values discounted to the present (i.e. before its first period) by a specified rate
|
|
256
|
+
"""
|
|
257
|
+
resampled = self.resample(period_type)
|
|
258
|
+
frame = resampled.movements.to_frame()
|
|
259
|
+
frame.insert(0, 'index', range(resampled.movements.index.size))
|
|
260
|
+
frame['Discounted Flow'] = frame.apply(
|
|
261
|
+
lambda movement: movement[self.name] / math.pow((1 + discount_rate), movement['index'] + 1), axis=1)
|
|
262
|
+
if name is None:
|
|
263
|
+
name = 'Discounted ' + self.name
|
|
264
|
+
return self.__class__(
|
|
265
|
+
movements=frame['Discounted Flow'],
|
|
266
|
+
units=self.units,
|
|
267
|
+
name=name)
|
|
268
|
+
|
|
269
|
+
def xirr(self) -> float:
|
|
270
|
+
return pyxirr.xirr(
|
|
271
|
+
dates=[datetime.date() for datetime in list(self.movements.index.array)],
|
|
272
|
+
amounts=self.movements.to_list())
|
|
273
|
+
|
|
274
|
+
def xnpv(
|
|
275
|
+
self,
|
|
276
|
+
rate: float) -> float:
|
|
277
|
+
return pyxirr.xnpv(
|
|
278
|
+
rate=rate,
|
|
279
|
+
dates=[datetime.date() for datetime in list(self.movements.index.array)],
|
|
280
|
+
amounts=self.movements.to_list())
|
|
281
|
+
|
|
282
|
+
def resample(
|
|
283
|
+
self,
|
|
284
|
+
period_type: rk.periodicity.Type) -> Flow:
|
|
285
|
+
"""
|
|
286
|
+
Returns a Flow with movements summed to specified frequency of dates
|
|
287
|
+
"""
|
|
288
|
+
return rk.flux.Flow(
|
|
289
|
+
movements=self.movements.copy(deep=True).resample(rule=period_type.value).sum(),
|
|
290
|
+
units=self.units,
|
|
291
|
+
name=self.name)
|
|
292
|
+
|
|
293
|
+
def to_periods(
|
|
294
|
+
self,
|
|
295
|
+
period_type: rk.periodicity.Type) -> pd.Series:
|
|
296
|
+
"""
|
|
297
|
+
Returns a pd.Series (of index pd.PeriodIndex) with movements summed to specified periodicity
|
|
298
|
+
"""
|
|
299
|
+
return self \
|
|
300
|
+
.resample(period_type=period_type) \
|
|
301
|
+
.movements.to_period(freq=period_type.value, copy=True) \
|
|
302
|
+
.rename_axis('period') \
|
|
303
|
+
.groupby(level='period') \
|
|
304
|
+
.sum()
|
|
305
|
+
|
|
306
|
+
def get_frequency(self) -> str:
|
|
307
|
+
return self.movements.index.freq
|
|
308
|
+
|
|
309
|
+
def trim_to_span(
|
|
310
|
+
self,
|
|
311
|
+
span: rk.span.Span,
|
|
312
|
+
name: str = None) -> Flow:
|
|
313
|
+
"""
|
|
314
|
+
Returns a Flow with movements trimmed to the specified span
|
|
315
|
+
"""
|
|
316
|
+
return self.__class__(
|
|
317
|
+
movements=self.movements.copy(deep=True).truncate(
|
|
318
|
+
before=span.start_date,
|
|
319
|
+
after=span.end_date),
|
|
320
|
+
units=self.units,
|
|
321
|
+
name=self.name if name is None else name)
|
|
322
|
+
|
|
323
|
+
def to_stream(
|
|
324
|
+
self,
|
|
325
|
+
period_type: rk.periodicity.Type,
|
|
326
|
+
name: str = None) -> Stream:
|
|
327
|
+
"""
|
|
328
|
+
Returns a Stream with the flow as flow
|
|
329
|
+
resampled at the specified periodicity
|
|
330
|
+
:param period_type:
|
|
331
|
+
:param name:
|
|
332
|
+
"""
|
|
333
|
+
return Stream(
|
|
334
|
+
name=name if name is not None else self.name,
|
|
335
|
+
flows=[self],
|
|
336
|
+
period_type=period_type)
|
|
337
|
+
|
|
338
|
+
|
|
339
|
+
class Stream:
|
|
340
|
+
name: str
|
|
341
|
+
flows: [Flow]
|
|
342
|
+
period_type: rk.periodicity.Type
|
|
343
|
+
start_date: pd.Timestamp
|
|
344
|
+
end_date: pd.Timestamp
|
|
345
|
+
frame: pd.DataFrame
|
|
346
|
+
|
|
347
|
+
"""
|
|
348
|
+
A `Stream` collects flow (constituent) Flows
|
|
349
|
+
and resamples them with a specified periodicity.
|
|
350
|
+
"""
|
|
351
|
+
|
|
352
|
+
def __init__(
|
|
353
|
+
self,
|
|
354
|
+
name: str,
|
|
355
|
+
flows: [Flow],
|
|
356
|
+
period_type: rk.periodicity.Type):
|
|
357
|
+
|
|
358
|
+
# Name:
|
|
359
|
+
self.name = name
|
|
360
|
+
|
|
361
|
+
# Units:
|
|
362
|
+
# if all(flow.units == flows[0].units for flow in flows):
|
|
363
|
+
# self.units = flows[0].units
|
|
364
|
+
# else:
|
|
365
|
+
# raise Exception("Input Flows have dissimilar units. Cannot aggregate into Stream.")
|
|
366
|
+
|
|
367
|
+
# Flows:
|
|
368
|
+
self.flows = flows
|
|
369
|
+
"""The set of input Flows that are aggregated in this Stream"""
|
|
370
|
+
|
|
371
|
+
self.units = {flow.name: flow.units for flow in self.flows}
|
|
372
|
+
|
|
373
|
+
# Periodicity Type:
|
|
374
|
+
self.period_type = period_type
|
|
375
|
+
|
|
376
|
+
# Stream:
|
|
377
|
+
flows_dates = list(
|
|
378
|
+
itertools.chain.from_iterable(list(flow.movements.index.array) for flow in self.flows))
|
|
379
|
+
self.start_date = min(flows_dates)
|
|
380
|
+
self.end_date = max(flows_dates)
|
|
381
|
+
|
|
382
|
+
index = rk.periodicity.period_index(
|
|
383
|
+
include_start=self.start_date,
|
|
384
|
+
period_type=self.period_type,
|
|
385
|
+
bound=self.end_date)
|
|
386
|
+
self._resampled_flows = [flow.to_periods(period_type=self.period_type) for flow in self.flows]
|
|
387
|
+
self.frame = pd.concat(self._resampled_flows, axis=1).fillna(0).sort_index()
|
|
388
|
+
"""
|
|
389
|
+
A pd.DataFrame of the Stream's flow Flows accumulated into the Stream's periodicity
|
|
390
|
+
"""
|
|
391
|
+
|
|
392
|
+
def __str__(self):
|
|
393
|
+
return str(self._format_flows())
|
|
394
|
+
|
|
395
|
+
def _repr_html_(self):
|
|
396
|
+
return self._format_flows().to_markdown(
|
|
397
|
+
stralign="right",
|
|
398
|
+
numalign="right",
|
|
399
|
+
tablefmt='html',
|
|
400
|
+
floatfmt=".2f")
|
|
401
|
+
|
|
402
|
+
def _format_flows(
|
|
403
|
+
self,
|
|
404
|
+
decimals: int = 2) -> pd.DataFrame:
|
|
405
|
+
|
|
406
|
+
formatted_flows = []
|
|
407
|
+
for flow in self.flows:
|
|
408
|
+
series = flow.to_periods(period_type=self.period_type)
|
|
409
|
+
formatted_flows.append(_format_series(
|
|
410
|
+
series=series,
|
|
411
|
+
units=flow.units,
|
|
412
|
+
decimals=decimals))
|
|
413
|
+
# formatted_flows = [flow._format_movements(decimals=decimals) for flow in self._resampled_flows]
|
|
414
|
+
frame = pd.concat(formatted_flows, axis=1).fillna(0).sort_index()
|
|
415
|
+
return frame
|
|
416
|
+
|
|
417
|
+
def display(
|
|
418
|
+
self,
|
|
419
|
+
tablefmt: str = 'github',
|
|
420
|
+
decimals: int = 2):
|
|
421
|
+
|
|
422
|
+
floatfmt = "." + str(decimals) + "f"
|
|
423
|
+
|
|
424
|
+
linebreak = os.linesep
|
|
425
|
+
name = 'Name: ' + self.name
|
|
426
|
+
units = 'Units: ' + json.dumps({flow.name: flow.units.__str__() for flow in self.flows}, indent=4)
|
|
427
|
+
flows = 'Flows: ' + linebreak + self._format_flows(decimals=decimals).to_markdown(
|
|
428
|
+
tablefmt=tablefmt,
|
|
429
|
+
stralign="right",
|
|
430
|
+
numalign="right",
|
|
431
|
+
floatfmt=floatfmt)
|
|
432
|
+
|
|
433
|
+
print(linebreak.join([name, units, flows, os.linesep]))
|
|
434
|
+
|
|
435
|
+
# def display(
|
|
436
|
+
# self,
|
|
437
|
+
# decimals: int = 2):
|
|
438
|
+
# format = self._format(decimals=decimals)
|
|
439
|
+
# print(format + os.linesep)
|
|
440
|
+
|
|
441
|
+
def duplicate(self) -> Stream:
|
|
442
|
+
return self.__class__(
|
|
443
|
+
name=self.name,
|
|
444
|
+
flows=[flow.duplicate() for flow in self.flows],
|
|
445
|
+
period_type=self.period_type)
|
|
446
|
+
|
|
447
|
+
@classmethod
|
|
448
|
+
def from_DataFrame(
|
|
449
|
+
cls,
|
|
450
|
+
name: str,
|
|
451
|
+
data: pd.DataFrame,
|
|
452
|
+
units: pint.Unit) -> Stream:
|
|
453
|
+
flows = []
|
|
454
|
+
for column in data.columns:
|
|
455
|
+
series = data[column]
|
|
456
|
+
flows.append(
|
|
457
|
+
Flow(
|
|
458
|
+
movements=series,
|
|
459
|
+
units=units,
|
|
460
|
+
name=series.name))
|
|
461
|
+
return cls(
|
|
462
|
+
name=name,
|
|
463
|
+
flows=flows,
|
|
464
|
+
period_type=rk.periodicity.from_value(data.index.freqstr))
|
|
465
|
+
|
|
466
|
+
def plot(
|
|
467
|
+
self,
|
|
468
|
+
flows: Dict[str, tuple] = None,
|
|
469
|
+
normalize: bool = False,
|
|
470
|
+
):
|
|
471
|
+
"""
|
|
472
|
+
Plots the specified flows against each respective value range (min-max)
|
|
473
|
+
|
|
474
|
+
:param flows: A dictionary of flows to plot, by name and value range (as a tuple)
|
|
475
|
+
"""
|
|
476
|
+
|
|
477
|
+
flows = flows if flows is not None else {flow.name: (flow.min(), flow.max()) for flow in self._resampled_flows}
|
|
478
|
+
dates = list(self.frame.index.astype(str))
|
|
479
|
+
|
|
480
|
+
fig, host = plt.subplots(nrows=1, ncols=1)
|
|
481
|
+
tkw = dict(size=4, width=1)
|
|
482
|
+
|
|
483
|
+
datums = []
|
|
484
|
+
axes = []
|
|
485
|
+
|
|
486
|
+
host.set_xlabel('Date')
|
|
487
|
+
# host.set_xticklabels(host.get_xticks(), rotation=90)
|
|
488
|
+
host.set_facecolor('white')
|
|
489
|
+
host.grid(axis='x',
|
|
490
|
+
color='gainsboro',
|
|
491
|
+
which='major',
|
|
492
|
+
linewidth=1)
|
|
493
|
+
host.grid(axis='y',
|
|
494
|
+
color='gainsboro',
|
|
495
|
+
which='major',
|
|
496
|
+
linewidth=1)
|
|
497
|
+
host.grid(axis='y',
|
|
498
|
+
color='whitesmoke',
|
|
499
|
+
which='minor',
|
|
500
|
+
linestyle='-.',
|
|
501
|
+
linewidth=0.5)
|
|
502
|
+
|
|
503
|
+
primary_flow_name = list(flows.keys())[0]
|
|
504
|
+
primary, = host.plot(dates,
|
|
505
|
+
self.frame[primary_flow_name],
|
|
506
|
+
color=plt.cm.viridis(0),
|
|
507
|
+
label=primary_flow_name)
|
|
508
|
+
host.spines.left.set_linewidth(1)
|
|
509
|
+
host.set_ylabel(primary_flow_name)
|
|
510
|
+
host.yaxis.label.set_color(primary.get_color())
|
|
511
|
+
host.spines.left.set_color(primary.get_color())
|
|
512
|
+
host.tick_params(axis='y', colors=primary.get_color(), **tkw)
|
|
513
|
+
|
|
514
|
+
datums.append(primary)
|
|
515
|
+
axes.append(host)
|
|
516
|
+
# host.minorticks_on()
|
|
517
|
+
|
|
518
|
+
if normalize:
|
|
519
|
+
for i in range(1, len(flows)):
|
|
520
|
+
additional_flow_name = list(flows.keys())[i]
|
|
521
|
+
additional, = host.plot(
|
|
522
|
+
dates,
|
|
523
|
+
self.frame[additional_flow_name],
|
|
524
|
+
color=plt.cm.viridis(1 / (len(flows) + 1)),
|
|
525
|
+
label=additional_flow_name)
|
|
526
|
+
datums.append(additional)
|
|
527
|
+
|
|
528
|
+
else:
|
|
529
|
+
host.set_ylim([flows[primary_flow_name][0], flows[primary_flow_name][1]])
|
|
530
|
+
|
|
531
|
+
if len(flows) > 1:
|
|
532
|
+
secondary_flow_name = list(flows.keys())[1]
|
|
533
|
+
right = host.twinx()
|
|
534
|
+
secondary, = right.plot(dates,
|
|
535
|
+
self.frame[secondary_flow_name],
|
|
536
|
+
color=plt.cm.viridis(1 / (len(flows) + 1)),
|
|
537
|
+
label=secondary_flow_name)
|
|
538
|
+
right.spines.right.set_visible(True)
|
|
539
|
+
right.spines.right.set_linewidth(1)
|
|
540
|
+
right.set_ylabel(secondary_flow_name)
|
|
541
|
+
right.grid(False)
|
|
542
|
+
right.yaxis.label.set_color(secondary.get_color())
|
|
543
|
+
right.spines.right.set_color(secondary.get_color())
|
|
544
|
+
right.tick_params(axis='y', colors=secondary.get_color(), **tkw)
|
|
545
|
+
# if flows[secondary_flow] is not None:
|
|
546
|
+
right.set_ylim(bottom=flows[secondary_flow_name][0], top=flows[secondary_flow_name][1])
|
|
547
|
+
|
|
548
|
+
datums.append(secondary)
|
|
549
|
+
axes.append(right)
|
|
550
|
+
|
|
551
|
+
if len(flows) > 2:
|
|
552
|
+
for i in range(2, len(flows)):
|
|
553
|
+
additional_flow_name = list(flows.keys())[i]
|
|
554
|
+
supplementary = host.twinx()
|
|
555
|
+
additional, = supplementary.plot(dates,
|
|
556
|
+
self.frame[additional_flow_name],
|
|
557
|
+
color=plt.cm.viridis(i / (len(flows) + 1)),
|
|
558
|
+
label=additional_flow_name)
|
|
559
|
+
supplementary.spines.right.set_position(('axes', 1 + (i - 1) / 7.5))
|
|
560
|
+
supplementary.spines.right.set_visible(True)
|
|
561
|
+
supplementary.spines.right.set_linewidth(1)
|
|
562
|
+
supplementary.set_ylabel(additional_flow_name)
|
|
563
|
+
supplementary.grid(False)
|
|
564
|
+
supplementary.yaxis.label.set_color(additional.get_color())
|
|
565
|
+
supplementary.spines.right.set_color(additional.get_color())
|
|
566
|
+
supplementary.tick_params(axis='y', colors=additional.get_color(), **tkw)
|
|
567
|
+
if flows[additional_flow_name] is not None:
|
|
568
|
+
supplementary.set_ylim(
|
|
569
|
+
[flows[additional_flow_name][0], flows[additional_flow_name][1]])
|
|
570
|
+
|
|
571
|
+
datums.append(additional)
|
|
572
|
+
axes.append(supplementary)
|
|
573
|
+
|
|
574
|
+
labels = [datum.get_label() for datum in datums]
|
|
575
|
+
legend = axes[-1].legend(
|
|
576
|
+
datums,
|
|
577
|
+
labels,
|
|
578
|
+
loc='best',
|
|
579
|
+
title=self.name,
|
|
580
|
+
title_fontsize=8,
|
|
581
|
+
fontsize=7,
|
|
582
|
+
facecolor="white",
|
|
583
|
+
frameon=True,
|
|
584
|
+
framealpha=1,
|
|
585
|
+
borderpad=.75,
|
|
586
|
+
edgecolor='grey')
|
|
587
|
+
fig.autofmt_xdate(rotation=90)
|
|
588
|
+
plt.minorticks_on()
|
|
589
|
+
# plt.tight_layout()
|
|
590
|
+
plt.show(block=True)
|
|
591
|
+
|
|
592
|
+
def extract(
|
|
593
|
+
self,
|
|
594
|
+
flow_name: str) -> Flow:
|
|
595
|
+
"""
|
|
596
|
+
Extract an Stream's resampled flow as a Flow
|
|
597
|
+
:param flow_name:
|
|
598
|
+
:return:
|
|
599
|
+
"""
|
|
600
|
+
return Flow.from_periods(
|
|
601
|
+
name=flow_name,
|
|
602
|
+
data=list(self.frame[flow_name]),
|
|
603
|
+
index=self.frame.index,
|
|
604
|
+
units=self.units[flow_name])
|
|
605
|
+
|
|
606
|
+
def sum(
|
|
607
|
+
self,
|
|
608
|
+
name: str = None) -> Flow:
|
|
609
|
+
"""
|
|
610
|
+
Returns a Flow whose movements are the sum of the Stream's flows by period
|
|
611
|
+
:return: Flow
|
|
612
|
+
"""
|
|
613
|
+
# Check if all units are the same:
|
|
614
|
+
if not len(list(set(list(self.units.values())))) == 1:
|
|
615
|
+
raise ValueError("Error: summation requires all flows' units to be the same. Units: {0}".format(
|
|
616
|
+
json.dumps(
|
|
617
|
+
{flow.name: flow.units.__str__() for flow in self.flows}, indent=4)))
|
|
618
|
+
|
|
619
|
+
return Flow.from_periods(
|
|
620
|
+
name=name if name is not None else self.name,
|
|
621
|
+
index=self.frame.index, # .to_period(),
|
|
622
|
+
data=self.frame.sum(axis=1).to_list(),
|
|
623
|
+
units=next(iter(self.units.values())))
|
|
624
|
+
|
|
625
|
+
def product(
|
|
626
|
+
self,
|
|
627
|
+
name: str = None,
|
|
628
|
+
registry: pint.UnitRegistry = None,
|
|
629
|
+
scope: Optional[dict] = None) -> Flow:
|
|
630
|
+
"""
|
|
631
|
+
Returns a Flow whose movements are the product of the Stream's flows by period
|
|
632
|
+
:return: Flow
|
|
633
|
+
"""
|
|
634
|
+
|
|
635
|
+
# Produce resultant units:
|
|
636
|
+
# singleton = ['1 * units.' + str(value) for value in self.units.values()]
|
|
637
|
+
# singleton = eval(' * '.join(singleton), scope)
|
|
638
|
+
registry = registry if registry is not None else rk.measure.Index.registry
|
|
639
|
+
units = rk.measure.multiply_units(
|
|
640
|
+
units=list(self.units.values()),
|
|
641
|
+
registry=registry)
|
|
642
|
+
reduced_units = rk.measure.remove_dimension(
|
|
643
|
+
quantity=registry.Quantity(1, units),
|
|
644
|
+
dimension='[time]',
|
|
645
|
+
registry=registry).units
|
|
646
|
+
|
|
647
|
+
return Flow.from_periods(
|
|
648
|
+
name=name if name is not None else self.name,
|
|
649
|
+
index=self.frame.index, # .to_period(),
|
|
650
|
+
data=self.frame.prod(axis=1).to_list(),
|
|
651
|
+
units=reduced_units)
|
|
652
|
+
|
|
653
|
+
def collapse(self) -> Stream:
|
|
654
|
+
"""
|
|
655
|
+
Returns an Stream with Flows' movements collapsed (summed) to the Stream's final period
|
|
656
|
+
:return: Stream
|
|
657
|
+
"""
|
|
658
|
+
flows = [self.extract(flow_name=flow_name) for flow_name in list(self.frame.columns)]
|
|
659
|
+
return self.__class__(
|
|
660
|
+
name=self.name,
|
|
661
|
+
flows=[flow.collapse() for flow in flows],
|
|
662
|
+
period_type=self.period_type)
|
|
663
|
+
|
|
664
|
+
def append(
|
|
665
|
+
self,
|
|
666
|
+
flows: [Flow]) -> None:
|
|
667
|
+
"""
|
|
668
|
+
Appends a list of Flows to the Stream
|
|
669
|
+
:param flows:
|
|
670
|
+
:type flows:
|
|
671
|
+
:return:
|
|
672
|
+
:rtype:
|
|
673
|
+
"""
|
|
674
|
+
# if any(flow.units != self.units for flow in flows):
|
|
675
|
+
# raise Exception("Input Flows have dissimilar units. Cannot aggregate into Stream.")
|
|
676
|
+
|
|
677
|
+
# Append Flows:
|
|
678
|
+
self.flows.extend(flows)
|
|
679
|
+
|
|
680
|
+
# Append Units:
|
|
681
|
+
self.units.update({flow.name: flow.units for flow in flows})
|
|
682
|
+
|
|
683
|
+
# Stream:
|
|
684
|
+
flows_dates = list(
|
|
685
|
+
itertools.chain.from_iterable(list(flow.movements.index.array) for flow in self.flows))
|
|
686
|
+
self.start_date = min(flows_dates)
|
|
687
|
+
self.end_date = max(flows_dates)
|
|
688
|
+
|
|
689
|
+
index = rk.periodicity.period_index(
|
|
690
|
+
include_start=self.start_date,
|
|
691
|
+
period_type=self.period_type,
|
|
692
|
+
bound=self.end_date)
|
|
693
|
+
_resampled_flows = [flow.to_periods(period_type=self.period_type) for flow in self.flows]
|
|
694
|
+
self.frame = pd.concat(_resampled_flows, axis=1).fillna(0)
|
|
695
|
+
|
|
696
|
+
def resample(
|
|
697
|
+
self,
|
|
698
|
+
period_type: rk.periodicity.Type) -> Stream:
|
|
699
|
+
return Stream(
|
|
700
|
+
name=self.name,
|
|
701
|
+
flows=self.flows,
|
|
702
|
+
period_type=period_type)
|
|
703
|
+
|
|
704
|
+
def trim_to_span(
|
|
705
|
+
self,
|
|
706
|
+
span: rk.span.Span) -> Stream:
|
|
707
|
+
"""
|
|
708
|
+
Returns an Stream with all flows trimmed to the specified Span
|
|
709
|
+
:param span:
|
|
710
|
+
:return:
|
|
711
|
+
"""
|
|
712
|
+
return self.__class__(
|
|
713
|
+
name=self.name,
|
|
714
|
+
flows=[flow.duplicate().trim_to_span(span) for flow in self.flows],
|
|
715
|
+
period_type=self.period_type)
|
|
716
|
+
|
|
717
|
+
@classmethod
|
|
718
|
+
def merge(
|
|
719
|
+
cls,
|
|
720
|
+
streams,
|
|
721
|
+
name: str,
|
|
722
|
+
period_type: rk.periodicity.Type) -> Stream:
|
|
723
|
+
# Check Units:
|
|
724
|
+
if any(stream.units != streams[0].units for stream in streams):
|
|
725
|
+
raise Exception("Input Streams have dissimilar units. Cannot merge into Stream.")
|
|
726
|
+
|
|
727
|
+
# Aggregands:
|
|
728
|
+
flows = [flow for stream in streams for flow in stream.flows]
|
|
729
|
+
|
|
730
|
+
return cls(
|
|
731
|
+
name=name,
|
|
732
|
+
flows=flows,
|
|
733
|
+
period_type=period_type)
|