rangekeeper 0.7.3__tar.gz → 0.7.5__tar.gz
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-0.7.3 → rangekeeper-0.7.5}/PKG-INFO +3 -5
- {rangekeeper-0.7.3 → rangekeeper-0.7.5}/pyproject.toml +3 -5
- {rangekeeper-0.7.3 → rangekeeper-0.7.5}/rangekeeper/flux.py +322 -307
- {rangekeeper-0.7.3 → rangekeeper-0.7.5}/rangekeeper/formula/financial.py +4 -1
- {rangekeeper-0.7.3 → rangekeeper-0.7.5}/README.md +0 -0
- {rangekeeper-0.7.3 → rangekeeper-0.7.5}/rangekeeper/__init__.py +0 -0
- {rangekeeper-0.7.3 → rangekeeper-0.7.5}/rangekeeper/api.py +0 -0
- {rangekeeper-0.7.3 → rangekeeper-0.7.5}/rangekeeper/conftest.py +0 -0
- {rangekeeper-0.7.3 → rangekeeper-0.7.5}/rangekeeper/distribution.py +0 -0
- {rangekeeper-0.7.3 → rangekeeper-0.7.5}/rangekeeper/duration.py +0 -0
- {rangekeeper-0.7.3 → rangekeeper-0.7.5}/rangekeeper/dynamics/__init__.py +0 -0
- {rangekeeper-0.7.3 → rangekeeper-0.7.5}/rangekeeper/dynamics/black_swan.py +0 -0
- {rangekeeper-0.7.3 → rangekeeper-0.7.5}/rangekeeper/dynamics/cyclicality.py +0 -0
- {rangekeeper-0.7.3 → rangekeeper-0.7.5}/rangekeeper/dynamics/market.py +0 -0
- {rangekeeper-0.7.3 → rangekeeper-0.7.5}/rangekeeper/dynamics/noise.py +0 -0
- {rangekeeper-0.7.3 → rangekeeper-0.7.5}/rangekeeper/dynamics/trend.py +0 -0
- {rangekeeper-0.7.3 → rangekeeper-0.7.5}/rangekeeper/dynamics/volatility.py +0 -0
- {rangekeeper-0.7.3 → rangekeeper-0.7.5}/rangekeeper/extrapolation.py +0 -0
- {rangekeeper-0.7.3 → rangekeeper-0.7.5}/rangekeeper/format.py +0 -0
- {rangekeeper-0.7.3 → rangekeeper-0.7.5}/rangekeeper/formula/__init__.py +0 -0
- {rangekeeper-0.7.3 → rangekeeper-0.7.5}/rangekeeper/graph.py +0 -0
- {rangekeeper-0.7.3 → rangekeeper-0.7.5}/rangekeeper/measure.py +0 -0
- {rangekeeper-0.7.3 → rangekeeper-0.7.5}/rangekeeper/policy.py +0 -0
- {rangekeeper-0.7.3 → rangekeeper-0.7.5}/rangekeeper/projection.py +0 -0
- {rangekeeper-0.7.3 → rangekeeper-0.7.5}/rangekeeper/segmentation.py +0 -0
- {rangekeeper-0.7.3 → rangekeeper-0.7.5}/rangekeeper/space.py +0 -0
- {rangekeeper-0.7.3 → rangekeeper-0.7.5}/rangekeeper/span.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: Rangekeeper
|
|
3
|
-
Version: 0.7.
|
|
3
|
+
Version: 0.7.5
|
|
4
4
|
Summary: A Python library assisting financial modelling in real estate asset & development planning, decision-making, cashflow forecasting, and scenario analysis.
|
|
5
5
|
License: MPL-2.0
|
|
6
6
|
Author: Daniel Fink
|
|
@@ -12,20 +12,18 @@ Classifier: Programming Language :: Python :: 3.10
|
|
|
12
12
|
Classifier: Programming Language :: Python :: 3.11
|
|
13
13
|
Classifier: Programming Language :: Python :: 3.12
|
|
14
14
|
Requires-Dist: aenum (>=3.1,<4.0)
|
|
15
|
+
Requires-Dist: black (>=25.1.0,<26.0.0)
|
|
15
16
|
Requires-Dist: matplotlib (>=3.10,<4.0)
|
|
16
|
-
Requires-Dist: mpld3 (>=0.5,<0.6)
|
|
17
17
|
Requires-Dist: multiprocess (>=0.70,<0.71)
|
|
18
18
|
Requires-Dist: networkx (>=3.3,<4.0)
|
|
19
19
|
Requires-Dist: numba (>=0.61,<0.62)
|
|
20
20
|
Requires-Dist: numpy-financial (>=1.0,<2.0)
|
|
21
|
-
Requires-Dist: pandas (>=2,<3)
|
|
21
|
+
Requires-Dist: pandas (>=2.2,<3.0)
|
|
22
22
|
Requires-Dist: pint (>=0.24,<0.25)
|
|
23
23
|
Requires-Dist: plotly (>=6.0,<7.0)
|
|
24
24
|
Requires-Dist: py-moneyed (>=3.0,<4.0)
|
|
25
|
-
Requires-Dist: pyarrow (>=19.0,<20.0)
|
|
26
25
|
Requires-Dist: pyvis (>=0.3,<0.4)
|
|
27
26
|
Requires-Dist: pyxirr (>=0.10,<0.11)
|
|
28
|
-
Requires-Dist: pyyaml (>=6.0,<7.0)
|
|
29
27
|
Requires-Dist: scipy (>=1.14,<2.0)
|
|
30
28
|
Requires-Dist: seaborn (>=0.13,<0.14)
|
|
31
29
|
Requires-Dist: specklepy (>=2.21,<3.0)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[tool.poetry]
|
|
2
2
|
name = "Rangekeeper"
|
|
3
|
-
version = "0.7.
|
|
3
|
+
version = "0.7.5"
|
|
4
4
|
description = "A Python library assisting financial modelling in real estate asset & development planning, decision-making, cashflow forecasting, and scenario analysis."
|
|
5
5
|
authors = ["Daniel Fink <danfink@mit.edu>"]
|
|
6
6
|
license = "MPL-2.0"
|
|
@@ -9,24 +9,22 @@ readme = "README.md"
|
|
|
9
9
|
[tool.poetry.dependencies]
|
|
10
10
|
python = ">=3.10, <3.13"
|
|
11
11
|
matplotlib = "^3.10"
|
|
12
|
-
pandas = "^2"
|
|
12
|
+
pandas = "^2.2"
|
|
13
13
|
pint = "^0.24"
|
|
14
14
|
pyxirr = "^0.10"
|
|
15
15
|
scipy = "^1.14"
|
|
16
16
|
networkx = "^3.3"
|
|
17
17
|
py-moneyed = "^3.0"
|
|
18
18
|
tabulate = "^0.9"
|
|
19
|
-
pyyaml = "^6.0"
|
|
20
19
|
specklepy = "^2.21"
|
|
21
20
|
numba = "^0.61"
|
|
22
21
|
multiprocess = "^0.70"
|
|
23
22
|
seaborn = "^0.13"
|
|
24
23
|
pyvis = "^0.3"
|
|
25
24
|
plotly = "^6.0"
|
|
26
|
-
mpld3 = "^0.5"
|
|
27
|
-
pyarrow = "^19.0"
|
|
28
25
|
aenum = "^3.1"
|
|
29
26
|
numpy-financial = "^1.0"
|
|
27
|
+
black = "^25.1.0"
|
|
30
28
|
|
|
31
29
|
[tool.poetry.dev-dependencies]
|
|
32
30
|
pytest = "^8.3"
|
|
@@ -17,21 +17,20 @@ import pyxirr
|
|
|
17
17
|
import rangekeeper as rk
|
|
18
18
|
|
|
19
19
|
|
|
20
|
-
def _format_series(
|
|
21
|
-
|
|
22
|
-
units: pint.Unit,
|
|
23
|
-
decimals: int = 2):
|
|
24
|
-
if units.dimensionality == '[currency]':
|
|
20
|
+
def _format_series(series: pd.Series, units: pint.Unit, decimals: int = 2):
|
|
21
|
+
if units.dimensionality == "[currency]":
|
|
25
22
|
formatted = pd.Series(
|
|
26
23
|
data=[str(locale.currency(value, grouping=True)) for value in series],
|
|
27
|
-
index=pd.Series(series.index, name=
|
|
28
|
-
name=series.name
|
|
24
|
+
index=pd.Series(series.index, name="date"),
|
|
25
|
+
name=series.name,
|
|
26
|
+
)
|
|
29
27
|
else:
|
|
30
28
|
floatfmt = "{:." + str(decimals) + "f}"
|
|
31
29
|
formatted = pd.Series(
|
|
32
30
|
data=[str(floatfmt.format(value)) for value in series],
|
|
33
|
-
index=pd.Series(series.index, name=
|
|
34
|
-
name=series.name
|
|
31
|
+
index=pd.Series(series.index, name="date"),
|
|
32
|
+
name=series.name,
|
|
33
|
+
)
|
|
35
34
|
|
|
36
35
|
return formatted
|
|
37
36
|
|
|
@@ -46,10 +45,8 @@ class Flow:
|
|
|
46
45
|
"""
|
|
47
46
|
|
|
48
47
|
def __init__(
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
units: Optional[pint.Unit] = None,
|
|
52
|
-
name: str = None):
|
|
48
|
+
self, movements: pd.Series, units: Optional[pint.Unit] = None, name: str = None
|
|
49
|
+
):
|
|
53
50
|
"""
|
|
54
51
|
Initializes a Flow. If units are not provided, a scalar (dimensionless) unit is assumed.
|
|
55
52
|
|
|
@@ -68,7 +65,11 @@ class Flow:
|
|
|
68
65
|
try:
|
|
69
66
|
movements = movements.astype(float)
|
|
70
67
|
except Exception as e:
|
|
71
|
-
raise Exception(
|
|
68
|
+
raise Exception(
|
|
69
|
+
"Error: Flow's movements' dtype cannot be cast to float: {0}".format(
|
|
70
|
+
str(e)
|
|
71
|
+
)
|
|
72
|
+
)
|
|
72
73
|
self.movements = movements
|
|
73
74
|
if name:
|
|
74
75
|
self.name = name
|
|
@@ -81,60 +82,56 @@ class Flow:
|
|
|
81
82
|
elif isinstance(units, pint.Unit):
|
|
82
83
|
self.units = units
|
|
83
84
|
else:
|
|
84
|
-
raise ValueError(
|
|
85
|
+
raise ValueError("Error: Units must be of type pint.Unit")
|
|
85
86
|
|
|
86
87
|
def __str__(self):
|
|
87
|
-
return os.linesep + str(_format_series(
|
|
88
|
-
series=self.movements,
|
|
89
|
-
units=self.units)
|
|
90
|
-
)
|
|
88
|
+
return os.linesep + str(_format_series(series=self.movements, units=self.units))
|
|
91
89
|
|
|
92
90
|
def _repr_html_(self):
|
|
93
|
-
return _format_series(
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
tablefmt='html')
|
|
99
|
-
|
|
100
|
-
def duplicate(
|
|
101
|
-
self,
|
|
102
|
-
name: str = None) -> Flow:
|
|
91
|
+
return _format_series(series=self.movements, units=self.units).to_markdown(
|
|
92
|
+
stralign="right", numalign="right", tablefmt="html"
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
def duplicate(self, name: str = None) -> Flow:
|
|
103
96
|
return self.__class__(
|
|
104
97
|
movements=self.movements.copy(deep=True),
|
|
105
98
|
units=self.units,
|
|
106
|
-
name=self.name if name is None else name
|
|
99
|
+
name=self.name if name is None else name,
|
|
100
|
+
)
|
|
107
101
|
|
|
108
102
|
# def _format(
|
|
109
103
|
# self,
|
|
110
104
|
# decimals: int = 2):
|
|
111
105
|
|
|
112
|
-
def display(
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
106
|
+
def display(self, tablefmt: str = "github", decimals: int = 2):
|
|
107
|
+
name = "Name: " + self.name
|
|
108
|
+
units = "Units: " + str(self.units)
|
|
109
|
+
movements = (
|
|
110
|
+
"Movements: "
|
|
111
|
+
+ os.linesep
|
|
112
|
+
+ _format_series(
|
|
113
|
+
series=self.movements, units=self.units, decimals=decimals
|
|
114
|
+
).to_markdown(
|
|
115
|
+
stralign="right",
|
|
116
|
+
numalign="right",
|
|
117
|
+
tablefmt=tablefmt,
|
|
118
|
+
floatfmt="." + str(decimals) + "f",
|
|
119
|
+
)
|
|
120
|
+
)
|
|
126
121
|
|
|
127
122
|
print(os.linesep + os.linesep.join([name, units, movements]))
|
|
128
123
|
# print(self._format(decimals=decimals) + os.linesep)
|
|
129
124
|
|
|
130
125
|
def plot(
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
126
|
+
self,
|
|
127
|
+
bounds: Optional[Tuple[float, float]] = None,
|
|
128
|
+
normalize: bool = False,
|
|
129
|
+
*args,
|
|
130
|
+
**kwargs,
|
|
131
|
+
):
|
|
135
132
|
self.movements.plot(*args, **kwargs)
|
|
136
|
-
plt.legend(loc=
|
|
137
|
-
plt.xlabel(
|
|
133
|
+
plt.legend(loc="best")
|
|
134
|
+
plt.xlabel("Date")
|
|
138
135
|
plt.ylabel(self.units.__doc__)
|
|
139
136
|
if bounds is not None:
|
|
140
137
|
plt.ylim(bottom=bounds[0], top=bounds[1])
|
|
@@ -144,34 +141,30 @@ class Flow:
|
|
|
144
141
|
|
|
145
142
|
@classmethod
|
|
146
143
|
def from_sequence(
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
144
|
+
cls,
|
|
145
|
+
sequence: pd.PeriodIndex,
|
|
146
|
+
data: [float],
|
|
147
|
+
units: Optional[pint.Unit] = None,
|
|
148
|
+
name: str = None,
|
|
149
|
+
) -> Flow:
|
|
152
150
|
"""
|
|
153
151
|
Returns a Flow where movement dates are defined by the end-dates of the specified sequence of periods (a pd.PeriodIndex)
|
|
154
152
|
"""
|
|
155
153
|
|
|
156
154
|
if sequence.size != len(data):
|
|
157
155
|
raise ValueError("Error: count of periods and data must match")
|
|
158
|
-
dates = [
|
|
156
|
+
dates = [
|
|
157
|
+
pd.Timestamp(period.to_timestamp(how="end").date()) for period in sequence
|
|
158
|
+
]
|
|
159
159
|
series = pd.Series(
|
|
160
|
-
data=data,
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
dtype=float)
|
|
164
|
-
return cls(
|
|
165
|
-
movements=series,
|
|
166
|
-
units=units,
|
|
167
|
-
name=name)
|
|
160
|
+
data=data, index=pd.Series(data=dates, name="date"), name=name, dtype=float
|
|
161
|
+
)
|
|
162
|
+
return cls(movements=series, units=units, name=name)
|
|
168
163
|
|
|
169
164
|
@classmethod
|
|
170
165
|
def from_dict(
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
units: pint.Unit,
|
|
174
|
-
name: str = None) -> Flow:
|
|
166
|
+
cls, movements: Dict[pd.Timestamp, float], units: pint.Unit, name: str = None
|
|
167
|
+
) -> Flow:
|
|
175
168
|
"""
|
|
176
169
|
Returns a Flow where movements are defined by key-value pairs of pd.Timestamps and amounts.
|
|
177
170
|
"""
|
|
@@ -179,21 +172,20 @@ class Flow:
|
|
|
179
172
|
dates = movements.keys()
|
|
180
173
|
series = pd.Series(
|
|
181
174
|
data=list(movements.values()),
|
|
182
|
-
index=pd.Series(dates, name=
|
|
175
|
+
index=pd.Series(dates, name="date"),
|
|
183
176
|
name=name,
|
|
184
|
-
dtype=float
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
units=units,
|
|
188
|
-
name=name)
|
|
177
|
+
dtype=float,
|
|
178
|
+
)
|
|
179
|
+
return cls(movements=series, units=units, name=name)
|
|
189
180
|
|
|
190
181
|
@classmethod
|
|
191
182
|
def from_projection(
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
183
|
+
cls,
|
|
184
|
+
value: Union[float, pint.Quantity, rk.distribution.Form],
|
|
185
|
+
proj: rk.projection,
|
|
186
|
+
units: pint.Unit = None,
|
|
187
|
+
name: str = None,
|
|
188
|
+
) -> Flow:
|
|
197
189
|
"""
|
|
198
190
|
Generate a Flow from a projection of a value, with an optional timing
|
|
199
191
|
offset in the application of the projection to the value. (i.e., the
|
|
@@ -211,7 +203,10 @@ class Flow:
|
|
|
211
203
|
value = value.sample()[0]
|
|
212
204
|
|
|
213
205
|
if isinstance(proj, rk.projection.Extrapolation):
|
|
214
|
-
if
|
|
206
|
+
if (
|
|
207
|
+
proj.form.type == rk.extrapolation.Type.STRAIGHT_LINE
|
|
208
|
+
or proj.form.type == rk.extrapolation.Type.RECURRING
|
|
209
|
+
):
|
|
215
210
|
movements = value + proj.terms()
|
|
216
211
|
elif proj.form.type == rk.extrapolation.Type.COMPOUNDING:
|
|
217
212
|
movements = value * proj.terms()
|
|
@@ -226,10 +221,7 @@ class Flow:
|
|
|
226
221
|
# movements = movements[proj.bounds[0].to_timestamp(how='start'):proj.bounds[1].to_timestamp(how='end')] # Fix for >yearly periodicities
|
|
227
222
|
# TODO: Fix for issues with >yearly periodicities inducing movements at the end of multi-year periods beyond the end of the projection
|
|
228
223
|
|
|
229
|
-
return cls(
|
|
230
|
-
movements=movements,
|
|
231
|
-
units=units,
|
|
232
|
-
name=name)
|
|
224
|
+
return cls(movements=movements, units=units, name=name)
|
|
233
225
|
|
|
234
226
|
def invert(self) -> Flow:
|
|
235
227
|
"""
|
|
@@ -238,7 +230,8 @@ class Flow:
|
|
|
238
230
|
return self.__class__(
|
|
239
231
|
movements=self.movements.copy(deep=True).multiply(-1),
|
|
240
232
|
units=self.units,
|
|
241
|
-
name=self.name
|
|
233
|
+
name=self.name,
|
|
234
|
+
)
|
|
242
235
|
|
|
243
236
|
def collapse(self) -> Flow:
|
|
244
237
|
"""
|
|
@@ -248,7 +241,8 @@ class Flow:
|
|
|
248
241
|
return self.__class__.from_dict(
|
|
249
242
|
name=self.name,
|
|
250
243
|
movements={self.movements.index[-1]: self.movements.sum()},
|
|
251
|
-
units=self.units
|
|
244
|
+
units=self.units,
|
|
245
|
+
)
|
|
252
246
|
|
|
253
247
|
def total(self) -> np.float64:
|
|
254
248
|
"""
|
|
@@ -256,85 +250,77 @@ class Flow:
|
|
|
256
250
|
"""
|
|
257
251
|
return self.collapse().movements[0]
|
|
258
252
|
|
|
259
|
-
def pv(
|
|
260
|
-
self,
|
|
261
|
-
frequency: rk.duration.Type,
|
|
262
|
-
rate: float,
|
|
263
|
-
name: str = None) -> Flow:
|
|
253
|
+
def pv(self, frequency: rk.duration.Type, rate: float, name: str = None) -> Flow:
|
|
264
254
|
"""
|
|
265
255
|
Returns a Flow with values discounted to the present (i.e. before its first period) by a specified rate
|
|
266
256
|
"""
|
|
267
257
|
resampled = self.resample(frequency)
|
|
268
258
|
frame = resampled.movements.to_frame()
|
|
269
|
-
frame.insert(0,
|
|
270
|
-
frame[
|
|
271
|
-
lambda movement: movement[self.name]
|
|
259
|
+
frame.insert(0, "index", range(resampled.movements.index.size))
|
|
260
|
+
frame["Discounted Flow"] = frame.apply(
|
|
261
|
+
lambda movement: movement[self.name]
|
|
262
|
+
/ np.power((1 + rate), movement["index"] + 1),
|
|
263
|
+
axis=1,
|
|
264
|
+
)
|
|
272
265
|
if name is None:
|
|
273
|
-
name =
|
|
266
|
+
name = "Discounted " + self.name
|
|
274
267
|
return self.__class__(
|
|
275
|
-
movements=frame[
|
|
276
|
-
|
|
277
|
-
name=name)
|
|
268
|
+
movements=frame["Discounted Flow"], units=self.units, name=name
|
|
269
|
+
)
|
|
278
270
|
|
|
279
271
|
def xirr(self) -> float:
|
|
280
272
|
return pyxirr.xirr(
|
|
281
273
|
dates=[datetime.date() for datetime in list(self.movements.index.array)],
|
|
282
|
-
amounts=self.movements.to_list()
|
|
274
|
+
amounts=self.movements.to_list(),
|
|
275
|
+
)
|
|
283
276
|
|
|
284
|
-
def xnpv(
|
|
285
|
-
self,
|
|
286
|
-
rate: float) -> float:
|
|
277
|
+
def xnpv(self, rate: float) -> float:
|
|
287
278
|
return pyxirr.xnpv(
|
|
288
279
|
rate=rate,
|
|
289
280
|
dates=[datetime.date() for datetime in list(self.movements.index.array)],
|
|
290
|
-
amounts=self.movements.to_list()
|
|
281
|
+
amounts=self.movements.to_list(),
|
|
282
|
+
)
|
|
291
283
|
|
|
292
|
-
def resample(
|
|
293
|
-
self,
|
|
294
|
-
frequency: rk.duration.Type) -> Flow:
|
|
284
|
+
def resample(self, frequency: rk.duration.Type) -> Flow:
|
|
295
285
|
"""
|
|
296
|
-
Returns a Flow with movements summed to specified frequency of
|
|
286
|
+
Returns a Flow with movements summed to specified frequency of periods
|
|
297
287
|
"""
|
|
298
288
|
return rk.flux.Flow(
|
|
299
|
-
movements=self.movements.copy(deep=True)
|
|
289
|
+
movements=self.movements.copy(deep=True)
|
|
290
|
+
.resample(rule=rk.duration.Type.period(frequency))
|
|
291
|
+
.sum(),
|
|
300
292
|
units=self.units,
|
|
301
|
-
name=self.name
|
|
293
|
+
name=self.name,
|
|
294
|
+
)
|
|
302
295
|
|
|
303
|
-
def to_periods(
|
|
304
|
-
self,
|
|
305
|
-
frequency: rk.duration.Type) -> pd.Series:
|
|
296
|
+
def to_periods(self, frequency: rk.duration.Type) -> pd.Series:
|
|
306
297
|
"""
|
|
307
298
|
Returns a pd.Series (of index pd.PeriodIndex) with movements summed to specified frequency
|
|
308
299
|
"""
|
|
309
|
-
return (
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
300
|
+
return (
|
|
301
|
+
self.resample(frequency=frequency)
|
|
302
|
+
.movements.to_period(freq=rk.duration.Type.period(frequency), copy=True)
|
|
303
|
+
.rename_axis("period")
|
|
304
|
+
.groupby(level="period")
|
|
305
|
+
.sum()
|
|
306
|
+
)
|
|
316
307
|
|
|
317
308
|
def get_frequency(self) -> str:
|
|
318
309
|
return self.movements.index.freq
|
|
319
310
|
|
|
320
|
-
def trim_to_span(
|
|
321
|
-
self,
|
|
322
|
-
span: rk.span.Span,
|
|
323
|
-
name: str = None) -> Flow:
|
|
311
|
+
def trim_to_span(self, span: rk.span.Span, name: str = None) -> Flow:
|
|
324
312
|
"""
|
|
325
313
|
Returns a Flow with movements trimmed to the specified span
|
|
326
314
|
"""
|
|
327
315
|
return self.__class__(
|
|
328
316
|
movements=self.movements.copy(deep=True).truncate(
|
|
329
|
-
before=span.start_date,
|
|
330
|
-
|
|
317
|
+
before=span.start_date, after=span.end_date
|
|
318
|
+
),
|
|
331
319
|
units=self.units,
|
|
332
|
-
name=self.name if name is None else name
|
|
320
|
+
name=self.name if name is None else name,
|
|
321
|
+
)
|
|
333
322
|
|
|
334
|
-
def to_stream(
|
|
335
|
-
self,
|
|
336
|
-
frequency: rk.duration.Type,
|
|
337
|
-
name: str = None) -> Stream:
|
|
323
|
+
def to_stream(self, frequency: rk.duration.Type, name: str = None) -> Stream:
|
|
338
324
|
"""
|
|
339
325
|
Returns a Stream with the flow as flow
|
|
340
326
|
resampled at the specified frequency
|
|
@@ -342,7 +328,8 @@ class Flow:
|
|
|
342
328
|
return Stream(
|
|
343
329
|
name=name if name is not None else self.name,
|
|
344
330
|
flows=[self],
|
|
345
|
-
frequency=frequency
|
|
331
|
+
frequency=frequency,
|
|
332
|
+
)
|
|
346
333
|
|
|
347
334
|
|
|
348
335
|
class Stream:
|
|
@@ -358,13 +345,14 @@ class Stream:
|
|
|
358
345
|
"""
|
|
359
346
|
|
|
360
347
|
def __init__(
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
348
|
+
self,
|
|
349
|
+
flows: [Flow],
|
|
350
|
+
frequency: rk.duration.Type,
|
|
351
|
+
name: str = None,
|
|
352
|
+
):
|
|
365
353
|
|
|
366
354
|
# Name:
|
|
367
|
-
self.name = name if name is not None else
|
|
355
|
+
self.name = name if name is not None else "Unnamed"
|
|
368
356
|
|
|
369
357
|
# Units:
|
|
370
358
|
# if all(flow.units == flows[0].units for flow in flows):
|
|
@@ -383,15 +371,19 @@ class Stream:
|
|
|
383
371
|
|
|
384
372
|
# Stream:
|
|
385
373
|
flows_dates = list(
|
|
386
|
-
itertools.chain.from_iterable(
|
|
374
|
+
itertools.chain.from_iterable(
|
|
375
|
+
list(flow.movements.index.array) for flow in self.flows
|
|
376
|
+
)
|
|
377
|
+
)
|
|
387
378
|
self.start_date = min(flows_dates)
|
|
388
379
|
self.end_date = max(flows_dates)
|
|
389
380
|
|
|
390
381
|
index = rk.duration.Sequence.from_bounds(
|
|
391
|
-
include_start=self.start_date,
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
382
|
+
include_start=self.start_date, frequency=self.frequency, bound=self.end_date
|
|
383
|
+
)
|
|
384
|
+
self._resampled_flows = [
|
|
385
|
+
flow.to_periods(frequency=self.frequency) for flow in self.flows
|
|
386
|
+
]
|
|
395
387
|
self.frame = pd.concat(self._resampled_flows, axis=1).fillna(0).sort_index()
|
|
396
388
|
"""
|
|
397
389
|
A pd.DataFrame of the Stream's flow Flows accumulated into the Stream's frequency
|
|
@@ -402,40 +394,36 @@ class Stream:
|
|
|
402
394
|
|
|
403
395
|
def _repr_html_(self):
|
|
404
396
|
return self._format_flows().to_markdown(
|
|
405
|
-
stralign="right",
|
|
406
|
-
|
|
407
|
-
tablefmt='html',
|
|
408
|
-
floatfmt=".2f")
|
|
397
|
+
stralign="right", numalign="right", tablefmt="html", floatfmt=".2f"
|
|
398
|
+
)
|
|
409
399
|
|
|
410
|
-
def _format_flows(
|
|
411
|
-
self,
|
|
412
|
-
decimals: int = 2) -> pd.DataFrame:
|
|
400
|
+
def _format_flows(self, decimals: int = 2) -> pd.DataFrame:
|
|
413
401
|
|
|
414
402
|
formatted_flows = []
|
|
415
403
|
for flow in self.flows:
|
|
416
404
|
series = flow.to_periods(frequency=self.frequency)
|
|
417
|
-
formatted_flows.append(
|
|
418
|
-
series=series,
|
|
419
|
-
|
|
420
|
-
decimals=decimals))
|
|
405
|
+
formatted_flows.append(
|
|
406
|
+
_format_series(series=series, units=flow.units, decimals=decimals)
|
|
407
|
+
)
|
|
421
408
|
# formatted_flows = [flow._format_movements(decimals=decimals) for flow in self._resampled_flows]
|
|
422
409
|
frame = pd.concat(formatted_flows, axis=1).fillna(0).sort_index()
|
|
423
410
|
return frame
|
|
424
411
|
|
|
425
|
-
def display(
|
|
426
|
-
self,
|
|
427
|
-
tablefmt: str = 'github',
|
|
428
|
-
decimals: int = 2):
|
|
412
|
+
def display(self, tablefmt: str = "github", decimals: int = 2):
|
|
429
413
|
|
|
430
414
|
floatfmt = "." + str(decimals) + "f"
|
|
431
415
|
|
|
432
|
-
name =
|
|
433
|
-
units =
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
416
|
+
name = "Name: " + self.name if self.name is not None else ""
|
|
417
|
+
units = "Units: " + json.dumps(
|
|
418
|
+
{flow.name: flow.units.__str__() for flow in self.flows}, indent=4
|
|
419
|
+
)
|
|
420
|
+
flows = (
|
|
421
|
+
"Flows: "
|
|
422
|
+
+ os.linesep
|
|
423
|
+
+ self._format_flows(decimals=decimals).to_markdown(
|
|
424
|
+
tablefmt=tablefmt, stralign="right", numalign="right", floatfmt=floatfmt
|
|
425
|
+
)
|
|
426
|
+
)
|
|
439
427
|
|
|
440
428
|
print(os.linesep + os.linesep.join([name, units, flows]))
|
|
441
429
|
|
|
@@ -453,11 +441,7 @@ class Stream:
|
|
|
453
441
|
flows.extend(other.flows)
|
|
454
442
|
else:
|
|
455
443
|
raise Exception("Cannot add type " + type(other).__name__ + " to Stream.")
|
|
456
|
-
return self.__class__(
|
|
457
|
-
name=self.name,
|
|
458
|
-
flows=flows,
|
|
459
|
-
frequency=self.frequency)
|
|
460
|
-
|
|
444
|
+
return self.__class__(name=self.name, flows=flows, frequency=self.frequency)
|
|
461
445
|
|
|
462
446
|
def is_homogeneous(self) -> bool:
|
|
463
447
|
return len(list(set(list(self.units.values())))) == 1
|
|
@@ -466,39 +450,37 @@ class Stream:
|
|
|
466
450
|
return self.__class__(
|
|
467
451
|
name=self.name,
|
|
468
452
|
flows=[flow.duplicate() for flow in self.flows],
|
|
469
|
-
frequency=self.frequency
|
|
453
|
+
frequency=self.frequency,
|
|
454
|
+
)
|
|
470
455
|
|
|
471
456
|
@classmethod
|
|
472
|
-
def from_DataFrame(
|
|
473
|
-
cls,
|
|
474
|
-
name: str,
|
|
475
|
-
data: pd.DataFrame,
|
|
476
|
-
units: pint.Unit) -> Stream:
|
|
457
|
+
def from_DataFrame(cls, name: str, data: pd.DataFrame, units: pint.Unit) -> Stream:
|
|
477
458
|
flows = []
|
|
478
459
|
for column in data.columns:
|
|
479
460
|
series = data[column]
|
|
480
|
-
flows.append(
|
|
481
|
-
Flow(
|
|
482
|
-
movements=series,
|
|
483
|
-
units=units,
|
|
484
|
-
name=series.name))
|
|
461
|
+
flows.append(Flow(movements=series, units=units, name=series.name))
|
|
485
462
|
return cls(
|
|
486
463
|
name=name,
|
|
487
464
|
flows=flows,
|
|
488
|
-
frequency=rk.duration.Type.from_value(data.index.freqstr)
|
|
465
|
+
frequency=rk.duration.Type.from_value(data.index.freqstr),
|
|
466
|
+
)
|
|
489
467
|
|
|
490
468
|
def plot(
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
469
|
+
self,
|
|
470
|
+
flows: Dict[str, tuple] = None,
|
|
471
|
+
normalize: bool = False,
|
|
472
|
+
):
|
|
495
473
|
"""
|
|
496
474
|
Plots the specified flows against each respective value range (min-max)
|
|
497
475
|
|
|
498
476
|
:param flows: A dictionary of flows to plot, by name and value range (as a tuple)
|
|
499
477
|
"""
|
|
500
478
|
|
|
501
|
-
flows =
|
|
479
|
+
flows = (
|
|
480
|
+
flows
|
|
481
|
+
if flows is not None
|
|
482
|
+
else {flow.name: (flow.min(), flow.max()) for flow in self._resampled_flows}
|
|
483
|
+
)
|
|
502
484
|
dates = list(self.frame.index.astype(str))
|
|
503
485
|
|
|
504
486
|
fig, host = plt.subplots(nrows=1, ncols=1)
|
|
@@ -507,33 +489,27 @@ class Stream:
|
|
|
507
489
|
datums = []
|
|
508
490
|
axes = []
|
|
509
491
|
|
|
510
|
-
host.set_xlabel(
|
|
492
|
+
host.set_xlabel("Date")
|
|
511
493
|
# host.set_xticklabels(host.get_xticks(), rotation=90)
|
|
512
|
-
host.set_facecolor(
|
|
513
|
-
host.grid(axis=
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
color='gainsboro',
|
|
519
|
-
which='major',
|
|
520
|
-
linewidth=1)
|
|
521
|
-
host.grid(axis='y',
|
|
522
|
-
color='whitesmoke',
|
|
523
|
-
which='minor',
|
|
524
|
-
linestyle='-.',
|
|
525
|
-
linewidth=0.5)
|
|
494
|
+
host.set_facecolor("white")
|
|
495
|
+
host.grid(axis="x", color="gainsboro", which="major", linewidth=1)
|
|
496
|
+
host.grid(axis="y", color="gainsboro", which="major", linewidth=1)
|
|
497
|
+
host.grid(
|
|
498
|
+
axis="y", color="whitesmoke", which="minor", linestyle="-.", linewidth=0.5
|
|
499
|
+
)
|
|
526
500
|
|
|
527
501
|
primary_flow_name = list(flows.keys())[0]
|
|
528
|
-
primary, = host.plot(
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
502
|
+
(primary,) = host.plot(
|
|
503
|
+
dates,
|
|
504
|
+
self.frame[primary_flow_name],
|
|
505
|
+
color=plt.cm.viridis(0),
|
|
506
|
+
label=primary_flow_name,
|
|
507
|
+
)
|
|
532
508
|
host.spines.left.set_linewidth(1)
|
|
533
509
|
host.set_ylabel(primary_flow_name)
|
|
534
510
|
host.yaxis.label.set_color(primary.get_color())
|
|
535
511
|
host.spines.left.set_color(primary.get_color())
|
|
536
|
-
host.tick_params(axis=
|
|
512
|
+
host.tick_params(axis="y", colors=primary.get_color(), **tkw)
|
|
537
513
|
|
|
538
514
|
datums.append(primary)
|
|
539
515
|
axes.append(host)
|
|
@@ -542,11 +518,12 @@ class Stream:
|
|
|
542
518
|
if normalize:
|
|
543
519
|
for i in range(1, len(flows)):
|
|
544
520
|
additional_flow_name = list(flows.keys())[i]
|
|
545
|
-
additional, = host.plot(
|
|
521
|
+
(additional,) = host.plot(
|
|
546
522
|
dates,
|
|
547
523
|
self.frame[additional_flow_name],
|
|
548
524
|
color=plt.cm.viridis(1 / (len(flows) + 1)),
|
|
549
|
-
label=additional_flow_name
|
|
525
|
+
label=additional_flow_name,
|
|
526
|
+
)
|
|
550
527
|
datums.append(additional)
|
|
551
528
|
|
|
552
529
|
else:
|
|
@@ -555,19 +532,24 @@ class Stream:
|
|
|
555
532
|
if len(flows) > 1:
|
|
556
533
|
secondary_flow_name = list(flows.keys())[1]
|
|
557
534
|
right = host.twinx()
|
|
558
|
-
secondary, = right.plot(
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
535
|
+
(secondary,) = right.plot(
|
|
536
|
+
dates,
|
|
537
|
+
self.frame[secondary_flow_name],
|
|
538
|
+
color=plt.cm.viridis(1 / (len(flows) + 1)),
|
|
539
|
+
label=secondary_flow_name,
|
|
540
|
+
)
|
|
562
541
|
right.spines.right.set_visible(True)
|
|
563
542
|
right.spines.right.set_linewidth(1)
|
|
564
543
|
right.set_ylabel(secondary_flow_name)
|
|
565
544
|
right.grid(False)
|
|
566
545
|
right.yaxis.label.set_color(secondary.get_color())
|
|
567
546
|
right.spines.right.set_color(secondary.get_color())
|
|
568
|
-
right.tick_params(axis=
|
|
547
|
+
right.tick_params(axis="y", colors=secondary.get_color(), **tkw)
|
|
569
548
|
# if flows[secondary_flow] is not None:
|
|
570
|
-
right.set_ylim(
|
|
549
|
+
right.set_ylim(
|
|
550
|
+
bottom=flows[secondary_flow_name][0],
|
|
551
|
+
top=flows[secondary_flow_name][1],
|
|
552
|
+
)
|
|
571
553
|
|
|
572
554
|
datums.append(secondary)
|
|
573
555
|
axes.append(right)
|
|
@@ -576,21 +558,31 @@ class Stream:
|
|
|
576
558
|
for i in range(2, len(flows)):
|
|
577
559
|
additional_flow_name = list(flows.keys())[i]
|
|
578
560
|
supplementary = host.twinx()
|
|
579
|
-
additional, = supplementary.plot(
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
561
|
+
(additional,) = supplementary.plot(
|
|
562
|
+
dates,
|
|
563
|
+
self.frame[additional_flow_name],
|
|
564
|
+
color=plt.cm.viridis(i / (len(flows) + 1)),
|
|
565
|
+
label=additional_flow_name,
|
|
566
|
+
)
|
|
567
|
+
supplementary.spines.right.set_position(
|
|
568
|
+
("axes", 1 + (i - 1) / 7.5)
|
|
569
|
+
)
|
|
584
570
|
supplementary.spines.right.set_visible(True)
|
|
585
571
|
supplementary.spines.right.set_linewidth(1)
|
|
586
572
|
supplementary.set_ylabel(additional_flow_name)
|
|
587
573
|
supplementary.grid(False)
|
|
588
574
|
supplementary.yaxis.label.set_color(additional.get_color())
|
|
589
575
|
supplementary.spines.right.set_color(additional.get_color())
|
|
590
|
-
supplementary.tick_params(
|
|
576
|
+
supplementary.tick_params(
|
|
577
|
+
axis="y", colors=additional.get_color(), **tkw
|
|
578
|
+
)
|
|
591
579
|
if flows[additional_flow_name] is not None:
|
|
592
580
|
supplementary.set_ylim(
|
|
593
|
-
[
|
|
581
|
+
[
|
|
582
|
+
flows[additional_flow_name][0],
|
|
583
|
+
flows[additional_flow_name][1],
|
|
584
|
+
]
|
|
585
|
+
)
|
|
594
586
|
|
|
595
587
|
datums.append(additional)
|
|
596
588
|
axes.append(supplementary)
|
|
@@ -599,23 +591,22 @@ class Stream:
|
|
|
599
591
|
legend = axes[-1].legend(
|
|
600
592
|
datums,
|
|
601
593
|
labels,
|
|
602
|
-
loc=
|
|
594
|
+
loc="best",
|
|
603
595
|
title=self.name,
|
|
604
596
|
title_fontsize=8,
|
|
605
597
|
fontsize=7,
|
|
606
598
|
facecolor="white",
|
|
607
599
|
frameon=True,
|
|
608
600
|
framealpha=1,
|
|
609
|
-
borderpad
|
|
610
|
-
edgecolor=
|
|
601
|
+
borderpad=0.75,
|
|
602
|
+
edgecolor="grey",
|
|
603
|
+
)
|
|
611
604
|
fig.autofmt_xdate(rotation=90)
|
|
612
605
|
plt.minorticks_on()
|
|
613
606
|
# plt.tight_layout()
|
|
614
607
|
plt.show(block=True)
|
|
615
608
|
|
|
616
|
-
def extract(
|
|
617
|
-
self,
|
|
618
|
-
flow_name: str) -> Flow:
|
|
609
|
+
def extract(self, flow_name: str) -> Flow:
|
|
619
610
|
"""
|
|
620
611
|
Extract an Stream's resampled flow as a Flow
|
|
621
612
|
:param flow_name:
|
|
@@ -625,32 +616,38 @@ class Stream:
|
|
|
625
616
|
name=flow_name,
|
|
626
617
|
data=list(self.frame[flow_name]),
|
|
627
618
|
sequence=self.frame.index,
|
|
628
|
-
units=self.units[flow_name]
|
|
619
|
+
units=self.units[flow_name],
|
|
620
|
+
)
|
|
629
621
|
|
|
630
|
-
def sum(
|
|
631
|
-
self,
|
|
632
|
-
name: str = None) -> Flow:
|
|
622
|
+
def sum(self, name: str = None) -> Flow:
|
|
633
623
|
"""
|
|
634
624
|
Returns a Flow whose movements are the sum of the Stream's flows by period
|
|
635
625
|
:return: Flow
|
|
636
626
|
"""
|
|
637
627
|
# Check if all units are the same:
|
|
638
628
|
if not len(list(set(list(self.units.values())))) == 1:
|
|
639
|
-
raise ValueError(
|
|
640
|
-
|
|
641
|
-
|
|
629
|
+
raise ValueError(
|
|
630
|
+
"Error: summation requires all flows' units to be the same. Units: {0}".format(
|
|
631
|
+
json.dumps(
|
|
632
|
+
{flow.name: flow.units.__str__() for flow in self.flows},
|
|
633
|
+
indent=4,
|
|
634
|
+
)
|
|
635
|
+
)
|
|
636
|
+
)
|
|
642
637
|
|
|
643
638
|
return Flow.from_sequence(
|
|
644
|
-
name=name if name is not None else self.name +
|
|
639
|
+
name=name if name is not None else self.name + " (sum)",
|
|
645
640
|
sequence=self.frame.index,
|
|
646
641
|
data=self.frame.sum(axis=1).to_list(),
|
|
647
|
-
units=next(iter(self.units.values()))
|
|
642
|
+
units=next(iter(self.units.values())),
|
|
643
|
+
)
|
|
648
644
|
|
|
649
645
|
def product(
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
646
|
+
self,
|
|
647
|
+
name: str = None,
|
|
648
|
+
registry: pint.UnitRegistry = None,
|
|
649
|
+
scope: Optional[dict] = None,
|
|
650
|
+
) -> Flow:
|
|
654
651
|
"""
|
|
655
652
|
Returns a Flow whose movements are the product of the Stream's flows by period
|
|
656
653
|
:return: Flow
|
|
@@ -661,72 +658,116 @@ class Stream:
|
|
|
661
658
|
# singleton = eval(' * '.join(singleton), scope)
|
|
662
659
|
registry = registry if registry is not None else rk.measure.Index.registry
|
|
663
660
|
units = rk.measure.multiply_units(
|
|
664
|
-
units=list(self.units.values()),
|
|
665
|
-
|
|
661
|
+
units=list(self.units.values()), registry=registry
|
|
662
|
+
)
|
|
666
663
|
reduced_units = rk.measure.remove_dimension(
|
|
667
|
-
quantity=registry.Quantity(1, units),
|
|
668
|
-
|
|
669
|
-
registry=registry).units
|
|
664
|
+
quantity=registry.Quantity(1, units), dimension="[time]", registry=registry
|
|
665
|
+
).units
|
|
670
666
|
|
|
671
667
|
return Flow.from_sequence(
|
|
672
|
-
name=name if name is not None else self.name +
|
|
668
|
+
name=name if name is not None else self.name + " (product)",
|
|
673
669
|
sequence=self.frame.index, # .to_period(),
|
|
674
670
|
data=self.frame.prod(axis=1).to_list(),
|
|
675
|
-
units=reduced_units
|
|
671
|
+
units=reduced_units,
|
|
672
|
+
)
|
|
676
673
|
|
|
677
|
-
def min(
|
|
678
|
-
self,
|
|
679
|
-
name):
|
|
674
|
+
def min(self, name):
|
|
680
675
|
"""
|
|
681
676
|
Returns a Flow whose movements are the minimum of the Stream's flows by period
|
|
682
677
|
"""
|
|
683
678
|
if not self.is_homogeneous():
|
|
684
|
-
raise ValueError(
|
|
685
|
-
|
|
686
|
-
|
|
679
|
+
raise ValueError(
|
|
680
|
+
"Error: minimum requires all flows' units to be the same. Units: {0}".format(
|
|
681
|
+
json.dumps(
|
|
682
|
+
{flow.name: flow.units.__str__() for flow in self.flows},
|
|
683
|
+
indent=4,
|
|
684
|
+
)
|
|
685
|
+
)
|
|
686
|
+
)
|
|
687
687
|
else:
|
|
688
|
-
name=name if name is not None else self.name +
|
|
688
|
+
name = name if name is not None else self.name + " (min)"
|
|
689
689
|
return Flow.from_sequence(
|
|
690
690
|
sequence=self.frame.index,
|
|
691
691
|
data=self.frame.min(axis=1).to_list(),
|
|
692
692
|
units=next(iter(self.units.values())),
|
|
693
|
-
name=name
|
|
693
|
+
name=name,
|
|
694
|
+
)
|
|
694
695
|
|
|
695
|
-
def max(
|
|
696
|
-
self,
|
|
697
|
-
name):
|
|
696
|
+
def max(self, name):
|
|
698
697
|
"""
|
|
699
698
|
Returns a Flow whose movements are the maximum of the Stream's flows by period
|
|
700
699
|
"""
|
|
701
700
|
if not self.is_homogeneous():
|
|
702
|
-
raise ValueError(
|
|
703
|
-
|
|
704
|
-
|
|
701
|
+
raise ValueError(
|
|
702
|
+
"Error: maximum requires all flows' units to be the same. Units: {0}".format(
|
|
703
|
+
json.dumps(
|
|
704
|
+
{flow.name: flow.units.__str__() for flow in self.flows},
|
|
705
|
+
indent=4,
|
|
706
|
+
)
|
|
707
|
+
)
|
|
708
|
+
)
|
|
705
709
|
else:
|
|
706
|
-
name=name if name is not None else self.name +
|
|
710
|
+
name = name if name is not None else self.name + " (max)"
|
|
707
711
|
return Flow.from_sequence(
|
|
708
712
|
sequence=self.frame.index,
|
|
709
713
|
data=self.frame.max(axis=1).to_list(),
|
|
710
714
|
units=next(iter(self.units.values())),
|
|
711
|
-
name=name
|
|
715
|
+
name=name,
|
|
716
|
+
)
|
|
712
717
|
|
|
713
718
|
def collapse(self) -> Stream:
|
|
714
719
|
"""
|
|
715
720
|
Returns an Stream with Flows' movements collapsed (summed) to the Stream's final period
|
|
716
721
|
:return: Stream
|
|
717
722
|
"""
|
|
718
|
-
flows = [
|
|
723
|
+
flows = [
|
|
724
|
+
self.extract(flow_name=flow_name) for flow_name in list(self.frame.columns)
|
|
725
|
+
]
|
|
719
726
|
return self.__class__(
|
|
720
727
|
name=self.name,
|
|
721
728
|
flows=[flow.collapse() for flow in flows],
|
|
722
|
-
frequency=self.frequency
|
|
729
|
+
frequency=self.frequency,
|
|
730
|
+
)
|
|
723
731
|
|
|
724
732
|
def total(self) -> np.float64:
|
|
725
733
|
return self.sum().collapse().movements.iloc[0]
|
|
726
734
|
|
|
735
|
+
# def append(
|
|
736
|
+
# self,
|
|
737
|
+
# flows: [Flow]) -> None:
|
|
738
|
+
# """
|
|
739
|
+
# Appends a list of Flows to the Stream
|
|
740
|
+
# :param flows:
|
|
741
|
+
# :type flows:
|
|
742
|
+
# :return:
|
|
743
|
+
# :rtype:
|
|
744
|
+
# """
|
|
745
|
+
# # if any(flow.units != self.units for flow in flows):
|
|
746
|
+
# # raise Exception("Input Flows have dissimilar units. Cannot aggregate into Stream.")
|
|
747
|
+
#
|
|
748
|
+
# # Append Flows:
|
|
749
|
+
# self.flows.extend(flows)
|
|
750
|
+
#
|
|
751
|
+
# # Append Units:
|
|
752
|
+
# self.units.update({flow.name: flow.units for flow in flows})
|
|
753
|
+
#
|
|
754
|
+
# # Stream:
|
|
755
|
+
# flows_dates = list(
|
|
756
|
+
# itertools.chain.from_iterable(list(flow.movements.index.array) for flow in self.flows))
|
|
757
|
+
# self.start_date = min(flows_dates)
|
|
758
|
+
# self.end_date = max(flows_dates)
|
|
759
|
+
#
|
|
760
|
+
# # sequence = rk.duration.Sequence.from_bounds(
|
|
761
|
+
# # include_start=self.start_date,
|
|
762
|
+
# # frequency=self.frequency,
|
|
763
|
+
# # bound=self.end_date)
|
|
764
|
+
# _resampled_flows = [flow.to_periods(frequency=self.frequency) for flow in self.flows]
|
|
765
|
+
# self.frame = pd.concat(_resampled_flows, axis=1).fillna(0)
|
|
766
|
+
|
|
727
767
|
def append(
|
|
728
|
-
|
|
729
|
-
|
|
768
|
+
self,
|
|
769
|
+
flows: [Flow],
|
|
770
|
+
):
|
|
730
771
|
"""
|
|
731
772
|
Appends a list of Flows to the Stream
|
|
732
773
|
:param flows:
|
|
@@ -734,39 +775,17 @@ class Stream:
|
|
|
734
775
|
:return:
|
|
735
776
|
:rtype:
|
|
736
777
|
"""
|
|
737
|
-
# if any(flow.units != self.units for flow in flows):
|
|
738
|
-
# raise Exception("Input Flows have dissimilar units. Cannot aggregate into Stream.")
|
|
739
|
-
|
|
740
|
-
# Append Flows:
|
|
741
|
-
self.flows.extend(flows)
|
|
742
|
-
|
|
743
|
-
# Append Units:
|
|
744
|
-
self.units.update({flow.name: flow.units for flow in flows})
|
|
745
|
-
|
|
746
|
-
# Stream:
|
|
747
|
-
flows_dates = list(
|
|
748
|
-
itertools.chain.from_iterable(list(flow.movements.index.array) for flow in self.flows))
|
|
749
|
-
self.start_date = min(flows_dates)
|
|
750
|
-
self.end_date = max(flows_dates)
|
|
751
|
-
|
|
752
|
-
# sequence = rk.duration.Sequence.from_bounds(
|
|
753
|
-
# include_start=self.start_date,
|
|
754
|
-
# frequency=self.frequency,
|
|
755
|
-
# bound=self.end_date)
|
|
756
|
-
_resampled_flows = [flow.to_periods(frequency=self.frequency) for flow in self.flows]
|
|
757
|
-
self.frame = pd.concat(_resampled_flows, axis=1).fillna(0)
|
|
758
778
|
|
|
759
|
-
|
|
760
|
-
self,
|
|
761
|
-
frequency: rk.duration.Type) -> Stream:
|
|
762
|
-
return Stream(
|
|
779
|
+
return self.__class__(
|
|
763
780
|
name=self.name,
|
|
764
|
-
flows=self.flows,
|
|
765
|
-
frequency=frequency
|
|
781
|
+
flows=self.flows + flows,
|
|
782
|
+
frequency=self.frequency,
|
|
783
|
+
)
|
|
784
|
+
|
|
785
|
+
def resample(self, frequency: rk.duration.Type) -> Stream:
|
|
786
|
+
return Stream(name=self.name, flows=self.flows, frequency=frequency)
|
|
766
787
|
|
|
767
|
-
def trim_to_span(
|
|
768
|
-
self,
|
|
769
|
-
span: rk.span.Span) -> Stream:
|
|
788
|
+
def trim_to_span(self, span: rk.span.Span) -> Stream:
|
|
770
789
|
"""
|
|
771
790
|
Returns an Stream with all flows trimmed to the specified Span
|
|
772
791
|
:param span:
|
|
@@ -775,22 +794,18 @@ class Stream:
|
|
|
775
794
|
return self.__class__(
|
|
776
795
|
name=self.name,
|
|
777
796
|
flows=[flow.duplicate().trim_to_span(span) for flow in self.flows],
|
|
778
|
-
frequency=self.frequency
|
|
797
|
+
frequency=self.frequency,
|
|
798
|
+
)
|
|
779
799
|
|
|
780
800
|
@classmethod
|
|
781
|
-
def merge(
|
|
782
|
-
cls,
|
|
783
|
-
streams,
|
|
784
|
-
name: str,
|
|
785
|
-
frequency: rk.duration.Type) -> Stream:
|
|
801
|
+
def merge(cls, streams, name: str, frequency: rk.duration.Type) -> Stream:
|
|
786
802
|
# Check Units:
|
|
787
803
|
if any(stream.units != streams[0].units for stream in streams):
|
|
788
|
-
raise Exception(
|
|
804
|
+
raise Exception(
|
|
805
|
+
"Input Streams have dissimilar units. Cannot merge into Stream."
|
|
806
|
+
)
|
|
789
807
|
|
|
790
808
|
# Aggregands:
|
|
791
809
|
flows = [flow for stream in streams for flow in stream.flows]
|
|
792
810
|
|
|
793
|
-
return cls(
|
|
794
|
-
name=name,
|
|
795
|
-
flows=flows,
|
|
796
|
-
frequency=frequency)
|
|
811
|
+
return cls(name=name, flows=flows, frequency=frequency)
|
|
@@ -60,8 +60,11 @@ class Balance:
|
|
|
60
60
|
starts = self.startings.movements.where(self.startings.movements < 0).fillna(0)
|
|
61
61
|
ends = self.endings.movements.where(self.endings.movements < 0).fillna(0)
|
|
62
62
|
|
|
63
|
+
overdrafts = ends - starts
|
|
64
|
+
overdrafts = overdrafts.where(overdrafts < 0).dropna()
|
|
65
|
+
|
|
63
66
|
return rk.flux.Flow(
|
|
64
|
-
movements=
|
|
67
|
+
movements=overdrafts,
|
|
65
68
|
units=self.startings.units,
|
|
66
69
|
name='Overdraft' if name is None else name)
|
|
67
70
|
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|