detquantlib 2.0.4__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.
@@ -0,0 +1,174 @@
1
+ # Python built-in packages
2
+ import shutil
3
+ from pathlib import Path
4
+
5
+ # Third-party packages
6
+ import pandas as pd
7
+
8
+ # Internal modules
9
+ from detquantlib.data.sftp.sftp import Sftp
10
+
11
+
12
+ class Entsoe:
13
+ """
14
+ A class that handles ENTSOE data, including importing and processing data from their SFTP
15
+ server.
16
+ """
17
+
18
+ def __init__(self, sftp: Sftp = None):
19
+ self.sftp = sftp
20
+
21
+ @staticmethod
22
+ def get_sftp_folder_dir_day_ahead_spot_prices():
23
+ # Directory of SFTP folder containing day-ahead spot prices.
24
+ return "/TP_export/EnergyPrices_12.1.D_r3/"
25
+
26
+ @staticmethod
27
+ def get_sftp_filename_day_ahead_spot_prices(year: int, month: int) -> str:
28
+ """
29
+ Name of SFTP files containing day-ahead spot prices.
30
+
31
+ Args:
32
+ year: Delivery year
33
+ month: Delivery month
34
+
35
+ Returns:
36
+ Filename of day-ahead spot prices
37
+ """
38
+ year_str = str(year)
39
+ month_str = str(month) if month >= 10 else f"0{month}"
40
+ return f"{year_str}_{month_str}_EnergyPrices_12.1.D_r3.csv"
41
+
42
+ def import_day_ahead_spot_prices_file_from_sftp(
43
+ self, year: int, month: int, local_folder_dir: str
44
+ ):
45
+ """
46
+ Downloads the file containing the day-ahead spot prices of a given delivery month from
47
+ the ENTSOE SFTP server to a local directory.
48
+
49
+ Args:
50
+ year: Delivery year
51
+ month: Delivery month
52
+ local_folder_dir: Local directory where file will be copied
53
+ """
54
+ filename = Entsoe.get_sftp_filename_day_ahead_spot_prices(year=year, month=month)
55
+ remote_folder_dir = Entsoe.get_sftp_folder_dir_day_ahead_spot_prices()
56
+ remote_dir = f"/{remote_folder_dir}/{filename}"
57
+ local_dir = f"{local_folder_dir}/{filename}"
58
+
59
+ self.sftp.open_session()
60
+ self.sftp.get_file(remote_dir, local_dir)
61
+ self.sftp.close_session()
62
+
63
+ @staticmethod
64
+ def read_day_ahead_spot_prices_from_file(
65
+ country: str,
66
+ timezone: str,
67
+ year: int,
68
+ month: int,
69
+ local_folder_dir: str,
70
+ ) -> pd.DataFrame:
71
+ """
72
+ Reads and processes the content of the file containing day-ahead spot prices for a given
73
+ delivery month.
74
+
75
+ Args:
76
+ country: Country/area of requested day-ahead spot prices
77
+ timezone: Timezone of requested day-ahead spot prices
78
+ year: Delivery year
79
+ month: Delivery month
80
+ local_folder_dir: Local directory of folder containing the price data file
81
+
82
+ Returns:
83
+ Dataframe containing day-ahead spot prices
84
+ """
85
+ # Read data from csv
86
+ filename = Entsoe.get_sftp_filename_day_ahead_spot_prices(year=year, month=month)
87
+ file_dir = f"{local_folder_dir}/{filename}"
88
+ df = pd.read_csv(file_dir, sep="\t")
89
+
90
+ # Filter columns
91
+ columns = ["DateTime(UTC)", "ResolutionCode", "MapCode", "Price[Currency/MWh]", "Currency"]
92
+ df = df[columns]
93
+
94
+ # Filter rows for relevant country
95
+ idx = df["MapCode"] == country
96
+ df = df.loc[idx, :]
97
+ df.reset_index(drop=True, inplace=True)
98
+
99
+ # Convert dates from strings to datetime
100
+ df["DateTime(UTC)"] = pd.to_datetime(
101
+ df["DateTime(UTC)"], format="%Y-%m-%d %H:%M:%S", utc=True
102
+ )
103
+
104
+ # Convert from UTC timezone to local timezone
105
+ df["DateTime(UTC)"] = df["DateTime(UTC)"].dt.tz_convert(timezone)
106
+ df["DateTime(UTC)"] = df["DateTime(UTC)"].dt.tz_localize(None)
107
+
108
+ return df
109
+
110
+ def get_day_ahead_spot_prices_from_sftp(
111
+ self, country: str, timezone: str, year: int, month: int, keep_local_file: bool = False
112
+ ) -> pd.DataFrame:
113
+ """
114
+ Main method to fetch and process day-ahead spot price data from the ENSTOE SFTP server.
115
+
116
+ The function performs the following steps:
117
+ 1) Download the file containing day-ahead spot prices from the SFTP to a temporary local
118
+ directory.
119
+ 2) Read, process and store the price data into a dataframe.
120
+ 3) Delete the temporary local directory containing the downloaded file, unless the user
121
+ indicates that the file should be kept.
122
+
123
+ Args:
124
+ country: Country/area of requested day-ahead spot prices
125
+ timezone: Timezone of requested day-ahead spot prices
126
+ year: Delivery year
127
+ month: Delivery month
128
+ keep_local_file: Indicates whether the downloaded data file should be kept or not
129
+
130
+ Returns:
131
+ Dataframe containing day-ahead spot prices
132
+ """
133
+ # Define local directory where price data file will be stored
134
+ input_folder_dir = Path.cwd().joinpath("Inputs")
135
+ entsoe_data_folder_dir = input_folder_dir.joinpath("EntsoeData")
136
+
137
+ # Check if folders already exist
138
+ input_folder_exists = input_folder_dir.exists()
139
+ entsoe_data_folder_exists = entsoe_data_folder_dir.exists()
140
+
141
+ # Create local directory if it doesn't exist
142
+ entsoe_data_folder_dir.mkdir(parents=True, exist_ok=True)
143
+ entsoe_data_folder_dir = str(entsoe_data_folder_dir)
144
+
145
+ # Import price data file from ENTSOE SFTP to local directory
146
+ self.import_day_ahead_spot_prices_file_from_sftp(
147
+ year=year, month=month, local_folder_dir=entsoe_data_folder_dir
148
+ )
149
+
150
+ # Read price data file
151
+ df = self.read_day_ahead_spot_prices_from_file(
152
+ country=country,
153
+ timezone=timezone,
154
+ year=year,
155
+ month=month,
156
+ local_folder_dir=entsoe_data_folder_dir,
157
+ )
158
+
159
+ # Delete price data file
160
+ if not keep_local_file:
161
+ if input_folder_exists and entsoe_data_folder_exists:
162
+ # Local directory already existed. Only delete the price data file.
163
+ filename = Entsoe.get_sftp_filename_day_ahead_spot_prices(year=year, month=month)
164
+ file_dir = Path(entsoe_data_folder_dir).joinpath(filename)
165
+ file_dir.unlink()
166
+ elif input_folder_exists:
167
+ # Only the Inputs folder already existed. Delete the EntsoeData folder and its
168
+ # content.
169
+ shutil.rmtree(entsoe_data_folder_dir)
170
+ else:
171
+ # The entire temporary local directory did not exist. Delete the entire directory.
172
+ shutil.rmtree(input_folder_dir)
173
+
174
+ return df
@@ -0,0 +1,89 @@
1
+ import paramiko
2
+
3
+
4
+ class Sftp:
5
+ """A class that handles imports and exports of data from and to an SFTP server."""
6
+
7
+ def __init__(
8
+ self,
9
+ hostname: str,
10
+ port: int,
11
+ username: str,
12
+ password: str = None,
13
+ private_key_dir: str = None,
14
+ authentication_type: str = "password",
15
+ sftp_session: paramiko.SFTPClient = None,
16
+ transport: paramiko.Transport = None,
17
+ ):
18
+ """
19
+ Constructor method.
20
+
21
+ Args:
22
+ hostname: SFTP server hostname
23
+ port: SFTP server port
24
+ username: Username to connect to the SFTP server
25
+ password: Password to connect to the SFTP server (only needed if
26
+ authentication_type="password")
27
+ private_key_dir: Directory of file containing the private key to connect to the SFTP
28
+ server (only needed if authentication_type="private_key")
29
+ authentication_type: Can take value "password" or "private_key"
30
+ sftp_session: SFTP session object
31
+ transport: Transport object
32
+
33
+ Raises:
34
+ ValueError: Raises an error in case of invalid authentication type
35
+ """
36
+ # Input validation
37
+ valid_authentication_types = ["password", "private_key"]
38
+ if authentication_type not in valid_authentication_types:
39
+ raise ValueError(
40
+ f"Invalid input 'authentication_type' value '{authentication_type}'. "
41
+ f"Supported values: {valid_authentication_types}."
42
+ )
43
+
44
+ self.hostname = hostname
45
+ self.port = port
46
+ self.username = username
47
+ self.password = password
48
+ self.private_key_dir = private_key_dir
49
+ self.authentication_type = authentication_type
50
+ self.sftp_session = sftp_session
51
+ self.transport = transport
52
+
53
+ def open_session(self):
54
+ """Opens an SFTP session."""
55
+ # Establish an SSH client and connect to the server
56
+ try:
57
+ self.transport = paramiko.Transport((self.hostname, self.port))
58
+ self.transport.connect(username=self.username, password=self.password)
59
+
60
+ # Open an SFTP session
61
+ self.sftp_session = paramiko.SFTPClient.from_transport(self.transport)
62
+
63
+ except Exception as e:
64
+ print(f"Error: {e}")
65
+
66
+ def close_session(self):
67
+ """Closes an SFTP session and transport."""
68
+ self.sftp_session.close()
69
+ self.transport.close()
70
+
71
+ def get_file(self, remote_dir: str, local_dir: str):
72
+ """
73
+ Imports a file from an SFTP server to a local directory.
74
+
75
+ Args:
76
+ remote_dir: SFTP server file directory
77
+ local_dir: Local file directory
78
+ """
79
+ self.sftp_session.get(remote_dir, local_dir)
80
+
81
+ def put_file(self, local_dir: str, remote_dir: str):
82
+ """
83
+ Exports a file from a local directory to an SFTP server.
84
+
85
+ Args:
86
+ local_dir: Local file directory
87
+ remote_dir: SFTP server file directory
88
+ """
89
+ self.sftp_session.put(local_dir, remote_dir)
@@ -0,0 +1,154 @@
1
+ # Python built-in packages
2
+ from datetime import datetime
3
+ from typing import Literal
4
+
5
+ # Third-party packages
6
+ import pandas as pd
7
+ from dateutil.relativedelta import *
8
+
9
+
10
+ def count_delivery_periods(
11
+ start_date: datetime,
12
+ end_date: datetime,
13
+ delivery_frequency: str,
14
+ full_periods_only: bool = False,
15
+ timezone: str = None,
16
+ ) -> int:
17
+ """
18
+ Counts the number of delivery periods within a time interval.
19
+
20
+ Note: When the end date is exactly equal to the start of the next delivery period, that next
21
+ period is not included (because it has not yet started). For example, if the end date is
22
+ 15-Jan-2025 00:00:00 and the delivery frequency is daily, then the period
23
+ 15-Jan-2025 00:00:00 to 16-Jan-2025 00:00:00 is not included.
24
+
25
+ Args:
26
+ start_date: Delivery start date
27
+ end_date: Delivery end date
28
+ delivery_frequency: Delivery frequency, expressed as Pandas offset aliases
29
+ full_periods_only: Indicates whether to count only fully elapsed periods.
30
+ For example, suppose that start date is 15-Jan-2025 00:15:00, end date is
31
+ 3-Feb-2025 03:45:00, and delivery frequency is hourly. Then:
32
+ - If full_periods_only=True, the number of periods is 2 (01:00:00-02:00:00 and
33
+ 02:00:00-03:00:00), because the hours 00:00:00-01:00:00 and 00:03:00-04:00:00
34
+ are not full.
35
+ - If full_periods_only=False, the number of periods is 4, because incomplete hours
36
+ 00:00:00-01:00:00 and 00:03:00-04:00:00 are also included. Note:
37
+ - If end date is 3-Feb-2025 04:00:00, hour 04:00:00-05:00:00 is not included.
38
+ - If end date is 3-Feb-2025 04:00:01, hour 04:00:00-05:00:00 is included.
39
+ timezone: Timezone (needed to account for DST switches)
40
+
41
+ Returns:
42
+ Number of delivery periods in the interval
43
+ """
44
+ # Convert to pandas timestamp
45
+ start_date = pd.Timestamp(start_date)
46
+ end_date = pd.Timestamp(end_date)
47
+
48
+ if full_periods_only:
49
+ start_date = start_date.ceil(delivery_frequency)
50
+ end_date = end_date.floor(delivery_frequency)
51
+ else:
52
+ start_date = start_date.floor(delivery_frequency)
53
+
54
+ periods = pd.date_range(
55
+ start=start_date,
56
+ end=end_date,
57
+ freq=delivery_frequency,
58
+ inclusive="left",
59
+ tz=timezone,
60
+ )
61
+ nr_periods = len(periods)
62
+ return nr_periods
63
+
64
+
65
+ def calc_months_diff(
66
+ start_date: datetime,
67
+ end_date: datetime,
68
+ diff_method: Literal["month", "time", "full_months_only"] = "month",
69
+ ) -> int:
70
+ """
71
+ Calculate the month difference between 2 dates.
72
+
73
+ Args:
74
+ start_date: Start date
75
+ end_date: End date
76
+ diff_method: Method to count the month difference.
77
+ - diff_method="month": Counts the month difference based on the months of the start
78
+ date and end date, irrespective of the day. For example, suppose that start date
79
+ is 15-Jan-2025 and end date is 3-Mar-2025. Then, the month difference between
80
+ Jan-2025 and Mar-2025 is 2.
81
+ - diff_method="time": Counts the month difference, accounting the day and time of the
82
+ start and end dates. For example:
83
+ - Suppose that start date is 15-Jan-2025 and end date is 3-Mar-2025. Then:
84
+ - start date + 1 months = 15-Feb-2025
85
+ - start date + 2 months = 15-Mar-2025
86
+ - The month difference is 1, because
87
+ [start date + 1 months] <= end date < [start date + 2 months].
88
+ - Suppose that start date is 15-Jan-2025 and end date is 16-Mar-2025. Then:
89
+ - start date + 2 months = 15-Mar-2025
90
+ - start date + 3 months = 15-Apr-2025
91
+ - The month difference is 2, because
92
+ [start date + 2 months] <= end date < [start date + 3 months].
93
+ - diff_method="full_months_only": Counts the number of fully elapsed months between
94
+ the start date and the end date. For example, suppose that start date
95
+ is 15-Jan-2025 and end date is 3-Mar-2025. Then, the month difference is 1,
96
+ because only Feb-2025 was elapsed from start to finish.
97
+
98
+ Returns:
99
+ Month difference between 2 dates
100
+
101
+ Raises:
102
+ ValueError: Raises an error when end_date < start_date
103
+ ValueError: Raises an error when the input argument 'diff_method' is invalid
104
+ """
105
+ # Input validation
106
+ if end_date < start_date:
107
+ raise ValueError("End date cannot be smaller than start date.")
108
+
109
+ # Calculate month difference
110
+ start_mc = datetime_to_month_code(start_date)
111
+ end_mc = datetime_to_month_code(end_date)
112
+
113
+ if diff_method == "month":
114
+ diff = end_mc - start_mc
115
+ elif diff_method == "time":
116
+ # Check month difference, accounting for day and time. For example, suppose that start
117
+ # date is 10-Jan-2025 14:00:00. Then:
118
+ # - If end date is 10-Mar-2025 14:00:00, month difference is 2.
119
+ # - If end date is 10-Mar-2025 13:55:00, month difference is 1.
120
+ start_day = start_date + relativedelta(year=2000, month=1)
121
+ end_day = end_date + relativedelta(year=2000, month=1)
122
+ diff = end_mc - start_mc
123
+ if end_day < start_day:
124
+ diff -= 1
125
+ elif diff_method == "full_months_only":
126
+ # Calculate number of fully elapsed months
127
+ # Note: We need to use the max operator, otherwise diff = -1 when start month = end month.
128
+ if start_date > datetime(start_date.year, start_date.month, 1):
129
+ start_mc += 1
130
+ diff = max(end_mc - start_mc, 0)
131
+ else:
132
+ raise ValueError("Invalid value of input argument 'diff_method'.")
133
+
134
+ return diff
135
+
136
+
137
+ def datetime_to_month_code(d: datetime) -> int:
138
+ """
139
+ Converts a datetime to its corresponding month code. A month code is calculated as the number
140
+ of months since 1 January 1900 (including the month of the input datetime).
141
+
142
+ Args:
143
+ d: Datetime
144
+
145
+ Returns:
146
+ Corresponding month code
147
+
148
+ Raises:
149
+ ValueError: Raises an error if the input datetime is before 1 January 1900
150
+ """
151
+ if d < datetime(1900, 1, 1):
152
+ raise ValueError("Input date cannot be before 1 January 1900.")
153
+ month_code = (d.year - 1900) * 12 + d.month
154
+ return month_code
@@ -0,0 +1,82 @@
1
+ # Python built-in packages
2
+ from pathlib import Path
3
+
4
+ # Third-party packages
5
+ import plotly.graph_objects as go
6
+ import plotly.io as pio
7
+
8
+
9
+ def get_default_plotly_folder_dir():
10
+ # Default path to output folder containing plotly figures.
11
+ return Path.cwd().joinpath("Outputs", "PlotlyFigures")
12
+
13
+
14
+ def set_standard_layout(fig: go.Figure) -> go.Figure:
15
+ """
16
+ Short helper function to improve and standardize the layout of plotly figures.
17
+
18
+ Args:
19
+ fig: Plotly figure
20
+
21
+ Returns:
22
+ Plotly figure with updated layout options
23
+ """
24
+ fig.update_layout(
25
+ template="plotly_white",
26
+ title_x=0.5,
27
+ xaxis=dict(showline=True, linecolor="black", showgrid=False, ticks="outside"),
28
+ yaxis=dict(showline=True, linecolor="black", showgrid=True, ticks="outside"),
29
+ hovermode="x",
30
+ )
31
+ return fig
32
+
33
+
34
+ def save_plotly_fig_to_json(
35
+ fig: go.Figure,
36
+ filename: str,
37
+ folder_dir: Path = get_default_plotly_folder_dir(),
38
+ standard_layout: bool = True,
39
+ ):
40
+ """
41
+ Saves a plotly figure object to a json file.
42
+
43
+ Args:
44
+ fig: Plotly figure
45
+ filename: Name of json file in which plotly figure will be saved
46
+ folder_dir: Folder directory where the json file will be saved
47
+ standard_layout: If true, enforces a standard plotly layout
48
+ """
49
+ # Create plotly folder if it doesn't exist yet
50
+ folder_dir.mkdir(parents=True, exist_ok=True)
51
+
52
+ # Define json file directory
53
+ file_dir = folder_dir.joinpath(f"{filename}.json")
54
+
55
+ # Adjust figure layout
56
+ if standard_layout:
57
+ fig = set_standard_layout(fig)
58
+
59
+ # Save figure to json
60
+ fig_json = fig.to_json()
61
+ with open(file_dir, "w") as f:
62
+ f.write(fig_json)
63
+
64
+
65
+ def show_plotly_fig_json(filename: str, folder_dir: Path = get_default_plotly_folder_dir()):
66
+ """
67
+ Short helper function to display a plotly figure stored in a json file.
68
+
69
+ Args:
70
+ filename: Name of json file containing the plotly figure
71
+ folder_dir: Folder directory containing the json file
72
+ """
73
+ # Get json file directory
74
+ file_dir = folder_dir.joinpath(f"{filename}.json")
75
+
76
+ # Load figure from json
77
+ with open(file_dir, "r") as f:
78
+ fig_json = f.read()
79
+ fig = pio.from_json(fig_json)
80
+
81
+ # Display figure
82
+ fig.show()
@@ -0,0 +1,143 @@
1
+ import numpy as np
2
+ import pandas as pd
3
+ import plotly.graph_objects as go
4
+
5
+ from detquantlib.figures.plotly_figures import set_standard_layout
6
+
7
+
8
+ def filter_outliers(
9
+ df: pd.DataFrame,
10
+ column: str,
11
+ stats_radius: int,
12
+ std_dev_excl_factor: int = 3,
13
+ max_iter: int = 10,
14
+ plot_series_at_every_iter: bool = False,
15
+ ) -> pd.DataFrame:
16
+ """
17
+ Filters out outliers from a given time series.
18
+
19
+ Args:
20
+ df: Times series dataframe
21
+ column: Name of the column containing outliers to be filtered out
22
+ stats_radius: For an observation at a given time t, the mean and the standard deviation
23
+ will be computed over the interval [t - stats_radius, t + stats_radius]
24
+ std_dev_excl_factor: An observation at a given time t will be flagged as an outlier if it
25
+ is outside the interval
26
+ [mean(t) - std(t) * std_dev_excl_factor, mean(t) + std(t) * std_dev_excl_factor]
27
+ max_iter: Outliers affect the mean and standard deviation of neighbouring observations,
28
+ such that smaller neighbouring outliers may go undetected if we perform the filtering
29
+ process only once. Hence, the operation needs to be repeated multiple times. Parameter
30
+ 'max_iter' determines the maximum number of iterations of the filtering processes.
31
+ plot_series_at_every_iter: If plot_series=True, the time series will be plotted at
32
+ every iteration. This parameter can be used to assess the effectiveness of the
33
+ other input parameters in the outliers filtering process.
34
+
35
+ Returns:
36
+ df: Time series dataframe with outliers removed
37
+ """
38
+ # Initialize number of outliers found in time series (filtering will stop when this reaches 0)
39
+ nr_outliers = 1
40
+
41
+ # Iteratively remove outliers
42
+ for count in range(max_iter):
43
+ if nr_outliers > 0:
44
+ # Count number of rows in df
45
+ nr_rows = df.shape[0]
46
+
47
+ # Calculate rolling mean and standard deviation per observation
48
+ for i, loc in enumerate(df.index):
49
+ # Determine time interval
50
+ min_i = max(i - stats_radius, 0)
51
+ max_i = min(i + stats_radius, nr_rows)
52
+
53
+ # Calculate mean and standard deviation around current observation
54
+ idx = df.index[min_i:max_i]
55
+ df.loc[loc, "mean"] = np.mean(df.loc[idx, column])
56
+ df.loc[loc, "std_dev"] = np.std(df.loc[idx, column], ddof=1)
57
+
58
+ # Identify lower and upper bounds
59
+ lower_bound = df["mean"] - df["std_dev"] * std_dev_excl_factor
60
+ upper_bound = df["mean"] + df["std_dev"] * std_dev_excl_factor
61
+
62
+ # Plot time series
63
+ if plot_series_at_every_iter:
64
+ create_outliers_iteration_plot(
65
+ x_values=df.index,
66
+ data=df[column],
67
+ lower_bound=lower_bound,
68
+ upper_bound=upper_bound,
69
+ y_label=column,
70
+ title=f"Outliers Filtering (Iteration {count+1}/{max_iter})",
71
+ )
72
+
73
+ # Exclude outliers, i.e. observations outside std dev boundaries
74
+ idx_keep = (df[column] >= lower_bound) & (df[column] <= upper_bound)
75
+ df = df.loc[idx_keep, :]
76
+
77
+ # Count number of outliers found in time series
78
+ nr_outliers = np.sum(np.invert(idx_keep))
79
+
80
+ if nr_outliers == 0:
81
+ print(f"No (more) outliers detected (iteration {count+1} out of max {max_iter}).")
82
+ else:
83
+ print(
84
+ f"Outliers filtering excluded {nr_outliers}/{idx_keep.shape[0]}"
85
+ f" observations (iteration {count+1} out of max {max_iter})."
86
+ )
87
+
88
+ # Plot time series
89
+ if plot_series_at_every_iter:
90
+ create_outliers_iteration_plot(
91
+ x_values=df.index,
92
+ data=df[column],
93
+ lower_bound=lower_bound,
94
+ upper_bound=upper_bound,
95
+ y_label=column,
96
+ title="Outliers Filtering (Final Data)",
97
+ )
98
+
99
+ # Dataframe post-processing
100
+ df.reset_index(drop=True, inplace=True)
101
+ df.drop(["mean", "std_dev"], axis=1, inplace=True)
102
+
103
+ return df
104
+
105
+
106
+ def create_outliers_iteration_plot(
107
+ x_values: pd.Series,
108
+ data: pd.Series,
109
+ lower_bound: pd.Series,
110
+ upper_bound: pd.Series,
111
+ y_label: str,
112
+ title: str,
113
+ ):
114
+ """
115
+ Short utility function used in the 'filter_outliers()' function to plot outliers at every
116
+ iteration.
117
+
118
+ Args:
119
+ x_values: X-axis values
120
+ data: Time series data
121
+ lower_bound: Time series lower bound to identify outliers
122
+ upper_bound: Time series upper bound to identify outliers
123
+ y_label: Y-axis label
124
+ title: Plot title
125
+ """
126
+ # Define plot line colors
127
+ data_color = "blue"
128
+ data_line = dict(color=data_color)
129
+ bounds_color = "#ED595C"
130
+ bounds_line = dict(color=bounds_color)
131
+
132
+ # Create plot
133
+ fig = go.Figure()
134
+ fig.add_trace(
135
+ go.Scatter(x=x_values, y=upper_bound, mode="lines", name="Upper bound", line=bounds_line)
136
+ )
137
+ fig.add_trace(go.Scatter(x=x_values, y=data, mode="lines", name="Data", line=data_line))
138
+ fig.add_trace(
139
+ go.Scatter(x=x_values, y=lower_bound, mode="lines", name="Lower bound", line=bounds_line)
140
+ )
141
+ fig.update_layout(yaxis_title=y_label, title=title)
142
+ fig = set_standard_layout(fig)
143
+ fig.show()