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/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)