rangekeeper 0.7.4__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.
Files changed (27) hide show
  1. {rangekeeper-0.7.4 → rangekeeper-0.7.5}/PKG-INFO +2 -1
  2. {rangekeeper-0.7.4 → rangekeeper-0.7.5}/pyproject.toml +2 -1
  3. {rangekeeper-0.7.4 → rangekeeper-0.7.5}/rangekeeper/flux.py +321 -306
  4. {rangekeeper-0.7.4 → rangekeeper-0.7.5}/README.md +0 -0
  5. {rangekeeper-0.7.4 → rangekeeper-0.7.5}/rangekeeper/__init__.py +0 -0
  6. {rangekeeper-0.7.4 → rangekeeper-0.7.5}/rangekeeper/api.py +0 -0
  7. {rangekeeper-0.7.4 → rangekeeper-0.7.5}/rangekeeper/conftest.py +0 -0
  8. {rangekeeper-0.7.4 → rangekeeper-0.7.5}/rangekeeper/distribution.py +0 -0
  9. {rangekeeper-0.7.4 → rangekeeper-0.7.5}/rangekeeper/duration.py +0 -0
  10. {rangekeeper-0.7.4 → rangekeeper-0.7.5}/rangekeeper/dynamics/__init__.py +0 -0
  11. {rangekeeper-0.7.4 → rangekeeper-0.7.5}/rangekeeper/dynamics/black_swan.py +0 -0
  12. {rangekeeper-0.7.4 → rangekeeper-0.7.5}/rangekeeper/dynamics/cyclicality.py +0 -0
  13. {rangekeeper-0.7.4 → rangekeeper-0.7.5}/rangekeeper/dynamics/market.py +0 -0
  14. {rangekeeper-0.7.4 → rangekeeper-0.7.5}/rangekeeper/dynamics/noise.py +0 -0
  15. {rangekeeper-0.7.4 → rangekeeper-0.7.5}/rangekeeper/dynamics/trend.py +0 -0
  16. {rangekeeper-0.7.4 → rangekeeper-0.7.5}/rangekeeper/dynamics/volatility.py +0 -0
  17. {rangekeeper-0.7.4 → rangekeeper-0.7.5}/rangekeeper/extrapolation.py +0 -0
  18. {rangekeeper-0.7.4 → rangekeeper-0.7.5}/rangekeeper/format.py +0 -0
  19. {rangekeeper-0.7.4 → rangekeeper-0.7.5}/rangekeeper/formula/__init__.py +0 -0
  20. {rangekeeper-0.7.4 → rangekeeper-0.7.5}/rangekeeper/formula/financial.py +0 -0
  21. {rangekeeper-0.7.4 → rangekeeper-0.7.5}/rangekeeper/graph.py +0 -0
  22. {rangekeeper-0.7.4 → rangekeeper-0.7.5}/rangekeeper/measure.py +0 -0
  23. {rangekeeper-0.7.4 → rangekeeper-0.7.5}/rangekeeper/policy.py +0 -0
  24. {rangekeeper-0.7.4 → rangekeeper-0.7.5}/rangekeeper/projection.py +0 -0
  25. {rangekeeper-0.7.4 → rangekeeper-0.7.5}/rangekeeper/segmentation.py +0 -0
  26. {rangekeeper-0.7.4 → rangekeeper-0.7.5}/rangekeeper/space.py +0 -0
  27. {rangekeeper-0.7.4 → 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.4
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,6 +12,7 @@ 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
17
  Requires-Dist: multiprocess (>=0.70,<0.71)
17
18
  Requires-Dist: networkx (>=3.3,<4.0)
@@ -1,6 +1,6 @@
1
1
  [tool.poetry]
2
2
  name = "Rangekeeper"
3
- version = "0.7.4"
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"
@@ -24,6 +24,7 @@ pyvis = "^0.3"
24
24
  plotly = "^6.0"
25
25
  aenum = "^3.1"
26
26
  numpy-financial = "^1.0"
27
+ black = "^25.1.0"
27
28
 
28
29
  [tool.poetry.dev-dependencies]
29
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
- series: pd.Series,
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='date'),
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='date'),
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
- self,
50
- movements: pd.Series,
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("Error: Flow's movements' dtype cannot be cast to float: {0}".format(str(e)))
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('Error: Units must be of type pint.Unit')
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
- series=self.movements,
95
- units=self.units).to_markdown(
96
- stralign="right",
97
- numalign="right",
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
- self,
114
- tablefmt: str = 'github',
115
- decimals: int = 2):
116
- name = 'Name: ' + self.name
117
- units = 'Units: ' + str(self.units)
118
- movements = 'Movements: ' + os.linesep + _format_series(
119
- series=self.movements,
120
- units=self.units,
121
- decimals=decimals).to_markdown(
122
- stralign="right",
123
- numalign="right",
124
- tablefmt=tablefmt,
125
- floatfmt='.' + str(decimals) + 'f')
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
- self,
132
- bounds: Optional[Tuple[float, float]] = None,
133
- normalize: bool = False,
134
- *args, **kwargs):
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='best')
137
- plt.xlabel('Date')
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
- cls,
148
- sequence: pd.PeriodIndex,
149
- data: [float],
150
- units: Optional[pint.Unit] = None,
151
- name: str = None) -> Flow:
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 = [pd.Timestamp(period.to_timestamp(how='end').date()) for period in sequence]
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
- index=pd.Series(data=dates, name='date'),
162
- name=name,
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
- cls,
172
- movements: Dict[pd.Timestamp, float],
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='date'),
175
+ index=pd.Series(dates, name="date"),
183
176
  name=name,
184
- dtype=float)
185
- return cls(
186
- movements=series,
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
- cls,
193
- value: Union[float, pint.Quantity, rk.distribution.Form],
194
- proj: rk.projection,
195
- units: pint.Unit = None,
196
- name: str = None) -> Flow:
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 proj.form.type == rk.extrapolation.Type.STRAIGHT_LINE or proj.form.type == rk.extrapolation.Type.RECURRING:
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, 'index', range(resampled.movements.index.size))
270
- frame['Discounted Flow'] = frame.apply(
271
- lambda movement: movement[self.name] / np.power((1 + rate), movement['index'] + 1), axis=1)
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 = 'Discounted ' + self.name
266
+ name = "Discounted " + self.name
274
267
  return self.__class__(
275
- movements=frame['Discounted Flow'],
276
- units=self.units,
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
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).resample(rule=rk.duration.Type.period(frequency)).sum(),
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 (self
310
- .resample(frequency=frequency)
311
- .movements.to_period(freq=rk.duration.Type.period(frequency), copy=True)
312
- .rename_axis('period')
313
- .groupby(level='period')
314
- .sum()
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
- after=span.end_date),
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
- self,
362
- flows: [Flow],
363
- frequency: rk.duration.Type,
364
- name: str = None):
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 'Unnamed'
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(list(flow.movements.index.array) for flow in self.flows))
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
- frequency=self.frequency,
393
- bound=self.end_date)
394
- self._resampled_flows = [flow.to_periods(frequency=self.frequency) for flow in self.flows]
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
- numalign="right",
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(_format_series(
418
- series=series,
419
- units=flow.units,
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 = 'Name: ' + self.name if self.name is not None else ''
433
- units = 'Units: ' + json.dumps({flow.name: flow.units.__str__() for flow in self.flows}, indent=4)
434
- flows = 'Flows: ' + os.linesep + self._format_flows(decimals=decimals).to_markdown(
435
- tablefmt=tablefmt,
436
- stralign="right",
437
- numalign="right",
438
- floatfmt=floatfmt)
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
- self,
492
- flows: Dict[str, tuple] = None,
493
- normalize: bool = False,
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 = flows if flows is not None else {flow.name: (flow.min(), flow.max()) for flow in self._resampled_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('Date')
492
+ host.set_xlabel("Date")
511
493
  # host.set_xticklabels(host.get_xticks(), rotation=90)
512
- host.set_facecolor('white')
513
- host.grid(axis='x',
514
- color='gainsboro',
515
- which='major',
516
- linewidth=1)
517
- host.grid(axis='y',
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(dates,
529
- self.frame[primary_flow_name],
530
- color=plt.cm.viridis(0),
531
- label=primary_flow_name)
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='y', colors=primary.get_color(), **tkw)
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(dates,
559
- self.frame[secondary_flow_name],
560
- color=plt.cm.viridis(1 / (len(flows) + 1)),
561
- label=secondary_flow_name)
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='y', colors=secondary.get_color(), **tkw)
547
+ right.tick_params(axis="y", colors=secondary.get_color(), **tkw)
569
548
  # if flows[secondary_flow] is not None:
570
- right.set_ylim(bottom=flows[secondary_flow_name][0], top=flows[secondary_flow_name][1])
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(dates,
580
- self.frame[additional_flow_name],
581
- color=plt.cm.viridis(i / (len(flows) + 1)),
582
- label=additional_flow_name)
583
- supplementary.spines.right.set_position(('axes', 1 + (i - 1) / 7.5))
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(axis='y', colors=additional.get_color(), **tkw)
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
- [flows[additional_flow_name][0], flows[additional_flow_name][1]])
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='best',
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=.75,
610
- edgecolor='grey')
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("Error: summation requires all flows' units to be the same. Units: {0}".format(
640
- json.dumps(
641
- {flow.name: flow.units.__str__() for flow in self.flows}, indent=4)))
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 + ' (sum)',
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
- self,
651
- name: str = None,
652
- registry: pint.UnitRegistry = None,
653
- scope: Optional[dict] = None) -> Flow:
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
- registry=registry)
661
+ units=list(self.units.values()), registry=registry
662
+ )
666
663
  reduced_units = rk.measure.remove_dimension(
667
- quantity=registry.Quantity(1, units),
668
- dimension='[time]',
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 + ' (product)',
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("Error: minimum requires all flows' units to be the same. Units: {0}".format(
685
- json.dumps(
686
- {flow.name: flow.units.__str__() for flow in self.flows}, indent=4)))
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 + ' (min)'
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("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}, indent=4)))
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 + ' (max)'
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 = [self.extract(flow_name=flow_name) for flow_name in list(self.frame.columns)]
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
- self,
729
- flows: [Flow]) -> None:
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
- def resample(
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("Input Streams have dissimilar units. Cannot merge into Stream.")
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)
File without changes