bulum 0.3.0__tar.gz → 0.3.2__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.
- {bulum-0.3.0/src/bulum.egg-info → bulum-0.3.2}/PKG-INFO +1 -1
- {bulum-0.3.0 → bulum-0.3.2}/src/bulum/io/lqn_io.py +25 -13
- {bulum-0.3.0 → bulum-0.3.2}/src/bulum/stats/reliability_stats_class.py +114 -94
- bulum-0.3.2/src/bulum/stats/stochastic_data_check.py +541 -0
- bulum-0.3.2/src/bulum/stats/storage_level_assessment.py +836 -0
- {bulum-0.3.0 → bulum-0.3.2}/src/bulum/utils/dataframe_extensions.py +11 -11
- {bulum-0.3.0 → bulum-0.3.2}/src/bulum/utils/dataframe_functions.py +143 -39
- bulum-0.3.2/src/bulum/utils/datetime_functions.py +651 -0
- bulum-0.3.2/src/bulum/utils/interpolation.py +62 -0
- {bulum-0.3.0 → bulum-0.3.2}/src/bulum/version.py +1 -1
- {bulum-0.3.0 → bulum-0.3.2/src/bulum.egg-info}/PKG-INFO +1 -1
- bulum-0.3.0/src/bulum/stats/stochastic_data_check.py +0 -414
- bulum-0.3.0/src/bulum/stats/storage_level_assessment.py +0 -254
- bulum-0.3.0/src/bulum/utils/datetime_functions.py +0 -327
- bulum-0.3.0/src/bulum/utils/interpolation.py +0 -15
- {bulum-0.3.0 → bulum-0.3.2}/LICENSE.md +0 -0
- {bulum-0.3.0 → bulum-0.3.2}/README.md +0 -0
- {bulum-0.3.0 → bulum-0.3.2}/setup.cfg +0 -0
- {bulum-0.3.0 → bulum-0.3.2}/setup.py +0 -0
- {bulum-0.3.0 → bulum-0.3.2}/src/bulum/__init__.py +0 -0
- {bulum-0.3.0 → bulum-0.3.2}/src/bulum/clim/__init__.py +0 -0
- {bulum-0.3.0 → bulum-0.3.2}/src/bulum/clim/clim.py +0 -0
- {bulum-0.3.0 → bulum-0.3.2}/src/bulum/demo.py +0 -0
- {bulum-0.3.0 → bulum-0.3.2}/src/bulum/io/__init__.py +0 -0
- {bulum-0.3.0 → bulum-0.3.2}/src/bulum/io/csv_io.py +0 -0
- {bulum-0.3.0 → bulum-0.3.2}/src/bulum/io/general_io.py +0 -0
- {bulum-0.3.0 → bulum-0.3.2}/src/bulum/io/idx_io.py +0 -0
- {bulum-0.3.0 → bulum-0.3.2}/src/bulum/io/idx_io_native.py +0 -0
- {bulum-0.3.0 → bulum-0.3.2}/src/bulum/io/iqqm_out_reader.py +0 -0
- {bulum-0.3.0 → bulum-0.3.2}/src/bulum/io/res_csv_io.py +0 -0
- {bulum-0.3.0 → bulum-0.3.2}/src/bulum/maps/__init__.py +0 -0
- {bulum-0.3.0 → bulum-0.3.2}/src/bulum/maps/station_maps.py +0 -0
- {bulum-0.3.0 → bulum-0.3.2}/src/bulum/plots/__init__.py +0 -0
- {bulum-0.3.0 → bulum-0.3.2}/src/bulum/plots/altair_plots.py +0 -0
- {bulum-0.3.0 → bulum-0.3.2}/src/bulum/plots/ensemble_altair_plots.py +0 -0
- {bulum-0.3.0 → bulum-0.3.2}/src/bulum/plots/node_diagrams.py +0 -0
- {bulum-0.3.0 → bulum-0.3.2}/src/bulum/plots/plot_functions.py +0 -0
- {bulum-0.3.0 → bulum-0.3.2}/src/bulum/plots/plotly_helpers.py +0 -0
- {bulum-0.3.0 → bulum-0.3.2}/src/bulum/plots/tests/__init__.py +0 -0
- {bulum-0.3.0 → bulum-0.3.2}/src/bulum/plots/tests/test_plot_functions.py +0 -0
- {bulum-0.3.0 → bulum-0.3.2}/src/bulum/stats/__init__.py +0 -0
- {bulum-0.3.0 → bulum-0.3.2}/src/bulum/stats/aggregate_stats.py +0 -0
- {bulum-0.3.0 → bulum-0.3.2}/src/bulum/stats/ensemble_stats.py +0 -0
- {bulum-0.3.0 → bulum-0.3.2}/src/bulum/stats/negflo.py +0 -0
- {bulum-0.3.0 → bulum-0.3.2}/src/bulum/stats/negflo_helpers.py +0 -0
- {bulum-0.3.0 → bulum-0.3.2}/src/bulum/stats/swflo2s/__init__.py +0 -0
- {bulum-0.3.0 → bulum-0.3.2}/src/bulum/stats/swflo2s/swflo2s.py +0 -0
- {bulum-0.3.0 → bulum-0.3.2}/src/bulum/stoch/__init__.py +0 -0
- {bulum-0.3.0 → bulum-0.3.2}/src/bulum/stoch/analyse.py +0 -0
- {bulum-0.3.0 → bulum-0.3.2}/src/bulum/stoch/generate.py +0 -0
- {bulum-0.3.0 → bulum-0.3.2}/src/bulum/test_circular_import.py +0 -0
- {bulum-0.3.0 → bulum-0.3.2}/src/bulum/trans/__init__.py +0 -0
- {bulum-0.3.0 → bulum-0.3.2}/src/bulum/trans/transformers.py +0 -0
- {bulum-0.3.0 → bulum-0.3.2}/src/bulum/utils/__init__.py +0 -0
- {bulum-0.3.0 → bulum-0.3.2}/src/bulum.egg-info/SOURCES.txt +0 -0
- {bulum-0.3.0 → bulum-0.3.2}/src/bulum.egg-info/dependency_links.txt +0 -0
- {bulum-0.3.0 → bulum-0.3.2}/src/bulum.egg-info/requires.txt +0 -0
- {bulum-0.3.0 → bulum-0.3.2}/src/bulum.egg-info/top_level.txt +0 -0
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
"""
|
|
2
|
-
IO functions for IQQM listquan.
|
|
2
|
+
IO functions for IQQM listquan output files.
|
|
3
|
+
|
|
4
|
+
This module provides functionality to read IQQM listquan output files,
|
|
5
|
+
which are space-separated text files containing time series data.
|
|
3
6
|
"""
|
|
4
7
|
import os
|
|
5
8
|
|
|
@@ -9,28 +12,34 @@ import pandas as pd
|
|
|
9
12
|
from bulum import utils
|
|
10
13
|
|
|
11
14
|
|
|
12
|
-
def read_iqqm_lqn_output(filename, col_name
|
|
13
|
-
*, data_start_row=7
|
|
15
|
+
def read_iqqm_lqn_output(filename: str | os.PathLike, col_name: str | None = None,
|
|
16
|
+
df: pd.DataFrame | None = None, *, data_start_row: int = 7,
|
|
17
|
+
dropna: bool = False) -> utils.TimeseriesDataframe:
|
|
14
18
|
"""
|
|
15
|
-
Reads the output of IQQM listquan. This is a space-separated
|
|
19
|
+
Reads the output of IQQM listquan. This is a space-separated format with
|
|
16
20
|
two columns (date, value) and data starting on line 7.
|
|
17
21
|
|
|
18
22
|
Parameters
|
|
19
23
|
----------
|
|
20
|
-
filename
|
|
24
|
+
filename : str or :class:`os.PathLike`
|
|
21
25
|
Path to the file to be read.
|
|
22
|
-
col_name : optional
|
|
26
|
+
col_name : str, optional
|
|
23
27
|
If supplied, sets the name for the resulting output column, otherwise
|
|
24
|
-
uses the filename.
|
|
25
|
-
df : DataFrame
|
|
26
|
-
If supplied, joins the output to `df`.
|
|
28
|
+
uses the filename. Default is None.
|
|
29
|
+
df : :class:`pandas.DataFrame`, optional
|
|
30
|
+
If supplied, joins the output to `df`. (Does not modify `df`.)
|
|
31
|
+
Default is None.
|
|
27
32
|
data_start_row : int, optional
|
|
28
33
|
Optionally specify the start row; may be useful for reading other IQQM
|
|
29
|
-
TEXT outputs.
|
|
34
|
+
TEXT outputs. Default is 7.
|
|
35
|
+
dropna : bool, optional
|
|
36
|
+
Remove NA values from dataframe before reading. Default is False.
|
|
30
37
|
|
|
31
38
|
Returns
|
|
32
39
|
-------
|
|
33
|
-
utils.TimeseriesDataframe
|
|
40
|
+
:class:`~bulum.utils.TimeseriesDataframe`
|
|
41
|
+
A time series dataframe containing the parsed data with standardized
|
|
42
|
+
date index and the specified column name.
|
|
34
43
|
"""
|
|
35
44
|
# If no df was supplied, instantiate a new one
|
|
36
45
|
if df is None:
|
|
@@ -42,11 +51,14 @@ def read_iqqm_lqn_output(filename, col_name=None, df=None,
|
|
|
42
51
|
temp = pd.read_csv(filename, skiprows=(data_start_row-2),
|
|
43
52
|
sep=r'\s+', names=["Date", col_name], header=None)
|
|
44
53
|
# temp = utils.set_index_dt(temp, format='%d/%m/%Y')
|
|
54
|
+
if dropna:
|
|
55
|
+
temp = temp.dropna()
|
|
45
56
|
temp.set_index(temp.columns[0], inplace=True)
|
|
46
|
-
temp.index = utils.standardize_datestring_format(temp.index)
|
|
47
|
-
temp.index.name = "Date"
|
|
57
|
+
temp.index = utils.standardize_datestring_format(temp.index) # type: ignore
|
|
58
|
+
temp.index.name = "Date" # type: ignore
|
|
48
59
|
temp = temp.replace(r'^\s*$', np.nan, regex=True)
|
|
49
60
|
df = df.join(temp, how="outer").sort_index()
|
|
61
|
+
|
|
50
62
|
# TODO: THERE IS NO GUARANTEE THAT THE DATES OVERLAP, THEREFORE WE MAY END UP WITH A DATAFRAME WITH INCOMPLETE DATES
|
|
51
63
|
# TODO: I SHOULD MAKE DEFAULT BEHAVIOUR AUTO-DETECT FORMAT DEPENDING ON *TYPE* AND *LOCATION* OF DELIMIT CHARS
|
|
52
64
|
# TODO: In the meantime we use the below to assert that the format of the resulting df meets our minimum standards.
|
|
@@ -8,8 +8,10 @@ from bulum import utils
|
|
|
8
8
|
|
|
9
9
|
|
|
10
10
|
class Reliability:
|
|
11
|
-
"""
|
|
12
|
-
|
|
11
|
+
"""Reliability of water supply statistics generator.
|
|
12
|
+
|
|
13
|
+
This class provides methods to calculate reliability statistics comparing
|
|
14
|
+
water supply against demand for different timescales (monthly, annual).
|
|
13
15
|
"""
|
|
14
16
|
|
|
15
17
|
def __init__(self, demand: Union[pd.Series, list, float, int], supply: pd.Series,
|
|
@@ -17,17 +19,17 @@ class Reliability:
|
|
|
17
19
|
demand_type: Literal["total", "daily_constant"] = "total",
|
|
18
20
|
ignore_leap_years=False, quiet=False) -> None:
|
|
19
21
|
"""Initialise Reliability class. Functions available are
|
|
20
|
-
- `Reliability.
|
|
22
|
+
- `Reliability.ReliabilityTS`,
|
|
21
23
|
- `Reliability.MonthlyReliability`, and
|
|
22
24
|
- `Reliability.AnnualReliability`.
|
|
23
|
-
|
|
25
|
+
|
|
24
26
|
Parameters
|
|
25
27
|
----------
|
|
26
|
-
demand :
|
|
28
|
+
demand : :class:`pandas.Series` or list or float or int
|
|
27
29
|
Demand timeseries with date string as index. Alternatively, a list
|
|
28
30
|
of monthly values or single demand which will be disaggregated
|
|
29
31
|
according to other input parameters.
|
|
30
|
-
supply :
|
|
32
|
+
supply : :class:`pandas.Series`
|
|
31
33
|
Daily supply timeseries with date as index.
|
|
32
34
|
demand_timescale : Literal["daily","monthly","yearly"], optional
|
|
33
35
|
If a float or int demand is provided, which timescale does it apply
|
|
@@ -42,38 +44,49 @@ class Reliability:
|
|
|
42
44
|
days in Feb, and value for the 29th Feb will be equal to the 28th of
|
|
43
45
|
Feb value. Defaults to False.
|
|
44
46
|
|
|
47
|
+
Raises
|
|
48
|
+
------
|
|
49
|
+
TypeError
|
|
50
|
+
If demand is not one of :class:`~pandas.Series`, list, float, or int.
|
|
51
|
+
If supply is not a :class:`~pandas.Series`.
|
|
52
|
+
If ignore_leap_years is not a boolean.
|
|
53
|
+
ValueError
|
|
54
|
+
If demand_type is not "total" or "daily_constant".
|
|
55
|
+
If demand_timescale is not "daily", "monthly", or "yearly".
|
|
56
|
+
If demand list has fewer than 12 elements.
|
|
57
|
+
|
|
45
58
|
"""
|
|
46
59
|
|
|
47
60
|
def maybe_print(string):
|
|
48
61
|
if not quiet:
|
|
49
62
|
print(string)
|
|
50
63
|
|
|
51
|
-
if
|
|
52
|
-
raise
|
|
53
|
-
|
|
54
|
-
if
|
|
55
|
-
raise
|
|
56
|
-
|
|
57
|
-
if
|
|
64
|
+
if not isinstance(demand, (pd.Series, list, float, int)):
|
|
65
|
+
raise TypeError("Demand must be one of timeseries, monthly list or single demand.")
|
|
66
|
+
|
|
67
|
+
if not isinstance(supply, pd.Series):
|
|
68
|
+
raise TypeError("Supply must be a single column of a date-indexed dataframe (pd.Series).")
|
|
69
|
+
|
|
70
|
+
if isinstance(demand, pd.Series):
|
|
58
71
|
maybe_print("Comparing provided demand timeseries with supply timeseries.")
|
|
59
72
|
state = "ts"
|
|
60
73
|
|
|
61
|
-
if
|
|
62
|
-
if demand_type not in ["total","daily_constant"]:
|
|
63
|
-
raise
|
|
64
|
-
|
|
65
|
-
if demand_timescale not in ["daily","monthly","yearly"]:
|
|
66
|
-
raise
|
|
67
|
-
|
|
74
|
+
if not isinstance(demand, pd.Series):
|
|
75
|
+
if demand_type not in ["total", "daily_constant"]:
|
|
76
|
+
raise ValueError("demand_type must be one of \"total\" or \"daily_constant\".")
|
|
77
|
+
|
|
78
|
+
if demand_timescale not in ["daily", "monthly", "yearly"]:
|
|
79
|
+
raise ValueError("demand_timescale must be one of \"daily\", \"monthly\" or \"yearly\".")
|
|
80
|
+
|
|
68
81
|
if ignore_leap_years not in [True, False]:
|
|
69
|
-
|
|
82
|
+
raise TypeError(f"ignore_leap_years must be one of {True} or {False}.")
|
|
70
83
|
|
|
71
84
|
lookup_leap = {True: 28, False: 29}
|
|
72
85
|
|
|
73
|
-
if
|
|
74
|
-
if len(demand)<12:
|
|
75
|
-
raise
|
|
76
|
-
|
|
86
|
+
if isinstance(demand, list):
|
|
87
|
+
if len(demand) < 12:
|
|
88
|
+
raise ValueError("Monthly demand list must have a length of 12.")
|
|
89
|
+
|
|
77
90
|
maybe_print("Comparing list of monthly demands with supply timeseries. demand_timescale parameter is unused.")
|
|
78
91
|
|
|
79
92
|
if demand_type == "daily_constant":
|
|
@@ -85,7 +98,7 @@ class Reliability:
|
|
|
85
98
|
maybe_print(f"Leap years will be disaggregated assuming {lookup_leap[ignore_leap_years]} days in February.")
|
|
86
99
|
state = "monthly_total_list"
|
|
87
100
|
|
|
88
|
-
if
|
|
101
|
+
if isinstance(demand, (float, int)):
|
|
89
102
|
if not quiet:
|
|
90
103
|
maybe_print("Comparing provided demand with supply timeseries.")
|
|
91
104
|
if (demand_timescale == "daily") or (demand_type == "daily_constant"):
|
|
@@ -97,86 +110,94 @@ class Reliability:
|
|
|
97
110
|
maybe_print("demand_type and ignore_leap_years parameters are unused.")
|
|
98
111
|
if (demand_type == "daily_constant") and (demand_timescale != "daily"):
|
|
99
112
|
maybe_print("demand_timescale and ignore_leap_years parameters are unused.")
|
|
100
|
-
state="daily_constant"
|
|
113
|
+
state = "daily_constant"
|
|
101
114
|
else:
|
|
102
115
|
maybe_print(f"Assumed that {demand} ML is the total demand over the {demand_timescale} timescale. {demand} ML {demand_timescale} will be disaggregated to ML/d.")
|
|
103
116
|
maybe_print(f"Leap years will be disaggregated assuming {lookup_leap[ignore_leap_years]} days in February.")
|
|
104
117
|
if demand_timescale == "yearly":
|
|
105
|
-
state="yearly_total"
|
|
118
|
+
state = "yearly_total"
|
|
106
119
|
if demand_timescale == "monthly":
|
|
107
|
-
state="monthly_total"
|
|
108
|
-
|
|
109
|
-
self.demand=demand
|
|
110
|
-
self.supply=supply
|
|
111
|
-
self.demand_timescale=demand_timescale
|
|
112
|
-
self.demand_type=demand_type
|
|
113
|
-
self.ignore_leap_years=ignore_leap_years
|
|
114
|
-
self.state=state
|
|
115
|
-
|
|
116
|
-
def ReliabilityTS(self, wy_month):
|
|
117
|
-
"""
|
|
120
|
+
state = "monthly_total"
|
|
121
|
+
|
|
122
|
+
self.demand = demand
|
|
123
|
+
self.supply = supply
|
|
124
|
+
self.demand_timescale = demand_timescale
|
|
125
|
+
self.demand_type = demand_type
|
|
126
|
+
self.ignore_leap_years = ignore_leap_years
|
|
127
|
+
self.state = state
|
|
128
|
+
|
|
129
|
+
def ReliabilityTS(self, wy_month: int) -> pd.Series:
|
|
130
|
+
"""Return demand as a timeseries for input to reliability statistics.
|
|
131
|
+
|
|
118
132
|
Matches date range of supply timeseries input.
|
|
119
133
|
|
|
134
|
+
Parameters
|
|
135
|
+
----------
|
|
136
|
+
wy_month : int
|
|
137
|
+
Water year start month
|
|
138
|
+
|
|
120
139
|
Returns
|
|
121
140
|
-------
|
|
122
|
-
|
|
141
|
+
:class:`pandas.Series`
|
|
123
142
|
Demand timeseries for input to reliability stats
|
|
124
143
|
"""
|
|
125
144
|
# If provided demand is a timeseries, just return timeseries.
|
|
126
|
-
if self.state=="ts":
|
|
127
|
-
common_dates=np.intersect1d(self.demand.index,self.supply.index)
|
|
128
|
-
demand_ts=self.demand[common_dates]
|
|
129
|
-
self.supply=self.supply[common_dates]
|
|
145
|
+
if self.state == "ts":
|
|
146
|
+
common_dates = np.intersect1d(self.demand.index, self.supply.index)
|
|
147
|
+
demand_ts = self.demand[common_dates]
|
|
148
|
+
self.supply = self.supply[common_dates]
|
|
130
149
|
return demand_ts
|
|
131
150
|
|
|
132
151
|
else:
|
|
133
152
|
# Copy date range of supply TS
|
|
134
|
-
demand_ts=self.supply.copy(deep=True)
|
|
153
|
+
demand_ts = self.supply.copy(deep=True).astype(float)
|
|
135
154
|
|
|
136
155
|
# Overwrite demand_ts with constant daily demand.
|
|
137
|
-
if self.state=="daily_constant":
|
|
138
|
-
demand_ts[:]=self.demand
|
|
156
|
+
if self.state == "daily_constant":
|
|
157
|
+
demand_ts[:] = self.demand
|
|
139
158
|
return demand_ts
|
|
140
|
-
|
|
159
|
+
|
|
141
160
|
# Overwrite demand_ts with respective month constant daily demand
|
|
142
|
-
if self.state=="monthly_constant_list":
|
|
143
|
-
month_list=list(utils.get_month(demand_ts.index))
|
|
144
|
-
demand_ts[:]=[self.demand[x-1] for x in month_list]
|
|
161
|
+
if self.state == "monthly_constant_list":
|
|
162
|
+
month_list = list(utils.get_month(demand_ts.index))
|
|
163
|
+
demand_ts[:] = [self.demand[x-1] for x in month_list]
|
|
145
164
|
return demand_ts
|
|
146
165
|
|
|
147
166
|
# Overwrite demand_ts with respective total month demand disaggregated to daily.
|
|
148
|
-
if self.state=="monthly_total_list":
|
|
149
|
-
year_month=utils.get_year_and_month(demand_ts.index)
|
|
150
|
-
if self.ignore_leap_years
|
|
151
|
-
demand_ts[:]=[self.demand[int(x[5:7])-1]/calendar.monthrange(int(x[0:4]),int(x[5:7]))[1]
|
|
167
|
+
if self.state == "monthly_total_list":
|
|
168
|
+
year_month = utils.get_year_and_month(demand_ts.index)
|
|
169
|
+
if not self.ignore_leap_years:
|
|
170
|
+
demand_ts[:] = [self.demand[int(x[5:7])-1]/calendar.monthrange(int(x[0:4]), int(x[5:7]))[1]
|
|
171
|
+
for x in year_month] # If using leap years, divide by 28 or 29 Feb days depending on year
|
|
152
172
|
else:
|
|
153
|
-
demand_ts[:]=[self.demand[int(x[5:7])-1]/calendar.monthrange(2002,int(x[5:7]))[1] for x in year_month]
|
|
173
|
+
demand_ts[:] = [self.demand[int(x[5:7])-1]/calendar.monthrange(2002, int(x[5:7]))[1] for x in year_month] # If not using leap years, only divide by 28 Feb days
|
|
154
174
|
return demand_ts
|
|
155
|
-
|
|
175
|
+
|
|
156
176
|
# Overwrite demand_ts with total month demand disaggregated to daily.
|
|
157
|
-
if self.state=="monthly_total":
|
|
158
|
-
year_month=utils.get_year_and_month(demand_ts.index)
|
|
159
|
-
if self.ignore_leap_years
|
|
160
|
-
demand_ts[:]=[self.demand/calendar.monthrange(int(x[0:4]),int(x[5:7]))[1] for x in year_month]
|
|
177
|
+
if self.state == "monthly_total":
|
|
178
|
+
year_month = utils.get_year_and_month(demand_ts.index)
|
|
179
|
+
if not self.ignore_leap_years:
|
|
180
|
+
demand_ts[:] = [self.demand/calendar.monthrange(int(x[0:4]), int(x[5:7]))[1] for x in year_month] # If using leap years, divide by 28 or 29 Feb days depending on year
|
|
161
181
|
else:
|
|
162
|
-
demand_ts[:]=[self.demand/calendar.monthrange(2002,int(x[5:7]))[1] for x in year_month]
|
|
182
|
+
demand_ts[:] = [self.demand/calendar.monthrange(2002, int(x[5:7]))[1] for x in year_month] # If not using leap years, only divide by 28 Feb days
|
|
163
183
|
return demand_ts
|
|
164
184
|
|
|
165
185
|
# Overwrite demand_ts with total annual demand disaggregated to daily.
|
|
166
|
-
if self.state=="yearly_total":
|
|
167
|
-
if self.ignore_leap_years
|
|
168
|
-
wy = utils.get_wy(demand_ts.index,wy_month,using_end_year=False)
|
|
169
|
-
if wy_month>2:
|
|
170
|
-
|
|
186
|
+
if self.state == "yearly_total":
|
|
187
|
+
if not self.ignore_leap_years:
|
|
188
|
+
wy = utils.get_wy(demand_ts.index, wy_month, using_end_year=False)
|
|
189
|
+
if wy_month > 2:
|
|
190
|
+
# If using leap years, divide by 365 or 366 days depending on year. If wy_month > 2, leap day will occur in the next calendar year
|
|
191
|
+
demand_ts[:] = [self.demand/(365+calendar.isleap(x+1)) for x in wy]
|
|
171
192
|
else:
|
|
172
|
-
|
|
193
|
+
# If using leap years, divide by 365 or 366 days depending on year. If wy_month <= 2, leap day will occur in this calendar year
|
|
194
|
+
demand_ts[:] = [self.demand/(365+calendar.isleap(x)) for x in wy]
|
|
173
195
|
else:
|
|
174
|
-
demand_ts[:]=self.demand/365
|
|
196
|
+
demand_ts[:] = self.demand/365 # If not using leap years, only ever divide by 365
|
|
175
197
|
return demand_ts
|
|
176
198
|
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
"""Returns the monthly reliability statistic for a daily timeseries of demand and supply.
|
|
199
|
+
def MonthlyReliability(self, tol: float = 1, allow_part_months: bool = False, wy_month: int = 7) -> float:
|
|
200
|
+
"""Calculate the monthly reliability statistic for daily timeseries of demand and supply.
|
|
180
201
|
|
|
181
202
|
Parameters
|
|
182
203
|
----------
|
|
@@ -194,37 +215,36 @@ class Reliability:
|
|
|
194
215
|
|
|
195
216
|
"""
|
|
196
217
|
# Pass input to reliability_ts first and enforce common date range.
|
|
197
|
-
demand_ts=self.ReliabilityTS(wy_month)
|
|
218
|
+
demand_ts = self.ReliabilityTS(wy_month)
|
|
198
219
|
|
|
199
220
|
# Collate timeseries data to monthly
|
|
200
221
|
if allow_part_months:
|
|
201
|
-
dem_month=demand_ts.groupby(utils.get_year_and_month(demand_ts.index)).sum()
|
|
202
|
-
sup_month=self.supply.groupby(utils.get_year_and_month(self.supply.index)).sum()
|
|
222
|
+
dem_month = demand_ts.groupby(utils.get_year_and_month(demand_ts.index)).sum()
|
|
223
|
+
sup_month = self.supply.groupby(utils.get_year_and_month(self.supply.index)).sum()
|
|
203
224
|
else:
|
|
204
|
-
if self.supply.index[0][8:10]=="01":
|
|
205
|
-
#First date is the start of a month; use this as start date.
|
|
206
|
-
start_date=self.supply.index[0]
|
|
225
|
+
if self.supply.index[0][8:10] == "01": # 0123-56-89
|
|
226
|
+
# First date is the start of a month; use this as start date.
|
|
227
|
+
start_date = self.supply.index[0]
|
|
207
228
|
else:
|
|
208
|
-
#Start on the first date of the next month
|
|
229
|
+
# Start on the first date of the next month
|
|
209
230
|
start_date = utils.get_next_month_start(self.supply.index[0])
|
|
210
231
|
if self.supply.index[-1] == utils.get_this_month_end(self.supply.index[-1]):
|
|
211
|
-
end_date=self.supply.index[-1]
|
|
232
|
+
end_date = self.supply.index[-1]
|
|
212
233
|
else:
|
|
213
|
-
end_date=utils.get_prev_month_end(self.supply.index[-1])
|
|
234
|
+
end_date = utils.get_prev_month_end(self.supply.index[-1])
|
|
214
235
|
demand_trim = demand_ts[start_date:end_date]
|
|
215
236
|
supply_trim = self.supply[start_date:end_date]
|
|
216
237
|
year_month = utils.get_year_and_month(demand_trim.index)
|
|
217
|
-
dem_month=demand_trim.groupby(year_month).sum()
|
|
218
|
-
sup_month=supply_trim.groupby(year_month).sum()
|
|
238
|
+
dem_month = demand_trim.groupby(year_month).sum()
|
|
239
|
+
sup_month = supply_trim.groupby(year_month).sum()
|
|
219
240
|
|
|
220
241
|
# Check whether demand is met within given tolerance (to 6 decimal places) and express as a percentage
|
|
221
|
-
rel=np.where((sup_month-(dem_month*tol)
|
|
242
|
+
rel = np.where((sup_month-(dem_month*tol) < -0.000001), 0, 1).sum()/sup_month.count()
|
|
222
243
|
|
|
223
244
|
return rel
|
|
224
245
|
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
"""Returns the annual reliability statistic for a daily timeseries of demand and supply.
|
|
246
|
+
def AnnualReliability(self, tol: float = 1, wy_month: int = 7, allow_part_years: bool = False) -> float:
|
|
247
|
+
"""Calculate the annual reliability statistic for daily timeseries of demand and supply.
|
|
228
248
|
|
|
229
249
|
Parameters
|
|
230
250
|
----------
|
|
@@ -240,10 +260,10 @@ class Reliability:
|
|
|
240
260
|
float
|
|
241
261
|
Annual reliability (%)
|
|
242
262
|
"""
|
|
243
|
-
|
|
263
|
+
|
|
244
264
|
# Pass input to reliability_ts first and enforce common date range.
|
|
245
|
-
demand_ts=self.ReliabilityTS(wy_month)
|
|
246
|
-
|
|
265
|
+
demand_ts = self.ReliabilityTS(wy_month)
|
|
266
|
+
|
|
247
267
|
# Collate timeseries data to annual
|
|
248
268
|
if not allow_part_years:
|
|
249
269
|
demand_ts = utils.crop_to_wy(demand_ts, wy_month)
|
|
@@ -252,11 +272,11 @@ class Reliability:
|
|
|
252
272
|
supply = self.supply
|
|
253
273
|
if (len(demand_ts) == 0):
|
|
254
274
|
return np.nan
|
|
255
|
-
dem_annual=demand_ts.groupby(utils.get_wy(demand_ts.index, wy_month)).sum()
|
|
256
|
-
sup_annual=supply.groupby(utils.get_wy(supply.index, wy_month)).sum()
|
|
257
|
-
no_years=sup_annual.count()
|
|
275
|
+
dem_annual = demand_ts.groupby(utils.get_wy(demand_ts.index, wy_month)).sum()
|
|
276
|
+
sup_annual = supply.groupby(utils.get_wy(supply.index, wy_month)).sum()
|
|
277
|
+
no_years = sup_annual.count()
|
|
258
278
|
|
|
259
279
|
# Check whether demand is met within given tolerance (to 6 decimal places) and express as a percentage
|
|
260
|
-
rel=np.where((sup_annual-(dem_annual*tol)
|
|
280
|
+
rel = np.where((sup_annual-(dem_annual*tol) < -0.000001), 0, 1).sum()/no_years
|
|
261
281
|
|
|
262
282
|
return rel
|