The required input format is a dataframe with at least the following columns: * unique_id with a unique identifier for each time serie * ds with the datestamp and a column * y with the values of the serie.
+
Every other column is considered a static feature unless stated otherwise in TimeSeries.fit
+
+
series = generate_daily_series(20, n_static_features=2)
+series
+
+
+
+
+
+
+
+
+
unique_id
+
ds
+
y
+
static_0
+
static_1
+
+
+
+
+
0
+
id_00
+
2000-01-01
+
7.404529
+
27
+
53
+
+
+
1
+
id_00
+
2000-01-02
+
35.952624
+
27
+
53
+
+
+
2
+
id_00
+
2000-01-03
+
68.958353
+
27
+
53
+
+
+
3
+
id_00
+
2000-01-04
+
84.994505
+
27
+
53
+
+
+
4
+
id_00
+
2000-01-05
+
113.219810
+
27
+
53
+
+
+
...
+
...
+
...
+
...
+
...
+
...
+
+
+
4869
+
id_19
+
2000-03-25
+
400.606807
+
97
+
45
+
+
+
4870
+
id_19
+
2000-03-26
+
538.794824
+
97
+
45
+
+
+
4871
+
id_19
+
2000-03-27
+
620.202104
+
97
+
45
+
+
+
4872
+
id_19
+
2000-03-28
+
20.625426
+
97
+
45
+
+
+
4873
+
id_19
+
2000-03-29
+
141.513169
+
97
+
45
+
+
+
+
+
4874 rows × 5 columns
+
+
+
+
For simplicity we’ll just take one time serie here.
Utility class for storing and transforming time series data.
+
The TimeSeries class takes care of defining the transformations to be performed (lags, lag_transforms and date_features). The transformations can be computed using multithreading if num_threads > 1.
The transformations are stored as a dictionary where the key is the name of the transformation (name of the column in the dataframe with the computed features), which is built using build_transform_name and the value is a tuple where the first element is the lag it is applied to, then the function and then the function arguments.
Note that for lags we define the transformation as the identity function applied to its corresponding lag. This is because _transform_series takes the lag as an argument and shifts the array before computing the transformation.
Add the features to data and save the required information for the predictions step.
+
If not all features are static, specify which ones are in static_features. If you don’t want to drop rows with null values after the transformations set dropna=False If keep_last_n is not None then that number of observations is kept across all series for updates.
The series values are stored as a GroupedArray in an attribute ga. If the data type of the series values is an int then it is converted to np.float32, this is because lags generate np.nans so we need a float data type for them.
You can also specify keep_last_n in TimeSeries.fit_transform, which means that after computing the features for training we want to keep only the last n samples of each time serie for computing the updates. This saves both memory and time, since the updates are performed by running the transformation functions on all time series again and keeping only the last value (the update).
+
If you have very long time series and your updates only require a small sample it’s recommended that you set keep_last_n to the minimum number of samples required to compute the updates, which in this case is 15 since we have a rolling mean of size 14 over the lag 2 and in the first update the lag 2 becomes the lag 1. This is because in the first update the lag 1 is the last value of the series (or the lag 0), the lag 2 is the lag 1 and so on.
+
+
keep_last_n =15
+
+ts = TimeSeries(**flow_config)
+df = ts.fit_transform(series, id_col='unique_id', time_col='ds', target_col='y', keep_last_n=keep_last_n)
+ts._uids = ts.uids.tolist()
+ts._idxs = np.arange(len(ts.ga))
+ts._predict_setup()
+
+expected_lags = ['lag7', 'lag14']
+expected_transforms = ['rolling_mean_lag2_window_size7',
+'rolling_mean_lag2_window_size14']
+expected_date_features = ['dayofweek', 'month', 'year']
+
+test_eq(ts.features, expected_lags + expected_transforms + expected_date_features)
+test_eq(ts.static_features_.columns.tolist() + ts.features, df.columns.drop(['ds', 'y']).tolist())
+# we dropped 2 rows because of the lag 2 and 13 more to have the window of size 14
+test_eq(df.shape[0], series.shape[0] - (2+13) * ts.ga.ngroups)
+test_eq(ts.ga.data.size, ts.ga.ngroups * keep_last_n)
+
+
TimeSeries.fit_transform requires that the y column doesn’t have any null values. This is because the transformations could propagate them forward, so if you have null values in the y column you’ll get an error.
Compute the predictions for the next horizon steps.
+
+
+
+
+
+
+
+
+
+
+
Type
+
Default
+
Details
+
+
+
+
+
h
+
int
+
+
Forecast horizon.
+
+
+
before_predict_callback
+
typing.Optional[typing.Callable]
+
None
+
Function to call on the features before computing the predictions. This function will take the input dataframe that will be passed to the model for predicting and should return a dataframe with the same structure. The series identifier is on the index.
+
+
+
after_predict_callback
+
typing.Optional[typing.Callable]
+
None
+
Function to call on the predictions before updating the targets. This function will take a pandas Series with the predictions and should return another one with the same structure. The series identifier is on the index.
+
+
+
new_df
+
typing.Optional[~AnyDataFrame]
+
None
+
Series data of new observations for which forecasts are to be generated. This dataframe should have the same structure as the one used to fit the model, including any features and time series data. If new_df is not None, the method will generate forecasts for the new observations.
+
+
+
Returns
+
AnyDataFrame
+
+
Predictions for each serie and timestep, with one column per model.
Perform time series cross validation. Creates n_windows splits where each window has h test periods, trains the models, computes the predictions and merges the actuals.
+
+
+
+
+
+
+
+
+
+
+
Type
+
Default
+
Details
+
+
+
+
+
df
+
AnyDataFrame
+
+
Series data in long format.
+
+
+
n_windows
+
int
+
+
Number of windows to evaluate.
+
+
+
h
+
int
+
+
Number of test periods in each window.
+
+
+
id_col
+
str
+
unique_id
+
Column that identifies each serie.
+
+
+
time_col
+
str
+
ds
+
Column that identifies each timestep, its values can be timestamps or integers.
+
+
+
target_col
+
str
+
y
+
Column that contains the target.
+
+
+
step_size
+
typing.Optional[int]
+
None
+
Step size between each cross validation window. If None it will be equal to h.
+
+
+
static_features
+
typing.Optional[typing.List[str]]
+
None
+
Names of the features that are static and will be repeated when forecasting.
+
+
+
dropna
+
bool
+
True
+
Drop rows with missing values produced by the transformations.
+
+
+
keep_last_n
+
typing.Optional[int]
+
None
+
Keep only these many records from each serie for the forecasting step. Can save time and memory if your features allow it.
+
+
+
refit
+
bool
+
True
+
Retrain model for each cross validation window. If False, the models are trained at the beginning and then used to predict each window.
+
+
+
before_predict_callback
+
typing.Optional[typing.Callable]
+
None
+
Function to call on the features before computing the predictions. This function will take the input dataframe that will be passed to the model for predicting and should return a dataframe with the same structure. The series identifier is on the index.
+
+
+
after_predict_callback
+
typing.Optional[typing.Callable]
+
None
+
Function to call on the predictions before updating the targets. This function will take a pandas Series with the predictions and should return another one with the same structure. The series identifier is on the index.
+
+
+
input_size
+
typing.Optional[int]
+
None
+
Maximum training samples per serie in each window. If None, will use an expanding window.
+
+
+
Returns
+
AnyDataFrame
+
+
Predictions for each window with the series id, timestamp, target value and predictions from each model.
Wrapper of lightgbm.dask.DaskLGBMRegressor that adds a model_ property that contains the fitted booster and is sent to the workers to in the forecasting step.
Wrapper of xgboost.dask.DaskXGBRegressor that adds a model_ property that contains the fitted model and is sent to the workers in the forecasting step.
Specify the learning task and the corresponding learning objective or a custom objective function to be used (see note below).
+
+
+
booster
+
typing.Optional[str]
+
None
+
+
+
+
tree_method
+
typing.Optional[str]
+
None
+
+
+
+
n_jobs
+
typing.Optional[int]
+
None
+
Number of parallel threads used to run xgboost. When used with other Scikit-Learn algorithms like grid search, you may choose which algorithm to parallelize and balance the threads. Creating thread contention will significantly slow down both algorithms.
+
+
+
gamma
+
typing.Optional[float]
+
None
+
(min_split_loss) Minimum loss reduction required to make a further partition on a leaf node of the tree.
+
+
+
min_child_weight
+
typing.Optional[float]
+
None
+
Minimum sum of instance weight(hessian) needed in a child.
+
+
+
max_delta_step
+
typing.Optional[float]
+
None
+
Maximum delta step we allow each tree’s weight estimation to be.
+
+
+
subsample
+
typing.Optional[float]
+
None
+
Subsample ratio of the training instance.
+
+
+
sampling_method
+
typing.Optional[str]
+
None
+
Sampling method. Used only by the GPU version of hist tree method. - uniform: select random training instances uniformly. - gradient_based select random training instances with higher probability when the gradient and hessian are larger. (cf. CatBoost)
+
+
+
colsample_bytree
+
typing.Optional[float]
+
None
+
Subsample ratio of columns when constructing each tree.
+
+
+
colsample_bylevel
+
typing.Optional[float]
+
None
+
Subsample ratio of columns for each level.
+
+
+
colsample_bynode
+
typing.Optional[float]
+
None
+
Subsample ratio of columns for each split.
+
+
+
reg_alpha
+
typing.Optional[float]
+
None
+
L1 regularization term on weights (xgb’s alpha).
+
+
+
reg_lambda
+
typing.Optional[float]
+
None
+
L2 regularization term on weights (xgb’s lambda).
+
+
+
scale_pos_weight
+
typing.Optional[float]
+
None
+
Balancing of positive and negative weights.
+
+
+
base_score
+
typing.Optional[float]
+
None
+
The initial prediction score of all instances, global bias.
Constraints for interaction representing permitted interactions. The constraints must be specified in the form of a nested list, e.g. [[0, 1], [2,<br>3, 4]], where each inner list is a group of indices of features that are allowed to interact with each other. See :doc:tutorial<br></tutorials/feature_interaction_constraint> for more information
+
+
+
importance_type
+
typing.Optional[str]
+
None
+
+
+
+
device
+
typing.Optional[str]
+
None
+
.. versionadded:: 2.0.0
Device ordinal, available options are cpu, cuda, and gpu.
+
+
+
validate_parameters
+
typing.Optional[bool]
+
None
+
Give warnings for unknown parameter.
+
+
+
enable_categorical
+
bool
+
False
+
.. versionadded:: 1.5.0
.. note:: This parameter is experimental
Experimental support for categorical data. When enabled, cudf/pandas.DataFrame should be used to specify categorical data type. Also, JSON/UBJSON serialization format is required.
+
+
+
feature_types
+
typing.Optional[typing.Sequence[str]]
+
None
+
.. versionadded:: 1.7.0
Used for specifying feature types without constructing a dataframe. See :py:class:DMatrix for details.
+
+
+
max_cat_to_onehot
+
typing.Optional[int]
+
None
+
.. versionadded:: 1.6.0
.. note:: This parameter is experimental
A threshold for deciding whether XGBoost should use one-hot encoding based split for categorical data. When number of categories is lesser than the threshold then one-hot encoding is chosen, otherwise the categories will be partitioned into children nodes. Also, enable_categorical needs to be set to have categorical feature support. See :doc:Categorical Data<br></tutorials/categorical> and :ref:cat-param for details.
+
+
+
max_cat_threshold
+
typing.Optional[int]
+
None
+
.. versionadded:: 1.7.0
.. note:: This parameter is experimental
Maximum number of categories considered for each split. Used only by partition-based splits for preventing over-fitting. Also, enable_categorical needs to be set to have categorical feature support. See :doc:Categorical Data<br></tutorials/categorical> and :ref:cat-param for details.
+
+
+
multi_strategy
+
typing.Optional[str]
+
None
+
.. versionadded:: 2.0.0
.. note:: This parameter is working-in-progress.
The strategy used for training multi-target models, including multi-target regression and multi-class classification. See :doc:/tutorials/multioutput for more information.
- one_output_per_tree: One model for each target. - multi_output_tree: Use multi-target trees.
Metric used for monitoring the training result and early stopping. It can be a string or list of strings as names of predefined metric in XGBoost (See doc/parameter.rst), one of the metrics in :py:mod:sklearn.metrics, or any other user defined metric that looks like sklearn.metrics.
If custom objective is also provided, then custom metric should implement the corresponding reverse link function.
Unlike the scoring parameter commonly used in scikit-learn, when a callable object is provided, it’s assumed to be a cost function and by default XGBoost will minimize the result during early stopping.
For advanced usage on Early stopping like directly choosing to maximize instead of minimize, see :py:obj:xgboost.callback.EarlyStopping.
See :doc:Custom Objective and Evaluation Metric </tutorials/custom_metric_obj> for more.
.. note::
This parameter replaces eval_metric in :py:meth:fit method. The old one receives un-transformed prediction regardless of whether custom objective is being used.
.. code-block:: python
from sklearn.datasets import load_diabetes from sklearn.metrics import mean_absolute_error X, y = load_diabetes(return_X_y=True) reg = xgb.XGBRegressor( tree_method=“hist”, eval_metric=mean_absolute_error, ) reg.fit(X, y, eval_set=[(X, y)])
+
+
+
early_stopping_rounds
+
typing.Optional[int]
+
None
+
.. versionadded:: 1.6.0
- Activates early stopping. Validation metric needs to improve at least once in every early_stopping_rounds round(s) to continue training. Requires at least one item in eval_set in :py:meth:fit.
- If early stopping occurs, the model will have two additional attributes: :py:attr:best_score and :py:attr:best_iteration. These are used by the :py:meth:predict and :py:meth:apply methods to determine the optimal number of trees during inference. If users want to access the full model (including trees built after early stopping), they can specify the iteration_range in these inference methods. In addition, other utilities like model plotting can also use the entire model.
- If you prefer to discard the trees after best_iteration, consider using the callback function :py:class:xgboost.callback.EarlyStopping.
- If there’s more than one item in eval_set, the last entry will be used for early stopping. If there’s more than one metric in eval_metric, the last metric will be used for early stopping.
.. note::
This parameter replaces early_stopping_rounds in :py:meth:fit method.
List of callback functions that are applied at end of each iteration. It is possible to use predefined callbacks by using :ref:Callback API <callback_api>.
.. note::
States in callback are not preserved during training, which means callback objects can not be reused for multiple training sessions without reinitialization or deepcopy.
.. code-block:: python
for params in parameters_grid: # be sure to (re)initialize the callbacks before each run callbacks = [xgb.callback.LearningRateScheduler(custom_rates)] reg = xgboost.XGBRegressor(**params, callbacks=callbacks) reg.fit(X, y)
+
+
+
kwargs
+
typing.Any
+
+
Keyword arguments for XGBoost Booster object. Full documentation of parameters can be found :doc:here </parameter>. Attempting to set a parameter via the constructor args and **kwargs dict simultaneously will result in a TypeError.
.. note:: **kwargs unsupported by scikit-learn
**kwargs is unsupported by scikit-learn. We do not guarantee that parameters passed via this argument will interact properly with scikit-learn.
Wrapper of lightgbm.ray.RayLGBMRegressor that adds a model_ property that contains the fitted booster and is sent to the workers to in the forecasting step.
Specify the learning task and the corresponding learning objective or a custom objective function to be used (see note below).
+
+
+
kwargs
+
typing.Any
+
+
Keyword arguments for XGBoost Booster object. Full documentation of parameters can be found :doc:here </parameter>. Attempting to set a parameter via the constructor args and **kwargs dict simultaneously will result in a TypeError.
.. note:: **kwargs unsupported by scikit-learn
**kwargs is unsupported by scikit-learn. We do not guarantee that parameters passed via this argument will interact properly with scikit-learn.
.. note:: Custom objective function
A custom objective function can be provided for the objective parameter. In this case, it should have the signature objective(y_true, y_pred) -> grad, hess:
y_true: array_like of shape [n_samples] The target values y_pred: array_like of shape [n_samples] The predicted values
grad: array_like of shape [n_samples] The value of the gradient for each sample point. hess: array_like of shape [n_samples] The value of the second derivative for each sample point
Wrapper of synapse.ml.lightgbm.LightGBMRegressor that adds an extract_local_model method to get a local version of the trained model and broadcast it to the workers.
+
+
+
SparkLGBMForecast
+
+
SparkLGBMForecast ()
+
+
Initialize self. See help(type(self)) for accurate signature.
Wrapper of xgboost.spark.SparkXGBRegressor that adds an extract_local_model method to get a local version of the trained model and broadcast it to the workers.
SparkXGBRegressor is a PySpark ML estimator. It implements the XGBoost regression algorithm based on XGBoost python library, and it can be used in PySpark Pipeline and PySpark ML meta algorithms like - :py:class:~pyspark.ml.tuning.CrossValidator/ - :py:class:~pyspark.ml.tuning.TrainValidationSplit/ - :py:class:~pyspark.ml.classification.OneVsRest
+
SparkXGBRegressor automatically supports most of the parameters in :py:class:xgboost.XGBRegressor constructor and most of the parameters used in :py:meth:xgboost.XGBRegressor.fit and :py:meth:xgboost.XGBRegressor.predict method.
+
To enable GPU support, set device to cuda or gpu.
+
SparkXGBRegressor doesn’t support setting base_margin explicitly as well, but support another param called base_margin_col. see doc below for more details.
+
SparkXGBRegressor doesn’t support validate_features and output_margin param.
+
SparkXGBRegressor doesn’t support setting nthread xgboost param, instead, the nthread param for each xgboost worker will be set equal to spark.task.cpus config value.
We’ll take a look at our series to get ideas for transformations and features.
+
+
fig = plot_series(df, max_insample_length=24*14)
+
+
+
We can use the MLForecast.preprocess method to explore different transformations. It looks like these series have a strong seasonality on the hour of the day, so we can subtract the value from the same hour in the previous day to remove it. This can be done with the mlforecast.target_transforms.Differences transformer, which we pass through target_transforms.
+
+
from mlforecast import MLForecast
+from mlforecast.target_transforms import Differences
+
+
+
fcst = MLForecast(
+ models=[], # we're not interested in modeling yet
+ freq=1, # our series have integer timestamps, so we'll just add 1 in every timestep
+ target_transforms=[Differences([24])],
+)
+prep = fcst.preprocess(df)
+prep
+
+
+
+
+
+
+
+
+
unique_id
+
ds
+
y
+
+
+
+
+
24
+
H196
+
25
+
0.3
+
+
+
25
+
H196
+
26
+
0.3
+
+
+
26
+
H196
+
27
+
0.1
+
+
+
27
+
H196
+
28
+
0.2
+
+
+
28
+
H196
+
29
+
0.2
+
+
+
...
+
...
+
...
+
...
+
+
+
4027
+
H413
+
1004
+
39.0
+
+
+
4028
+
H413
+
1005
+
55.0
+
+
+
4029
+
H413
+
1006
+
14.0
+
+
+
4030
+
H413
+
1007
+
3.0
+
+
+
4031
+
H413
+
1008
+
4.0
+
+
+
+
+
3936 rows × 3 columns
+
+
+
+
This has subtacted the lag 24 from each value, we can see what our series look like now.
+
+
fig = plot_series(prep)
+
+
+
+
+
Adding features
+
+
Lags
+
Looks like the seasonality is gone, we can now try adding some lag features.
y 1.000000
+lag1 0.622531
+lag24 -0.234268
+Name: y, dtype: float64
+
+
+
+
+
Lag transforms
+
Lag transforms are defined as a dictionary where the keys are the lags and the values are lists of functions that transform an array. These must be numba jitted functions (so that computing the features doesn’t become a bottleneck). There are some implemented in the window-ops package but you can also implement your own.
+
If the function takes two or more arguments you can either:
You can see that both approaches get to the same result, you can use whichever one you feel most comfortable with.
+
+
+
Date features
+
If your time column is made of timestamps then it might make sense to extract features like week, dayofweek, quarter, etc. You can do that by passing a list of strings with pandas time/date components. You can also pass functions that will take the time column as input, as we’ll show here.
If you want to do some transformation to your target before computing the features and then re-apply it after predicting you can use the target_transforms argument, which takes a list of transformations. You can find the implemented ones in mlforecast.target_transforms or you can implement your own as described in the target transformations guide.
+
+
from mlforecast.target_transforms import LocalStandardScaler
Once you’ve decided the features, transformations and models that you want to use you can use the MLForecast.fit method instead, which will do the preprocessing and then train the models. The models can be specified as a list (which will name them by using their class name and an index if there are repeated classes) or as a dictionary where the keys are the names you want to give to the models, i.e. the name of the column that will hold their predictions, and the values are the models themselves.
After you’ve trained a forecast object you can save it and load it to use later using pickle or cloudpickle. If by the time you want to use it you already know the following values of the target you can use the MLForecast.ts.update method to incorporate these, which will allow you to use these new values when computing predictions.
+
+
If no new values are provided for a serie that’s currently stored, only the previous ones are kept.
+
If new series are included they are added to the existing ones.
In order to get an estimate of how well our model will be when predicting future data we can perform cross validation, which consist on training a few models independently on different subsets of the data, using them to predict a validation set and measuring their performance.
+
Since our data depends on time, we make our splits by removing the last portions of the series and using them as validation sets. This process is implemented in MLForecast.cross_validation.
+
+
fcst = MLForecast(
+ models=lgb.LGBMRegressor(**lgb_params),
+ freq=1,
+ target_transforms=[Differences([24])],
+ lags=[1, 24],
+ lag_transforms={
+1: [expanding_mean],
+24: [(rolling_mean, 48)],
+ },
+ date_features=[hour_index],
+)
+cv_result = fcst.cross_validation(
+ df,
+ n_windows=4, # number of models to train/splits to perform
+ h=48, # length of the validation set in each window
+)
+cv_result
You can quickly try different features and evaluate them this way. We can try removing the differencing and using an exponentially weighted average of the lag 1 instead of the expanding mean.
In the same spirit of estimating our model’s performance, LightGBMCV allows us to train a few LightGBM models on different partitions of the data. The main differences with MLForecast.cross_validation are:
+
+
It can only train LightGBM models.
+
It trains all models simultaneously and gives us per-iteration averages of the errors across the complete forecasting window, which allows us to find the best iteration.
As you can see this gives us the error by iteration (controlled by the eval_every argument) and performs early stopping (which can be configured with early_stopping_evals and early_stopping_pct). If you set compute_cv_preds=True the out-of-fold predictions are computed using the best iteration found and are saved in the cv_preds_ attribute.
You can use this class to quickly try different configurations of features and hyperparameters. Once you’ve found a combination that works you can train a model with those features and hyperparameters on all the data by creating an MLForecast object from the LightGBMCV one as follows:
+ Instructions to install the package from different sources.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Released versions
+
+
PyPI
+
+
Latest release
+
To install the latest release of mlforecast from PyPI you just have to run the following in a terminal:
+
pip install mlforecast
+
+
+
Specific version
+
If you want a specific version you can include a filter, for example:
+
+
pip install "mlforecast==0.3.0" to install the 0.3.0 version
+
pip install "mlforecast<0.4.0" to install any version prior to 0.4.0
+
+
+
+
+
Conda
+
+
Latest release
+
The mlforecast package is also published to conda-forge, which you can install by running the following in a terminal:
+
conda install -c conda-forge mlforecast
+
Note that this happens about a day later after it is published to PyPI, so you may have to wait to get the latest release.
+
+
+
Specific version
+
If you want a specific version you can include a filter, for example:
+
+
conda install -c conda-forge "mlforecast==0.3.0" to install the 0.3.0 version
+
conda install -c conda-forge "mlforecast<0.4.0" to install any version prior to 0.4.0
+
+
+
+
+
Distributed training
+
If you want to perform distributed training you can use either dask, ray or spark. Once you know which framework you want to use you can include its extra:
+
+
dask: pip install "mlforecast[dask]"
+
ray: pip install "mlforecast[ray]"
+
spark: pip install "mlforecast[spark]"
+
+
+
+
+
Development version
+
If you want to try out a new feature that hasn’t made it into a release yet you have the following options:
+
+
Install from github: pip install git+https://github.com/Nixtla/mlforecast
+
Clone and install:
+
+
git clone https://github.com/Nixtla/mlforecast
+
pip install mlforecast
+
+
+
which will install the version from the current main branch.
+ Minimal example of distributed training with MLForecast
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
The DistributedMLForecast class is a high level abstraction that encapsulates all the steps in the pipeline (preprocessing, fitting the model and computing predictions) and applies them in a distributed way.
+
The different things that you need to use DistributedMLForecast (as opposed to MLForecast) are:
+
+
You need to set up a cluster. We currently support dask, ray and spark.
+
Your data needs to be a distributed collection (dask, ray or spark dataframe).
+
You need to use a model that implements distributed training in your framework of choice, e.g. SynapseML for LightGBM in spark.
Here we define a client that connects to a dask.distributed.LocalCluster, however it could be any other kind of cluster.
+
+
+
Data setup
+
For dask, the data must be a dask.dataframe.DataFrame. You need to make sure that each time serie is only in one partition and it is recommended that you have as many partitions as you have workers. If you have more partitions than workers make sure to set num_threads=1 to avoid having nested parallelism.
+
The required input format is the same as for MLForecast, except that it’s a dask.dataframe.DataFrame instead of a pandas.Dataframe.
+
+
series = generate_daily_series(100, n_static_features=2, equal_ends=True, static_as_categorical=False)
+npartitions =10
+partitioned_series = dd.from_pandas(series.set_index('unique_id'), npartitions=npartitions) # make sure we split by the id_col
+partitioned_series = partitioned_series.map_partitions(lambda df: df.reset_index())
+partitioned_series['unique_id'] = partitioned_series['unique_id'].astype(str) # can't handle categoricals atm
+partitioned_series
+
+
Dask DataFrame Structure:
+
+
+
+
+
+
+
+
unique_id
+
ds
+
y
+
static_0
+
static_1
+
+
+
npartitions=10
+
+
+
+
+
+
+
+
+
+
id_00
+
object
+
datetime64[ns]
+
float64
+
int64
+
int64
+
+
+
id_10
+
...
+
...
+
...
+
...
+
...
+
+
+
...
+
...
+
...
+
...
+
...
+
...
+
+
+
id_89
+
...
+
...
+
...
+
...
+
...
+
+
+
id_99
+
...
+
...
+
...
+
...
+
...
+
+
+
+
+
+
Dask Name: assign, 5 graph layers
+
+
+
+
+
Models
+
In order to perform distributed forecasting, we need to use a model that is able to train in a distributed way using dask. The current implementations are in DaskLGBMForecast and DaskXGBForecast which are just wrappers around the native implementations.
+
+
from mlforecast.distributed.models.dask.lgb import DaskLGBMForecast
+from mlforecast.distributed.models.dask.xgb import DaskXGBForecast
Once we have our models we instantiate a DistributedMLForecast object defining our features. We can then call fit on this object passing our dask dataframe.
For spark, the data must be a pyspark DataFrame. You need to make sure that each time serie is only in one partition (which you can do using repartitionByRange, for example) and it is recommended that you have as many partitions as you have workers. If you have more partitions than workers make sure to set num_threads=1 to avoid having nested parallelism.
+
The required input format is the same as for MLForecast, i.e. it should have at least an id column, a time column and a target column.
In order to perform distributed forecasting, we need to use a model that is able to train in a distributed way using spark. The current implementations are in SparkLGBMForecast and SparkXGBForecast which are just wrappers around the native implementations.
/hdd/miniforge3/envs/mlforecast/lib/python3.10/site-packages/pyspark/sql/pandas/conversion.py:251: FutureWarning: Passing unit-less datetime64 dtype to .astype is deprecated and will raise in a future version. Pass 'datetime64[ns]' instead
+ series = series.astype(t, copy=False)
For ray, the data must be a ray DataFrame. It is recommended that you have as many partitions as you have workers. If you have more partitions than workers make sure to set num_threads=1 to avoid having nested parallelism.
+
The required input format is the same as for MLForecast, i.e. it should have at least an id column, a time column and a target column.
+
+
series = generate_daily_series(100, n_static_features=2, equal_ends=True, static_as_categorical=False)
+# we need noncategory unique_id
+series['unique_id'] = series['unique_id'].astype(str)
+ray_series = ray.data.from_pandas(series)
+
+
+
+
Models
+
The ray integration allows to include lightgbm (RayLGBMRegressor), and xgboost (RayXGBRegressor).
+
+
from mlforecast.distributed.models.ray.lgb import RayLGBMForecast
+from mlforecast.distributed.models.ray.xgb import RayXGBForecast
The main component of mlforecast is the MLForecast class, which abstracts away:
+
+
Feature engineering and model training through MLForecast.fit
+
Feature updates and multi step ahead predictions through MLForecast.predict
+
+
+
+
Data format
+
The data is expected to be a pandas dataframe in long format, that is, each row represents an observation of a single serie at a given time, with at least three columns:
+
+
id_col: column that identifies each serie.
+
target_col: column that has the series values at each timestamp.
+
time_col: column that contains the time the series value was observed. These are usually timestamps, but can also be consecutive integers.
+
+
Here we present an example using the classic Box & Jenkins airline data, which measures monthly totals of international airline passengers from 1949 to 1960. Source: Box, G. E. P., Jenkins, G. M. and Reinsel, G. C. (1976) Time Series Analysis, Forecasting and Control. Third Edition. Holden-Day. Series G.
+
+
import pandas as pd
+from utilsforecast.plotting import plot_series
Here the unique_id column has the same value for all rows because this is a single time series, you can have multiple time series by stacking them together and having a column that differentiates them.
+
We also have the ds column that contains the timestamps, in this case with a monthly frequency, and the y column that contains the series values in each timestamp.
+
+
+
Modeling
+
+
fig = plot_series(df)
+
+
+
We can see that the serie has a clear trend, so we can take the first difference, i.e. take each value and subtract the value at the previous month. This can be achieved by passing an mlforecast.target_transforms.Differences([1]) instance to target_transforms.
+
We can then train a linear regression using the value from the same month at the previous year (lag 12) as a feature, this is done by passing lags=[12].
fcst = MLForecast(
+ models=LinearRegression(),
+ freq='MS', # our serie has a monthly frequency
+ lags=[12],
+ target_transforms=[Differences([1])],
+)
+fcst.fit(df)
What MLForecast.fit does is save the required data for the predict step and also train the models (in this case the linear regression). The trained models are available in the MLForecast.models_ attribute, which is a dictionary where the keys are the model names and the values are the model themselves.
+
+
fcst.models_
+
+
{'lr': LinearRegression()}
+
+
+
+
+
Inspect parameters
+
We can access the linear regression coefficients in the following way:
Sometimes you want to determine why the model gave a specific prediction. In order to do this you need the input features, which aren’t returned by default, but you can retrieve them using a callback.
+ In this example, we’ll implement time series cross-validation to evaluate model’s performance.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+Prerequesites
+
+
+
+
+
+
This tutorial assumes basic familiarity with MLForecast. For a minimal example visit the Quick Start
+
+
+
+
+
Introduction
+
Time series cross-validation is a method for evaluating how a model would have performed in the past. It works by defining a sliding window across the historical data and predicting the period following it.
+
+
MLForecast has an implementation of time series cross-validation that is fast and easy to use. This implementation makes cross-validation a efficient operation, which makes it less time-consuming. In this notebook, we’ll use it on a subset of the M4 Competition hourly dataset.
+
Outline:
+
+
Install libraries
+
Load and explore data
+
Train model
+
Perform time series cross-validation
+
Evaluate results
+
+
+
+
+
+
+
+Tip
+
+
+
+
You can use Colab to run this Notebook interactively
+
+
+
+
+
Install libraries
+
We assume that you have MLForecast already installed. If not, check this guide for instructions on how to install MLForecast.
+
Install the necessary packages with pip install mlforecast.
+
+
# pip install mlforecast lightgbm
+
+
+
import pandas as pd
+
+from utilsforecast.plotting import plot_series
+
+from mlforecast import MLForecast # required to instantiate MLForecast object and use cross-validation method
+
+
+
+
Load and explore the data
+
As stated in the introduction, we’ll use the M4 Competition hourly dataset. We’ll first import the data from an URL using pandas.
+
+
Y_df = pd.read_csv('https://datasets-nixtla.s3.amazonaws.com/m4-hourly.csv') # load the data
+Y_df.head()
+
+
+
+
+
+
+
+
+
unique_id
+
ds
+
y
+
+
+
+
+
0
+
H1
+
1
+
605.0
+
+
+
1
+
H1
+
2
+
586.0
+
+
+
2
+
H1
+
3
+
586.0
+
+
+
3
+
H1
+
4
+
559.0
+
+
+
4
+
H1
+
5
+
511.0
+
+
+
+
+
+
+
+
The input to MLForecast is a data frame in long format with three columns: unique_id, ds and y:
+
+
The unique_id (string, int, or category) represents an identifier for the series.
+
The ds (datestamp or int) column should be either an integer indexing time or a datestamp in format YYYY-MM-DD or YYYY-MM-DD HH:MM:SS.
+
The y (numeric) represents the measurement we wish to forecast.
+
+
The data in this example already has this format, so no changes are needed.
+
We can plot the time series we’ll work with using the following function.
Notice that in each cutoff period, we generated a forecast for the next 24 hours using only the data y before said period.
+
+
+
Evaluate results
+
We can now compute the accuracy of the forecast using an appropiate accuracy metric. Here we’ll use the Root Mean Squared Error (RMSE). To do this, we can use utilsforecast, a Python library developed by Nixtla that includes a function to compute the RMSE.
+
+
from utilsforecast.losses import rmse
+
+
We’ll compute the rmse per time series and cutoff. To do this we’ll concatenate the id and the cutoff columns, then we will take the mean of the results.
+ Define your own functions to be used as date features
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
from mlforecast import MLForecast
+from mlforecast.utils import generate_daily_series
+
+
The date_features argument of MLForecast can take pandas date attributes as well as functions that take a pandas DatetimeIndex and return a numeric value. The name of the function is used as the name of the feature, so please use unique and descriptive names.
+
+
series = generate_daily_series(1, min_length=6, max_length=6)
+
+
+
def even_day(dates):
+"""Day of month is even"""
+return dates.day %2==0
+
+def month_start_or_end(dates):
+"""Date is month start or month end"""
+return dates.is_month_start | dates.is_month_end
+
+def is_monday(dates):
+"""Date is monday"""
+return dates.dayofweek ==0
+ Customize the training procedure for your models
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
mlforecast abstracts away most of the training details, which is useful for iterating quickly. However, sometimes you want more control over the fit parameters, the data that goes into the model, etc. This guide shows how you can train a model in a specific way and then giving it back to mlforecast to produce forecasts with it.
+
+
Data setup
+
+
from mlforecast.utils import generate_daily_series
series = generate_daily_series(
+100, equal_ends=True, n_static_features=2
+).rename(columns={'static_1': 'product_id'})
+series.head()
+
+
+
+
+
+
+
+
+
unique_id
+
ds
+
y
+
static_0
+
product_id
+
+
+
+
+
0
+
id_00
+
2000-10-05
+
39.811983
+
79
+
45
+
+
+
1
+
id_00
+
2000-10-06
+
103.274013
+
79
+
45
+
+
+
2
+
id_00
+
2000-10-07
+
176.574744
+
79
+
45
+
+
+
3
+
id_00
+
2000-10-08
+
258.987900
+
79
+
45
+
+
+
4
+
id_00
+
2000-10-09
+
344.940404
+
79
+
45
+
+
+
+
+
+
+
+
In mlforecast the required columns are the series identifier, time and target. Any extra columns you have, like static_0 and product_id here are considered to be static and are replicated when constructing the features for the next timestamp. You can disable this by passing static_features to MLForecast.preprocess or MLForecast.fit, which will only keep the columns you define there as static. Keep in mind that all features in your input dataframe will be used for training, so you’ll have to provide the future values of exogenous features to MLForecast.predict through the X_df argument.
+
Consider the following example. Suppose that we have a prices catalog for each id and date.
This dataframe will be passed to MLForecast.fit (or MLForecast.preprocess). However, since the price is dynamic we have to tell that method that only static_0 and product_id are static.
+ Train one model to predict each step of the forecasting horizon
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
By default mlforecast uses the recursive strategy, i.e. a model is trained to predict the next value and if we’re predicting several values we do it one at a time and then use the model’s predictions as the new target, recompute the features and predict the next step.
+
There’s another approach where if we want to predict 10 steps ahead we train 10 different models, where each model is trained to predict the value at each specific step, i.e. one model predicts the next value, another one predicts the value two steps ahead and so on. This can be very time consuming but can also provide better results. If you want to use this approach you can specify max_horizon in MLForecast.fit, which will train that many models and each model will predict its corresponding horizon when you call MLForecast.predict.
def avg_smape(df):
+"""Computes the SMAPE by serie and then averages it across all series."""
+ full = df.merge(valid)
+return (
+ evaluate(full, metrics=[smape])
+ .drop(columns='metric')
+ .set_index('unique_id')
+ .squeeze()
+ )
horizon =24
+# the following will train 24 models, one for each horizon
+individual_fcst = fcst.fit(train, max_horizon=horizon)
+individual_preds = individual_fcst.predict(horizon)
+avg_smape_individual = avg_smape(individual_preds).rename('individual')
+# the following will train a single model and use the recursive strategy
+recursive_fcst = fcst.fit(train)
+recursive_preds = recursive_fcst.predict(horizon)
+avg_smape_recursive = avg_smape(recursive_preds).rename('recursive')
+# results
+print('Average SMAPE per method and serie')
+avg_smape_individual.to_frame().join(avg_smape_recursive).applymap('{:.1%}'.format)
+ Get access to the input features and predictions in each forecasting horizon
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
If you want to do something to the input before predicting or something to the output before it gets used to update the target (and thus the next features that rely on lags), you can pass a function to run at any of these times.
Saving the features that are sent as input to the model in each timestamp can be helpful, for example to estimate SHAP values. This can be easily achieved with the SaveFeatures callback.
Once we’ve called predict we can just retrieve the features.
+
+
save_features_cbk.get_features()
+
+
+
+
+
+
+
+
+
unique_id
+
lag1
+
+
+
+
+
0
+
id_0
+
4.155930
+
+
+
0
+
id_0
+
5.281643
+
+
+
+
+
+
+
+
+
+
+
After predicting
+
When predicting with the recursive strategy (the default) the predictions for each timestamp are used to update the target and recompute the features. If you want to do something to these predictions before that happens you can use the after_predict_callback argument of MLForecast.predict.
+
+
Increasing predictions values
+
Suppose we know that our model always underestimates and we want to prevent that from happening by making our predictions 10% higher. We can achieve that with the following:
+
+
def increase_predictions(predictions):
+"""Increases all predictions by 10%"""
+return1.1* predictions
By default all series seen during training will be forecasted with the predict method. If you’re only interested in predicting a couple of them you can use the ids argument.
+
+
fcst.predict(1, ids=['id_0', 'id_4'])
+
+
+
+
+
+
+
+
+
unique_id
+
ds
+
lgb
+
+
+
+
+
0
+
id_0
+
2000-08-10
+
3.728396
+
+
+
1
+
id_4
+
2001-01-08
+
3.331394
+
+
+
+
+
+
+
+
Note that the ids must’ve been seen during training, if you try to predict an id that wasn’t there you’ll get an error.
+ In this example, we’ll implement prediction intervals
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+Prerequesites
+
+
+
+
+
+
This tutorial assumes basic familiarity with MLForecast. For a minimal example visit the Quick Start
+
+
+
+
+
Introduction
+
When we generate a forecast, we usually produce a single value known as the point forecast. This value, however, doesn’t tell us anything about the uncertainty associated with the forecast. To have a measure of this uncertainty, we need prediction intervals.
+
A prediction interval is a range of values that the forecast can take with a given probability. Hence, a 95% prediction interval should contain a range of values that include the actual future value with probability 95%. Probabilistic forecasting aims to generate the full forecast distribution. Point forecasting, on the other hand, usually returns the mean or the median or said distribution. However, in real-world scenarios, it is better to forecast not only the most probable future outcome, but many alternative outcomes as well.
+
With MLForecast you can train sklearn models to generate point forecasts. It also takes the advantages of ConformalPrediction to generate the same point forecasts and adds them prediction intervals. By the end of this tutorial, you’ll have a good understanding of how to add probabilistic intervals to sklearn models for time series forecasting. Furthermore, you’ll also learn how to generate plots with the historical data, the point forecasts, and the prediction intervals.
+
+
+
+
+
+
+Important
+
+
+
+
Although the terms are often confused, prediction intervals are not the same as confidence intervals.
+
+
+
+
+
+
+
+
+Warning
+
+
+
+
In practice, most prediction intervals are too narrow since models do not account for all sources of uncertainty. A discussion about this can be found here.
+
+
+
Outline:
+
+
Install libraries
+
Load and explore the data
+
Train models
+
Plot prediction intervals
+
+
+
+
+
+
+
+Tip
+
+
+
+
You can use Colab to run this Notebook interactively
+
+
+
+
+
Install libraries
+
Install the necessary packages using pip install mlforecast utilsforecast
+
+
+
Load and explore the data
+
For this example, we’ll use the hourly dataset from the M4 Competition. We first need to download the data from a URL and then load it as a pandas dataframe. Notice that we’ll load the train and the test data separately. We’ll also rename the y column of the test data as y_test.
+
+
import pandas as pd
+from utilsforecast.plotting import plot_series
Since the goal of this notebook is to generate prediction intervals, we’ll only use the first 8 series of the dataset to reduce the total computational time.
+
+
n_series =8
+uids = train['unique_id'].unique()[:n_series] # select first n_series of the dataset
+train = train.query('unique_id in @uids')
+test = test.query('unique_id in @uids')
+
+
We can plot these series using the plot_series function from the utilsforecast library. This function has multiple parameters, and the required ones to generate the plots in this notebook are explained below.
+
+
df: A pandas dataframe with columns [unique_id, ds, y].
+
forecasts_df: A pandas dataframe with columns [unique_id, ds] and models.
+
plot_random: bool = True. Plots the time series randomly.
+
models: List[str]. A list with the models we want to plot.
+
level: List[float]. A list with the prediction intervals we want to plot.
+
engine: str = matplotlib. It can also be plotly. plotly generates interactive plots, while matplotlib generates static plots.
# Create a list of models and instantiation parameters
+models = [
+ KNeighborsRegressor(),
+ Lasso(),
+ LinearRegression(),
+ MLPRegressor(),
+ Ridge(),
+]
+
+
To instantiate a new MLForecast object, we need the following parameters:
+
+
models: The list of models defined in the previous step.
+
+
target_transforms: Transformations to apply to the target before computing the features. These are restored at the forecasting step.
+
lags: Lags of the target to use as features.
+
+
+
mlf = MLForecast(
+ models=[Ridge(), Lasso(), LinearRegression(), KNeighborsRegressor(), MLPRegressor(random_state=0)],
+ target_transforms=[Differences([1])],
+ lags=[24* (i+1) for i inrange(7)],
+)
+
+
Now we’re ready to generate the point forecasts and the prediction intervals. To do this, we’ll use the fit method, which takes the following arguments:
+
+
data: Series data in long format.
+
id_col: Column that identifies each series. In our case, unique_id.
+
time_col: Column that identifies each timestep, its values can be timestamps or integers. In our case, ds.
+
target_col: Column that contains the target. In our case, y.
+
prediction_intervals: A PredicitonIntervals class. The class takes two parameters: n_windows and h. n_windows represents the number of cross-validation windows used to calibrate the intervals and h is the forecast horizon. The strategy will adjust the intervals for each horizon step, resulting in different widths for each step.
After fitting the models, we will call the predict method to generate forecasts with prediction intervals. The method takes the following arguments:
+
+
horizon: An integer that represent the forecasting horizon. In this case, we’ll forecast the next 48 hours.
+
level: A list of floats with the confidence levels of the prediction intervals. For example, level=[95] means that the range of values should include the actual future value with probability 95%.
test = test.merge(forecasts, how='left', on=['unique_id', 'ds'])
+
+
+
+
Plot prediction intervals
+
To plot the point and the prediction intervals, we’ll use the plot_series function again. Notice that now we also need to specify the model and the levels that we want to plot.
From these plots, we can conclude that the uncertainty around each forecast varies according to the model that is being used. For the same time series, one model can predict a wider range of possible future values than others.
Since mlforecast uses a single global model it can be helpful to apply some transformations to the target to ensure that all series have similar distributions. They can also help remove trend for models that can’t deal with it out of the box.
+
+
Data setup
+
For this example we’ll use a single serie from the M4 dataset.
+
+
import matplotlib.pyplot as plt
+import numpy as np
+import pandas as pd
+from datasetsforecast.m4 import M4
+from sklearn.base import BaseEstimator
+
+from mlforecast import MLForecast
+from mlforecast.target_transforms import Differences, LocalStandardScaler
We see that our serie is random noise now. Suppose we also want to standardize it, i.e. make it have a mean of 0 and variance of 1. We can add the LocalStandardScaler transformation after these differences.
Now that we’ve captured the components of the serie (trend + seasonality), we could try forecasting it with a model that always predicts 0, which will basically project the trend and seasonality.
There are some transformations that don’t require to learn any parameters, such as applying logarithm for example. These can be easily defined using the GlobalSklearnTransformer, which takes a scikit-learn compatible transformer and applies it to all series. Here’s an example on how to define a transformation that applies logarithm to each value of the series + 1, which can help avoid computing the log of 0.
In order to implement your own target transformation you have to define a class that inherits from mlforecast.target_transforms.BaseTargetTransform (this takes care of setting the column names as the id_col, time_col and target_col attributes) and implement the fit_transform and inverse_transform methods. Here’s an example on how to define a min-max scaler.
+
+
from mlforecast.target_transforms import BaseTargetTransform
+
+
+
class LocalMinMaxScaler(BaseTargetTransform):
+"""Scales each serie to be in the [0, 1] interval."""
+def fit_transform(self, df: pd.DataFrame) -> pd.DataFrame:
+self.stats_ = df.groupby(self.id_col)[self.target_col].agg(['min', 'max'])
+ df = df.merge(self.stats_, on=self.id_col)
+ df[self.target_col] = (df[self.target_col] - df['min']) / (df['max'] - df['min'])
+ df = df.drop(columns=['min', 'max'])
+return df
+
+def inverse_transform(self, df: pd.DataFrame) -> pd.DataFrame:
+ df = df.merge(self.stats_, on=self.id_col)
+for col in df.columns.drop([self.id_col, self.time_col, 'min', 'max']):
+ df[col] = df[col] * (df['max'] - df['min']) + df['min']
+ df = df.drop(columns=['min', 'max'])
+return df
+
+
And now you can pass an instance of this class to the target_transforms argument.
Transfer learning refers to the process of pre-training a flexible model on a large dataset and using it later on other data with little to no training. It is one of the most outstanding 🚀 achievements in Machine Learning and has many practical applications.
+
For time series forecasting, the technique allows you to get lightning-fast predictions ⚡ bypassing the tradeoff between accuracy and speed (more than 30 times faster than our already fast AutoARIMA for a similar accuracy).
+
This notebook shows how to generate a pre-trained model to forecast new time series never seen by the model.
+
Table of Contents
+
+
Installing MLForecast
+
Load M3 Monthly Data
+
Instantiate NeuralForecast core, Fit, and save
+
Use the pre-trained model to predict on AirPassengers
The M3 class will automatically download the complete M3 dataset and process it.
+
It return three Dataframes: Y_df contains the values for the target variables, X_df contains exogenous calendar features and S_df contains static features for each time-series. For this example we will only use Y_df.
+
If you want to use your own data just replace Y_df. Be sure to use a long format and have a simmilar structure than our data set.
In this tutorial we are only using 1_000 series to speed up computations. Remove the filter to use the whole dataset.
+
+
fig = plot_series(Y_df_M3)
+
+
+
+
+
Model Training
+
Using the MLForecast.fit method you can train a set of models to your dataset. You can modify the hyperparameters of the model to get a better accuracy, in this case we will use the default hyperparameters of lgb.LGBMRegressor.
+
+
models = [lgb.LGBMRegressor(verbosity=-1)]
+
+
The MLForecast object has the following parameters:
+
+
models: a list of sklearn-like (fit and predict) models.
Now we can transfer the trained model to forecast AirPassengers with the MLForecast.predict method, we just have to pass the new dataframe to the new_data argument.
+
+
Y_df = pd.read_csv('https://datasets-nixtla.s3.amazonaws.com/air-passengers.csv', parse_dates=['ds'])
+
+# We define the train df.
+Y_train_df = Y_df[Y_df.ds<='1959-12-31'] # 132 train
+Y_test_df = Y_df[Y_df.ds>'1959-12-31'] # 12 test
+ In this example we will show how to perform electricity load forecasting using MLForecast alongside many models. We also compare them against the prophet library.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Introduction
+
Some time series are generated from very low frequency data. These data generally exhibit multiple seasonalities. For example, hourly data may exhibit repeated patterns every hour (every 24 observations) or every day (every 24 * 7, hours per day, observations). This is the case for electricity load. Electricity load may vary hourly, e.g., during the evenings electricity consumption may be expected to increase. But also, the electricity load varies by week. Perhaps on weekends there is an increase in electrical activity.
+
In this example we will show how to model the two seasonalities of the time series to generate accurate forecasts in a short time. We will use hourly PJM electricity load data. The original data can be found here.
+
+
+
Libraries
+
In this example we will use the following libraries:
+
+
mlforecast. Accurate and ⚡️ fast forecasting withc lassical machine learning models.
PJM Interconnection LLC (PJM) is a regional transmission organization (RTO) in the United States. It is part of the Eastern Interconnection grid operating an electric transmission system serving all or parts of Delaware, Illinois, Indiana, Kentucky, Maryland, Michigan, New Jersey, North Carolina, Ohio, Pennsylvania, Tennessee, Virginia, West Virginia, and the District of Columbia. The hourly power consumption data comes from PJM’s website and are in megawatts (MW).
+
+
Let’s take a look to the data.
+
+
import matplotlib.pyplot as plt
+import numpy as np
+import pandas as pd
+from utilsforecast.plotting import plot_series
data_url ='https://raw.githubusercontent.com/panambY/Hourly_Energy_Consumption/master/data/PJM_Load_hourly.csv'
+df = pd.read_csv(data_url, parse_dates=['Datetime'])
+df.columns = ['ds', 'y']
+df.insert(0, 'unique_id', 'PJM_Load_hourly')
+df['ds'] = pd.to_datetime(df['ds'])
+df = df.sort_values(['unique_id', 'ds']).reset_index(drop=True)
+print(f'Shape of the data {df.shape}')
+df.tail()
+
+
Shape of the data (32896, 3)
+
+
+
+
+
+
+
+
+
+
unique_id
+
ds
+
y
+
+
+
+
+
32891
+
PJM_Load_hourly
+
2001-12-31 20:00:00
+
36392.0
+
+
+
32892
+
PJM_Load_hourly
+
2001-12-31 21:00:00
+
35082.0
+
+
+
32893
+
PJM_Load_hourly
+
2001-12-31 22:00:00
+
33890.0
+
+
+
32894
+
PJM_Load_hourly
+
2001-12-31 23:00:00
+
32590.0
+
+
+
32895
+
PJM_Load_hourly
+
2002-01-01 00:00:00
+
31569.0
+
+
+
+
+
+
+
+
+
fig = plot_series(df)
+
+
+
We clearly observe that the time series exhibits seasonal patterns. Moreover, the time series contains 32,896 observations, so it is necessary to use very computationally efficient methods to display them in production.
+
We are going to split our series in order to create a train and test set. The model will be tested using the last 24 hours of the timeseries.
First we must visualize the seasonalities of the model. As mentioned before, the electricity load presents seasonalities every 24 hours (Hourly) and every 24 * 7 (Daily) hours. Therefore, we will use [24, 24 * 7] as the seasonalities for the model. In order to analize how they affect our series we are going to use the Difference method.
+
+
from mlforecast import MLForecast
+from mlforecast.target_transforms import Differences
+
+
We can use the MLForecast.preprocess method to explore different transformations. It looks like these series have a strong seasonality on the hour of the day, so we can subtract the value from the same hour in the previous day to remove it. This can be done with the mlforecast.target_transforms.Differences transformer, which we pass through target_transforms.
+
In order to analize the trends individually and combined we are going to plot them individually and combined. Therefore, we can compare them against the original series. We can use the next function for that.
Since the seasonalities are present at 24 hours (daily) and 24*7 (weekly) we are going to substract them from the serie using Differences([24, 24*7]) and plot them.
As we can see when we extract the 24 difference (daily) in PJM_Load_hourly_24 the series seem to stabilize sisnce the peaks seem more uniform in comparison with the original series PJM_Load_hourly.
+
When we extrac the 24*7 (weekly) PJM_Load_hourly_168 difference we can see there is more periodicity in the peaks in comparison with the original series.
+
Finally we can see the result from the combined result from substracting all the differences PJM_Load_hourly_all_diff.
+
For modeling we are going to use both difference for the forecasting, therefore we are setting the argument target_transforms from the MLForecast object equal to [Differences([24, 24*7])], if we wanted to include a yearly difference we would need to add the term 24*365.
+
+
fcst = MLForecast(
+ models=[], # we're not interested in modeling yet
+ freq='H', # our series have hourly frequency
+ target_transforms=[Differences([24, 24*7])],
+)
+prep = fcst.preprocess(df_train)
+prep
+
+
+
+
+
+
+
+
+
unique_id
+
ds
+
y
+
+
+
+
+
192
+
PJM_Load_hourly
+
1998-04-09 02:00:00
+
831.0
+
+
+
193
+
PJM_Load_hourly
+
1998-04-09 03:00:00
+
918.0
+
+
+
194
+
PJM_Load_hourly
+
1998-04-09 04:00:00
+
760.0
+
+
+
195
+
PJM_Load_hourly
+
1998-04-09 05:00:00
+
849.0
+
+
+
196
+
PJM_Load_hourly
+
1998-04-09 06:00:00
+
710.0
+
+
+
...
+
...
+
...
+
...
+
+
+
32867
+
PJM_Load_hourly
+
2001-12-30 20:00:00
+
3417.0
+
+
+
32868
+
PJM_Load_hourly
+
2001-12-30 21:00:00
+
3596.0
+
+
+
32869
+
PJM_Load_hourly
+
2001-12-30 22:00:00
+
3501.0
+
+
+
32870
+
PJM_Load_hourly
+
2001-12-30 23:00:00
+
3939.0
+
+
+
32871
+
PJM_Load_hourly
+
2001-12-31 00:00:00
+
4235.0
+
+
+
+
+
32680 rows × 3 columns
+
+
+
+
+
fig = plot_series(prep)
+
+
+
+
+
Model Selection with Cross-Validation
+
We can test many models simoultaneously using MLForecast cross_validation. We can import lightgbm and scikit-learn models and try different combinations of them, alongside different target transformations (as the ones we created previously) and historical variables.
+You can see an in-depth tutorial on how to use MLForecastCross Validation methods here
We can create a benchmark Naive model that uses the electricity load of the last hour as prediction lag1 as showed in the next cell. You can create your own models and try them with MLForecast using the same structure.
Now let’s try differen models from the scikit-learn library: Lasso, LinearRegression, Ridge, KNN, MLP and Random Forest alongside the LightGBM. You can add any model to the dictionary to train and compare them by adding them to the dictionary (models) as shown.
The we can instanciate the MLForecast class with the models we want to try along side target_transforms, lags, lag_transforms, and date_features. All this features are applied to the models we selected.
+
In this case we use the 1st, 12th and 24th lag, which are passed as a list. Potentially you could pass a range.
+
lags=[1,12,24]
+
Lag transforms are defined as a dictionary where the keys are the lags and the values are lists of functions that transform an array. These must be numba jitted functions (so that computing the features doesn’t become a bottleneck). There are some implemented in the window-ops package but you can also implement your own.
+For this example we applied an expanding mean to the first lag, and a rolling mean to the 24th lag.
For using the date features you need to be sure that your time column is made of timestamps. Then it might make sense to extract features like week, dayofweek, quarter, etc. You can do that by passing a list of strings with pandas time/date components. You can also pass functions that will take the time column as input, as we’ll show here.
+Here we add month, hour and dayofweek features:
+
date_features=['month', 'hour', 'dayofweek']
+
+
+
mlf = MLForecast(
+ models = models,
+ freq='H', # our series have hourly frequency
+ target_transforms=[Differences([24, 24*7])],
+ lags=[1,12,24], # Lags to be used as features
+ lag_transforms={
+1: [expanding_mean],
+24: [(rolling_mean, 48)],
+ },
+ date_features=['month', 'hour', 'dayofweek']
+)
+
+
Now we use the cross_validation method to train and evalaute the models. + df: Receives the training data + h: Forecast horizon + n_windows: The number of folds we want to predict
+
You can specify the names of the time series id, time and target columns. + id_col:Column that identifies each serie ( Default unique_id ) + time_col: Column that identifies each timestep, its values can be timestamps or integer( Default ds ) + target_col:Column that contains the target ( Default y )
Visually examining the forecasts can give us some idea of how the model is behaving, yet in order to asses the performace we need to evaluate them trough metrics. For that we use the utilsforecast library that contains many useful metrics and an evaluate function.
+
+
from utilsforecast.losses import*
+from utilsforecast.evaluation import evaluate
+
+
+
# Metrics to be used for evaluation
+metrics = [
+ mae,
+ rmse,
+ mape,
+ smape
+ ]
We can se that the model lgbm has top performance in most metrics folowed by the lasso regression. Both models perform way better than the naive.
+
+
+
Test Evaluation
+
Now we are going to evaluate their perfonce in the test set. We can use both of them for forecasting the test alongside some prediction intervals. For that we can use the PredictionIntervals function in mlforecast.utils.
+You can see an in-depth tutotorial of Probabilistic Forecasting here
Now we’re ready to generate the point forecasts and the prediction intervals. To do this, we’ll use the fit method, which takes the following arguments:
+
+
df: Series data in long format.
+
id_col: Column that identifies each series. In our case, unique_id.
+
time_col: Column that identifies each timestep, its values can be timestamps or integers. In our case, ds.
+
target_col: Column that contains the target. In our case, y.
+
+
The PredictionIntervals function is used to compute prediction intervals for the models using Conformal Prediction. The function takes the following arguments: + n_windows: represents the number of cross-validation windows used to calibrate the intervals + h: the forecast horizon
Now that the model has been trained we are going to forecast the next 24 hours using the predict method so we can compare them to our test data. Additionally, we are going to create prediction intervals at levels[90,95].
The predict method returns a DataFrame witht the predictions for each model (lasso and lgbm) along side the prediction tresholds. The high-threshold is indicated by the keyword hi, the low-threshold by the keyword lo, and the level by the number in the column names.
+
+
test = df_last_24_hours.merge(forecasts, how='left', on=['unique_id', 'ds'])
+test.head()
+
+
+
+
+
+
+
+
+
unique_id
+
ds
+
y
+
lgbm
+
lasso
+
lgbm-lo-95
+
lgbm-lo-90
+
lgbm-hi-90
+
lgbm-hi-95
+
lasso-lo-95
+
lasso-lo-90
+
lasso-hi-90
+
lasso-hi-95
+
+
+
+
+
0
+
PJM_Load_hourly
+
2001-12-31 01:00:00
+
29001.0
+
28847.573176
+
29124.085976
+
28544.593464
+
28567.603130
+
29127.543222
+
29150.552888
+
28762.752269
+
28772.604275
+
29475.567677
+
29485.419682
+
+
+
1
+
PJM_Load_hourly
+
2001-12-31 02:00:00
+
28138.0
+
27862.589195
+
28365.330749
+
27042.311414
+
27128.839888
+
28596.338503
+
28682.866977
+
27528.548959
+
27619.065224
+
29111.596275
+
29202.112539
+
+
+
2
+
PJM_Load_hourly
+
2001-12-31 03:00:00
+
27830.0
+
27044.418960
+
27712.161676
+
25596.659896
+
25688.230426
+
28400.607493
+
28492.178023
+
26236.955369
+
26338.087102
+
29086.236251
+
29187.367984
+
+
+
3
+
PJM_Load_hourly
+
2001-12-31 04:00:00
+
27874.0
+
26976.104125
+
27661.572733
+
25249.961527
+
25286.024722
+
28666.183529
+
28702.246724
+
25911.133521
+
25959.815715
+
29363.329750
+
29412.011944
+
+
+
4
+
PJM_Load_hourly
+
2001-12-31 05:00:00
+
28427.0
+
26694.246238
+
27393.922370
+
25044.220845
+
25051.548832
+
28336.943644
+
28344.271631
+
25751.547897
+
25762.524815
+
29025.319924
+
29036.296843
+
+
+
+
+
+
+
+
Now we can evaluate the metrics and performance in the test set.
We can see that the lasso regression performed slighty better than the LightGBM for the test set. Additonally, we can also plot the forecasts alongside their prediction intervals. For that we can use the plot_series method available in utilsforecast.plotting.
+
We can plot one or many models at once alongside their coinfidence intervals.
One of the most widely used models for time series forecasting is Prophet. This model is known for its ability to model different seasonalities (weekly, daily yearly). We will use this model as a benchmark to see if the lgbm alongside MLForecast adds value for this time series.
+
+
from prophet import Prophet
+from time import time
print(f'lgbm with MLForecast has a speedup of {time_prophet/time_lgbm:.2f} compared with prophet')
+
+
lgbm with MLForecast has a speedup of 27.62 compared with prophet
+
+
+
We can see that lgbm with MLForecast was able to provide metrics at least twice as good as Prophet as seen in the column improvement above, and way faster.
+ In this example we will show how to perform electricity load forecasting on the ERCOT (Texas) market for detecting daily peaks.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Introduction
+
Predicting peaks in different markets is useful. In the electricity market, consuming electricity at peak demand is penalized with higher tarifs. When an individual or company consumes electricity when its most demanded, regulators calls that a coincident peak (CP).
+
In the Texas electricity market (ERCOT), the peak is the monthly 15-minute interval when the ERCOT Grid is at a point of highest capacity. The peak is caused by all consumers’ combined demand on the electrical grid. The coincident peak demand is an important factor used by ERCOT to determine final electricity consumption bills. ERCOT registers the CP demand of each client for 4 months, between June and September, and uses this to adjust electricity prices. Clients can therefore save on electricity bills by reducing the coincident peak demand.
+
In this example we will train a LightGBM model on historic load data to forecast day-ahead peaks on September 2022. Multiple seasonality is traditionally present in low sampled electricity data. Demand exhibits daily and weekly seasonality, with clear patterns for specific hours of the day such as 6:00pm vs 3:00am or for specific days such as Sunday vs Friday.
+
First, we will load ERCOT historic demand, then we will use the MLForecast.cross_validation method to fit the LightGBM model and forecast daily load during September. Finally, we show how to use the forecasts to detect the coincident peak.
+
Outline
+
+
Install libraries
+
Load and explore the data
+
Fit LightGBM model and forecast
+
Peak detection
+
+
+
+
+
+
+
+Tip
+
+
+
+
You can use Colab to run this Notebook interactively
+
+
+
+
+
Libraries
+
We assume you have MLForecast already installed. Check this guide for instructions on how to install MLForecast.
+
Install the necessary packages using pip install mlforecast.
+
Also we have to install LightGBM using pip install lightgbm.
+
+
+
Load Data
+
The input to MLForecast is always a data frame in long format with three columns: unique_id, ds and y:
+
+
The unique_id (string, int or category) represents an identifier for the series.
+
The ds (datestamp or int) column should be either an integer indexing time or a datestamp ideally like YYYY-MM-DD for a date or YYYY-MM-DD HH:MM:SS for a timestamp.
+
The y (numeric) represents the measurement we wish to forecast. We will rename the
+
+
First, read the 2022 historic total demand of the ERCOT market. We processed the original data (available here), by adding the missing hour due to daylight saving time, parsing the date to datetime format, and filtering columns of interest.
+
+
import numpy as np
+import pandas as pd
+from utilsforecast.plotting import plot_series
We observe that the time series exhibits seasonal patterns. Moreover, the time series contains 6,552 observations, so it is necessary to use computationally efficient methods to deploy them in production.
+
+
+
Fit and Forecast LightGBM model
+
Import the MLForecast class and the models you need.
target_transforms: Transformations to apply to the target before computing the features. These are restored at the forecasting step.
+
lags: Lags of the target to use as features.
+
+
+
# Instantiate MLForecast class as mlf
+mlf = MLForecast(
+ models=models,
+ freq='H',
+ target_transforms=[Differences([24])],
+ lags=range(1, 25)
+)
+
+
+
+
+
+
+
+Tip
+
+
+
+
In this example, we are only using differences and lags to produce features. See the full documentation to see all available features.
+
+
+
The cross_validation method allows the user to simulate multiple historic forecasts, greatly simplifying pipelines by replacing for loops with fit and predict methods. This method re-trains the model and forecast each window. See this tutorial for an animation of how the windows are defined.
+
Use the cross_validation method to produce all the daily forecasts for September. To produce daily forecasts set the forecasting horizon window_size as 24. In this example we are simulating deploying the pipeline during September, so set the number of windows as 30 (one for each day). Finally, the step size between windows is 24 (equal to the window_size). This ensure to only produce one forecast per day.
+
Additionally,
+
+
id_col: identifies each time series.
+
time_col: indetifies the temporal column of the time series.
When using cross_validation make sure the forecasts are produced at the desired timestamps. Check the cutoff column which specifices the last timestamp before the forecasting window.
+
+
+
+
+
Peak Detection
+
Finally, we use the forecasts in crossvaldation_df to detect the daily hourly demand peaks. For each day, we set the detected peaks as the highest forecasts. In this case, we want to predict one peak (npeaks); depending on your setting and goals, this parameter might change. For example, the number of peaks can correspond to how many hours a battery can be discharged to reduce demand.
+
+
npeaks =1# Number of peaks
+
+
For the ERCOT 4CP detection task we are interested in correctly predicting the highest monthly load. Next, we filter the day in September with the highest hourly demand and predict the peak.
+
+
crossvalidation_df = crossvalidation_df.reset_index()[['ds','y','LGBMRegressor']]
+max_day = crossvalidation_df.iloc[crossvalidation_df['y'].argmax()].ds.day # Day with maximum load
+cv_df_day = crossvalidation_df.query('ds.dt.day == @max_day')
+max_hour = cv_df_day['y'].argmax()
+peaks = cv_df_day['LGBMRegressor'].argsort().iloc[-npeaks:].values # Predicted peaks
+
+
In the following plot we see how the LightGBM model is able to correctly detect the coincident peak for September 2022.
In this example we only include September. However, MLForecast and LightGBM can correctly predict the peaks for the 4 months of 2022. You can try this by increasing the n_windows parameter of cross_validation or filtering the Y_df dataset.
+
+
+
+
+
Next steps
+
MLForecast and LightGBM in particular are good benchmarking models for peak detection. However, it might be useful to explore further and newer forecasting algorithms or perform hyperparameter optimization.
The objective of the following article is to obtain a step-by-step guide on building Prediction intervals in forecasting models using mlforecast.
+
During this walkthrough, we will become familiar with the main MlForecast class and some relevant methods such as MLForecast.fit, MLForecast.predict and MLForecast.cross_validation in other.
The target of our prediction is something unknown (otherwise we wouldn’t be making a prediction), so we can think of it as a random variable. For example, the total sales for the next month could have different possible values, and we won’t know what the exact value will be until we get the actual sales at the end of the month. Until next month’s sales are known, this is a random amount.
+
By the time the next month draws near, we usually have a pretty good idea of possible sales values. However, if we are forecasting sales for the same month next year, the possible values can vary much more. In most forecasting cases, the variability associated with what we are forecasting reduces as we get closer to the event. In other words, the further back in time we make the prediction, the more uncertainty there is.
+
We can imagine many possible future scenarios, each yielding a different value for what we are trying to forecast.
+
When we obtain a forecast, we are estimating the middle of the range of possible values the random variable could take. Often, a forecast is accompanied by a prediction interval giving a range of values the random variable could take with relatively high probability. For example, a 95% prediction interval contains a range of values which should include the actual future value with probability 95%.
+
Rather than plotting individual possible futures , we usually show these prediction intervals instead.
+
When we generate a forecast, we usually produce a single value known as the point forecast. This value, however, doesn’t tell us anything about the uncertainty associated with the forecast. To have a measure of this uncertainty, we need prediction intervals.
+
A prediction interval is a range of values that the forecast can take with a given probability. Hence, a 95% prediction interval should contain a range of values that include the actual future value with probability 95%. Probabilistic forecasting aims to generate the full forecast distribution. Point forecasting, on the other hand, usually returns the mean or the median or said distribution. However, in real-world scenarios, it is better to forecast not only the most probable future outcome, but many alternative outcomes as well.
+
The problem is that some timeseries models provide forecast distributions, but some other ones only provide point forecasts. How can we then estimate the uncertainty of predictions?
+
+
+
Forecasts and prediction intervals
+
There are at least four sources of uncertainty in forecasting using time series models:
+
+
The random error term;
+
The parameter estimates;
+
The choice of model for the historical data;
+
The continuation of the historical data generating process into the future.
+
+
When we produce prediction intervals for time series models, we generally only take into account the first of these sources of uncertainty. It would be possible to account for 2 and 3 using simulations, but that is almost never done because it would take too much time to compute. As computing speeds increase, it might become a viable approach in the future.
+
Even if we ignore the model uncertainty and the DGP uncertainty (sources 3 and 4), and just try to allow for parameter uncertainty as well as the random error term (sources 1 and 2), there are no closed form solutions apart from some simple special cases. see full article Rob J Hyndman
+
+
Forecast distributions
+
We use forecast distributions to express the uncertainty in our predictions. These probability distributions describe the probability of observing different future values using the fitted model. The point forecast corresponds to the mean of this distribution. Most time series models generate forecasts that follow a normal distribution, which implies that we assume that possible future values follow a normal distribution. However, later in this section we will look at some alternatives to normal distributions.
+
+
Importance of Confidence Interval Prediction in Time Series:
+
+
Uncertainty Estimation: The confidence interval provides a measure of the uncertainty associated with time series predictions. It enables variability and the range of possible future values to be quantified, which is essential for making informed decisions.
+
Precision evaluation: By having a confidence interval, the precision of the predictions can be evaluated. If the interval is narrow, it indicates that the forecast is more accurate and reliable. On the other hand, if the interval is wide, it indicates greater uncertainty and less precision in the predictions.
+
Risk management: The confidence interval helps in risk management by providing information about possible future scenarios. It allows identifying the ranges in which the real values could be located and making decisions based on those possible scenarios.
+
Effective communication: The confidence interval is a useful tool for communicating predictions clearly and accurately. It allows the variability and uncertainty associated with the predictions to be conveyed to the stakeholders, avoiding a wrong or overly optimistic interpretation of the results.
+
+
Therefore, confidence interval prediction in time series is essential to understand and manage uncertainty, assess the accuracy of predictions, and make informed decisions based on possible future scenarios.
+
+
+
+
Prediction intervals
+
A prediction interval gives us a range in which we expect \(y_t\) to lie with a specified probability. For example, if we assume that the distribution of future observations follows a normal distribution, a 95% prediction interval for the forecast of step h would be represented by the range
+
\[\hat{y}_{T+h|T} \pm 1.96 \hat\sigma_h,\]
+
Where \(\hat\sigma_h\) is an estimate of the standard deviation of the h -step forecast distribution.
+
More generally, a prediction interval can be written as
+
\[\hat{y}_{T+h|T} \pm c \hat\sigma_h\]
+
In this context, the term “multiplier c” is associated with the probability of coverage. In this article, intervals of 80% and 95% are typically calculated, but any other percentage can be used. The table below shows the values of c corresponding to different coverage probabilities, assuming a normal forecast distribution.
+
+
+
+
Percentage
+
Multiplier
+
+
+
+
+
50
+
0.67
+
+
+
55
+
0.76
+
+
+
60
+
0.84
+
+
+
65
+
0.93
+
+
+
70
+
1.04
+
+
+
75
+
1.15
+
+
+
80
+
1.28
+
+
+
85
+
1.44
+
+
+
90
+
1.64
+
+
+
95
+
1.96
+
+
+
96
+
2.05
+
+
+
97
+
2.17
+
+
+
98
+
2.33
+
+
+
99
+
2.58
+
+
+
+
Prediction intervals are valuable because they reflect the uncertainty in the predictions. If we only generate point forecasts, we cannot assess how accurate those forecasts are. However, by providing prediction intervals, the amount of uncertainty associated with each forecast becomes apparent. For this reason, point forecasts may lack significant value without the inclusion of corresponding forecast intervals.
+
+
+
One-step prediction intervals
+
When making a prediction for a future step, it is possible to estimate the standard deviation of the forecast distribution using the standard deviation of the residuals, which is calculated by
where \(K\) is the number of parameters estimated in the forecasting method, and \(M\) is the number of missing values in the residuals. (For example, \(M=1\) for a naive forecast, because we can’t forecast the first observation.)
+
+
+
Multi-step prediction intervals
+
A typical feature of forecast intervals is that they tend to increase in length as the forecast horizon lengthens. As we move further out in time, there is greater uncertainty associated with the prediction, resulting in wider prediction intervals. In general, σh tends to increase as h increases (although there are some nonlinear forecasting methods that do not follow this property).
+
To generate a prediction interval, it is necessary to have an estimate of σh. As mentioned above, for one-step forecasts (h=1), equation (1) provides a good estimate of the standard deviation of the forecast, σ1. However, for multi-step forecasts, a more complex calculation method is required. These calculations assume that the residuals are uncorrelated with each other.
+
+
+
Benchmark methods
+
For the four benchmark methods, it is possible to mathematically derive the forecast standard deviation under the assumption of uncorrelated residuals. If \(\hat{\sigma}_h\) denotes the standard deviation of the \(h\) -step forecast distribution, and \(\hat{\sigma}\) is the residual standard deviation given by (1), then we can use the expressions shown in next Table. Note that when \(h=1\) and \(T\) is large, these all give the same approximate value \(\hat{\sigma}\).
+
+
+
+
Method
+
h-step forecast standard deviation
+
+
+
+
+
Mean forecasts
+
\(\hat\sigma_h = \hat\sigma\sqrt{1 + 1/T}\)
+
+
+
Naïve forecasts
+
\(\hat\sigma_h = \hat\sigma\sqrt{h}\)
+
+
+
Seasonal naïve forecasts
+
\(\hat\sigma_h = \hat\sigma\sqrt{k+1}\)
+
+
+
Drift forecasts
+
\(\hat\sigma_h = \hat\sigma\sqrt{h(1+h/T)}\)
+
+
+
+
Note that when \(h=1\) and \(T\) is large, these all give the same approximate value \(\hat{\sigma}\).
+
+
+
Prediction intervals from bootstrapped residuals
+
When a normal distribution for the residuals is an unreasonable assumption, one alternative is to use bootstrapping, which only assumes that the residuals are uncorrelated with constant variance. We will illustrate the procedure using a naïve forecasting method.
+
A one-step forecast error is defined as \(e_t = y_t - \hat{y}_{t|t-1}\). For a naïve forecasting method, {t|t-1} = y{t-1}, so we can rewrite this as \[y_t = y_{t-1} + e_t.\]
+
Assuming future errors will be similar to past errors, when \(t>T\) we can replace \(e_{t}\) by sampling from the collection of errors we have seen in the past (i.e., the residuals). So we can simulate the next observation of a time series using
+
\[y^*_{T+1} = y_{T} + e^*_{T+1}\]
+
where \(e^*_{T+1}\) is a randomly sampled error from the past, and \(y^*_{T+1}\) is the possible future value that would arise if that particular error value occurred. We use We use a * to indicate that this is not the observed \(y_{T+1}\) value, but one possible future that could occur. Adding the new simulated observation to our data set, we can repeat the process to obtain
+
\[y^*_{T+2} = y_{T+1}^* + e^*_{T+2},\]
+
where \(e^*_{T+2}\) is another draw from the collection of residuals. Continuing in this way, we can simulate an entire set of future values for our time series.
+
+
+
Conformal Prediction
+
Multi-quantile losses and statistical models can provide provide prediction intervals, but the problem is that these are uncalibrated, meaning that the actual frequency of observations falling within the interval does not align with the confidence level associated with it. For example, a calibrated 95% prediction interval should contain the true value 95% of the time in repeated sampling. An uncalibrated 95% prediction interval, on the other hand, might contain the true value only 80% of the time, or perhaps 99% of the time. In the first case, the interval is too narrow and underestimates the uncertainty, while in the second case, it is too wide and overestimates the uncertainty.
+
Statistical methods also assume normality. Here, we talk about another method called conformal prediction that doesn’t require any distributional assumptions.
+
Conformal prediction intervals use cross-validation on a point forecaster model to generate the intervals. This means that no prior probabilities are needed, and the output is well-calibrated. No additional training is needed, and the model is treated as a black box. The approach is compatible with any model
+
mlforecast now supports Conformal Prediction on all available models.
+
+
+
+
Installing mlforecast
+
+
using pip:
+
+
pip install mlforecast
+
+
using with conda:
+
+
conda install -c conda-forge mlforecast
+
+
+
+
+
Loading libraries and data
+
+
# Handling and processing of Data
+# ==============================================================================
+import numpy as np
+import pandas as pd
+
+import scipy.stats as stats
+
+# Handling and processing of Data for Date (time)
+# ==============================================================================
+import datetime
+import time
+from datetime import datetime, timedelta
+
+#
+# ==============================================================================
+from statsmodels.tsa.stattools import adfuller
+import statsmodels.api as sm
+import statsmodels.tsa.api as smt
+from statsmodels.tsa.seasonal import seasonal_decompose
+#
+# ==============================================================================
+from utilsforecast.plotting import plot_series
Plot some series using the plot method from the StatsForecast class. This method prints 8 random series from the dataset and is useful for basic EDA.
+
+
fig = plot_series(df)
+
+
+
+
The Augmented Dickey-Fuller Test
+
An Augmented Dickey-Fuller (ADF) test is a type of statistical test that determines whether a unit root is present in time series data. Unit roots can cause unpredictable results in time series analysis. A null hypothesis is formed in the unit root test to determine how strongly time series data is affected by a trend. By accepting the null hypothesis, we accept the evidence that the time series data is not stationary. By rejecting the null hypothesis or accepting the alternative hypothesis, we accept the evidence that the time series data is generated by a stationary process. This process is also known as stationary trend. The values of the ADF test statistic are negative. Lower ADF values indicate a stronger rejection of the null hypothesis.
+
Augmented Dickey-Fuller Test is a common statistical test used to test whether a given time series is stationary or not. We can achieve this by defining the null and alternate hypothesis.
+
+
Null Hypothesis: Time Series is non-stationary. It gives a time-dependent trend.
+
Alternate Hypothesis: Time Series is stationary. In another term, the series doesn’t depend on time.
+
ADF or t Statistic < critical values: Reject the null hypothesis, time series is stationary.
+
ADF or t Statistic > critical values: Failed to reject the null hypothesis, time series is non-stationary.
+
+
+
def augmented_dickey_fuller_test(series , column_name):
+print (f'Dickey-Fuller test results for columns: {column_name}')
+ dftest = adfuller(series, autolag='AIC')
+ dfoutput = pd.Series(dftest[0:4], index=['Test Statistic','p-value','No Lags Used','Number of observations used'])
+for key,value in dftest[4].items():
+ dfoutput['Critical Value (%s)'%key] = value
+print (dfoutput)
+if dftest[1] <=0.05:
+print("Conclusion:====>")
+print("Reject the null hypothesis")
+print("The data is stationary")
+else:
+print("Conclusion:====>")
+print("The null hypothesis cannot be rejected")
+print("The data is not stationary")
+
+
+
augmented_dickey_fuller_test(df["y"],'Ads')
+
+
Dickey-Fuller test results for columns: Ads
+Test Statistic -1.076452e+01
+p-value 2.472132e-19
+No Lags Used 3.900000e+01
+Number of observations used 1.028000e+04
+Critical Value (1%) -3.430986e+00
+Critical Value (5%) -2.861821e+00
+Critical Value (10%) -2.566920e+00
+dtype: float64
+Conclusion:====>
+Reject the null hypothesis
+The data is stationary
+
+
+
+
+
Autocorrelation plots
+
+
Autocorrelation Function
+
Definition 1. Let \(\{x_t;1 ≤ t ≤ n\}\) be a time series sample of size n from \(\{X_t\}\). 1. \(\bar x = \sum_{t=1}^n \frac{x_t}{n}\) is called the sample mean of \(\{X_t\}\). 2. \(c_k =\sum_{t=1}^{n−k} (x_{t+k}- \bar x)(x_t−\bar x)/n\) is known as the sample autocovariance function of \(\{X_t\}\). 3. \(r_k = c_k /c_0\) is said to be the sample autocorrelation function of \(\{X_t\}\).
+
Note the following remarks about this definition:
+
+
Like most literature, this guide uses ACF to denote the sample autocorrelation function as well as the autocorrelation function. What is denoted by ACF can easily be identified in context.
+
Clearly c0 is the sample variance of \(\{X_t\}\). Besides, \(r_0 = c_0/c_0 = 1\) and for any integer \(k, |r_k| ≤ 1\).
+
When we compute the ACF of any sample series with a fixed length \(n\), we cannot put too much confidence in the values of \(r_k\) for large k’s, since fewer pairs of \((x_{t +k }, x_t )\) are available for calculating \(r_k\) as \(k\) is large. One rule of thumb is not to estimate \(r_k\) for \(k > n/3\), and another is \(n ≥ 50, k ≤ n/4\). In any case, it is always a good idea to be careful.
+
We also compute the ACF of a nonstationary time series sample by Definition 1. In this case, however, the ACF or \(r_k\) very slowly or hardly tapers off as \(k\) increases.
+
Plotting the ACF \((r_k)\) against lag \(k\) is easy but very helpful in analyzing time series sample. Such an ACF plot is known as a correlogram.
+
If \(\{X_t\}\) is stationary with \(E(X_t)=0\) and \(\rho_k =0\) for all \(k \neq 0\),thatis,itisa white noise series, then the sampling distribution of \(r_k\) is asymptotically normal with the mean 0 and the variance of \(1/n\). Hence, there is about 95% chance that \(r_k\) falls in the interval \([−1.96/√n, 1.96/√n]\).
+
+
Now we can give a summary that (1) if the time series plot of a time series clearly shows a trend or/and seasonality, it is surely nonstationary; (2) if the ACF \(r_k\) very slowly or hardly tapers off as lag \(k\) increases, the time series should also be nonstationary.
In time series analysis to forecast new values, it is very important to know past data. More formally, we can say that it is very important to know the patterns that values follow over time. There can be many reasons that cause our forecast values to fall in the wrong direction. Basically, a time series consists of four components. The variation of those components causes the change in the pattern of the time series. These components are:
+
+
Level: This is the primary value that averages over time.
+
Trend: The trend is the value that causes increasing or decreasing patterns in a time series.
+
Seasonality: This is a cyclical event that occurs in a time series for a short time and causes short-term increasing or decreasing patterns in a time series.
+
Residual/Noise: These are the random variations in the time series.
+
+
Combining these components over time leads to the formation of a time series. Most time series consist of level and noise/residual and trend or seasonality are optional values.
+
If seasonality and trend are part of the time series, then there will be effects on the forecast value. As the pattern of the forecasted time series may be different from the previous time series.
+
The combination of the components in time series can be of two types: * Additive * multiplicative
+
Additive time series
+
If the components of the time series are added to make the time series. Then the time series is called the additive time series. By visualization, we can say that the time series is additive if the increasing or decreasing pattern of the time series is similar throughout the series. The mathematical function of any additive time series can be represented by: \[y(t) = level + Trend + seasonality + noise\]
+
+
+
Multiplicative time series
+
If the components of the time series are multiplicative together, then the time series is called a multiplicative time series. For visualization, if the time series is having exponential growth or decline with time, then the time series can be considered as the multiplicative time series. The mathematical function of the multiplicative time series can be represented as.
+
\[y(t) = Level * Trend * seasonality * Noise\]
+
+
Additive
+
+
a = seasonal_decompose(df["y"], model ="additive", period=24).plot()
+a.savefig('../../figs/prediction_intervals_in_forecasting_models__seasonal_decompose_aditive.png', bbox_inches='tight')
+plt.close()
+
+
+
+
+
Multiplicative
+
+
b = seasonal_decompose(df["y"], model ="Multiplicative", period=24).plot()
+b.savefig('../../figs/prediction_intervals_in_forecasting_models__seasonal_decompose_multiplicative.png', bbox_inches='tight')
+plt.close();
+
+
+
+
+
+
+
Split the data into training and testing
+
Let’s divide our data into sets 1. Data to train our model. 2. Data to test our model
+
For the test data we will use the last 500 hours to test and evaluate the performance of our model.
Now let’s plot the training data and the test data.
+
+
fig = plot_series(train,test)
+
+
+
+
+
Modeling with mlforecast
+
+
Building Model
+
We define the model that we want to use, for our example we are going to use the XGBoost model.
+
+
model1 = [xgb.XGBRegressor()]
+
+
We can use the MLForecast.preprocess method to explore different transformations.
+
If it is true that the series we are working with is a stationary series see (Dickey fuller test), however for the sake of practice and instruction in this guide, we will apply the difference to our series, we will do this using the target_transforms parameter and calling the diff function like: mlforecast.target_transforms.Differences
It is important to take into account when we use the parameter target_transforms=[Differences([1])] in case the series is stationary we can use a difference, or in the case that the series is not stationary, we can use more than one difference so that the series is constant over time, that is, that it is constant in mean and in variance.
+
+
prep = mlf.preprocess(df)
+prep
+
+
+
+
+
+
+
+
+
ds
+
y
+
unique_id
+
+
+
+
+
1
+
2014-07-01 00:30:00
+
-2717.0
+
1
+
+
+
2
+
2014-07-01 01:00:00
+
-1917.0
+
1
+
+
+
3
+
2014-07-01 01:30:00
+
-1554.0
+
1
+
+
+
4
+
2014-07-01 02:00:00
+
-836.0
+
1
+
+
+
5
+
2014-07-01 02:30:00
+
-947.0
+
1
+
+
+
...
+
...
+
...
+
...
+
+
+
10315
+
2015-01-31 21:30:00
+
951.0
+
1
+
+
+
10316
+
2015-01-31 22:00:00
+
1051.0
+
1
+
+
+
10317
+
2015-01-31 22:30:00
+
1588.0
+
1
+
+
+
10318
+
2015-01-31 23:00:00
+
-718.0
+
1
+
+
+
10319
+
2015-01-31 23:30:00
+
-303.0
+
1
+
+
+
+
+
10319 rows × 3 columns
+
+
+
+
This has subtacted the lag 1 from each value, we can see what our series look like now.
+
+
fig = plot_series(prep)
+
+
+
+
+
Adding features
+
+
Lags
+
Looks like the seasonality is gone, we can now try adding some lag features.
y 1.000000
+lag1 0.663082
+lag24 0.155366
+Name: y, dtype: float64
+
+
+
+
+
+
Lag transforms
+
Lag transforms are defined as a dictionary where the keys are the lags and the values are lists of functions that transform an array. These must be numba jitted functions (so that computing the features doesn’t become a bottleneck). There are some implemented in the window-ops package but you can also implement your own.
+
If the function takes two or more arguments you can either:
You can see that both approaches get to the same result, you can use whichever one you feel most comfortable with.
+
+
+
Date features
+
If your time column is made of timestamps then it might make sense to extract features like week, dayofweek, quarter, etc. You can do that by passing a list of strings with pandas time/date components. You can also pass functions that will take the time column as input, as we’ll show here.
It’s important to note that we can only use this method if we assume that the residuals of our validation predictions are normally distributed. To see if this is the case, we will use a PP-plot and test its normality with the Anderson-Darling, Kolmogorov-Smirnov, and D’Agostino K^2 tests.
+
The PP-plot(Probability-to-Probability) plots the data sample against the normal distribution plot in such a way that if normally distributed, the data points will form a straight line.
+
The three normality tests determine how likely a data sample is from a normally distributed population using p-values. The null hypothesis for each test is that “the sample came from a normally distributed population”. This means that if the resulting p-values are below a chosen alpha value, then the null hypothesis is rejected. Thus there is evidence to suggest that the data comes from a non-normal distribution. For this article, we will use an Alpha value of 0.01.
Now let’s visualize the result of our forecast and the historical data of our time series, also let’s draw the confidence interval that we have obtained when making the prediction with 95% confidence.
The confidence interval is a range of values that has a high probability of containing the true value of a variable. In machine learning time series models, the confidence interval is used to estimate the uncertainty in the predictions.
+
One of the main benefits of using the confidence interval is that it allows users to understand the accuracy of the predictions. For example, if the confidence interval is very wide, it means that the prediction is less accurate. Conversely, if the confidence interval is very narrow, it means that the prediction is more accurate.
+
Another benefit of the confidence interval is that it helps users make informed decisions. For example, if a prediction is within the confidence interval, it means that it is likely to come true. Conversely, if a prediction is outside the confidence interval, it means that it is less likely to come true.
+
In general, the confidence interval is an important tool for machine learning time series models. It helps users understand the accuracy of the forecasts and make informed decisions.
+
+
+
+
References
+
+
Changquan Huang • Alla Petukhina. Springer series (2022). Applied Time Series Analysis and Forecasting with Python.
Transformations that will be applied to the target before computing the features and restored after the forecasting step.
+
+
+
+
The MLForecast object encapsulates the feature engineering + training the models + forecasting. When we initialize it we define:
+
+
The models we want to train
+
The series frequency. This is added to the last dates seen in train for the forecast step, if the time column contains integer values we can leave it empty or set it to 1.
+
The feature engineering:
+
+
Lags to use as features
+
Transformations on the lags
+
Date features
+
Transformations on the target
+
+
Number of threads to use when computing the features.
Apply the feature engineering and train the models.
+
+
+
+
+
+
+
+
+
+
+
Type
+
Default
+
Details
+
+
+
+
+
df
+
DataFrame
+
+
Series data in long format.
+
+
+
id_col
+
str
+
unique_id
+
Column that identifies each serie.
+
+
+
time_col
+
str
+
ds
+
Column that identifies each timestep, its values can be timestamps or integers.
+
+
+
target_col
+
str
+
y
+
Column that contains the target.
+
+
+
static_features
+
typing.Optional[typing.List[str]]
+
None
+
Names of the features that are static and will be repeated when forecasting. If None, will consider all columns (except id_col and time_col) as static.
+
+
+
dropna
+
bool
+
True
+
Drop rows with missing values produced by the transformations.
+
+
+
keep_last_n
+
typing.Optional[int]
+
None
+
Keep only these many records from each serie for the forecasting step. Can save time and memory if your features allow it.
Function to call on the features before computing the predictions. This function will take the input dataframe that will be passed to the model for predicting and should return a dataframe with the same structure. The series identifier is on the index.
+
+
+
after_predict_callback
+
typing.Optional[typing.Callable]
+
None
+
Function to call on the predictions before updating the targets. This function will take a pandas Series with the predictions and should return another one with the same structure. The series identifier is on the index.
+
+
+
new_df
+
typing.Optional[pandas.core.frame.DataFrame]
+
None
+
Series data of new observations for which forecasts are to be generated. This dataframe should have the same structure as the one used to fit the model, including any features and time series data. If new_df is not None, the method will generate forecasts for the new observations.
With MLForecast, you can generate prediction intervals using Conformal Prediction. To configure Conformal Prediction, you need to pass an instance of the PredictionIntervals class to the prediction_intervals argument of the fit method. The class takes three parameters: n_windows, h and method.
+
+
n_windows represents the number of cross-validation windows used to calibrate the intervals
+
h is the forecast horizon
+
method can be conformal_distribution or conformal_error; conformal_distribution (default) creates forecasts paths based on the cross-validation errors and calculate quantiles using those paths, on the other hand conformal_error calculates the error quantiles to produce prediction intervals. The strategy will adjust the intervals for each horizon step, resulting in different widths for each step. Please note that a minimum of 2 cross-validation windows must be used.
If you want to reduce the computational time and produce intervals with the same width for the whole forecast horizon, simple pass h=1 to the PredictionIntervals class. The caveat of this strategy is that in some cases, variance of the absolute residuals maybe be small (even zero), so the intervals may be too narrow.
MLForecast allows you to use a pretrained model to generate forecasts for a new dataset. Simply provide a pandas dataframe containing the new observations as the value for the new_df argument when calling the predict method. The dataframe should have the same structure as the one used to fit the model, including any features and time series data. The function will then use the pretrained model to generate forecasts for the new observations. This allows you to easily apply a pretrained model to a new dataset and generate forecasts without the need to retrain the model.
+
+
ercot_df = pd.read_csv('https://datasets-nixtla.s3.amazonaws.com/ERCOT-clean.csv')
+# we have to convert the ds column to integers
+# since MLForecast was trained with that structure
+ercot_df['ds'] = np.arange(1, len(ercot_df) +1)
+# use the `new_df` argument to pass the ercot dataset
+ercot_fcsts = fcst.predict(horizon, new_df=ercot_df)
+fig = plot_series(ercot_df, ercot_fcsts, max_insample_length=48*2)
+fig.get_axes()[0].set_title('ERCOT forecasts trained on M4-Hourly dataset')
+fig.savefig('figs/forecast__ercot.png', bbox_inches='tight')
+
+
+
If you want to take a look at the data that will be used to train the models you can call Forecast.preprocess.
Perform time series cross validation. Creates n_windows splits where each window has h test periods, trains the models, computes the predictions and merges the actuals.
+
+
+
+
+
+
+
+
+
+
+
Type
+
Default
+
Details
+
+
+
+
+
df
+
DataFrame
+
+
Series data in long format.
+
+
+
n_windows
+
int
+
+
Number of windows to evaluate.
+
+
+
h
+
int
+
+
Forecast horizon.
+
+
+
id_col
+
str
+
unique_id
+
Column that identifies each serie.
+
+
+
time_col
+
str
+
ds
+
Column that identifies each timestep, its values can be timestamps or integers.
+
+
+
target_col
+
str
+
y
+
Column that contains the target.
+
+
+
step_size
+
typing.Optional[int]
+
None
+
Step size between each cross validation window. If None it will be equal to h.
+
+
+
static_features
+
typing.Optional[typing.List[str]]
+
None
+
Names of the features that are static and will be repeated when forecasting.
+
+
+
dropna
+
bool
+
True
+
Drop rows with missing values produced by the transformations.
+
+
+
keep_last_n
+
typing.Optional[int]
+
None
+
Keep only these many records from each serie for the forecasting step. Can save time and memory if your features allow it.
+
+
+
refit
+
typing.Union[bool, int]
+
True
+
Retrain model for each cross validation window. If False, the models are trained at the beginning and then used to predict each window. If positive int, the models are retrained every refit windows.
+
+
+
max_horizon
+
typing.Optional[int]
+
None
+
+
+
+
before_predict_callback
+
typing.Optional[typing.Callable]
+
None
+
Function to call on the features before computing the predictions. This function will take the input dataframe that will be passed to the model for predicting and should return a dataframe with the same structure. The series identifier is on the index.
+
+
+
after_predict_callback
+
typing.Optional[typing.Callable]
+
None
+
Function to call on the predictions before updating the targets. This function will take a pandas Series with the predictions and should return another one with the same structure. The series identifier is on the index.
Confidence levels between 0 and 100 for prediction intervals.
+
+
+
input_size
+
typing.Optional[int]
+
None
+
Maximum training samples per serie in each window. If None, will use an expanding window.
+
+
+
fitted
+
bool
+
False
+
Store the in-sample predictions.
+
+
+
Returns
+
pandas DataFrame
+
+
Predictions for each window with the series id, timestamp, last train date, target value and predictions from each model.
+
+
+
+
If we would like to know how good our forecast will be for a specific model and set of features then we can perform cross validation. What cross validation does is take our data and split it in two parts, where the first part is used for training and the second one for validation. Since the data is time dependant we usually take the last x observations from our data as the validation set.
+
This process is implemented in MLForecast.cross_validation, which takes our data and performs the process described above for n_windows times where each window has h validation samples in it. For example, if we have 100 samples and we want to perform 2 backtests each of size 14, the splits will be as follows:
+
+
Train: 1 to 72. Validation: 73 to 86.
+
Train: 1 to 86. Validation: 87 to 100.
+
+
You can control the size between each cross validation window using the step_size argument. For example, if we have 100 samples and we want to perform 2 backtests each of size 14 and move one step ahead in each fold (step_size=1), the splits will be as follows:
+
+
Train: 1 to 85. Validation: 86 to 99.
+
Train: 1 to 86. Validation: 87 to 100.
+
+
You can also perform cross validation without refitting your models for each window by setting refit=False. This allows you to evaluate the performance of your models using multiple window sizes without having to retrain them each time.
Once you’ve found a set of features and parameters that work for your problem you can build a forecast object from it using MLForecast.from_cv, which takes the trained LightGBMCV object and builds an MLForecast object that will use the same features and parameters. Then you can call fit and predict as you normally would.
Array made up of different groups. Can be thought of (and iterated) as a list of arrays.
+
All the data is stored in a single 1d array data. The indices for the group boundaries are stored in another 1d array indptr.
+
+
from fastcore.test import test_eq, test_fail
+
+
+
# The `GroupedArray` is used internally for storing the series values and performing transformations.
+data = np.arange(10, dtype=np.float32)
+indptr = np.array([0, 2, 10]) # group 1: [0, 1], group 2: [2..9]
+ga = GroupedArray(data, indptr)
+test_eq(len(ga), 2)
+test_eq(str(ga), 'GroupedArray(ndata=10, ngroups=2)')
+
+
+
# Iterate through the groups
+ga_iter =iter(ga)
+np.testing.assert_equal(next(ga_iter), np.array([0, 1]))
+np.testing.assert_equal(next(ga_iter), np.arange(2, 10))
+
+
+
# Take the last two observations from every group
+last_2 = ga.take_from_groups(slice(-2, None))
+np.testing.assert_equal(last_2.data, np.array([0, 1, 8, 9]))
+np.testing.assert_equal(last_2.indptr, np.array([0, 2, 4]))
+
+
+
# Take the last four observations from every group. Note that since group 1 only has two elements, only these are returned.
+last_4 = ga.take_from_groups(slice(-4, None))
+np.testing.assert_equal(last_4.data, np.array([0, 1, 6, 7, 8, 9]))
+np.testing.assert_equal(last_4.indptr, np.array([0, 2, 6]))
+
+
+
# Select a specific subset of groups
+indptr = np.array([0, 2, 4, 7, 10])
+ga2 = GroupedArray(data, indptr)
+subset = ga2.take([0, 2])
+np.testing.assert_allclose(subset[0].data, ga2[0].data)
+np.testing.assert_allclose(subset[1].data, ga2[2].data)
+
+
+
# The groups are [0, 1], [2, ..., 9]. expand_target(2) should take rolling pairs of them and fill with nans when there aren't enough
+np.testing.assert_equal(
+ ga.expand_target(2),
+ np.array([
+ [0, 1],
+ [1, np.nan],
+ [2, 3],
+ [3, 4],
+ [4, 5],
+ [5, 6],
+ [6, 7],
+ [7, 8],
+ [8, 9],
+ [9, np.nan]
+ ])
+)
+
+
+
# try to append new values that don't match the number of groups
+test_fail(lambda: ga.append(np.array([1., 2., 3.])), contains='new must be of size 2')
+Scalable machine learning for time series forecasting
+
+
+
mlforecast is a framework to perform time series forecasting using machine learning models, with the option to scale to massive amounts of data using remote clusters.
+
+
+
Install
+
+
PyPI
+
pip install mlforecast
+
+
+
conda-forge
+
conda install -c conda-forge mlforecast
+
For more detailed instructions you can refer to the installation page.
Current Python alternatives for machine learning models are slow, inaccurate and don’t scale well. So we created a library that can be used to forecast in production environments. MLForecast includes efficient feature engineering to train any machine learning model (with fit and predict methods such as sklearn) to fit millions of time series.
+
+
+
Features
+
+
Fastest implementations of feature engineering for time series forecasting in Python.
+
Out-of-the-box compatibility with Spark, Dask, and Ray.
+
Probabilistic Forecasting with Conformal Prediction.
+
Support for exogenous variables and static covariates.
+
Familiar sklearn syntax: .fit and .predict.
+
+
Missing something? Please open an issue or write us in
+
+
+
Examples and Guides
+
📚 End to End Walkthrough: model training, evaluation and selection for multiple time series.
Next define your models. If you want to use the local interface this can be any regressor that follows the scikit-learn API. For distributed training there are LGBMForecast and XGBForecast.
Now instantiate a MLForecast object with the models and the features that you want to use. The features can be lags, transformations on the lags and date features. The lag transformations are defined as numbajitted functions that transform an array, if they have additional arguments you can either supply a tuple (transform_func, arg1, arg2, …) or define new functions fixing the arguments. You can also define differences to apply to the series before fitting that will be restored when predicting.
To get the forecasts for the next n days call predict(n) on the forecast object. This will automatically handle the updates required by the features using a recursive strategy.
What LightGBMCV does is emulate LightGBM’s cv function where several Boosters are trained simultaneously on different partitions of the data, that is, one boosting iteration is performed on all of them at a time. This allows to have an estimate of the error by iteration, so if we combine this with early stopping we can find the best iteration to train a final model using all the data or even use these individual models’ predictions to compute an ensemble.
+
In order to have a good estimate of the forecasting performance of our model we compute predictions for the whole test period and compute a metric on that. Since this step can slow down training, there’s an eval_every parameter that can be used to control this, that is, if eval_every=10 (the default) every 10 boosting iterations we’re going to compute forecasts for the complete window and report the error.
+
We also have early stopping parameters:
+
+
early_stopping_evals: how many evaluations of the full window should we go without improving to stop training?
+
early_stopping_pct: what’s the minimum percentage improvement we want in these early_stopping_evals in order to keep training?
+
+
This makes the LightGBMCV class a good tool to quickly test different configurations of the model. Consider the following example, where we’re going to try to find out which features can improve the performance of our model. We start just using lags.
+
+
static_fit_config =dict(
+ n_windows=2,
+ h=horizon,
+ params={'verbose': -1},
+ compute_cv_preds=True,
+)
+cv = LightGBMCV(
+ freq=1,
+ lags=[24* (i+1) for i inrange(7)], # one week of lags
+)
Train boosters simultaneously and assess their performance on the complete forecasting window.
+
+
+
+
+
+
+
+
+
+
+
Type
+
Default
+
Details
+
+
+
+
+
df
+
DataFrame
+
+
Series data in long format.
+
+
+
n_windows
+
int
+
+
Number of windows to evaluate.
+
+
+
h
+
int
+
+
Forecast horizon.
+
+
+
id_col
+
str
+
unique_id
+
Column that identifies each serie.
+
+
+
time_col
+
str
+
ds
+
Column that identifies each timestep, its values can be timestamps or integers.
+
+
+
target_col
+
str
+
y
+
Column that contains the target.
+
+
+
step_size
+
typing.Optional[int]
+
None
+
Step size between each cross validation window. If None it will be equal to h.
+
+
+
num_iterations
+
int
+
100
+
Maximum number of boosting iterations to run.
+
+
+
params
+
typing.Optional[typing.Dict[str, typing.Any]]
+
None
+
Parameters to be passed to the LightGBM Boosters.
+
+
+
static_features
+
typing.Optional[typing.List[str]]
+
None
+
Names of the features that are static and will be repeated when forecasting.
+
+
+
dropna
+
bool
+
True
+
Drop rows with missing values produced by the transformations.
+
+
+
keep_last_n
+
typing.Optional[int]
+
None
+
Keep only these many records from each serie for the forecasting step. Can save time and memory if your features allow it.
+
+
+
eval_every
+
int
+
10
+
Number of boosting iterations to train before evaluating on the whole forecast window.
+
+
+
weights
+
typing.Optional[typing.Sequence[float]]
+
None
+
Weights to multiply the metric of each window. If None, all windows have the same weight.
+
+
+
metric
+
typing.Union[str, typing.Callable]
+
mape
+
Metric used to assess the performance of the models and perform early stopping.
+
+
+
verbose_eval
+
bool
+
True
+
Print the metrics of each evaluation.
+
+
+
early_stopping_evals
+
int
+
2
+
Maximum number of evaluations to run without improvement.
+
+
+
early_stopping_pct
+
float
+
0.01
+
Minimum percentage improvement in metric value in early_stopping_evals evaluations.
+
+
+
compute_cv_preds
+
bool
+
False
+
Compute predictions for each window after finding the best iteration.
+
+
+
before_predict_callback
+
typing.Optional[typing.Callable]
+
None
+
Function to call on the features before computing the predictions. This function will take the input dataframe that will be passed to the model for predicting and should return a dataframe with the same structure. The series identifier is on the index.
+
+
+
after_predict_callback
+
typing.Optional[typing.Callable]
+
None
+
Function to call on the predictions before updating the targets. This function will take a pandas Series with the predictions and should return another one with the same structure. The series identifier is on the index.
+
+
+
input_size
+
typing.Optional[int]
+
None
+
Maximum training samples per serie in each window. If None, will use an expanding window.
Compute predictions with each of the trained boosters.
+
+
+
+
+
+
+
+
+
+
+
Type
+
Default
+
Details
+
+
+
+
+
h
+
int
+
+
Forecast horizon.
+
+
+
before_predict_callback
+
typing.Optional[typing.Callable]
+
None
+
Function to call on the features before computing the predictions. This function will take the input dataframe that will be passed to the model for predicting and should return a dataframe with the same structure. The series identifier is on the index.
+
+
+
after_predict_callback
+
typing.Optional[typing.Callable]
+
None
+
Function to call on the predictions before updating the targets. This function will take a pandas Series with the predictions and should return another one with the same structure. The series identifier is on the index.
+
+
+
X_df
+
typing.Optional[pandas.core.frame.DataFrame]
+
None
+
Dataframe with the future exogenous features. Should have the id column and the time column.
+
+
+
Returns
+
DataFrame
+
+
Predictions for each serie and timestep, with one column per window.
+
+
+
+
+
preds = cv.predict(horizon)
+preds
+
+
+
+
+
+
+
+
+
unique_id
+
ds
+
Booster0
+
Booster1
+
+
+
+
+
0
+
H196
+
961
+
15.670252
+
15.848888
+
+
+
1
+
H196
+
962
+
15.522924
+
15.697399
+
+
+
2
+
H196
+
963
+
14.985832
+
15.166213
+
+
+
3
+
H196
+
964
+
14.985832
+
14.723238
+
+
+
4
+
H196
+
965
+
14.562152
+
14.451092
+
+
+
...
+
...
+
...
+
...
+
...
+
+
+
187
+
H413
+
1004
+
70.695242
+
65.917620
+
+
+
188
+
H413
+
1005
+
66.216580
+
62.615788
+
+
+
189
+
H413
+
1006
+
63.896573
+
67.848598
+
+
+
190
+
H413
+
1007
+
46.922797
+
50.981950
+
+
+
191
+
H413
+
1008
+
45.006541
+
42.752819
+
+
+
+
+
192 rows × 4 columns
+
+
+
+
We can average these predictions and evaluate them.
Now, since these series are hourly, maybe we can try to remove the daily seasonality by taking the 168th (24 * 7) difference, that is, substract the value at the same hour from one week ago, thus our target will be \(z_t = y_{t} - y_{t-168}\). The features will be computed from this target and when we predict they will be automatically re-applied.
+
+
cv2 = LightGBMCV(
+ freq=1,
+ target_transforms=[Differences([24*7])],
+ lags=[24* (i+1) for i inrange(7)],
+)
+hist2 = cv2.fit(train, **static_fit_config)
+
+
[LightGBM] [Info] Start training from score 0.519010
+[10] mape: 0.089024
+[20] mape: 0.090683
+[30] mape: 0.092316
+Early stopping at round 30
+Using best iteration: 10
+
+
+
+
assert hist2[-1][1] < hist[-1][1]
+
+
Nice! We achieve a better score in less iterations. Let’s see if this improvement translates to the validation set as well.
Great! Maybe we can try some lag transforms now. We’ll try the seasonal rolling mean that averages the values “every season”, that is, if we set season_length=24 and window_size=7 then we’ll average the value at the same hour for every day of the week.
Nice! mlforecast also supports date features, but in this case our time column is made from integers so there aren’t many possibilites here. As you can see this allows you to iterate faster and get better estimates of the forecasting performance you can expect from your model.
+
If you’re doing hyperparameter tuning it’s useful to be able to run a couple of iterations, assess the performance, and determine if this particular configuration isn’t promising and should be discarded. For example, optuna has pruners that you can call with your current score and it decides if the trial should be discarded. We’ll now show how to do that.
+
Since the CV requires a bit of setup, like the LightGBM datasets and the internal features, we have this setup method.
Function to call on the features before computing the predictions. This function will take the input dataframe that will be passed to the model for predicting and should return a dataframe with the same structure. The series identifier is on the index.
+
+
+
after_predict_callback
+
typing.Optional[typing.Callable]
+
None
+
Function to call on the predictions before updating the targets. This function will take a pandas Series with the predictions and should return another one with the same structure. The series identifier is on the index.
+
+
+
Returns
+
float
+
+
Weighted metric after training for num_iterations.
+
+
+
+
+
score = cv4.partial_fit(10)
+score
+
+
[LightGBM] [Info] Start training from score 51.745632
+
+
+
0.5906900462828166
+
+
+
This is equal to the first evaluation from our first example.
+
+
assert hist[0][1] == score
+
+
We can now use this score to decide if this configuration is promising. If we want to we can train some more iterations.
+
+
score2 = cv4.partial_fit(20)
+
+
This is now equal to our third metric from the first example, since this time we trained for 20 iterations.
+
+
assert hist[2][1] == score2
+
+
+
+
Using a custom metric
+
The built-in metrics are MAPE and RMSE, which are computed by serie and then averaged across all series. If you want to do something different or use a different metric entirely, you can define your own metric like the following:
+
+
def weighted_mape(
+ y_true: pd.Series,
+ y_pred: pd.Series,
+ ids: pd.Series,
+ dates: pd.Series,
+):
+"""Weighs the MAPE by the magnitude of the series values"""
+ abs_pct_err =abs(y_true - y_pred) /abs(y_true)
+ mape_by_serie = abs_pct_err.groupby(ids).mean()
+ totals_per_serie = y_pred.groupby(ids).sum()
+ series_weights = totals_per_serie / totals_per_serie.sum()
+return (mape_by_serie * series_weights).sum()
+
+
+
+
\ No newline at end of file
diff --git a/robots.txt b/robots.txt
new file mode 100644
index 00000000..d06c0806
--- /dev/null
+++ b/robots.txt
@@ -0,0 +1 @@
+Sitemap: https://Nixtla.github.io/sitemap.xml
diff --git a/search.json b/search.json
new file mode 100644
index 00000000..bfaf0785
--- /dev/null
+++ b/search.json
@@ -0,0 +1,877 @@
+[
+ {
+ "objectID": "forecast.html",
+ "href": "forecast.html",
+ "title": "MLForecast",
+ "section": "",
+ "text": "Data\nThis shows an example with just 4 series of the M4 dataset. If you want to run it yourself on all of them, you can refer to this notebook.\n\nimport random\n\nimport lightgbm as lgb\nimport matplotlib.pyplot as plt\nimport numpy as np\nimport xgboost as xgb\nfrom datasetsforecast.m4 import M4, M4Info\nfrom sklearn.linear_model import LinearRegression\nfrom sklearn.metrics import mean_squared_error\nfrom utilsforecast.evaluation import evaluate\nfrom utilsforecast.losses import mape\nfrom utilsforecast.plotting import plot_series\nfrom window_ops.ewm import ewm_mean\nfrom window_ops.expanding import expanding_mean\nfrom window_ops.rolling import rolling_mean\n\nfrom mlforecast.lgb_cv import LightGBMCV\nfrom mlforecast.target_transforms import Differences, LocalStandardScaler\nfrom mlforecast.utils import generate_daily_series, generate_prices_for_series\n\n\ngroup = 'Hourly'\nawait M4.async_download('data', group=group)\ndf, *_ = M4.load(directory='data', group=group)\ndf['ds'] = df['ds'].astype('int')\nids = df['unique_id'].unique()\nrandom.seed(0)\nsample_ids = random.choices(ids, k=4)\nsample_df = df[df['unique_id'].isin(sample_ids)]\nsample_df\n\n\n\n\n\n\n\n\nunique_id\nds\ny\n\n\n\n\n86796\nH196\n1\n11.8\n\n\n86797\nH196\n2\n11.4\n\n\n86798\nH196\n3\n11.1\n\n\n86799\nH196\n4\n10.8\n\n\n86800\nH196\n5\n10.6\n\n\n...\n...\n...\n...\n\n\n325235\nH413\n1004\n99.0\n\n\n325236\nH413\n1005\n88.0\n\n\n325237\nH413\n1006\n47.0\n\n\n325238\nH413\n1007\n41.0\n\n\n325239\nH413\n1008\n34.0\n\n\n\n\n4032 rows × 3 columns\n\n\n\nWe now split this data into train and validation.\n\ninfo = M4Info[group]\nhorizon = info.horizon\nvalid = sample_df.groupby('unique_id').tail(horizon)\ntrain = sample_df.drop(valid.index)\ntrain.shape, valid.shape\n\n((3840, 3), (192, 3))\n\n\n\n\n\nMLForecast\n\n MLForecast (models:Union[sklearn.base.BaseEstimator,List[sklearn.base.Bas\n eEstimator],Dict[str,sklearn.base.BaseEstimator]], freq:Union\n [int,str,pandas._libs.tslibs.offsets.BaseOffset,NoneType]=Non\n e, lags:Optional[Iterable[int]]=None, lag_transforms:Optional\n [Dict[int,List[Union[Callable,Tuple[Callable,Any]]]]]=None,\n date_features:Optional[Iterable[Union[str,Callable]]]=None,\n num_threads:int=1, target_transforms:Optional[List[mlforecast\n .target_transforms.BaseTargetTransform]]=None)\n\nForecasting pipeline\n\n\n\n\n\n\n\n\n\n\nType\nDefault\nDetails\n\n\n\n\nmodels\ntyping.Union[sklearn.base.BaseEstimator, typing.List[sklearn.base.BaseEstimator], typing.Dict[str, sklearn.base.BaseEstimator]]\n\nModels that will be trained and used to compute the forecasts.\n\n\nfreq\ntyping.Union[int, str, pandas._libs.tslibs.offsets.BaseOffset, NoneType]\nNone\nPandas offset, pandas offset alias, e.g. ‘D’, ‘W-THU’ or integer denoting the frequency of the series.\n\n\nlags\ntyping.Optional[typing.Iterable[int]]\nNone\nLags of the target to use as features.\n\n\nlag_transforms\ntyping.Optional[typing.Dict[int, typing.List[typing.Union[typing.Callable, typing.Tuple[typing.Callable, typing.Any]]]]]\nNone\nMapping of target lags to their transformations.\n\n\ndate_features\ntyping.Optional[typing.Iterable[typing.Union[str, typing.Callable]]]\nNone\nFeatures computed from the dates. Can be pandas date attributes or functions that will take the dates as input.\n\n\nnum_threads\nint\n1\nNumber of threads to use when computing the features.\n\n\ntarget_transforms\ntyping.Optional[typing.List[mlforecast.target_transforms.BaseTargetTransform]]\nNone\nTransformations that will be applied to the target before computing the features and restored after the forecasting step.\n\n\n\nThe MLForecast object encapsulates the feature engineering + training the models + forecasting. When we initialize it we define:\n\nThe models we want to train\nThe series frequency. This is added to the last dates seen in train for the forecast step, if the time column contains integer values we can leave it empty or set it to 1.\nThe feature engineering:\n\nLags to use as features\nTransformations on the lags\nDate features\nTransformations on the target\n\nNumber of threads to use when computing the features.\n\n\nfcst = MLForecast(\n models=lgb.LGBMRegressor(random_state=0, verbosity=-1),\n lags=[24 * (i+1) for i in range(7)],\n lag_transforms={\n 48: [(ewm_mean, 0.3)],\n },\n num_threads=1,\n target_transforms=[Differences([24])],\n)\nfcst\n\nMLForecast(models=[LGBMRegressor], freq=1, lag_features=['lag24', 'lag48', 'lag72', 'lag96', 'lag120', 'lag144', 'lag168', 'ewm_mean_lag48_alpha0.3'], date_features=[], num_threads=1)\n\n\nOnce we have this setup we can compute the features and fit the model.\n\n\n\nMLForecast.fit\n\n MLForecast.fit (df:pandas.core.frame.DataFrame, id_col:str='unique_id',\n time_col:str='ds', target_col:str='y',\n static_features:Optional[List[str]]=None,\n dropna:bool=True, keep_last_n:Optional[int]=None,\n max_horizon:Optional[int]=None, prediction_intervals:Opti\n onal[mlforecast.utils.PredictionIntervals]=None,\n fitted:bool=False)\n\nApply the feature engineering and train the models.\n\n\n\n\n\n\n\n\n\n\nType\nDefault\nDetails\n\n\n\n\ndf\nDataFrame\n\nSeries data in long format.\n\n\nid_col\nstr\nunique_id\nColumn that identifies each serie.\n\n\ntime_col\nstr\nds\nColumn that identifies each timestep, its values can be timestamps or integers.\n\n\ntarget_col\nstr\ny\nColumn that contains the target.\n\n\nstatic_features\ntyping.Optional[typing.List[str]]\nNone\nNames of the features that are static and will be repeated when forecasting. If None, will consider all columns (except id_col and time_col) as static.\n\n\ndropna\nbool\nTrue\nDrop rows with missing values produced by the transformations.\n\n\nkeep_last_n\ntyping.Optional[int]\nNone\nKeep only these many records from each serie for the forecasting step. Can save time and memory if your features allow it.\n\n\nmax_horizon\ntyping.Optional[int]\nNone\n\n\n\nprediction_intervals\ntyping.Optional[mlforecast.utils.PredictionIntervals]\nNone\nConfiguration to calibrate prediction intervals (Conformal Prediction).\n\n\nfitted\nbool\nFalse\nSave in-sample predictions.\n\n\nReturns\nMLForecast\n\nForecast object with series values and trained models.\n\n\n\n\nfcst.fit(train, fitted=True);\n\n\n\n\nMLForecast.forecast_fitted_values\n\n MLForecast.forecast_fitted_values ()\n\nAccess in-sample predictions.\n\nfcst.forecast_fitted_values()\n\n\n\n\n\n\n\n\nunique_id\nds\ny\nLGBMRegressor\n\n\n\n\n0\nH196\n193\n12.7\n12.671271\n\n\n1\nH196\n194\n12.3\n12.271271\n\n\n2\nH196\n195\n11.9\n11.871271\n\n\n3\nH196\n196\n11.7\n11.671271\n\n\n4\nH196\n197\n11.4\n11.471271\n\n\n...\n...\n...\n...\n...\n\n\n3067\nH413\n956\n59.0\n68.280574\n\n\n3068\nH413\n957\n58.0\n70.427570\n\n\n3069\nH413\n958\n53.0\n44.767965\n\n\n3070\nH413\n959\n38.0\n48.691257\n\n\n3071\nH413\n960\n46.0\n46.652238\n\n\n\n\n3072 rows × 4 columns\n\n\n\nOnce we’ve run this we’re ready to compute our predictions.\n\n\n\nMLForecast.predict\n\n MLForecast.predict (h:int,\n before_predict_callback:Optional[Callable]=None,\n after_predict_callback:Optional[Callable]=None,\n new_df:Optional[pandas.core.frame.DataFrame]=None,\n level:Optional[List[Union[int,float]]]=None,\n X_df:Optional[pandas.core.frame.DataFrame]=None,\n ids:Optional[List[str]]=None)\n\nCompute the predictions for the next h steps.\n\n\n\n\n\n\n\n\n\n\nType\nDefault\nDetails\n\n\n\n\nh\nint\n\nNumber of periods to predict.\n\n\nbefore_predict_callback\ntyping.Optional[typing.Callable]\nNone\nFunction to call on the features before computing the predictions. This function will take the input dataframe that will be passed to the model for predicting and should return a dataframe with the same structure. The series identifier is on the index.\n\n\nafter_predict_callback\ntyping.Optional[typing.Callable]\nNone\nFunction to call on the predictions before updating the targets. This function will take a pandas Series with the predictions and should return another one with the same structure. The series identifier is on the index.\n\n\nnew_df\ntyping.Optional[pandas.core.frame.DataFrame]\nNone\nSeries data of new observations for which forecasts are to be generated. This dataframe should have the same structure as the one used to fit the model, including any features and time series data. If new_df is not None, the method will generate forecasts for the new observations.\n\n\nlevel\ntyping.Optional[typing.List[typing.Union[int, float]]]\nNone\nConfidence levels between 0 and 100 for prediction intervals.\n\n\nX_df\ntyping.Optional[pandas.core.frame.DataFrame]\nNone\nDataframe with the future exogenous features. Should have the id column and the time column.\n\n\nids\ntyping.Optional[typing.List[str]]\nNone\nList with subset of ids seen during training for which the forecasts should be computed.\n\n\nReturns\nDataFrame\n\nPredictions for each serie and timestep, with one column per model.\n\n\n\n\npredictions = fcst.predict(horizon)\n\nWe can see at a couple of results.\n\nresults = valid.merge(predictions, on=['unique_id', 'ds'])\nfig = plot_series(train, results, max_insample_length=0)\nfig.savefig('figs/forecast__predict.png', bbox_inches='tight')\n\n\n\nPrediction intervals\nWith MLForecast, you can generate prediction intervals using Conformal Prediction. To configure Conformal Prediction, you need to pass an instance of the PredictionIntervals class to the prediction_intervals argument of the fit method. The class takes three parameters: n_windows, h and method.\n\nn_windows represents the number of cross-validation windows used to calibrate the intervals\nh is the forecast horizon\nmethod can be conformal_distribution or conformal_error; conformal_distribution (default) creates forecasts paths based on the cross-validation errors and calculate quantiles using those paths, on the other hand conformal_error calculates the error quantiles to produce prediction intervals. The strategy will adjust the intervals for each horizon step, resulting in different widths for each step. Please note that a minimum of 2 cross-validation windows must be used.\n\n\nfcst.fit(\n train, \n prediction_intervals=PredictionIntervals(n_windows=3, h=48)\n);\n\nAfter that, you just have to include your desired confidence levels to the predict method using the level argument. Levels must lie between 0 and 100.\n\npredictions_w_intervals = fcst.predict(48, level=[50, 80, 95])\n\n\npredictions_w_intervals.head()\n\n\n\n\n\n\n\n\nunique_id\nds\nLGBMRegressor\nLGBMRegressor-lo-95\nLGBMRegressor-lo-80\nLGBMRegressor-lo-50\nLGBMRegressor-hi-50\nLGBMRegressor-hi-80\nLGBMRegressor-hi-95\n\n\n\n\n0\nH196\n961\n16.071271\n15.958042\n15.971271\n16.005091\n16.137452\n16.171271\n16.184501\n\n\n1\nH196\n962\n15.671271\n15.553632\n15.553632\n15.578632\n15.763911\n15.788911\n15.788911\n\n\n2\nH196\n963\n15.271271\n15.153632\n15.153632\n15.162452\n15.380091\n15.388911\n15.388911\n\n\n3\nH196\n964\n14.971271\n14.858042\n14.871271\n14.905091\n15.037452\n15.071271\n15.084501\n\n\n4\nH196\n965\n14.671271\n14.553632\n14.553632\n14.562452\n14.780091\n14.788911\n14.788911\n\n\n\n\n\n\n\n\n# test we can forecast horizon lower than h \n# with prediction intervals\nfor method in ['conformal_distribution', 'conformal_errors']:\n fcst.fit(\n train, \n prediction_intervals=PredictionIntervals(n_windows=3, h=48)\n )\n\n preds_h_lower_h = fcst.predict(1, level=[50, 80, 95])\n preds_h_lower_h = fcst.predict(30, level=[50, 80, 95])\n\n # test monotonicity of intervals\n test_eq(\n preds_h_lower_h.filter(regex='lo|hi').apply(\n lambda x: x.is_monotonic_increasing,\n axis=1\n ).sum(),\n len(preds_h_lower_h)\n )\n\nLet’s explore the generated intervals.\n\nresults = valid.merge(predictions_w_intervals, on=['unique_id', 'ds'])\nfig = plot_series(train, results, max_insample_length=0, level=[50, 80, 95])\nfig.savefig('figs/forecast__predict_intervals.png', bbox_inches='tight')\n\n\nIf you want to reduce the computational time and produce intervals with the same width for the whole forecast horizon, simple pass h=1 to the PredictionIntervals class. The caveat of this strategy is that in some cases, variance of the absolute residuals maybe be small (even zero), so the intervals may be too narrow.\n\nfcst.fit(\n train, \n prediction_intervals=PredictionIntervals(n_windows=3, h=1)\n);\n\n\npredictions_w_intervals_ws_1 = fcst.predict(48, level=[80, 90, 95])\n\nLet’s explore the generated intervals.\n\nresults = valid.merge(predictions_w_intervals_ws_1, on=['unique_id', 'ds'])\nfig = plot_series(train, results, max_insample_length=0, level=[90])\nfig.savefig('figs/forecast__predict_intervals_window_size_1.png', bbox_inches='tight')\n\n\n\n\nForecast using a pretrained model\nMLForecast allows you to use a pretrained model to generate forecasts for a new dataset. Simply provide a pandas dataframe containing the new observations as the value for the new_df argument when calling the predict method. The dataframe should have the same structure as the one used to fit the model, including any features and time series data. The function will then use the pretrained model to generate forecasts for the new observations. This allows you to easily apply a pretrained model to a new dataset and generate forecasts without the need to retrain the model.\n\nercot_df = pd.read_csv('https://datasets-nixtla.s3.amazonaws.com/ERCOT-clean.csv')\n# we have to convert the ds column to integers\n# since MLForecast was trained with that structure\nercot_df['ds'] = np.arange(1, len(ercot_df) + 1)\n# use the `new_df` argument to pass the ercot dataset \nercot_fcsts = fcst.predict(horizon, new_df=ercot_df)\nfig = plot_series(ercot_df, ercot_fcsts, max_insample_length=48 * 2)\nfig.get_axes()[0].set_title('ERCOT forecasts trained on M4-Hourly dataset')\nfig.savefig('figs/forecast__ercot.png', bbox_inches='tight')\n\n\nIf you want to take a look at the data that will be used to train the models you can call Forecast.preprocess.\n\n\n\n\nMLForecast.preprocess\n\n MLForecast.preprocess (df:pandas.core.frame.DataFrame,\n id_col:str='unique_id', time_col:str='ds',\n target_col:str='y',\n static_features:Optional[List[str]]=None,\n dropna:bool=True, keep_last_n:Optional[int]=None,\n max_horizon:Optional[int]=None,\n return_X_y:bool=False)\n\nAdd the features to data.\n\n\n\n\n\n\n\n\n\n\nType\nDefault\nDetails\n\n\n\n\ndf\nDataFrame\n\nSeries data in long format.\n\n\nid_col\nstr\nunique_id\nColumn that identifies each serie.\n\n\ntime_col\nstr\nds\nColumn that identifies each timestep, its values can be timestamps or integers.\n\n\ntarget_col\nstr\ny\nColumn that contains the target.\n\n\nstatic_features\ntyping.Optional[typing.List[str]]\nNone\nNames of the features that are static and will be repeated when forecasting.\n\n\ndropna\nbool\nTrue\nDrop rows with missing values produced by the transformations.\n\n\nkeep_last_n\ntyping.Optional[int]\nNone\nKeep only these many records from each serie for the forecasting step. Can save time and memory if your features allow it.\n\n\nmax_horizon\ntyping.Optional[int]\nNone\n\n\n\nreturn_X_y\nbool\nFalse\n\n\n\nReturns\ntyping.Union[pandas.core.frame.DataFrame, typing.Tuple[pandas.core.frame.DataFrame, typing.Union[pandas.core.series.Series, pandas.core.frame.DataFrame]]]\n\ndf plus added features and target(s).\n\n\n\n\nprep_df = fcst.preprocess(train)\nprep_df\n\n\n\n\n\n\n\n\nunique_id\nds\ny\nlag24\nlag48\nlag72\nlag96\nlag120\nlag144\nlag168\newm_mean_lag48_alpha0.3\n\n\n\n\n86988\nH196\n193\n0.1\n0.0\n0.0\n0.0\n0.3\n0.1\n0.1\n0.3\n0.002810\n\n\n86989\nH196\n194\n0.1\n-0.1\n0.1\n0.0\n0.3\n0.1\n0.1\n0.3\n0.031967\n\n\n86990\nH196\n195\n0.1\n-0.1\n0.1\n0.0\n0.3\n0.1\n0.2\n0.1\n0.052377\n\n\n86991\nH196\n196\n0.1\n0.0\n0.0\n0.0\n0.3\n0.2\n0.1\n0.2\n0.036664\n\n\n86992\nH196\n197\n0.0\n0.0\n0.0\n0.1\n0.2\n0.2\n0.1\n0.2\n0.025665\n\n\n...\n...\n...\n...\n...\n...\n...\n...\n...\n...\n...\n...\n\n\n325187\nH413\n956\n0.0\n10.0\n1.0\n6.0\n-53.0\n44.0\n-21.0\n21.0\n7.963225\n\n\n325188\nH413\n957\n9.0\n10.0\n10.0\n-7.0\n-46.0\n27.0\n-19.0\n24.0\n8.574257\n\n\n325189\nH413\n958\n16.0\n8.0\n5.0\n-9.0\n-36.0\n32.0\n-13.0\n8.0\n7.501980\n\n\n325190\nH413\n959\n-3.0\n17.0\n-7.0\n2.0\n-31.0\n22.0\n5.0\n-2.0\n3.151386\n\n\n325191\nH413\n960\n15.0\n11.0\n-6.0\n-5.0\n-17.0\n22.0\n-18.0\n10.0\n0.405970\n\n\n\n\n3072 rows × 11 columns\n\n\n\nIf we do this we then have to call Forecast.fit_models, since this only stores the series information.\n\n\n\nMLForecast.fit_models\n\n MLForecast.fit_models (X:pandas.core.frame.DataFrame,\n y:Union[pandas.core.series.Series,pandas.core.fram\n e.DataFrame])\n\nManually train models. Use this if you called Forecast.preprocess beforehand.\n\n\n\n\n\n\n\n\n\nType\nDetails\n\n\n\n\nX\nDataFrame\nFeatures.\n\n\ny\ntyping.Union[pandas.core.series.Series, pandas.core.frame.DataFrame]\nTarget.\n\n\nReturns\nMLForecast\nForecast object with trained models.\n\n\n\n\nX, y = prep_df.drop(columns=['unique_id', 'ds', 'y']), prep_df['y']\nfcst.fit_models(X, y)\n\nMLForecast(models=[LGBMRegressor], freq=1, lag_features=['lag24', 'lag48', 'lag72', 'lag96', 'lag120', 'lag144', 'lag168', 'ewm_mean_lag48_alpha0.3'], date_features=[], num_threads=1)\n\n\n\npredictions2 = fcst.predict(horizon)\npd.testing.assert_frame_equal(predictions, predictions2)\n\n\n\n\nMLForecast.cross_validation\n\n MLForecast.cross_validation (df:pandas.core.frame.DataFrame,\n n_windows:int, h:int,\n id_col:str='unique_id', time_col:str='ds',\n target_col:str='y',\n step_size:Optional[int]=None,\n static_features:Optional[List[str]]=None,\n dropna:bool=True,\n keep_last_n:Optional[int]=None,\n refit:Union[bool,int]=True,\n max_horizon:Optional[int]=None, before_predi\n ct_callback:Optional[Callable]=None, after_p\n redict_callback:Optional[Callable]=None, pre\n diction_intervals:Optional[mlforecast.utils.\n PredictionIntervals]=None,\n level:Optional[List[Union[int,float]]]=None,\n input_size:Optional[int]=None,\n fitted:bool=False)\n\nPerform time series cross validation. Creates n_windows splits where each window has h test periods, trains the models, computes the predictions and merges the actuals.\n\n\n\n\n\n\n\n\n\n\nType\nDefault\nDetails\n\n\n\n\ndf\nDataFrame\n\nSeries data in long format.\n\n\nn_windows\nint\n\nNumber of windows to evaluate.\n\n\nh\nint\n\nForecast horizon.\n\n\nid_col\nstr\nunique_id\nColumn that identifies each serie.\n\n\ntime_col\nstr\nds\nColumn that identifies each timestep, its values can be timestamps or integers.\n\n\ntarget_col\nstr\ny\nColumn that contains the target.\n\n\nstep_size\ntyping.Optional[int]\nNone\nStep size between each cross validation window. If None it will be equal to h.\n\n\nstatic_features\ntyping.Optional[typing.List[str]]\nNone\nNames of the features that are static and will be repeated when forecasting.\n\n\ndropna\nbool\nTrue\nDrop rows with missing values produced by the transformations.\n\n\nkeep_last_n\ntyping.Optional[int]\nNone\nKeep only these many records from each serie for the forecasting step. Can save time and memory if your features allow it.\n\n\nrefit\ntyping.Union[bool, int]\nTrue\nRetrain model for each cross validation window.If False, the models are trained at the beginning and then used to predict each window.If positive int, the models are retrained every refit windows.\n\n\nmax_horizon\ntyping.Optional[int]\nNone\n\n\n\nbefore_predict_callback\ntyping.Optional[typing.Callable]\nNone\nFunction to call on the features before computing the predictions. This function will take the input dataframe that will be passed to the model for predicting and should return a dataframe with the same structure. The series identifier is on the index.\n\n\nafter_predict_callback\ntyping.Optional[typing.Callable]\nNone\nFunction to call on the predictions before updating the targets. This function will take a pandas Series with the predictions and should return another one with the same structure. The series identifier is on the index.\n\n\nprediction_intervals\ntyping.Optional[mlforecast.utils.PredictionIntervals]\nNone\nConfiguration to calibrate prediction intervals (Conformal Prediction).\n\n\nlevel\ntyping.Optional[typing.List[typing.Union[int, float]]]\nNone\nConfidence levels between 0 and 100 for prediction intervals.\n\n\ninput_size\ntyping.Optional[int]\nNone\nMaximum training samples per serie in each window. If None, will use an expanding window.\n\n\nfitted\nbool\nFalse\nStore the in-sample predictions.\n\n\nReturns\npandas DataFrame\n\nPredictions for each window with the series id, timestamp, last train date, target value and predictions from each model.\n\n\n\nIf we would like to know how good our forecast will be for a specific model and set of features then we can perform cross validation. What cross validation does is take our data and split it in two parts, where the first part is used for training and the second one for validation. Since the data is time dependant we usually take the last x observations from our data as the validation set.\nThis process is implemented in MLForecast.cross_validation, which takes our data and performs the process described above for n_windows times where each window has h validation samples in it. For example, if we have 100 samples and we want to perform 2 backtests each of size 14, the splits will be as follows:\n\nTrain: 1 to 72. Validation: 73 to 86.\nTrain: 1 to 86. Validation: 87 to 100.\n\nYou can control the size between each cross validation window using the step_size argument. For example, if we have 100 samples and we want to perform 2 backtests each of size 14 and move one step ahead in each fold (step_size=1), the splits will be as follows:\n\nTrain: 1 to 85. Validation: 86 to 99.\nTrain: 1 to 86. Validation: 87 to 100.\n\nYou can also perform cross validation without refitting your models for each window by setting refit=False. This allows you to evaluate the performance of your models using multiple window sizes without having to retrain them each time.\n\nfcst = MLForecast(\n models=lgb.LGBMRegressor(random_state=0, verbosity=-1),\n lags=[24 * (i+1) for i in range(7)],\n lag_transforms={\n 1: [(rolling_mean, 24)],\n 24: [(rolling_mean, 24)],\n 48: [(ewm_mean, 0.3)],\n },\n num_threads=1,\n target_transforms=[Differences([24])],\n)\ncv_results = fcst.cross_validation(\n train,\n n_windows=4,\n h=horizon,\n step_size=horizon,\n fitted=True,\n)\ncv_results\n\n\n\n\n\n\n\n\nunique_id\nds\ncutoff\ny\nLGBMRegressor\n\n\n\n\n0\nH196\n769\n768\n15.2\n15.167163\n\n\n1\nH196\n770\n768\n14.8\n14.767163\n\n\n2\nH196\n771\n768\n14.4\n14.467163\n\n\n3\nH196\n772\n768\n14.1\n14.167163\n\n\n4\nH196\n773\n768\n13.8\n13.867163\n\n\n...\n...\n...\n...\n...\n...\n\n\n187\nH413\n956\n912\n59.0\n64.284167\n\n\n188\nH413\n957\n912\n58.0\n64.830429\n\n\n189\nH413\n958\n912\n53.0\n40.726851\n\n\n190\nH413\n959\n912\n38.0\n42.739657\n\n\n191\nH413\n960\n912\n46.0\n52.802769\n\n\n\n\n768 rows × 5 columns\n\n\n\nSince we set fitted=True we can access the predictions for the training sets as well with the cross_validation_fitted_values method.\n\nfcst.cross_validation_fitted_values()\n\n\n\n\n\n\n\n\nunique_id\nds\nfold\ny\nLGBMRegressor\n\n\n\n\n0\nH196\n193\n0\n12.7\n12.667163\n\n\n1\nH196\n194\n0\n12.3\n12.267163\n\n\n2\nH196\n195\n0\n11.9\n11.867163\n\n\n3\nH196\n196\n0\n11.7\n11.667163\n\n\n4\nH196\n197\n0\n11.4\n11.467163\n\n\n...\n...\n...\n...\n...\n...\n\n\n10363\nH413\n908\n3\n49.0\n50.620196\n\n\n10364\nH413\n909\n3\n39.0\n35.972331\n\n\n10365\nH413\n910\n3\n29.0\n29.359678\n\n\n10366\nH413\n911\n3\n24.0\n25.784563\n\n\n10367\nH413\n912\n3\n20.0\n23.168413\n\n\n\n\n10368 rows × 5 columns\n\n\n\nWe can also compute prediction intervals by passing a configuration to prediction_intervals as well as values for the width through levels.\n\ncv_results_intervals = fcst.cross_validation(\n train,\n n_windows=4,\n h=horizon,\n step_size=horizon,\n prediction_intervals=PredictionIntervals(h=horizon),\n level=[80, 90]\n)\ncv_results_intervals\n\n\n\n\n\n\n\n\nunique_id\nds\ncutoff\ny\nLGBMRegressor\nLGBMRegressor-lo-90\nLGBMRegressor-lo-80\nLGBMRegressor-hi-80\nLGBMRegressor-hi-90\n\n\n\n\n0\nH196\n769\n768\n15.2\n15.167163\n15.141751\n15.141751\n15.192575\n15.192575\n\n\n1\nH196\n770\n768\n14.8\n14.767163\n14.741751\n14.741751\n14.792575\n14.792575\n\n\n2\nH196\n771\n768\n14.4\n14.467163\n14.399951\n14.407328\n14.526998\n14.534374\n\n\n3\nH196\n772\n768\n14.1\n14.167163\n14.092575\n14.092575\n14.241751\n14.241751\n\n\n4\nH196\n773\n768\n13.8\n13.867163\n13.792575\n13.792575\n13.941751\n13.941751\n\n\n...\n...\n...\n...\n...\n...\n...\n...\n...\n...\n\n\n187\nH413\n956\n912\n59.0\n64.284167\n29.890099\n34.371545\n94.196788\n98.678234\n\n\n188\nH413\n957\n912\n58.0\n64.830429\n56.874572\n57.827689\n71.833169\n72.786285\n\n\n189\nH413\n958\n912\n53.0\n40.726851\n35.296195\n35.846206\n45.607495\n46.157506\n\n\n190\nH413\n959\n912\n38.0\n42.739657\n35.292153\n35.807640\n49.671674\n50.187161\n\n\n191\nH413\n960\n912\n46.0\n52.802769\n42.465597\n43.895670\n61.709869\n63.139941\n\n\n\n\n768 rows × 9 columns\n\n\n\nThe refit argument allows us to control if we want to retrain the models in every window. It can either be:\n\nA boolean: True will retrain on every window and False only on the first one.\nA positive integer: The models will be trained on the first window and then every refit windows.\n\n\nfcst = MLForecast(\n models=LinearRegression(),\n lags=[1, 24],\n)\nfor refit, expected_models in zip([True, False, 2], [4, 1, 2]):\n fcst.cross_validation(\n train,\n n_windows=4,\n h=horizon,\n refit=refit,\n )\n test_eq(len(fcst.cv_models_), expected_models)\n\n\nfig = plot_series(cv_results, cv_results.drop(columns='cutoff'), max_insample_length=0)\nfig.savefig('figs/forecast__cross_validation.png', bbox_inches='tight')\n\n\n\nfig = plot_series(cv_results_intervals, cv_results_intervals.drop(columns='cutoff'), level=[90], max_insample_length=0)\nfig.savefig('figs/forecast__cross_validation_intervals.png', bbox_inches='tight')\n\n\n\n\n\nMLForecast.from_cv\n\n MLForecast.from_cv (cv:mlforecast.lgb_cv.LightGBMCV)\n\nOnce you’ve found a set of features and parameters that work for your problem you can build a forecast object from it using MLForecast.from_cv, which takes the trained LightGBMCV object and builds an MLForecast object that will use the same features and parameters. Then you can call fit and predict as you normally would.\n\ncv = LightGBMCV(\n freq=1,\n lags=[24 * (i+1) for i in range(7)],\n lag_transforms={\n 48: [(ewm_mean, 0.3)],\n },\n num_threads=1,\n target_transforms=[Differences([24])]\n)\nhist = cv.fit(\n train,\n n_windows=2,\n h=horizon,\n params={'verbosity': -1},\n)\n\n[LightGBM] [Info] Start training from score 0.084340\n[10] mape: 0.118569\n[20] mape: 0.111506\n[30] mape: 0.107314\n[40] mape: 0.106089\n[50] mape: 0.106630\nEarly stopping at round 50\nUsing best iteration: 40\n\n\n\nfcst = MLForecast.from_cv(cv)\nassert cv.best_iteration_ == fcst.models['LGBMRegressor'].n_estimators\n\n\nfcst.fit(train)\nfcst.predict(horizon)\n\n\n\n\n\n\n\n\nunique_id\nds\nLGBMRegressor\n\n\n\n\n0\nH196\n961\n16.111079\n\n\n1\nH196\n962\n15.711079\n\n\n2\nH196\n963\n15.311079\n\n\n3\nH196\n964\n15.011079\n\n\n4\nH196\n965\n14.711079\n\n\n...\n...\n...\n...\n\n\n187\nH413\n1004\n92.722032\n\n\n188\nH413\n1005\n69.153603\n\n\n189\nH413\n1006\n68.811675\n\n\n190\nH413\n1007\n53.693346\n\n\n191\nH413\n1008\n46.055481\n\n\n\n\n192 rows × 3 columns\n\n\n\n\n\n\n\nGive us a ⭐ on Github"
+ },
+ {
+ "objectID": "docs/getting-started/quick_start_local.html",
+ "href": "docs/getting-started/quick_start_local.html",
+ "title": "Quick start (local)",
+ "section": "",
+ "text": "The main component of mlforecast is the MLForecast class, which abstracts away:\n\nFeature engineering and model training through MLForecast.fit\nFeature updates and multi step ahead predictions through MLForecast.predict\nGive us a ⭐ on Github"
+ },
+ {
+ "objectID": "docs/getting-started/quick_start_local.html#main-concepts",
+ "href": "docs/getting-started/quick_start_local.html#main-concepts",
+ "title": "Quick start (local)",
+ "section": "",
+ "text": "The main component of mlforecast is the MLForecast class, which abstracts away:\n\nFeature engineering and model training through MLForecast.fit\nFeature updates and multi step ahead predictions through MLForecast.predict"
+ },
+ {
+ "objectID": "docs/getting-started/quick_start_local.html#data-format",
+ "href": "docs/getting-started/quick_start_local.html#data-format",
+ "title": "Quick start (local)",
+ "section": "Data format",
+ "text": "Data format\nThe data is expected to be a pandas dataframe in long format, that is, each row represents an observation of a single serie at a given time, with at least three columns:\n\nid_col: column that identifies each serie.\ntarget_col: column that has the series values at each timestamp.\ntime_col: column that contains the time the series value was observed. These are usually timestamps, but can also be consecutive integers.\n\nHere we present an example using the classic Box & Jenkins airline data, which measures monthly totals of international airline passengers from 1949 to 1960. Source: Box, G. E. P., Jenkins, G. M. and Reinsel, G. C. (1976) Time Series Analysis, Forecasting and Control. Third Edition. Holden-Day. Series G.\n\nimport pandas as pd\nfrom utilsforecast.plotting import plot_series\n\n\ndf = pd.read_csv('https://datasets-nixtla.s3.amazonaws.com/air-passengers.csv', parse_dates=['ds'])\ndf.head()\n\n\n\n\n\n\n\n\nunique_id\nds\ny\n\n\n\n\n0\nAirPassengers\n1949-01-01\n112\n\n\n1\nAirPassengers\n1949-02-01\n118\n\n\n2\nAirPassengers\n1949-03-01\n132\n\n\n3\nAirPassengers\n1949-04-01\n129\n\n\n4\nAirPassengers\n1949-05-01\n121\n\n\n\n\n\n\n\n\ndf['unique_id'].value_counts()\n\nAirPassengers 144\nName: unique_id, dtype: int64\n\n\nHere the unique_id column has the same value for all rows because this is a single time series, you can have multiple time series by stacking them together and having a column that differentiates them.\nWe also have the ds column that contains the timestamps, in this case with a monthly frequency, and the y column that contains the series values in each timestamp."
+ },
+ {
+ "objectID": "docs/getting-started/quick_start_local.html#modeling",
+ "href": "docs/getting-started/quick_start_local.html#modeling",
+ "title": "Quick start (local)",
+ "section": "Modeling",
+ "text": "Modeling\n\nfig = plot_series(df)\n\n\nWe can see that the serie has a clear trend, so we can take the first difference, i.e. take each value and subtract the value at the previous month. This can be achieved by passing an mlforecast.target_transforms.Differences([1]) instance to target_transforms.\nWe can then train a linear regression using the value from the same month at the previous year (lag 12) as a feature, this is done by passing lags=[12].\n\nfrom mlforecast import MLForecast\nfrom mlforecast.target_transforms import Differences\nfrom sklearn.linear_model import LinearRegression\n\n\nfcst = MLForecast(\n models=LinearRegression(),\n freq='MS', # our serie has a monthly frequency\n lags=[12],\n target_transforms=[Differences([1])],\n)\nfcst.fit(df)\n\nMLForecast(models=[LinearRegression], freq=<MonthBegin>, lag_features=['lag12'], date_features=[], num_threads=1)\n\n\nThe previous line computed the features and trained the model, so now we’re ready to compute our forecasts."
+ },
+ {
+ "objectID": "docs/getting-started/quick_start_local.html#forecasting",
+ "href": "docs/getting-started/quick_start_local.html#forecasting",
+ "title": "Quick start (local)",
+ "section": "Forecasting",
+ "text": "Forecasting\nCompute the forecast for the next 12 months\n\npreds = fcst.predict(12)\npreds\n\n\n\n\n\n\n\n\nunique_id\nds\nLinearRegression\n\n\n\n\n0\nAirPassengers\n1961-01-01\n444.656555\n\n\n1\nAirPassengers\n1961-02-01\n417.470734\n\n\n2\nAirPassengers\n1961-03-01\n446.903046\n\n\n3\nAirPassengers\n1961-04-01\n491.014130\n\n\n4\nAirPassengers\n1961-05-01\n502.622223\n\n\n5\nAirPassengers\n1961-06-01\n568.751465\n\n\n6\nAirPassengers\n1961-07-01\n660.044312\n\n\n7\nAirPassengers\n1961-08-01\n643.343323\n\n\n8\nAirPassengers\n1961-09-01\n540.666687\n\n\n9\nAirPassengers\n1961-10-01\n491.462708\n\n\n10\nAirPassengers\n1961-11-01\n417.095154\n\n\n11\nAirPassengers\n1961-12-01\n461.206238"
+ },
+ {
+ "objectID": "docs/getting-started/quick_start_local.html#visualize-results",
+ "href": "docs/getting-started/quick_start_local.html#visualize-results",
+ "title": "Quick start (local)",
+ "section": "Visualize results",
+ "text": "Visualize results\nWe can visualize what our prediction looks like.\n\nfig = plot_series(df, preds)\n\n\nAnd that’s it! You’ve trained a linear regression to predict the air passengers for 1961."
+ },
+ {
+ "objectID": "docs/getting-started/end_to_end_walkthrough.html",
+ "href": "docs/getting-started/end_to_end_walkthrough.html",
+ "title": "End to end walkthrough",
+ "section": "",
+ "text": "For this example we’ll use a subset of the M4 hourly dataset. You can find the a notebook with the full dataset here.\n\nimport random\n\nimport pandas as pd\nfrom datasetsforecast.m4 import M4\nfrom utilsforecast.plotting import plot_series\n\n\nawait M4.async_download('data', group='Hourly')\ndf, *_ = M4.load('data', 'Hourly')\nuids = df['unique_id'].unique()\nrandom.seed(0)\nsample_uids = random.choices(uids, k=4)\ndf = df[df['unique_id'].isin(sample_uids)].reset_index(drop=True)\ndf['ds'] = df['ds'].astype('int64')\ndf\n\n\n\n\n\n\n\n\nunique_id\nds\ny\n\n\n\n\n0\nH196\n1\n11.8\n\n\n1\nH196\n2\n11.4\n\n\n2\nH196\n3\n11.1\n\n\n3\nH196\n4\n10.8\n\n\n4\nH196\n5\n10.6\n\n\n...\n...\n...\n...\n\n\n4027\nH413\n1004\n99.0\n\n\n4028\nH413\n1005\n88.0\n\n\n4029\nH413\n1006\n47.0\n\n\n4030\nH413\n1007\n41.0\n\n\n4031\nH413\n1008\n34.0\n\n\n\n\n4032 rows × 3 columns\nGive us a ⭐ on Github"
+ },
+ {
+ "objectID": "docs/getting-started/end_to_end_walkthrough.html#data-setup",
+ "href": "docs/getting-started/end_to_end_walkthrough.html#data-setup",
+ "title": "End to end walkthrough",
+ "section": "",
+ "text": "For this example we’ll use a subset of the M4 hourly dataset. You can find the a notebook with the full dataset here.\n\nimport random\n\nimport pandas as pd\nfrom datasetsforecast.m4 import M4\nfrom utilsforecast.plotting import plot_series\n\n\nawait M4.async_download('data', group='Hourly')\ndf, *_ = M4.load('data', 'Hourly')\nuids = df['unique_id'].unique()\nrandom.seed(0)\nsample_uids = random.choices(uids, k=4)\ndf = df[df['unique_id'].isin(sample_uids)].reset_index(drop=True)\ndf['ds'] = df['ds'].astype('int64')\ndf\n\n\n\n\n\n\n\n\nunique_id\nds\ny\n\n\n\n\n0\nH196\n1\n11.8\n\n\n1\nH196\n2\n11.4\n\n\n2\nH196\n3\n11.1\n\n\n3\nH196\n4\n10.8\n\n\n4\nH196\n5\n10.6\n\n\n...\n...\n...\n...\n\n\n4027\nH413\n1004\n99.0\n\n\n4028\nH413\n1005\n88.0\n\n\n4029\nH413\n1006\n47.0\n\n\n4030\nH413\n1007\n41.0\n\n\n4031\nH413\n1008\n34.0\n\n\n\n\n4032 rows × 3 columns"
+ },
+ {
+ "objectID": "docs/getting-started/end_to_end_walkthrough.html#eda",
+ "href": "docs/getting-started/end_to_end_walkthrough.html#eda",
+ "title": "End to end walkthrough",
+ "section": "EDA",
+ "text": "EDA\nWe’ll take a look at our series to get ideas for transformations and features.\n\nfig = plot_series(df, max_insample_length=24 * 14)\n\n\nWe can use the MLForecast.preprocess method to explore different transformations. It looks like these series have a strong seasonality on the hour of the day, so we can subtract the value from the same hour in the previous day to remove it. This can be done with the mlforecast.target_transforms.Differences transformer, which we pass through target_transforms.\n\nfrom mlforecast import MLForecast\nfrom mlforecast.target_transforms import Differences\n\n\nfcst = MLForecast(\n models=[], # we're not interested in modeling yet\n freq=1, # our series have integer timestamps, so we'll just add 1 in every timestep\n target_transforms=[Differences([24])],\n)\nprep = fcst.preprocess(df)\nprep\n\n\n\n\n\n\n\n\nunique_id\nds\ny\n\n\n\n\n24\nH196\n25\n0.3\n\n\n25\nH196\n26\n0.3\n\n\n26\nH196\n27\n0.1\n\n\n27\nH196\n28\n0.2\n\n\n28\nH196\n29\n0.2\n\n\n...\n...\n...\n...\n\n\n4027\nH413\n1004\n39.0\n\n\n4028\nH413\n1005\n55.0\n\n\n4029\nH413\n1006\n14.0\n\n\n4030\nH413\n1007\n3.0\n\n\n4031\nH413\n1008\n4.0\n\n\n\n\n3936 rows × 3 columns\n\n\n\nThis has subtacted the lag 24 from each value, we can see what our series look like now.\n\nfig = plot_series(prep)"
+ },
+ {
+ "objectID": "docs/getting-started/end_to_end_walkthrough.html#adding-features",
+ "href": "docs/getting-started/end_to_end_walkthrough.html#adding-features",
+ "title": "End to end walkthrough",
+ "section": "Adding features",
+ "text": "Adding features\n\nLags\nLooks like the seasonality is gone, we can now try adding some lag features.\n\nfcst = MLForecast(\n models=[],\n freq=1,\n lags=[1, 24],\n target_transforms=[Differences([24])], \n)\nprep = fcst.preprocess(df)\nprep\n\n\n\n\n\n\n\n\nunique_id\nds\ny\nlag1\nlag24\n\n\n\n\n48\nH196\n49\n0.1\n0.1\n0.3\n\n\n49\nH196\n50\n0.1\n0.1\n0.3\n\n\n50\nH196\n51\n0.2\n0.1\n0.1\n\n\n51\nH196\n52\n0.1\n0.2\n0.2\n\n\n52\nH196\n53\n0.1\n0.1\n0.2\n\n\n...\n...\n...\n...\n...\n...\n\n\n4027\nH413\n1004\n39.0\n29.0\n1.0\n\n\n4028\nH413\n1005\n55.0\n39.0\n-25.0\n\n\n4029\nH413\n1006\n14.0\n55.0\n-20.0\n\n\n4030\nH413\n1007\n3.0\n14.0\n0.0\n\n\n4031\nH413\n1008\n4.0\n3.0\n-16.0\n\n\n\n\n3840 rows × 5 columns\n\n\n\n\nprep.drop(columns=['unique_id', 'ds']).corr()['y']\n\ny 1.000000\nlag1 0.622531\nlag24 -0.234268\nName: y, dtype: float64\n\n\n\n\nLag transforms\nLag transforms are defined as a dictionary where the keys are the lags and the values are lists of functions that transform an array. These must be numba jitted functions (so that computing the features doesn’t become a bottleneck). There are some implemented in the window-ops package but you can also implement your own.\nIf the function takes two or more arguments you can either:\n\nsupply a tuple (tfm_func, arg1, arg2, …)\ndefine a new function fixing the arguments\n\n\nfrom numba import njit\nfrom window_ops.expanding import expanding_mean\nfrom window_ops.rolling import rolling_mean\n\n\n@njit\ndef rolling_mean_48(x):\n return rolling_mean(x, window_size=48)\n\n\nfcst = MLForecast(\n models=[],\n freq=1,\n target_transforms=[Differences([24])], \n lag_transforms={\n 1: [expanding_mean],\n 24: [(rolling_mean, 48), rolling_mean_48],\n },\n)\nprep = fcst.preprocess(df)\nprep\n\n\n\n\n\n\n\n\nunique_id\nds\ny\nexpanding_mean_lag1\nrolling_mean_lag24_window_size48\nrolling_mean_48_lag24\n\n\n\n\n95\nH196\n96\n0.1\n0.174648\n0.150000\n0.150000\n\n\n96\nH196\n97\n0.3\n0.173611\n0.145833\n0.145833\n\n\n97\nH196\n98\n0.3\n0.175342\n0.141667\n0.141667\n\n\n98\nH196\n99\n0.3\n0.177027\n0.141667\n0.141667\n\n\n99\nH196\n100\n0.3\n0.178667\n0.141667\n0.141667\n\n\n...\n...\n...\n...\n...\n...\n...\n\n\n4027\nH413\n1004\n39.0\n0.242084\n3.437500\n3.437500\n\n\n4028\nH413\n1005\n55.0\n0.281633\n2.708333\n2.708333\n\n\n4029\nH413\n1006\n14.0\n0.337411\n2.125000\n2.125000\n\n\n4030\nH413\n1007\n3.0\n0.351324\n1.770833\n1.770833\n\n\n4031\nH413\n1008\n4.0\n0.354018\n1.208333\n1.208333\n\n\n\n\n3652 rows × 6 columns\n\n\n\nYou can see that both approaches get to the same result, you can use whichever one you feel most comfortable with.\n\n\nDate features\nIf your time column is made of timestamps then it might make sense to extract features like week, dayofweek, quarter, etc. You can do that by passing a list of strings with pandas time/date components. You can also pass functions that will take the time column as input, as we’ll show here.\n\ndef hour_index(times):\n return times % 24\n\nfcst = MLForecast(\n models=[],\n freq=1,\n target_transforms=[Differences([24])],\n date_features=[hour_index],\n)\nfcst.preprocess(df)\n\n\n\n\n\n\n\n\nunique_id\nds\ny\nhour_index\n\n\n\n\n24\nH196\n25\n0.3\n1\n\n\n25\nH196\n26\n0.3\n2\n\n\n26\nH196\n27\n0.1\n3\n\n\n27\nH196\n28\n0.2\n4\n\n\n28\nH196\n29\n0.2\n5\n\n\n...\n...\n...\n...\n...\n\n\n4027\nH413\n1004\n39.0\n20\n\n\n4028\nH413\n1005\n55.0\n21\n\n\n4029\nH413\n1006\n14.0\n22\n\n\n4030\nH413\n1007\n3.0\n23\n\n\n4031\nH413\n1008\n4.0\n0\n\n\n\n\n3936 rows × 4 columns\n\n\n\n\n\nTarget transformations\nIf you want to do some transformation to your target before computing the features and then re-apply it after predicting you can use the target_transforms argument, which takes a list of transformations. You can find the implemented ones in mlforecast.target_transforms or you can implement your own as described in the target transformations guide.\n\nfrom mlforecast.target_transforms import LocalStandardScaler\n\n\nfcst = MLForecast(\n models=[],\n freq=1,\n lags=[1],\n target_transforms=[LocalStandardScaler()]\n)\nfcst.preprocess(df)\n\n\n\n\n\n\n\n\nunique_id\nds\ny\nlag1\n\n\n\n\n1\nH196\n2\n-1.493026\n-1.383286\n\n\n2\nH196\n3\n-1.575331\n-1.493026\n\n\n3\nH196\n4\n-1.657635\n-1.575331\n\n\n4\nH196\n5\n-1.712505\n-1.657635\n\n\n5\nH196\n6\n-1.794810\n-1.712505\n\n\n...\n...\n...\n...\n...\n\n\n4027\nH413\n1004\n3.062766\n2.425012\n\n\n4028\nH413\n1005\n2.523128\n3.062766\n\n\n4029\nH413\n1006\n0.511751\n2.523128\n\n\n4030\nH413\n1007\n0.217403\n0.511751\n\n\n4031\nH413\n1008\n-0.126003\n0.217403\n\n\n\n\n4028 rows × 4 columns\n\n\n\nWe can define a naive model to test this\n\nfrom sklearn.base import BaseEstimator\n\nclass Naive(BaseEstimator):\n def fit(self, X, y):\n return self\n\n def predict(self, X):\n return X['lag1']\n\n\nfcst = MLForecast(\n models=[Naive()],\n freq=1,\n lags=[1],\n target_transforms=[LocalStandardScaler()]\n)\nfcst.fit(df)\npreds = fcst.predict(1)\npreds\n\n\n\n\n\n\n\n\nunique_id\nds\nNaive\n\n\n\n\n0\nH196\n1009\n16.8\n\n\n1\nH256\n1009\n13.4\n\n\n2\nH381\n1009\n207.0\n\n\n3\nH413\n1009\n34.0\n\n\n\n\n\n\n\nWe compare this with the last values of our serie\n\nlast_vals = df.groupby('unique_id').tail(1)\nlast_vals\n\n\n\n\n\n\n\n\nunique_id\nds\ny\n\n\n\n\n1007\nH196\n1008\n16.8\n\n\n2015\nH256\n1008\n13.4\n\n\n3023\nH381\n1008\n207.0\n\n\n4031\nH413\n1008\n34.0\n\n\n\n\n\n\n\n\nimport numpy as np\n\n\nnp.testing.assert_allclose(preds['Naive'], last_vals['y'])"
+ },
+ {
+ "objectID": "docs/getting-started/end_to_end_walkthrough.html#training",
+ "href": "docs/getting-started/end_to_end_walkthrough.html#training",
+ "title": "End to end walkthrough",
+ "section": "Training",
+ "text": "Training\nOnce you’ve decided the features, transformations and models that you want to use you can use the MLForecast.fit method instead, which will do the preprocessing and then train the models. The models can be specified as a list (which will name them by using their class name and an index if there are repeated classes) or as a dictionary where the keys are the names you want to give to the models, i.e. the name of the column that will hold their predictions, and the values are the models themselves.\n\nimport lightgbm as lgb\n\n\nlgb_params = {\n 'verbosity': -1,\n 'num_leaves': 512,\n}\n\nfcst = MLForecast(\n models={\n 'avg': lgb.LGBMRegressor(**lgb_params),\n 'q75': lgb.LGBMRegressor(**lgb_params, objective='quantile', alpha=0.75),\n 'q25': lgb.LGBMRegressor(**lgb_params, objective='quantile', alpha=0.25),\n },\n freq=1,\n target_transforms=[Differences([24])],\n lags=[1, 24],\n lag_transforms={\n 1: [expanding_mean],\n 24: [(rolling_mean, 48)],\n },\n date_features=[hour_index],\n)\nfcst.fit(df)\n\nMLForecast(models=[avg, q75, q25], freq=1, lag_features=['lag1', 'lag24', 'expanding_mean_lag1', 'rolling_mean_lag24_window_size48'], date_features=[<function hour_index>], num_threads=1)\n\n\nThis computed the features and trained three different models using them. We can now compute our forecasts."
+ },
+ {
+ "objectID": "docs/getting-started/end_to_end_walkthrough.html#forecasting",
+ "href": "docs/getting-started/end_to_end_walkthrough.html#forecasting",
+ "title": "End to end walkthrough",
+ "section": "Forecasting",
+ "text": "Forecasting\n\npreds = fcst.predict(48)\npreds\n\n\n\n\n\n\n\n\nunique_id\nds\navg\nq75\nq25\n\n\n\n\n0\nH196\n1009\n16.295257\n16.385859\n16.320666\n\n\n1\nH196\n1010\n15.910282\n16.012728\n15.856905\n\n\n2\nH196\n1011\n15.728367\n15.784867\n15.656658\n\n\n3\nH196\n1012\n15.468414\n15.503223\n15.401462\n\n\n4\nH196\n1013\n15.081279\n15.163606\n15.048576\n\n\n...\n...\n...\n...\n...\n...\n\n\n187\nH413\n1052\n100.450617\n116.461898\n52.276952\n\n\n188\nH413\n1053\n88.426800\n114.257158\n50.866960\n\n\n189\nH413\n1054\n59.675737\n89.672526\n16.440738\n\n\n190\nH413\n1055\n57.580356\n84.680943\n14.248400\n\n\n191\nH413\n1056\n42.669879\n52.000559\n12.440984\n\n\n\n\n192 rows × 5 columns\n\n\n\n\nfig = plot_series(df, preds, max_insample_length=24 * 7)"
+ },
+ {
+ "objectID": "docs/getting-started/end_to_end_walkthrough.html#updating-series-values",
+ "href": "docs/getting-started/end_to_end_walkthrough.html#updating-series-values",
+ "title": "End to end walkthrough",
+ "section": "Updating series’ values",
+ "text": "Updating series’ values\nAfter you’ve trained a forecast object you can save it and load it to use later using pickle or cloudpickle. If by the time you want to use it you already know the following values of the target you can use the MLForecast.ts.update method to incorporate these, which will allow you to use these new values when computing predictions.\n\nIf no new values are provided for a serie that’s currently stored, only the previous ones are kept.\nIf new series are included they are added to the existing ones.\n\n\nfcst = MLForecast(\n models=[Naive()],\n freq=1,\n lags=[1, 2, 3],\n)\nfcst.fit(df)\nfcst.predict(1)\n\n\n\n\n\n\n\n\nunique_id\nds\nNaive\n\n\n\n\n0\nH196\n1009\n16.8\n\n\n1\nH256\n1009\n13.4\n\n\n2\nH381\n1009\n207.0\n\n\n3\nH413\n1009\n34.0\n\n\n\n\n\n\n\n\nnew_values = pd.DataFrame({\n 'unique_id': ['H196', 'H256'],\n 'ds': [1009, 1009],\n 'y': [17.0, 14.0],\n})\nfcst.ts.update(new_values)\npreds = fcst.predict(1)\npreds\n\n\n\n\n\n\n\n\nunique_id\nds\nNaive\n\n\n\n\n0\nH196\n1010\n17.0\n\n\n1\nH256\n1010\n14.0\n\n\n2\nH381\n1009\n207.0\n\n\n3\nH413\n1009\n34.0"
+ },
+ {
+ "objectID": "docs/getting-started/end_to_end_walkthrough.html#estimating-model-performance",
+ "href": "docs/getting-started/end_to_end_walkthrough.html#estimating-model-performance",
+ "title": "End to end walkthrough",
+ "section": "Estimating model performance",
+ "text": "Estimating model performance\n\nCross validation\nIn order to get an estimate of how well our model will be when predicting future data we can perform cross validation, which consist on training a few models independently on different subsets of the data, using them to predict a validation set and measuring their performance.\nSince our data depends on time, we make our splits by removing the last portions of the series and using them as validation sets. This process is implemented in MLForecast.cross_validation.\n\nfcst = MLForecast(\n models=lgb.LGBMRegressor(**lgb_params),\n freq=1,\n target_transforms=[Differences([24])],\n lags=[1, 24],\n lag_transforms={\n 1: [expanding_mean],\n 24: [(rolling_mean, 48)],\n },\n date_features=[hour_index],\n)\ncv_result = fcst.cross_validation(\n df,\n n_windows=4, # number of models to train/splits to perform\n h=48, # length of the validation set in each window\n)\ncv_result\n\n\n\n\n\n\n\n\nunique_id\nds\ncutoff\ny\nLGBMRegressor\n\n\n\n\n0\nH196\n817\n816\n15.3\n15.383165\n\n\n1\nH196\n818\n816\n14.9\n14.923219\n\n\n2\nH196\n819\n816\n14.6\n14.667834\n\n\n3\nH196\n820\n816\n14.2\n14.275964\n\n\n4\nH196\n821\n816\n13.9\n13.973491\n\n\n...\n...\n...\n...\n...\n...\n\n\n187\nH413\n1004\n960\n99.0\n65.644823\n\n\n188\nH413\n1005\n960\n88.0\n71.717097\n\n\n189\nH413\n1006\n960\n47.0\n76.704377\n\n\n190\nH413\n1007\n960\n41.0\n53.446638\n\n\n191\nH413\n1008\n960\n34.0\n54.902634\n\n\n\n\n768 rows × 5 columns\n\n\n\n\nfig = plot_series(cv_result, cv_result.drop(columns='cutoff'), max_insample_length=0)\n\n\nWe can compute the RMSE on each split.\n\nfrom utilsforecast.losses import rmse\n\n\ndef evaluate_cv(df):\n return rmse(df, models=['LGBMRegressor'], id_col='cutoff').set_index('cutoff')\n\nsplit_rmse = evaluate_cv(cv_result)\nsplit_rmse\n\n\n\n\n\n\n\n\nLGBMRegressor\n\n\ncutoff\n\n\n\n\n\n816\n29.418172\n\n\n864\n34.257598\n\n\n912\n13.145763\n\n\n960\n35.066261\n\n\n\n\n\n\n\nAnd the average RMSE across splits.\n\nsplit_rmse.mean()\n\nLGBMRegressor 27.971949\ndtype: float64\n\n\nYou can quickly try different features and evaluate them this way. We can try removing the differencing and using an exponentially weighted average of the lag 1 instead of the expanding mean.\n\nfrom window_ops.ewm import ewm_mean\n\n\nfcst = MLForecast(\n models=lgb.LGBMRegressor(**lgb_params),\n freq=1,\n lags=[1, 24],\n lag_transforms={\n 1: [(ewm_mean, 0.5)],\n 24: [(rolling_mean, 48)], \n },\n date_features=[hour_index], \n)\ncv_result2 = fcst.cross_validation(\n df,\n n_windows=4,\n h=48,\n)\nevaluate_cv(cv_result2).mean()\n\nLGBMRegressor 25.874446\ndtype: float64\n\n\n\n\nLightGBMCV\nIn the same spirit of estimating our model’s performance, LightGBMCV allows us to train a few LightGBM models on different partitions of the data. The main differences with MLForecast.cross_validation are:\n\nIt can only train LightGBM models.\nIt trains all models simultaneously and gives us per-iteration averages of the errors across the complete forecasting window, which allows us to find the best iteration.\n\n\nfrom mlforecast.lgb_cv import LightGBMCV\n\n\ncv = LightGBMCV(\n freq=1,\n target_transforms=[Differences([24])],\n lags=[1, 24],\n lag_transforms={\n 1: [expanding_mean],\n 24: [(rolling_mean, 48)],\n },\n date_features=[hour_index],\n num_threads=2,\n)\ncv_hist = cv.fit(\n df,\n n_windows=4,\n h=48,\n params=lgb_params,\n eval_every=5,\n early_stopping_evals=5, \n compute_cv_preds=True,\n)\n\n[5] mape: 0.158639\n[10] mape: 0.163739\n[15] mape: 0.161535\n[20] mape: 0.169491\n[25] mape: 0.163690\n[30] mape: 0.164198\nEarly stopping at round 30\nUsing best iteration: 5\n\n\nAs you can see this gives us the error by iteration (controlled by the eval_every argument) and performs early stopping (which can be configured with early_stopping_evals and early_stopping_pct). If you set compute_cv_preds=True the out-of-fold predictions are computed using the best iteration found and are saved in the cv_preds_ attribute.\n\ncv.cv_preds_\n\n\n\n\n\n\n\n\nunique_id\nds\ny\nBooster\nwindow\n\n\n\n\n0\nH196\n817\n15.3\n15.473182\n0\n\n\n1\nH196\n818\n14.9\n15.038571\n0\n\n\n2\nH196\n819\n14.6\n14.849409\n0\n\n\n3\nH196\n820\n14.2\n14.448379\n0\n\n\n4\nH196\n821\n13.9\n14.148379\n0\n\n\n...\n...\n...\n...\n...\n...\n\n\n187\nH413\n1004\n99.0\n61.425396\n3\n\n\n188\nH413\n1005\n88.0\n62.886890\n3\n\n\n189\nH413\n1006\n47.0\n57.886890\n3\n\n\n190\nH413\n1007\n41.0\n38.849009\n3\n\n\n191\nH413\n1008\n34.0\n44.720562\n3\n\n\n\n\n768 rows × 5 columns\n\n\n\n\nfig = plot_series(cv.cv_preds_, cv.cv_preds_.drop(columns='window'), max_insample_length=0)\n\n\nYou can use this class to quickly try different configurations of features and hyperparameters. Once you’ve found a combination that works you can train a model with those features and hyperparameters on all the data by creating an MLForecast object from the LightGBMCV one as follows:\n\nfinal_fcst = MLForecast.from_cv(cv)\nfinal_fcst.fit(df)\npreds = final_fcst.predict(48)\nfig = plot_series(df, preds, max_insample_length=24 * 14)"
+ },
+ {
+ "objectID": "docs/tutorials/prediction_intervals_in_forecasting_models.html",
+ "href": "docs/tutorials/prediction_intervals_in_forecasting_models.html",
+ "title": "Prediction intervals",
+ "section": "",
+ "text": "The objective of the following article is to obtain a step-by-step guide on building Prediction intervals in forecasting models using mlforecast.\nDuring this walkthrough, we will become familiar with the main MlForecast class and some relevant methods such as MLForecast.fit, MLForecast.predict and MLForecast.cross_validation in other.\nLet’s start!!!\nGive us a ⭐ on Github"
+ },
+ {
+ "objectID": "docs/tutorials/prediction_intervals_in_forecasting_models.html#forecast-distributions",
+ "href": "docs/tutorials/prediction_intervals_in_forecasting_models.html#forecast-distributions",
+ "title": "Prediction intervals",
+ "section": "Forecast distributions",
+ "text": "Forecast distributions\nWe use forecast distributions to express the uncertainty in our predictions. These probability distributions describe the probability of observing different future values using the fitted model. The point forecast corresponds to the mean of this distribution. Most time series models generate forecasts that follow a normal distribution, which implies that we assume that possible future values follow a normal distribution. However, later in this section we will look at some alternatives to normal distributions.\n\nImportance of Confidence Interval Prediction in Time Series:\n\nUncertainty Estimation: The confidence interval provides a measure of the uncertainty associated with time series predictions. It enables variability and the range of possible future values to be quantified, which is essential for making informed decisions.\nPrecision evaluation: By having a confidence interval, the precision of the predictions can be evaluated. If the interval is narrow, it indicates that the forecast is more accurate and reliable. On the other hand, if the interval is wide, it indicates greater uncertainty and less precision in the predictions.\nRisk management: The confidence interval helps in risk management by providing information about possible future scenarios. It allows identifying the ranges in which the real values could be located and making decisions based on those possible scenarios.\nEffective communication: The confidence interval is a useful tool for communicating predictions clearly and accurately. It allows the variability and uncertainty associated with the predictions to be conveyed to the stakeholders, avoiding a wrong or overly optimistic interpretation of the results.\n\nTherefore, confidence interval prediction in time series is essential to understand and manage uncertainty, assess the accuracy of predictions, and make informed decisions based on possible future scenarios."
+ },
+ {
+ "objectID": "docs/tutorials/prediction_intervals_in_forecasting_models.html#prediction-intervals",
+ "href": "docs/tutorials/prediction_intervals_in_forecasting_models.html#prediction-intervals",
+ "title": "Prediction intervals",
+ "section": "Prediction intervals",
+ "text": "Prediction intervals\nA prediction interval gives us a range in which we expect \\(y_t\\) to lie with a specified probability. For example, if we assume that the distribution of future observations follows a normal distribution, a 95% prediction interval for the forecast of step h would be represented by the range\n\\[\\hat{y}_{T+h|T} \\pm 1.96 \\hat\\sigma_h,\\]\nWhere \\(\\hat\\sigma_h\\) is an estimate of the standard deviation of the h -step forecast distribution.\nMore generally, a prediction interval can be written as\n\\[\\hat{y}_{T+h|T} \\pm c \\hat\\sigma_h\\]\nIn this context, the term “multiplier c” is associated with the probability of coverage. In this article, intervals of 80% and 95% are typically calculated, but any other percentage can be used. The table below shows the values of c corresponding to different coverage probabilities, assuming a normal forecast distribution.\n\n\n\nPercentage\nMultiplier\n\n\n\n\n50\n0.67\n\n\n55\n0.76\n\n\n60\n0.84\n\n\n65\n0.93\n\n\n70\n1.04\n\n\n75\n1.15\n\n\n80\n1.28\n\n\n85\n1.44\n\n\n90\n1.64\n\n\n95\n1.96\n\n\n96\n2.05\n\n\n97\n2.17\n\n\n98\n2.33\n\n\n99\n2.58\n\n\n\nPrediction intervals are valuable because they reflect the uncertainty in the predictions. If we only generate point forecasts, we cannot assess how accurate those forecasts are. However, by providing prediction intervals, the amount of uncertainty associated with each forecast becomes apparent. For this reason, point forecasts may lack significant value without the inclusion of corresponding forecast intervals."
+ },
+ {
+ "objectID": "docs/tutorials/prediction_intervals_in_forecasting_models.html#one-step-prediction-intervals",
+ "href": "docs/tutorials/prediction_intervals_in_forecasting_models.html#one-step-prediction-intervals",
+ "title": "Prediction intervals",
+ "section": "One-step prediction intervals",
+ "text": "One-step prediction intervals\nWhen making a prediction for a future step, it is possible to estimate the standard deviation of the forecast distribution using the standard deviation of the residuals, which is calculated by\n\\[\\begin{equation}\n \\hat{\\sigma} = \\sqrt{\\frac{1}{T-K-M}\\sum_{t=1}^T e_t^2}, \\tag{1}\n\\end{equation}\\]\nwhere \\(K\\) is the number of parameters estimated in the forecasting method, and \\(M\\) is the number of missing values in the residuals. (For example, \\(M=1\\) for a naive forecast, because we can’t forecast the first observation.)"
+ },
+ {
+ "objectID": "docs/tutorials/prediction_intervals_in_forecasting_models.html#multi-step-prediction-intervals",
+ "href": "docs/tutorials/prediction_intervals_in_forecasting_models.html#multi-step-prediction-intervals",
+ "title": "Prediction intervals",
+ "section": "Multi-step prediction intervals",
+ "text": "Multi-step prediction intervals\nA typical feature of forecast intervals is that they tend to increase in length as the forecast horizon lengthens. As we move further out in time, there is greater uncertainty associated with the prediction, resulting in wider prediction intervals. In general, σh tends to increase as h increases (although there are some nonlinear forecasting methods that do not follow this property).\nTo generate a prediction interval, it is necessary to have an estimate of σh. As mentioned above, for one-step forecasts (h=1), equation (1) provides a good estimate of the standard deviation of the forecast, σ1. However, for multi-step forecasts, a more complex calculation method is required. These calculations assume that the residuals are uncorrelated with each other."
+ },
+ {
+ "objectID": "docs/tutorials/prediction_intervals_in_forecasting_models.html#benchmark-methods",
+ "href": "docs/tutorials/prediction_intervals_in_forecasting_models.html#benchmark-methods",
+ "title": "Prediction intervals",
+ "section": "Benchmark methods",
+ "text": "Benchmark methods\nFor the four benchmark methods, it is possible to mathematically derive the forecast standard deviation under the assumption of uncorrelated residuals. If \\(\\hat{\\sigma}_h\\) denotes the standard deviation of the \\(h\\) -step forecast distribution, and \\(\\hat{\\sigma}\\) is the residual standard deviation given by (1), then we can use the expressions shown in next Table. Note that when \\(h=1\\) and \\(T\\) is large, these all give the same approximate value \\(\\hat{\\sigma}\\).\n\n\n\nMethod\nh-step forecast standard deviation\n\n\n\n\nMean forecasts\n\\(\\hat\\sigma_h = \\hat\\sigma\\sqrt{1 + 1/T}\\)\n\n\nNaïve forecasts\n\\(\\hat\\sigma_h = \\hat\\sigma\\sqrt{h}\\)\n\n\nSeasonal naïve forecasts\n\\(\\hat\\sigma_h = \\hat\\sigma\\sqrt{k+1}\\)\n\n\nDrift forecasts\n\\(\\hat\\sigma_h = \\hat\\sigma\\sqrt{h(1+h/T)}\\)\n\n\n\nNote that when \\(h=1\\) and \\(T\\) is large, these all give the same approximate value \\(\\hat{\\sigma}\\)."
+ },
+ {
+ "objectID": "docs/tutorials/prediction_intervals_in_forecasting_models.html#prediction-intervals-from-bootstrapped-residuals",
+ "href": "docs/tutorials/prediction_intervals_in_forecasting_models.html#prediction-intervals-from-bootstrapped-residuals",
+ "title": "Prediction intervals",
+ "section": "Prediction intervals from bootstrapped residuals",
+ "text": "Prediction intervals from bootstrapped residuals\nWhen a normal distribution for the residuals is an unreasonable assumption, one alternative is to use bootstrapping, which only assumes that the residuals are uncorrelated with constant variance. We will illustrate the procedure using a naïve forecasting method.\nA one-step forecast error is defined as \\(e_t = y_t - \\hat{y}_{t|t-1}\\). For a naïve forecasting method, {t|t-1} = y{t-1}, so we can rewrite this as \\[y_t = y_{t-1} + e_t.\\]\nAssuming future errors will be similar to past errors, when \\(t>T\\) we can replace \\(e_{t}\\) by sampling from the collection of errors we have seen in the past (i.e., the residuals). So we can simulate the next observation of a time series using\n\\[y^*_{T+1} = y_{T} + e^*_{T+1}\\]\nwhere \\(e^*_{T+1}\\) is a randomly sampled error from the past, and \\(y^*_{T+1}\\) is the possible future value that would arise if that particular error value occurred. We use We use a * to indicate that this is not the observed \\(y_{T+1}\\) value, but one possible future that could occur. Adding the new simulated observation to our data set, we can repeat the process to obtain\n\\[y^*_{T+2} = y_{T+1}^* + e^*_{T+2},\\]\nwhere \\(e^*_{T+2}\\) is another draw from the collection of residuals. Continuing in this way, we can simulate an entire set of future values for our time series."
+ },
+ {
+ "objectID": "docs/tutorials/prediction_intervals_in_forecasting_models.html#conformal-prediction",
+ "href": "docs/tutorials/prediction_intervals_in_forecasting_models.html#conformal-prediction",
+ "title": "Prediction intervals",
+ "section": "Conformal Prediction",
+ "text": "Conformal Prediction\nMulti-quantile losses and statistical models can provide provide prediction intervals, but the problem is that these are uncalibrated, meaning that the actual frequency of observations falling within the interval does not align with the confidence level associated with it. For example, a calibrated 95% prediction interval should contain the true value 95% of the time in repeated sampling. An uncalibrated 95% prediction interval, on the other hand, might contain the true value only 80% of the time, or perhaps 99% of the time. In the first case, the interval is too narrow and underestimates the uncertainty, while in the second case, it is too wide and overestimates the uncertainty.\nStatistical methods also assume normality. Here, we talk about another method called conformal prediction that doesn’t require any distributional assumptions.\nConformal prediction intervals use cross-validation on a point forecaster model to generate the intervals. This means that no prior probabilities are needed, and the output is well-calibrated. No additional training is needed, and the model is treated as a black box. The approach is compatible with any model\nmlforecast now supports Conformal Prediction on all available models."
+ },
+ {
+ "objectID": "docs/tutorials/prediction_intervals_in_forecasting_models.html#read-data",
+ "href": "docs/tutorials/prediction_intervals_in_forecasting_models.html#read-data",
+ "title": "Prediction intervals",
+ "section": "Read Data",
+ "text": "Read Data\n\ndata_url = \"https://raw.githubusercontent.com/Naren8520/Serie-de-tiempo-con-Machine-Learning/main/Data/nyc_taxi.csv\"\ndf = pd.read_csv(data_url, parse_dates=[\"timestamp\"])\ndf.head()\n\n\n\n\n\n\n\n\ntimestamp\nvalue\n\n\n\n\n0\n2014-07-01 00:00:00\n10844\n\n\n1\n2014-07-01 00:30:00\n8127\n\n\n2\n2014-07-01 01:00:00\n6210\n\n\n3\n2014-07-01 01:30:00\n4656\n\n\n4\n2014-07-01 02:00:00\n3820\n\n\n\n\n\n\n\nThe input to MlForecast is always a data frame in long format with three columns: unique_id, ds and y:\n\nThe unique_id (string, int or category) represents an identifier for the series.\nThe ds (datestamp) column should be of a format expected by Pandas, ideally YYYY-MM-DD for a date or YYYY-MM-DD HH:MM:SS for a timestamp.\nThe y (numeric) represents the measurement we wish to forecast.\n\n\ndf[\"unique_id\"] = \"1\"\ndf.columns=[\"ds\", \"y\", \"unique_id\"]\ndf.head()\n\n\n\n\n\n\n\n\nds\ny\nunique_id\n\n\n\n\n0\n2014-07-01 00:00:00\n10844\n1\n\n\n1\n2014-07-01 00:30:00\n8127\n1\n\n\n2\n2014-07-01 01:00:00\n6210\n1\n\n\n3\n2014-07-01 01:30:00\n4656\n1\n\n\n4\n2014-07-01 02:00:00\n3820\n1\n\n\n\n\n\n\n\n\ndf.info()\n\n<class 'pandas.core.frame.DataFrame'>\nRangeIndex: 10320 entries, 0 to 10319\nData columns (total 3 columns):\n # Column Non-Null Count Dtype \n--- ------ -------------- ----- \n 0 ds 10320 non-null datetime64[ns]\n 1 y 10320 non-null int64 \n 2 unique_id 10320 non-null object \ndtypes: datetime64[ns](1), int64(1), object(1)\nmemory usage: 242.0+ KB"
+ },
+ {
+ "objectID": "docs/tutorials/prediction_intervals_in_forecasting_models.html#the-augmented-dickey-fuller-test",
+ "href": "docs/tutorials/prediction_intervals_in_forecasting_models.html#the-augmented-dickey-fuller-test",
+ "title": "Prediction intervals",
+ "section": "The Augmented Dickey-Fuller Test",
+ "text": "The Augmented Dickey-Fuller Test\nAn Augmented Dickey-Fuller (ADF) test is a type of statistical test that determines whether a unit root is present in time series data. Unit roots can cause unpredictable results in time series analysis. A null hypothesis is formed in the unit root test to determine how strongly time series data is affected by a trend. By accepting the null hypothesis, we accept the evidence that the time series data is not stationary. By rejecting the null hypothesis or accepting the alternative hypothesis, we accept the evidence that the time series data is generated by a stationary process. This process is also known as stationary trend. The values of the ADF test statistic are negative. Lower ADF values indicate a stronger rejection of the null hypothesis.\nAugmented Dickey-Fuller Test is a common statistical test used to test whether a given time series is stationary or not. We can achieve this by defining the null and alternate hypothesis.\n\nNull Hypothesis: Time Series is non-stationary. It gives a time-dependent trend.\nAlternate Hypothesis: Time Series is stationary. In another term, the series doesn’t depend on time.\nADF or t Statistic < critical values: Reject the null hypothesis, time series is stationary.\nADF or t Statistic > critical values: Failed to reject the null hypothesis, time series is non-stationary.\n\n\ndef augmented_dickey_fuller_test(series , column_name):\n print (f'Dickey-Fuller test results for columns: {column_name}')\n dftest = adfuller(series, autolag='AIC')\n dfoutput = pd.Series(dftest[0:4], index=['Test Statistic','p-value','No Lags Used','Number of observations used'])\n for key,value in dftest[4].items():\n dfoutput['Critical Value (%s)'%key] = value\n print (dfoutput)\n if dftest[1] <= 0.05:\n print(\"Conclusion:====>\")\n print(\"Reject the null hypothesis\")\n print(\"The data is stationary\")\n else:\n print(\"Conclusion:====>\")\n print(\"The null hypothesis cannot be rejected\")\n print(\"The data is not stationary\")\n\n\naugmented_dickey_fuller_test(df[\"y\"],'Ads')\n\nDickey-Fuller test results for columns: Ads\nTest Statistic -1.076452e+01\np-value 2.472132e-19\nNo Lags Used 3.900000e+01\nNumber of observations used 1.028000e+04\nCritical Value (1%) -3.430986e+00\nCritical Value (5%) -2.861821e+00\nCritical Value (10%) -2.566920e+00\ndtype: float64\nConclusion:====>\nReject the null hypothesis\nThe data is stationary"
+ },
+ {
+ "objectID": "docs/tutorials/prediction_intervals_in_forecasting_models.html#autocorrelation-plots",
+ "href": "docs/tutorials/prediction_intervals_in_forecasting_models.html#autocorrelation-plots",
+ "title": "Prediction intervals",
+ "section": "Autocorrelation plots",
+ "text": "Autocorrelation plots\n\nAutocorrelation Function\nDefinition 1. Let \\(\\{x_t;1 ≤ t ≤ n\\}\\) be a time series sample of size n from \\(\\{X_t\\}\\). 1. \\(\\bar x = \\sum_{t=1}^n \\frac{x_t}{n}\\) is called the sample mean of \\(\\{X_t\\}\\). 2. \\(c_k =\\sum_{t=1}^{n−k} (x_{t+k}- \\bar x)(x_t−\\bar x)/n\\) is known as the sample autocovariance function of \\(\\{X_t\\}\\). 3. \\(r_k = c_k /c_0\\) is said to be the sample autocorrelation function of \\(\\{X_t\\}\\).\nNote the following remarks about this definition:\n\nLike most literature, this guide uses ACF to denote the sample autocorrelation function as well as the autocorrelation function. What is denoted by ACF can easily be identified in context.\nClearly c0 is the sample variance of \\(\\{X_t\\}\\). Besides, \\(r_0 = c_0/c_0 = 1\\) and for any integer \\(k, |r_k| ≤ 1\\).\nWhen we compute the ACF of any sample series with a fixed length \\(n\\), we cannot put too much confidence in the values of \\(r_k\\) for large k’s, since fewer pairs of \\((x_{t +k }, x_t )\\) are available for calculating \\(r_k\\) as \\(k\\) is large. One rule of thumb is not to estimate \\(r_k\\) for \\(k > n/3\\), and another is \\(n ≥ 50, k ≤ n/4\\). In any case, it is always a good idea to be careful.\nWe also compute the ACF of a nonstationary time series sample by Definition 1. In this case, however, the ACF or \\(r_k\\) very slowly or hardly tapers off as \\(k\\) increases.\nPlotting the ACF \\((r_k)\\) against lag \\(k\\) is easy but very helpful in analyzing time series sample. Such an ACF plot is known as a correlogram.\nIf \\(\\{X_t\\}\\) is stationary with \\(E(X_t)=0\\) and \\(\\rho_k =0\\) for all \\(k \\neq 0\\),thatis,itisa white noise series, then the sampling distribution of \\(r_k\\) is asymptotically normal with the mean 0 and the variance of \\(1/n\\). Hence, there is about 95% chance that \\(r_k\\) falls in the interval \\([−1.96/√n, 1.96/√n]\\).\n\nNow we can give a summary that (1) if the time series plot of a time series clearly shows a trend or/and seasonality, it is surely nonstationary; (2) if the ACF \\(r_k\\) very slowly or hardly tapers off as lag \\(k\\) increases, the time series should also be nonstationary.\n\nfig, axs = plt.subplots(nrows=1, ncols=2)\n\nplot_acf(df[\"y\"], lags=30, ax=axs[0],color=\"fuchsia\")\naxs[0].set_title(\"Autocorrelation\");\n\n# Grafico\nplot_pacf(df[\"y\"], lags=30, ax=axs[1],color=\"lime\")\naxs[1].set_title('Partial Autocorrelation')\nplt.savefig(\"../../figs/prediction_intervals_in_forecasting_models__autocorrelation.png\", bbox_inches='tight')\nplt.close();"
+ },
+ {
+ "objectID": "docs/tutorials/prediction_intervals_in_forecasting_models.html#decomposition-of-the-time-series",
+ "href": "docs/tutorials/prediction_intervals_in_forecasting_models.html#decomposition-of-the-time-series",
+ "title": "Prediction intervals",
+ "section": "Decomposition of the time series",
+ "text": "Decomposition of the time series\nHow to decompose a time series and why?\nIn time series analysis to forecast new values, it is very important to know past data. More formally, we can say that it is very important to know the patterns that values follow over time. There can be many reasons that cause our forecast values to fall in the wrong direction. Basically, a time series consists of four components. The variation of those components causes the change in the pattern of the time series. These components are:\n\nLevel: This is the primary value that averages over time.\nTrend: The trend is the value that causes increasing or decreasing patterns in a time series.\nSeasonality: This is a cyclical event that occurs in a time series for a short time and causes short-term increasing or decreasing patterns in a time series.\nResidual/Noise: These are the random variations in the time series.\n\nCombining these components over time leads to the formation of a time series. Most time series consist of level and noise/residual and trend or seasonality are optional values.\nIf seasonality and trend are part of the time series, then there will be effects on the forecast value. As the pattern of the forecasted time series may be different from the previous time series.\nThe combination of the components in time series can be of two types: * Additive * multiplicative\nAdditive time series\nIf the components of the time series are added to make the time series. Then the time series is called the additive time series. By visualization, we can say that the time series is additive if the increasing or decreasing pattern of the time series is similar throughout the series. The mathematical function of any additive time series can be represented by: \\[y(t) = level + Trend + seasonality + noise\\]"
+ },
+ {
+ "objectID": "docs/tutorials/prediction_intervals_in_forecasting_models.html#multiplicative-time-series",
+ "href": "docs/tutorials/prediction_intervals_in_forecasting_models.html#multiplicative-time-series",
+ "title": "Prediction intervals",
+ "section": "Multiplicative time series",
+ "text": "Multiplicative time series\nIf the components of the time series are multiplicative together, then the time series is called a multiplicative time series. For visualization, if the time series is having exponential growth or decline with time, then the time series can be considered as the multiplicative time series. The mathematical function of the multiplicative time series can be represented as.\n\\[y(t) = Level * Trend * seasonality * Noise\\]\n\nAdditive\n\na = seasonal_decompose(df[\"y\"], model = \"additive\", period=24).plot()\na.savefig('../../figs/prediction_intervals_in_forecasting_models__seasonal_decompose_aditive.png', bbox_inches='tight')\nplt.close()\n\n\n\n\nMultiplicative\n\nb = seasonal_decompose(df[\"y\"], model = \"Multiplicative\", period=24).plot()\nb.savefig('../../figs/prediction_intervals_in_forecasting_models__seasonal_decompose_multiplicative.png', bbox_inches='tight')\nplt.close();"
+ },
+ {
+ "objectID": "docs/tutorials/prediction_intervals_in_forecasting_models.html#building-model",
+ "href": "docs/tutorials/prediction_intervals_in_forecasting_models.html#building-model",
+ "title": "Prediction intervals",
+ "section": "Building Model",
+ "text": "Building Model\nWe define the model that we want to use, for our example we are going to use the XGBoost model.\n\nmodel1 = [xgb.XGBRegressor()]\n\nWe can use the MLForecast.preprocess method to explore different transformations.\nIf it is true that the series we are working with is a stationary series see (Dickey fuller test), however for the sake of practice and instruction in this guide, we will apply the difference to our series, we will do this using the target_transforms parameter and calling the diff function like: mlforecast.target_transforms.Differences\n\nmlf = MLForecast(models=model1,\n freq='30min', \n target_transforms=[Differences([1])],\n )\n\nIt is important to take into account when we use the parameter target_transforms=[Differences([1])] in case the series is stationary we can use a difference, or in the case that the series is not stationary, we can use more than one difference so that the series is constant over time, that is, that it is constant in mean and in variance.\n\nprep = mlf.preprocess(df)\nprep\n\n\n\n\n\n\n\n\nds\ny\nunique_id\n\n\n\n\n1\n2014-07-01 00:30:00\n-2717.0\n1\n\n\n2\n2014-07-01 01:00:00\n-1917.0\n1\n\n\n3\n2014-07-01 01:30:00\n-1554.0\n1\n\n\n4\n2014-07-01 02:00:00\n-836.0\n1\n\n\n5\n2014-07-01 02:30:00\n-947.0\n1\n\n\n...\n...\n...\n...\n\n\n10315\n2015-01-31 21:30:00\n951.0\n1\n\n\n10316\n2015-01-31 22:00:00\n1051.0\n1\n\n\n10317\n2015-01-31 22:30:00\n1588.0\n1\n\n\n10318\n2015-01-31 23:00:00\n-718.0\n1\n\n\n10319\n2015-01-31 23:30:00\n-303.0\n1\n\n\n\n\n10319 rows × 3 columns\n\n\n\nThis has subtacted the lag 1 from each value, we can see what our series look like now.\n\nfig = plot_series(prep)"
+ },
+ {
+ "objectID": "docs/tutorials/prediction_intervals_in_forecasting_models.html#adding-features",
+ "href": "docs/tutorials/prediction_intervals_in_forecasting_models.html#adding-features",
+ "title": "Prediction intervals",
+ "section": "Adding features",
+ "text": "Adding features\n\nLags\nLooks like the seasonality is gone, we can now try adding some lag features.\n\nmlf = MLForecast(models=model1,\n freq='30min', \n lags=[1,24],\n target_transforms=[Differences([1])],\n )\n\n\nprep = mlf.preprocess(df)\nprep\n\n\n\n\n\n\n\n\nds\ny\nunique_id\nlag1\nlag24\n\n\n\n\n25\n2014-07-01 12:30:00\n-22.0\n1\n445.0\n-2717.0\n\n\n26\n2014-07-01 13:00:00\n-708.0\n1\n-22.0\n-1917.0\n\n\n27\n2014-07-01 13:30:00\n1281.0\n1\n-708.0\n-1554.0\n\n\n28\n2014-07-01 14:00:00\n87.0\n1\n1281.0\n-836.0\n\n\n29\n2014-07-01 14:30:00\n1045.0\n1\n87.0\n-947.0\n\n\n...\n...\n...\n...\n...\n...\n\n\n10315\n2015-01-31 21:30:00\n951.0\n1\n428.0\n4642.0\n\n\n10316\n2015-01-31 22:00:00\n1051.0\n1\n951.0\n-519.0\n\n\n10317\n2015-01-31 22:30:00\n1588.0\n1\n1051.0\n2411.0\n\n\n10318\n2015-01-31 23:00:00\n-718.0\n1\n1588.0\n214.0\n\n\n10319\n2015-01-31 23:30:00\n-303.0\n1\n-718.0\n2595.0\n\n\n\n\n10295 rows × 5 columns\n\n\n\n\nprep.drop(columns=['unique_id', 'ds']).corr()['y']\n\ny 1.000000\nlag1 0.663082\nlag24 0.155366\nName: y, dtype: float64"
+ },
+ {
+ "objectID": "docs/tutorials/prediction_intervals_in_forecasting_models.html#lag-transforms",
+ "href": "docs/tutorials/prediction_intervals_in_forecasting_models.html#lag-transforms",
+ "title": "Prediction intervals",
+ "section": "Lag transforms",
+ "text": "Lag transforms\nLag transforms are defined as a dictionary where the keys are the lags and the values are lists of functions that transform an array. These must be numba jitted functions (so that computing the features doesn’t become a bottleneck). There are some implemented in the window-ops package but you can also implement your own.\nIf the function takes two or more arguments you can either:\n\nsupply a tuple (tfm_func, arg1, arg2, …)\ndefine a new function fixing the arguments\n\n\nfrom numba import njit\nfrom window_ops.expanding import expanding_mean\nfrom window_ops.rolling import rolling_mean\n\n\nmlf = MLForecast(models=model1,\n freq='30min', \n lags=[1,24],\n lag_transforms={1: [expanding_mean],24: [(rolling_mean, 7)] },\n target_transforms=[Differences([1])],\n )\n\n\nprep = mlf.preprocess(df)\nprep\n\n\n\n\n\n\n\n\nds\ny\nunique_id\nlag1\nlag24\nexpanding_mean_lag1\nrolling_mean_lag24_window_size7\n\n\n\n\n31\n2014-07-01 15:30:00\n-836.0\n1\n-1211.0\n-305.0\n284.533325\n-1254.285767\n\n\n32\n2014-07-01 16:00:00\n-2316.0\n1\n-836.0\n157.0\n248.387100\n-843.714294\n\n\n33\n2014-07-01 16:30:00\n-1215.0\n1\n-2316.0\n-63.0\n168.250000\n-578.857117\n\n\n34\n2014-07-01 17:00:00\n2190.0\n1\n-1215.0\n357.0\n126.333336\n-305.857147\n\n\n35\n2014-07-01 17:30:00\n2322.0\n1\n2190.0\n1849.0\n187.029419\n77.714287\n\n\n...\n...\n...\n...\n...\n...\n...\n...\n\n\n10315\n2015-01-31 21:30:00\n951.0\n1\n428.0\n4642.0\n1.248303\n2064.285645\n\n\n10316\n2015-01-31 22:00:00\n1051.0\n1\n951.0\n-519.0\n1.340378\n1873.428589\n\n\n10317\n2015-01-31 22:30:00\n1588.0\n1\n1051.0\n2411.0\n1.442129\n2179.000000\n\n\n10318\n2015-01-31 23:00:00\n-718.0\n1\n1588.0\n214.0\n1.595910\n1888.714233\n\n\n10319\n2015-01-31 23:30:00\n-303.0\n1\n-718.0\n2595.0\n1.526168\n2071.714355\n\n\n\n\n10289 rows × 7 columns\n\n\n\nYou can see that both approaches get to the same result, you can use whichever one you feel most comfortable with."
+ },
+ {
+ "objectID": "docs/tutorials/prediction_intervals_in_forecasting_models.html#date-features",
+ "href": "docs/tutorials/prediction_intervals_in_forecasting_models.html#date-features",
+ "title": "Prediction intervals",
+ "section": "Date features",
+ "text": "Date features\nIf your time column is made of timestamps then it might make sense to extract features like week, dayofweek, quarter, etc. You can do that by passing a list of strings with pandas time/date components. You can also pass functions that will take the time column as input, as we’ll show here.\n\nmlf = MLForecast(models=model1,\n freq='30min', \n lags=[1,24],\n lag_transforms={1: [expanding_mean],24: [(rolling_mean, 7)] },\n target_transforms=[Differences([1])],\n date_features=[\"year\", \"month\", \"day\", \"hour\"]) # Seasonal data\n\n\nprep = mlf.preprocess(df)\nprep\n\n\n\n\n\n\n\n\nds\ny\nunique_id\nlag1\nlag24\nexpanding_mean_lag1\nrolling_mean_lag24_window_size7\nyear\nmonth\nday\nhour\n\n\n\n\n31\n2014-07-01 15:30:00\n-836.0\n1\n-1211.0\n-305.0\n284.533325\n-1254.285767\n2014\n7\n1\n15\n\n\n32\n2014-07-01 16:00:00\n-2316.0\n1\n-836.0\n157.0\n248.387100\n-843.714294\n2014\n7\n1\n16\n\n\n33\n2014-07-01 16:30:00\n-1215.0\n1\n-2316.0\n-63.0\n168.250000\n-578.857117\n2014\n7\n1\n16\n\n\n34\n2014-07-01 17:00:00\n2190.0\n1\n-1215.0\n357.0\n126.333336\n-305.857147\n2014\n7\n1\n17\n\n\n35\n2014-07-01 17:30:00\n2322.0\n1\n2190.0\n1849.0\n187.029419\n77.714287\n2014\n7\n1\n17\n\n\n...\n...\n...\n...\n...\n...\n...\n...\n...\n...\n...\n...\n\n\n10315\n2015-01-31 21:30:00\n951.0\n1\n428.0\n4642.0\n1.248303\n2064.285645\n2015\n1\n31\n21\n\n\n10316\n2015-01-31 22:00:00\n1051.0\n1\n951.0\n-519.0\n1.340378\n1873.428589\n2015\n1\n31\n22\n\n\n10317\n2015-01-31 22:30:00\n1588.0\n1\n1051.0\n2411.0\n1.442129\n2179.000000\n2015\n1\n31\n22\n\n\n10318\n2015-01-31 23:00:00\n-718.0\n1\n1588.0\n214.0\n1.595910\n1888.714233\n2015\n1\n31\n23\n\n\n10319\n2015-01-31 23:30:00\n-303.0\n1\n-718.0\n2595.0\n1.526168\n2071.714355\n2015\n1\n31\n23\n\n\n\n\n10289 rows × 11 columns"
+ },
+ {
+ "objectID": "docs/tutorials/prediction_intervals_in_forecasting_models.html#fit-the-model",
+ "href": "docs/tutorials/prediction_intervals_in_forecasting_models.html#fit-the-model",
+ "title": "Prediction intervals",
+ "section": "Fit the Model",
+ "text": "Fit the Model\n\n# fit the models\nmlf.fit(df, \n fitted=True, \nprediction_intervals=PredictionIntervals(n_windows=5, h=30, method=\"conformal_distribution\" ) )\n\nMLForecast(models=[XGBRegressor], freq=<30 * Minutes>, lag_features=['lag1', 'lag24', 'expanding_mean_lag1', 'rolling_mean_lag24_window_size7'], date_features=['year', 'month', 'day', 'hour'], num_threads=1)\n\n\nLet’s see the results of our model in this case the XGBoost model. We can observe it with the following instruction:\nLet us now visualize the fitted values of our models.\n\nresult=mlf.forecast_fitted_values()\nresult=result.set_index(\"unique_id\")\nresult\n\n\n\n\n\n\n\n\nds\ny\nXGBRegressor\n\n\nunique_id\n\n\n\n\n\n\n\n1\n2014-07-01 15:30:00\n18544.0\n18243.291016\n\n\n1\n2014-07-01 16:00:00\n16228.0\n16489.828125\n\n\n1\n2014-07-01 16:30:00\n15013.0\n15105.728516\n\n\n1\n2014-07-01 17:00:00\n17203.0\n17362.349609\n\n\n1\n2014-07-01 17:30:00\n19525.0\n19678.052734\n\n\n...\n...\n...\n...\n\n\n1\n2015-01-31 21:30:00\n24670.0\n24801.906250\n\n\n1\n2015-01-31 22:00:00\n25721.0\n25812.089844\n\n\n1\n2015-01-31 22:30:00\n27309.0\n27192.630859\n\n\n1\n2015-01-31 23:00:00\n26591.0\n27066.931641\n\n\n1\n2015-01-31 23:30:00\n26288.0\n25945.341797\n\n\n\n\n10289 rows × 3 columns\n\n\n\n\nfrom statsmodels.stats.diagnostic import normal_ad\nfrom scipy import stats\n\n\nsw_result = stats.shapiro(result[\"XGBRegressor\"])\nad_result = normal_ad(np.array(result[\"XGBRegressor\"]), axis=0)\ndag_result = stats.normaltest(result[\"XGBRegressor\"], axis=0, nan_policy='propagate')\n\nIt’s important to note that we can only use this method if we assume that the residuals of our validation predictions are normally distributed. To see if this is the case, we will use a PP-plot and test its normality with the Anderson-Darling, Kolmogorov-Smirnov, and D’Agostino K^2 tests.\nThe PP-plot(Probability-to-Probability) plots the data sample against the normal distribution plot in such a way that if normally distributed, the data points will form a straight line.\nThe three normality tests determine how likely a data sample is from a normally distributed population using p-values. The null hypothesis for each test is that “the sample came from a normally distributed population”. This means that if the resulting p-values are below a chosen alpha value, then the null hypothesis is rejected. Thus there is evidence to suggest that the data comes from a non-normal distribution. For this article, we will use an Alpha value of 0.01.\n\nresult=mlf.forecast_fitted_values()\nfig, axs = plt.subplots(nrows=2, ncols=2)\n\n# plot[1,1]\nresult[\"XGBRegressor\"].plot(ax=axs[0,0])\naxs[0,0].set_title(\"Residuals model\");\n\n# plot\naxs[0,1].hist(result[\"XGBRegressor\"], density=True,bins=50, alpha=0.5 )\naxs[0,1].set_title(\"Density plot - Residual\");\n\n# plot\nstats.probplot(result[\"XGBRegressor\"], dist=\"norm\", plot=axs[1,0])\naxs[1,0].set_title('Plot Q-Q')\naxs[1,0].annotate(\"SW p-val: {:.4f}\".format(sw_result[1]), xy=(0.05,0.9), xycoords='axes fraction', fontsize=15,\n bbox=dict(boxstyle=\"round\", fc=\"none\", ec=\"gray\", pad=0.6))\n\naxs[1,0].annotate(\"AD p-val: {:.4f}\".format(ad_result[1]), xy=(0.05,0.8), xycoords='axes fraction', fontsize=15,\n bbox=dict(boxstyle=\"round\", fc=\"none\", ec=\"gray\", pad=0.6))\n\naxs[1,0].annotate(\"DAG p-val: {:.4f}\".format(dag_result[1]), xy=(0.05,0.7), xycoords='axes fraction', fontsize=15,\n bbox=dict(boxstyle=\"round\", fc=\"none\", ec=\"gray\", pad=0.6))\n# plot\nplot_acf(result[\"XGBRegressor\"], lags=35, ax=axs[1,1],color=\"fuchsia\")\naxs[1,1].set_title(\"Autocorrelation\");\n\nplt.savefig(\"../../figs/prediction_intervals_in_forecasting_models__plot_residual_model.png\", bbox_inches='tight')\nplt.close();"
+ },
+ {
+ "objectID": "docs/tutorials/prediction_intervals_in_forecasting_models.html#predict-method-with-prediction-intervals",
+ "href": "docs/tutorials/prediction_intervals_in_forecasting_models.html#predict-method-with-prediction-intervals",
+ "title": "Prediction intervals",
+ "section": "Predict method with prediction intervals",
+ "text": "Predict method with prediction intervals\nTo generate forecasts use the predict method.\n\nforecast_df = mlf.predict(h=30, level=[80,95])\nforecast_df.head()\n\n\n\n\n\n\n\n\nunique_id\nds\nXGBRegressor\nXGBRegressor-lo-95\nXGBRegressor-lo-80\nXGBRegressor-hi-80\nXGBRegressor-hi-95\n\n\n\n\n0\n1\n2015-02-01 00:00:00\n24608.865234\n24016.475873\n24085.588062\n25132.142407\n25201.254596\n\n\n1\n1\n2015-02-01 00:30:00\n23323.097656\n20511.105615\n21901.008398\n24745.186914\n26135.089697\n\n\n2\n1\n2015-02-01 01:00:00\n22223.435547\n20161.902002\n20995.971289\n23450.899805\n24284.969092\n\n\n3\n1\n2015-02-01 01:30:00\n20405.228516\n17227.147949\n17822.294922\n22988.162109\n23583.309082\n\n\n4\n1\n2015-02-01 02:00:00\n20014.324219\n17422.155518\n17923.692383\n22104.956055\n22606.492920"
+ },
+ {
+ "objectID": "docs/tutorials/prediction_intervals_in_forecasting_models.html#plot-prediction-intervals",
+ "href": "docs/tutorials/prediction_intervals_in_forecasting_models.html#plot-prediction-intervals",
+ "title": "Prediction intervals",
+ "section": "Plot prediction intervals",
+ "text": "Plot prediction intervals\nNow let’s visualize the result of our forecast and the historical data of our time series, also let’s draw the confidence interval that we have obtained when making the prediction with 95% confidence.\n\nfig = plot_series(df, forecast_df, level=[80,95], max_insample_length=200,engine=\"matplotlib\")\nfig.get_axes()[0].set_title(\"Prediction intervals\")\nfig.savefig('../../figs/prediction_intervals_in_forecasting_models__plot_forecasting_intervals.png', bbox_inches='tight')\n\n\nThe confidence interval is a range of values that has a high probability of containing the true value of a variable. In machine learning time series models, the confidence interval is used to estimate the uncertainty in the predictions.\nOne of the main benefits of using the confidence interval is that it allows users to understand the accuracy of the predictions. For example, if the confidence interval is very wide, it means that the prediction is less accurate. Conversely, if the confidence interval is very narrow, it means that the prediction is more accurate.\nAnother benefit of the confidence interval is that it helps users make informed decisions. For example, if a prediction is within the confidence interval, it means that it is likely to come true. Conversely, if a prediction is outside the confidence interval, it means that it is less likely to come true.\nIn general, the confidence interval is an important tool for machine learning time series models. It helps users understand the accuracy of the forecasts and make informed decisions."
+ },
+ {
+ "objectID": "docs/how-to-guides/custom_training.html",
+ "href": "docs/how-to-guides/custom_training.html",
+ "title": "Custom training",
+ "section": "",
+ "text": "mlforecast abstracts away most of the training details, which is useful for iterating quickly. However, sometimes you want more control over the fit parameters, the data that goes into the model, etc. This guide shows how you can train a model in a specific way and then giving it back to mlforecast to produce forecasts with it.\nGive us a ⭐ on Github"
+ },
+ {
+ "objectID": "docs/how-to-guides/custom_training.html#data-setup",
+ "href": "docs/how-to-guides/custom_training.html#data-setup",
+ "title": "Custom training",
+ "section": "Data setup",
+ "text": "Data setup\n\nfrom mlforecast.utils import generate_daily_series\n\n\nseries = generate_daily_series(5)"
+ },
+ {
+ "objectID": "docs/how-to-guides/custom_training.html#creating-forecast-object",
+ "href": "docs/how-to-guides/custom_training.html#creating-forecast-object",
+ "title": "Custom training",
+ "section": "Creating forecast object",
+ "text": "Creating forecast object\n\nimport numpy as np\nfrom lightgbm import LGBMRegressor\n\nfrom mlforecast import MLForecast\n\nInstantiate the forecast object without any models.\n\nfcst = MLForecast(\n freq='D', \n models={},\n date_features=['dayofweek'],\n)"
+ },
+ {
+ "objectID": "docs/how-to-guides/custom_training.html#generate-training-set",
+ "href": "docs/how-to-guides/custom_training.html#generate-training-set",
+ "title": "Custom training",
+ "section": "Generate training set",
+ "text": "Generate training set\nUse MLForecast.preprocess to generate the training data.\n\nprep = fcst.preprocess(series)\nprep.head()\n\n\n\n\n\n\n\n\nunique_id\nds\ny\ndayofweek\n\n\n\n\n0\nid_0\n2000-01-01\n0.428973\n5\n\n\n1\nid_0\n2000-01-02\n1.423626\n6\n\n\n2\nid_0\n2000-01-03\n2.311782\n0\n\n\n3\nid_0\n2000-01-04\n3.192191\n1\n\n\n4\nid_0\n2000-01-05\n4.148767\n2\n\n\n\n\n\n\n\n\nX = prep.drop(columns=['unique_id', 'ds', 'y'])\ny = prep['y']"
+ },
+ {
+ "objectID": "docs/how-to-guides/custom_training.html#training",
+ "href": "docs/how-to-guides/custom_training.html#training",
+ "title": "Custom training",
+ "section": "Training",
+ "text": "Training\nLet’s say you want to use sample weights to train the model, here we just use random weights for simplicity.\n\nrng = np.random.RandomState(0)\nweights = rng.rand(y.size)\n\nWe train the model as we normally would and provide the weights through the sample_weight argument.\n\nmodel = LGBMRegressor(verbosity=-1).fit(X, y, sample_weight=weights)"
+ },
+ {
+ "objectID": "docs/how-to-guides/custom_training.html#computing-forecasts",
+ "href": "docs/how-to-guides/custom_training.html#computing-forecasts",
+ "title": "Custom training",
+ "section": "Computing forecasts",
+ "text": "Computing forecasts\nNow we just assign this model to the MLForecast.models_ attribute. Note that you can assign as many models as you want.\n\nfcst.models_ = {'lgbm': model, 'same_lgbm': model}\n\nAnd now when calling MLForecast.predict mlforecast will use those models to compute the forecasts.\n\nfcst.predict(1)\n\n\n\n\n\n\n\n\nunique_id\nds\nlgbm\nsame_lgbm\n\n\n\n\n0\nid_0\n2000-08-10\n3.642456\n3.642456\n\n\n1\nid_1\n2000-04-07\n4.808618\n4.808618\n\n\n2\nid_2\n2000-06-16\n4.808618\n4.808618\n\n\n3\nid_3\n2000-08-30\n2.777129\n2.777129\n\n\n4\nid_4\n2001-01-08\n3.404631\n3.404631"
+ },
+ {
+ "objectID": "docs/how-to-guides/predict_subset.html",
+ "href": "docs/how-to-guides/predict_subset.html",
+ "title": "Predicting a subset of ids",
+ "section": "",
+ "text": "from lightgbm import LGBMRegressor\nfrom fastcore.test import test_fail\n\nfrom mlforecast import MLForecast\nfrom mlforecast.utils import generate_daily_series\n\n\nseries = generate_daily_series(5)\nfcst = MLForecast({'lgb': LGBMRegressor(verbosity=-1)}, freq='D', date_features=['dayofweek'])\nfcst.fit(series)\nall_preds = fcst.predict(1)\nall_preds\n\n\n\n\n\n\n\n\nunique_id\nds\nlgb\n\n\n\n\n0\nid_0\n2000-08-10\n3.728396\n\n\n1\nid_1\n2000-04-07\n4.749133\n\n\n2\nid_2\n2000-06-16\n4.749133\n\n\n3\nid_3\n2000-08-30\n2.758949\n\n\n4\nid_4\n2001-01-08\n3.331394\n\n\n\n\n\n\n\nBy default all series seen during training will be forecasted with the predict method. If you’re only interested in predicting a couple of them you can use the ids argument.\n\nfcst.predict(1, ids=['id_0', 'id_4'])\n\n\n\n\n\n\n\n\nunique_id\nds\nlgb\n\n\n\n\n0\nid_0\n2000-08-10\n3.728396\n\n\n1\nid_4\n2001-01-08\n3.331394\n\n\n\n\n\n\n\nNote that the ids must’ve been seen during training, if you try to predict an id that wasn’t there you’ll get an error.\n\ntest_fail(lambda: fcst.predict(1, ids=['fake_id']), contains='fake_id')\n\n\n\n\nGive us a ⭐ on Github"
+ },
+ {
+ "objectID": "docs/how-to-guides/analyzing_models.html",
+ "href": "docs/how-to-guides/analyzing_models.html",
+ "title": "Analyzing the trained models",
+ "section": "",
+ "text": "from mlforecast.utils import generate_daily_series\n\n\nseries = generate_daily_series(10)\nseries.head()\n\n\n\n\n\n\n\n\nunique_id\nds\ny\n\n\n\n\n0\nid_0\n2000-01-01\n0.322947\n\n\n1\nid_0\n2000-01-02\n1.218794\n\n\n2\nid_0\n2000-01-03\n2.445887\n\n\n3\nid_0\n2000-01-04\n3.481831\n\n\n4\nid_0\n2000-01-05\n4.191721\nGive us a ⭐ on Github"
+ },
+ {
+ "objectID": "docs/how-to-guides/analyzing_models.html#data-setup",
+ "href": "docs/how-to-guides/analyzing_models.html#data-setup",
+ "title": "Analyzing the trained models",
+ "section": "",
+ "text": "from mlforecast.utils import generate_daily_series\n\n\nseries = generate_daily_series(10)\nseries.head()\n\n\n\n\n\n\n\n\nunique_id\nds\ny\n\n\n\n\n0\nid_0\n2000-01-01\n0.322947\n\n\n1\nid_0\n2000-01-02\n1.218794\n\n\n2\nid_0\n2000-01-03\n2.445887\n\n\n3\nid_0\n2000-01-04\n3.481831\n\n\n4\nid_0\n2000-01-05\n4.191721"
+ },
+ {
+ "objectID": "docs/how-to-guides/analyzing_models.html#training",
+ "href": "docs/how-to-guides/analyzing_models.html#training",
+ "title": "Analyzing the trained models",
+ "section": "Training",
+ "text": "Training\nSuppose that you want to train a linear regression model using the day of the week and lag1 as features.\n\nfrom sklearn.linear_model import LinearRegression\n\nfrom mlforecast import MLForecast\n\n\nfcst = MLForecast(\n freq='D',\n models={'lr': LinearRegression()},\n lags=[1],\n date_features=['dayofweek'],\n)\n\n\nfcst.fit(series)\n\nMLForecast(models=[lr], freq=<Day>, lag_features=['lag1'], date_features=['dayofweek'], num_threads=1)\n\n\nWhat MLForecast.fit does is save the required data for the predict step and also train the models (in this case the linear regression). The trained models are available in the MLForecast.models_ attribute, which is a dictionary where the keys are the model names and the values are the model themselves.\n\nfcst.models_\n\n{'lr': LinearRegression()}"
+ },
+ {
+ "objectID": "docs/how-to-guides/analyzing_models.html#inspect-parameters",
+ "href": "docs/how-to-guides/analyzing_models.html#inspect-parameters",
+ "title": "Analyzing the trained models",
+ "section": "Inspect parameters",
+ "text": "Inspect parameters\nWe can access the linear regression coefficients in the following way:\n\nfcst.models_['lr'].intercept_, fcst.models_['lr'].coef_\n\n(3.2476337167384415, array([ 0.19896416, -0.21441331]))"
+ },
+ {
+ "objectID": "docs/how-to-guides/analyzing_models.html#shap",
+ "href": "docs/how-to-guides/analyzing_models.html#shap",
+ "title": "Analyzing the trained models",
+ "section": "SHAP",
+ "text": "SHAP\n\nimport shap\n\n\nTraining set\nIf you need to generate the training data you can use MLForecast.preprocess.\n\nprep = fcst.preprocess(series)\nprep.head()\n\n\n\n\n\n\n\n\nunique_id\nds\ny\nlag1\ndayofweek\n\n\n\n\n1\nid_0\n2000-01-02\n1.218794\n0.322947\n6\n\n\n2\nid_0\n2000-01-03\n2.445887\n1.218794\n0\n\n\n3\nid_0\n2000-01-04\n3.481831\n2.445887\n1\n\n\n4\nid_0\n2000-01-05\n4.191721\n3.481831\n2\n\n\n5\nid_0\n2000-01-06\n5.395863\n4.191721\n3\n\n\n\n\n\n\n\nWe extract the X, which involves dropping the info columns (id + times) and the target\n\nX = prep.drop(columns=['unique_id', 'ds', 'y'])\nX.head()\n\n\n\n\n\n\n\n\nlag1\ndayofweek\n\n\n\n\n1\n0.322947\n6\n\n\n2\n1.218794\n0\n\n\n3\n2.445887\n1\n\n\n4\n3.481831\n2\n\n\n5\n4.191721\n3\n\n\n\n\n\n\n\nWe can now compute the shap values\n\nX100 = shap.utils.sample(X, 100)\nexplainer = shap.Explainer(fcst.models_['lr'].predict, X100)\nshap_values = explainer(X)\n\nAnd visualize them\n\nshap.plots.beeswarm(shap_values)\n\n\n\n\n\n\nPredictions\nSometimes you want to determine why the model gave a specific prediction. In order to do this you need the input features, which aren’t returned by default, but you can retrieve them using a callback.\n\nfrom mlforecast.callbacks import SaveFeatures\n\n\nsave_feats = SaveFeatures()\npreds = fcst.predict(1, before_predict_callback=save_feats)\npreds.head()\n\n\n\n\n\n\n\n\nunique_id\nds\nlr\n\n\n\n\n0\nid_0\n2000-08-10\n3.468643\n\n\n1\nid_1\n2000-04-07\n3.016877\n\n\n2\nid_2\n2000-06-16\n2.815249\n\n\n3\nid_3\n2000-08-30\n4.048894\n\n\n4\nid_4\n2001-01-08\n3.524532\n\n\n\n\n\n\n\nYou can now retrieve the features by using SaveFeatures.get_features\n\nfeatures = save_feats.get_features()\nfeatures.head()\n\n\n\n\n\n\n\n\nlag1\ndayofweek\n\n\n\n\n0\n4.343744\n3\n\n\n1\n3.150799\n4\n\n\n2\n2.137412\n4\n\n\n3\n6.182456\n2\n\n\n4\n1.391698\n0\n\n\n\n\n\n\n\nAnd use those features to compute the shap values.\n\nshap_values_predictions = explainer(features)\n\nWe can now analyze what influenced the prediction for 'id_4'.\n\nround(preds.loc[4, 'lr'], 3)\n\n3.525\n\n\n\nshap.plots.waterfall(shap_values_predictions[4])"
+ },
+ {
+ "objectID": "docs/how-to-guides/cross_validation.html",
+ "href": "docs/how-to-guides/cross_validation.html",
+ "title": "Cross validation",
+ "section": "",
+ "text": "Prerequesites\n\n\n\n\n\nThis tutorial assumes basic familiarity with MLForecast. For a minimal example visit the Quick Start\nGive us a ⭐ on Github"
+ },
+ {
+ "objectID": "docs/how-to-guides/cross_validation.html#introduction",
+ "href": "docs/how-to-guides/cross_validation.html#introduction",
+ "title": "Cross validation",
+ "section": "Introduction",
+ "text": "Introduction\nTime series cross-validation is a method for evaluating how a model would have performed in the past. It works by defining a sliding window across the historical data and predicting the period following it.\n\nMLForecast has an implementation of time series cross-validation that is fast and easy to use. This implementation makes cross-validation a efficient operation, which makes it less time-consuming. In this notebook, we’ll use it on a subset of the M4 Competition hourly dataset.\nOutline:\n\nInstall libraries\nLoad and explore data\nTrain model\nPerform time series cross-validation\nEvaluate results\n\n\n\n\n\n\n\nTip\n\n\n\nYou can use Colab to run this Notebook interactively"
+ },
+ {
+ "objectID": "docs/how-to-guides/cross_validation.html#install-libraries",
+ "href": "docs/how-to-guides/cross_validation.html#install-libraries",
+ "title": "Cross validation",
+ "section": "Install libraries",
+ "text": "Install libraries\nWe assume that you have MLForecast already installed. If not, check this guide for instructions on how to install MLForecast.\nInstall the necessary packages with pip install mlforecast.\n\n# pip install mlforecast lightgbm\n\n\nimport pandas as pd \n\nfrom utilsforecast.plotting import plot_series\n\nfrom mlforecast import MLForecast # required to instantiate MLForecast object and use cross-validation method"
+ },
+ {
+ "objectID": "docs/how-to-guides/cross_validation.html#load-and-explore-the-data",
+ "href": "docs/how-to-guides/cross_validation.html#load-and-explore-the-data",
+ "title": "Cross validation",
+ "section": "Load and explore the data",
+ "text": "Load and explore the data\nAs stated in the introduction, we’ll use the M4 Competition hourly dataset. We’ll first import the data from an URL using pandas.\n\nY_df = pd.read_csv('https://datasets-nixtla.s3.amazonaws.com/m4-hourly.csv') # load the data \nY_df.head()\n\n\n\n\n\n\n\n\nunique_id\nds\ny\n\n\n\n\n0\nH1\n1\n605.0\n\n\n1\nH1\n2\n586.0\n\n\n2\nH1\n3\n586.0\n\n\n3\nH1\n4\n559.0\n\n\n4\nH1\n5\n511.0\n\n\n\n\n\n\n\nThe input to MLForecast is a data frame in long format with three columns: unique_id, ds and y:\n\nThe unique_id (string, int, or category) represents an identifier for the series.\nThe ds (datestamp or int) column should be either an integer indexing time or a datestamp in format YYYY-MM-DD or YYYY-MM-DD HH:MM:SS.\nThe y (numeric) represents the measurement we wish to forecast.\n\nThe data in this example already has this format, so no changes are needed.\nWe can plot the time series we’ll work with using the following function.\n\nfig = plot_series(Y_df, max_ids=4, plot_random=False, max_insample_length=24 * 14)"
+ },
+ {
+ "objectID": "docs/how-to-guides/cross_validation.html#define-forecast-object",
+ "href": "docs/how-to-guides/cross_validation.html#define-forecast-object",
+ "title": "Cross validation",
+ "section": "Define forecast object",
+ "text": "Define forecast object\nFor this example, we’ll use LightGBM. We first need to import it and then we need to instantiate a new MLForecast object.\nIn this example, we are only using differences and lags to produce features. See the full documentation to see all available features.\nAny settings are passed into the constructor. Then you call its fit method and pass in the historical data frame df.\n\nimport lightgbm as lgb\nfrom mlforecast.target_transforms import Differences\n\n\nmodels = [lgb.LGBMRegressor(verbosity=-1)]\n\nmlf = MLForecast(\n models = models, \n freq = 1,# our series have integer timestamps, so we'll just add 1 in every timeste, \n target_transforms=[Differences([24])],\n lags=range(1, 25, 1)\n)"
+ },
+ {
+ "objectID": "docs/how-to-guides/cross_validation.html#perform-time-series-cross-validation",
+ "href": "docs/how-to-guides/cross_validation.html#perform-time-series-cross-validation",
+ "title": "Cross validation",
+ "section": "Perform time series cross-validation",
+ "text": "Perform time series cross-validation\nOnce the MLForecast object has been instantiated, we can use the cross_validation method.\nFor this particular example, we’ll use 3 windows of 24 hours.\n\ncrossvalidation_df = mlf.cross_validation(\n df=Y_df,\n h=24,\n n_windows=3,\n)\n\nThe crossvaldation_df object is a new data frame that includes the following columns:\n\nunique_id: identifies each time series.\nds: datestamp or temporal index.\ncutoff: the last datestamp or temporal index for the n_windows.\ny: true value\n\"model\": columns with the model’s name and fitted value.\n\n\ncrossvalidation_df.head()\n\n\n\n\n\n\n\n\nunique_id\nds\ncutoff\ny\nLGBMRegressor\n\n\n\n\n0\nH1\n677\n676\n691.0\n673.703191\n\n\n1\nH1\n678\n676\n618.0\n552.306270\n\n\n2\nH1\n679\n676\n563.0\n541.778027\n\n\n3\nH1\n680\n676\n529.0\n502.778027\n\n\n4\nH1\n681\n676\n504.0\n480.778027\n\n\n\n\n\n\n\nWe’ll now plot the forecast for each cutoff period.\n\nimport matplotlib.pyplot as plt\n\n\ndef plot_cv(df, df_cv, uid, fname, last_n=24 * 14):\n cutoffs = df_cv.query('unique_id == @uid')['cutoff'].unique()\n fig, ax = plt.subplots(nrows=len(cutoffs), ncols=1, figsize=(14, 6), gridspec_kw=dict(hspace=0.8))\n for cutoff, axi in zip(cutoffs, ax.flat):\n df.query('unique_id == @uid').tail(last_n).set_index('ds').plot(ax=axi, title=uid, y='y')\n df_cv.query('unique_id == @uid & cutoff == @cutoff').set_index('ds').plot(ax=axi, title=uid, y='LGBMRegressor')\n fig.savefig(fname, bbox_inches='tight')\n plt.close()\n\n\nplot_cv(Y_df, crossvalidation_df, 'H1', '../../figs/cross_validation__predictions.png')\n\n\nNotice that in each cutoff period, we generated a forecast for the next 24 hours using only the data y before said period."
+ },
+ {
+ "objectID": "docs/how-to-guides/cross_validation.html#evaluate-results",
+ "href": "docs/how-to-guides/cross_validation.html#evaluate-results",
+ "title": "Cross validation",
+ "section": "Evaluate results",
+ "text": "Evaluate results\nWe can now compute the accuracy of the forecast using an appropiate accuracy metric. Here we’ll use the Root Mean Squared Error (RMSE). To do this, we can use utilsforecast, a Python library developed by Nixtla that includes a function to compute the RMSE.\n\nfrom utilsforecast.losses import rmse\n\nWe’ll compute the rmse per time series and cutoff. To do this we’ll concatenate the id and the cutoff columns, then we will take the mean of the results.\n\ncrossvalidation_df['id_cutoff'] = crossvalidation_df['unique_id'] + '_' + crossvalidation_df['cutoff'].astype(str)\ncv_rmse = rmse(crossvalidation_df, models=['LGBMRegressor'], id_col='id_cutoff')['LGBMRegressor'].mean()\nprint(\"RMSE using cross-validation: \", cv_rmse)\n\nRMSE using cross-validation: 249.90517171185527\n\n\nThis measure should better reflect the predictive abilities of our model, since it used different time periods to test its accuracy."
+ },
+ {
+ "objectID": "docs/how-to-guides/cross_validation.html#references",
+ "href": "docs/how-to-guides/cross_validation.html#references",
+ "title": "Cross validation",
+ "section": "References",
+ "text": "References\nRob J. Hyndman and George Athanasopoulos (2018). “Forecasting principles and practice, Time series cross-validation”."
+ },
+ {
+ "objectID": "docs/how-to-guides/one_model_per_horizon.html",
+ "href": "docs/how-to-guides/one_model_per_horizon.html",
+ "title": "One model per step",
+ "section": "",
+ "text": "By default mlforecast uses the recursive strategy, i.e. a model is trained to predict the next value and if we’re predicting several values we do it one at a time and then use the model’s predictions as the new target, recompute the features and predict the next step.\nThere’s another approach where if we want to predict 10 steps ahead we train 10 different models, where each model is trained to predict the value at each specific step, i.e. one model predicts the next value, another one predicts the value two steps ahead and so on. This can be very time consuming but can also provide better results. If you want to use this approach you can specify max_horizon in MLForecast.fit, which will train that many models and each model will predict its corresponding horizon when you call MLForecast.predict.\nGive us a ⭐ on Github"
+ },
+ {
+ "objectID": "docs/how-to-guides/one_model_per_horizon.html#setup",
+ "href": "docs/how-to-guides/one_model_per_horizon.html#setup",
+ "title": "One model per step",
+ "section": "Setup",
+ "text": "Setup\n\nimport random\nimport lightgbm as lgb\nimport pandas as pd\nfrom datasetsforecast.m4 import M4, M4Info\nfrom utilsforecast.evaluation import evaluate\nfrom utilsforecast.losses import smape\nfrom window_ops.ewm import ewm_mean\nfrom window_ops.rolling import rolling_mean\n\nfrom mlforecast import MLForecast\nfrom mlforecast.target_transforms import Differences\n\n\nData\nWe will use four random series from the M4 dataset\n\ngroup = 'Hourly'\nawait M4.async_download('data', group=group)\ndf, *_ = M4.load(directory='data', group=group)\ndf['ds'] = df['ds'].astype('int')\nids = df['unique_id'].unique()\nrandom.seed(0)\nsample_ids = random.choices(ids, k=4)\nsample_df = df[df['unique_id'].isin(sample_ids)]\ninfo = M4Info[group]\nhorizon = info.horizon\nvalid = sample_df.groupby('unique_id').tail(horizon)\ntrain = sample_df.drop(valid.index)\n\n\ndef avg_smape(df):\n \"\"\"Computes the SMAPE by serie and then averages it across all series.\"\"\"\n full = df.merge(valid)\n return (\n evaluate(full, metrics=[smape])\n .drop(columns='metric')\n .set_index('unique_id')\n .squeeze()\n )"
+ },
+ {
+ "objectID": "docs/how-to-guides/one_model_per_horizon.html#model",
+ "href": "docs/how-to-guides/one_model_per_horizon.html#model",
+ "title": "One model per step",
+ "section": "Model",
+ "text": "Model\n\nfcst = MLForecast(\n models=lgb.LGBMRegressor(random_state=0, verbosity=-1),\n lags=[24 * (i+1) for i in range(7)],\n lag_transforms={\n 1: [(rolling_mean, 24)],\n 24: [(rolling_mean, 24)],\n 48: [(ewm_mean, 0.3)],\n },\n num_threads=1,\n target_transforms=[Differences([24])],\n)\n\n\nhorizon = 24\n# the following will train 24 models, one for each horizon\nindividual_fcst = fcst.fit(train, max_horizon=horizon)\nindividual_preds = individual_fcst.predict(horizon)\navg_smape_individual = avg_smape(individual_preds).rename('individual')\n# the following will train a single model and use the recursive strategy\nrecursive_fcst = fcst.fit(train)\nrecursive_preds = recursive_fcst.predict(horizon)\navg_smape_recursive = avg_smape(recursive_preds).rename('recursive')\n# results\nprint('Average SMAPE per method and serie')\navg_smape_individual.to_frame().join(avg_smape_recursive).applymap('{:.1%}'.format)\n\nAverage SMAPE per method and serie\n\n\n\n\n\n\n\n\n\nindividual\nrecursive\n\n\nunique_id\n\n\n\n\n\n\nH196\n0.3%\n0.3%\n\n\nH256\n0.4%\n0.3%\n\n\nH381\n20.9%\n9.5%\n\n\nH413\n11.9%\n13.6%"
+ },
+ {
+ "objectID": "docs/how-to-guides/custom_date_features.html",
+ "href": "docs/how-to-guides/custom_date_features.html",
+ "title": "Custom date features",
+ "section": "",
+ "text": "from mlforecast import MLForecast\nfrom mlforecast.utils import generate_daily_series\n\nThe date_features argument of MLForecast can take pandas date attributes as well as functions that take a pandas DatetimeIndex and return a numeric value. The name of the function is used as the name of the feature, so please use unique and descriptive names.\n\nseries = generate_daily_series(1, min_length=6, max_length=6)\n\n\ndef even_day(dates):\n \"\"\"Day of month is even\"\"\"\n return dates.day % 2 == 0\n\ndef month_start_or_end(dates):\n \"\"\"Date is month start or month end\"\"\"\n return dates.is_month_start | dates.is_month_end\n\ndef is_monday(dates):\n \"\"\"Date is monday\"\"\"\n return dates.dayofweek == 0\n\n\nfcst = MLForecast(\n [],\n freq='D',\n date_features=['dayofweek', 'dayofyear', even_day, month_start_or_end, is_monday]\n)\nfcst.preprocess(series)\n\n\n\n\n\n\n\n\nunique_id\nds\ny\ndayofweek\ndayofyear\neven_day\nmonth_start_or_end\nis_monday\n\n\n\n\n0\nid_0\n2000-01-01\n0.274407\n5\n1\nFalse\nTrue\nFalse\n\n\n1\nid_0\n2000-01-02\n1.357595\n6\n2\nTrue\nFalse\nFalse\n\n\n2\nid_0\n2000-01-03\n2.301382\n0\n3\nFalse\nFalse\nTrue\n\n\n3\nid_0\n2000-01-04\n3.272442\n1\n4\nTrue\nFalse\nFalse\n\n\n4\nid_0\n2000-01-05\n4.211827\n2\n5\nFalse\nFalse\nFalse\n\n\n5\nid_0\n2000-01-06\n5.322947\n3\n6\nTrue\nFalse\nFalse\n\n\n\n\n\n\n\n\n\n\nGive us a ⭐ on Github"
+ },
+ {
+ "objectID": "distributed.forecast.html",
+ "href": "distributed.forecast.html",
+ "title": "Distributed Forecast",
+ "section": "",
+ "text": "DistributedMLForecast\n\n DistributedMLForecast (models,\n freq:Union[int,str,pandas._libs.tslibs.offsets.Bas\n eOffset,NoneType]=None,\n lags:Optional[Iterable[int]]=None, lag_transforms:\n Optional[Dict[int,List[Union[Callable,Tuple[Callab\n le,Any]]]]]=None, date_features:Optional[Iterable[\n Union[str,Callable]]]=None, num_threads:int=1, tar\n get_transforms:Optional[List[mlforecast.target_tra\n nsforms.BaseTargetTransform]]=None, engine=None,\n num_partitions:Optional[int]=None)\n\nMulti backend distributed pipeline\n\n\n\nDistributedMLForecast.fit\n\n DistributedMLForecast.fit (df:~AnyDataFrame, id_col:str='unique_id',\n time_col:str='ds', target_col:str='y',\n static_features:Optional[List[str]]=None,\n dropna:bool=True,\n keep_last_n:Optional[int]=None)\n\nApply the feature engineering and train the models.\n\n\n\n\n\n\n\n\n\n\nType\nDefault\nDetails\n\n\n\n\ndf\nAnyDataFrame\n\nSeries data in long format.\n\n\nid_col\nstr\nunique_id\nColumn that identifies each serie.\n\n\ntime_col\nstr\nds\nColumn that identifies each timestep, its values can be timestamps or integers.\n\n\ntarget_col\nstr\ny\nColumn that contains the target.\n\n\nstatic_features\ntyping.Optional[typing.List[str]]\nNone\nNames of the features that are static and will be repeated when forecasting.\n\n\ndropna\nbool\nTrue\nDrop rows with missing values produced by the transformations.\n\n\nkeep_last_n\ntyping.Optional[int]\nNone\nKeep only these many records from each serie for the forecasting step. Can save time and memory if your features allow it.\n\n\nReturns\nDistributedMLForecast\n\nForecast object with series values and trained models.\n\n\n\n\n\n\nDistributedMLForecast.predict\n\n DistributedMLForecast.predict (h:int,\n before_predict_callback:Optional[Callable]\n =None, after_predict_callback:Optional[Cal\n lable]=None,\n new_df:Optional[~AnyDataFrame]=None)\n\nCompute the predictions for the next horizon steps.\n\n\n\n\n\n\n\n\n\n\nType\nDefault\nDetails\n\n\n\n\nh\nint\n\nForecast horizon.\n\n\nbefore_predict_callback\ntyping.Optional[typing.Callable]\nNone\nFunction to call on the features before computing the predictions. This function will take the input dataframe that will be passed to the model for predicting and should return a dataframe with the same structure. The series identifier is on the index.\n\n\nafter_predict_callback\ntyping.Optional[typing.Callable]\nNone\nFunction to call on the predictions before updating the targets. This function will take a pandas Series with the predictions and should return another one with the same structure. The series identifier is on the index.\n\n\nnew_df\ntyping.Optional[~AnyDataFrame]\nNone\nSeries data of new observations for which forecasts are to be generated. This dataframe should have the same structure as the one used to fit the model, including any features and time series data. If new_df is not None, the method will generate forecasts for the new observations.\n\n\nReturns\nAnyDataFrame\n\nPredictions for each serie and timestep, with one column per model.\n\n\n\n\n\n\nDistributedMLForecast.preprocess\n\n DistributedMLForecast.preprocess (df:~AnyDataFrame,\n id_col:str='unique_id',\n time_col:str='ds', target_col:str='y', \n static_features:Optional[List[str]]=Non\n e, dropna:bool=True,\n keep_last_n:Optional[int]=None)\n\nAdd the features to data.\n\n\n\n\n\n\n\n\n\n\nType\nDefault\nDetails\n\n\n\n\ndf\nAnyDataFrame\n\nSeries data in long format.\n\n\nid_col\nstr\nunique_id\nColumn that identifies each serie.\n\n\ntime_col\nstr\nds\nColumn that identifies each timestep, its values can be timestamps or integers.\n\n\ntarget_col\nstr\ny\nColumn that contains the target.\n\n\nstatic_features\ntyping.Optional[typing.List[str]]\nNone\nNames of the features that are static and will be repeated when forecasting.\n\n\ndropna\nbool\nTrue\nDrop rows with missing values produced by the transformations.\n\n\nkeep_last_n\ntyping.Optional[int]\nNone\nKeep only these many records from each serie for the forecasting step. Can save time and memory if your features allow it.\n\n\nReturns\nAnyDataFrame\n\ndf with added features.\n\n\n\n\n\n\nDistributedMLForecast.cross_validation\n\n DistributedMLForecast.cross_validation (df:~AnyDataFrame, n_windows:int,\n h:int, id_col:str='unique_id',\n time_col:str='ds',\n target_col:str='y',\n step_size:Optional[int]=None, sta\n tic_features:Optional[List[str]]=\n None, dropna:bool=True,\n keep_last_n:Optional[int]=None,\n refit:bool=True, before_predict_c\n allback:Optional[Callable]=None, \n after_predict_callback:Optional[C\n allable]=None,\n input_size:Optional[int]=None)\n\nPerform time series cross validation. Creates n_windows splits where each window has h test periods, trains the models, computes the predictions and merges the actuals.\n\n\n\n\n\n\n\n\n\n\nType\nDefault\nDetails\n\n\n\n\ndf\nAnyDataFrame\n\nSeries data in long format.\n\n\nn_windows\nint\n\nNumber of windows to evaluate.\n\n\nh\nint\n\nNumber of test periods in each window.\n\n\nid_col\nstr\nunique_id\nColumn that identifies each serie.\n\n\ntime_col\nstr\nds\nColumn that identifies each timestep, its values can be timestamps or integers.\n\n\ntarget_col\nstr\ny\nColumn that contains the target.\n\n\nstep_size\ntyping.Optional[int]\nNone\nStep size between each cross validation window. If None it will be equal to h.\n\n\nstatic_features\ntyping.Optional[typing.List[str]]\nNone\nNames of the features that are static and will be repeated when forecasting.\n\n\ndropna\nbool\nTrue\nDrop rows with missing values produced by the transformations.\n\n\nkeep_last_n\ntyping.Optional[int]\nNone\nKeep only these many records from each serie for the forecasting step. Can save time and memory if your features allow it.\n\n\nrefit\nbool\nTrue\nRetrain model for each cross validation window.If False, the models are trained at the beginning and then used to predict each window.\n\n\nbefore_predict_callback\ntyping.Optional[typing.Callable]\nNone\nFunction to call on the features before computing the predictions. This function will take the input dataframe that will be passed to the model for predicting and should return a dataframe with the same structure. The series identifier is on the index.\n\n\nafter_predict_callback\ntyping.Optional[typing.Callable]\nNone\nFunction to call on the predictions before updating the targets. This function will take a pandas Series with the predictions and should return another one with the same structure. The series identifier is on the index.\n\n\ninput_size\ntyping.Optional[int]\nNone\nMaximum training samples per serie in each window. If None, will use an expanding window.\n\n\nReturns\nAnyDataFrame\n\nPredictions for each window with the series id, timestamp, target value and predictions from each model.\n\n\n\n\n\n\n\nGive us a ⭐ on Github"
+ },
+ {
+ "objectID": "lgb_cv.html",
+ "href": "lgb_cv.html",
+ "title": "LightGBMCV",
+ "section": "",
+ "text": "Give us a ⭐ on Github"
+ },
+ {
+ "objectID": "lgb_cv.html#example",
+ "href": "lgb_cv.html#example",
+ "title": "LightGBMCV",
+ "section": "Example",
+ "text": "Example\nThis shows an example with just 4 series of the M4 dataset. If you want to run it yourself on all of them, you can refer to this notebook.\n\nimport random\n\nfrom datasetsforecast.m4 import M4, M4Info\nfrom fastcore.test import test_eq, test_fail\nfrom mlforecast.target_transforms import Differences\nfrom nbdev import show_doc\nfrom window_ops.ewm import ewm_mean\nfrom window_ops.rolling import rolling_mean, seasonal_rolling_mean\n\n\ngroup = 'Hourly'\nawait M4.async_download('data', group=group)\ndf, *_ = M4.load(directory='data', group=group)\ndf['ds'] = df['ds'].astype('int')\nids = df['unique_id'].unique()\nrandom.seed(0)\nsample_ids = random.choices(ids, k=4)\nsample_df = df[df['unique_id'].isin(sample_ids)]\nsample_df\n\n\n\n\n\n\n\n\nunique_id\nds\ny\n\n\n\n\n86796\nH196\n1\n11.8\n\n\n86797\nH196\n2\n11.4\n\n\n86798\nH196\n3\n11.1\n\n\n86799\nH196\n4\n10.8\n\n\n86800\nH196\n5\n10.6\n\n\n...\n...\n...\n...\n\n\n325235\nH413\n1004\n99.0\n\n\n325236\nH413\n1005\n88.0\n\n\n325237\nH413\n1006\n47.0\n\n\n325238\nH413\n1007\n41.0\n\n\n325239\nH413\n1008\n34.0\n\n\n\n\n4032 rows × 3 columns\n\n\n\n\ninfo = M4Info[group]\nhorizon = info.horizon\nvalid = sample_df.groupby('unique_id').tail(horizon)\ntrain = sample_df.drop(valid.index)\ntrain.shape, valid.shape\n\n((3840, 3), (192, 3))\n\n\nWhat LightGBMCV does is emulate LightGBM’s cv function where several Boosters are trained simultaneously on different partitions of the data, that is, one boosting iteration is performed on all of them at a time. This allows to have an estimate of the error by iteration, so if we combine this with early stopping we can find the best iteration to train a final model using all the data or even use these individual models’ predictions to compute an ensemble.\nIn order to have a good estimate of the forecasting performance of our model we compute predictions for the whole test period and compute a metric on that. Since this step can slow down training, there’s an eval_every parameter that can be used to control this, that is, if eval_every=10 (the default) every 10 boosting iterations we’re going to compute forecasts for the complete window and report the error.\nWe also have early stopping parameters:\n\nearly_stopping_evals: how many evaluations of the full window should we go without improving to stop training?\nearly_stopping_pct: what’s the minimum percentage improvement we want in these early_stopping_evals in order to keep training?\n\nThis makes the LightGBMCV class a good tool to quickly test different configurations of the model. Consider the following example, where we’re going to try to find out which features can improve the performance of our model. We start just using lags.\n\nstatic_fit_config = dict(\n n_windows=2,\n h=horizon,\n params={'verbose': -1},\n compute_cv_preds=True,\n)\ncv = LightGBMCV(\n freq=1,\n lags=[24 * (i+1) for i in range(7)], # one week of lags\n)\n\n\n\nLightGBMCV.fit\n\n LightGBMCV.fit (df:pandas.core.frame.DataFrame, n_windows:int, h:int,\n id_col:str='unique_id', time_col:str='ds',\n target_col:str='y', step_size:Optional[int]=None,\n num_iterations:int=100,\n params:Optional[Dict[str,Any]]=None,\n static_features:Optional[List[str]]=None,\n dropna:bool=True, keep_last_n:Optional[int]=None,\n eval_every:int=10,\n weights:Optional[Sequence[float]]=None,\n metric:Union[str,Callable]='mape',\n verbose_eval:bool=True, early_stopping_evals:int=2,\n early_stopping_pct:float=0.01,\n compute_cv_preds:bool=False,\n before_predict_callback:Optional[Callable]=None,\n after_predict_callback:Optional[Callable]=None,\n input_size:Optional[int]=None)\n\nTrain boosters simultaneously and assess their performance on the complete forecasting window.\n\n\n\n\n\n\n\n\n\n\nType\nDefault\nDetails\n\n\n\n\ndf\nDataFrame\n\nSeries data in long format.\n\n\nn_windows\nint\n\nNumber of windows to evaluate.\n\n\nh\nint\n\nForecast horizon.\n\n\nid_col\nstr\nunique_id\nColumn that identifies each serie.\n\n\ntime_col\nstr\nds\nColumn that identifies each timestep, its values can be timestamps or integers.\n\n\ntarget_col\nstr\ny\nColumn that contains the target.\n\n\nstep_size\ntyping.Optional[int]\nNone\nStep size between each cross validation window. If None it will be equal to h.\n\n\nnum_iterations\nint\n100\nMaximum number of boosting iterations to run.\n\n\nparams\ntyping.Optional[typing.Dict[str, typing.Any]]\nNone\nParameters to be passed to the LightGBM Boosters.\n\n\nstatic_features\ntyping.Optional[typing.List[str]]\nNone\nNames of the features that are static and will be repeated when forecasting.\n\n\ndropna\nbool\nTrue\nDrop rows with missing values produced by the transformations.\n\n\nkeep_last_n\ntyping.Optional[int]\nNone\nKeep only these many records from each serie for the forecasting step. Can save time and memory if your features allow it.\n\n\neval_every\nint\n10\nNumber of boosting iterations to train before evaluating on the whole forecast window.\n\n\nweights\ntyping.Optional[typing.Sequence[float]]\nNone\nWeights to multiply the metric of each window. If None, all windows have the same weight.\n\n\nmetric\ntyping.Union[str, typing.Callable]\nmape\nMetric used to assess the performance of the models and perform early stopping.\n\n\nverbose_eval\nbool\nTrue\nPrint the metrics of each evaluation.\n\n\nearly_stopping_evals\nint\n2\nMaximum number of evaluations to run without improvement.\n\n\nearly_stopping_pct\nfloat\n0.01\nMinimum percentage improvement in metric value in early_stopping_evals evaluations.\n\n\ncompute_cv_preds\nbool\nFalse\nCompute predictions for each window after finding the best iteration.\n\n\nbefore_predict_callback\ntyping.Optional[typing.Callable]\nNone\nFunction to call on the features before computing the predictions. This function will take the input dataframe that will be passed to the model for predicting and should return a dataframe with the same structure. The series identifier is on the index.\n\n\nafter_predict_callback\ntyping.Optional[typing.Callable]\nNone\nFunction to call on the predictions before updating the targets. This function will take a pandas Series with the predictions and should return another one with the same structure. The series identifier is on the index.\n\n\ninput_size\ntyping.Optional[int]\nNone\nMaximum training samples per serie in each window. If None, will use an expanding window.\n\n\nReturns\ntyping.List[typing.Tuple[int, float]]\n\nList of (boosting rounds, metric value) tuples.\n\n\n\n\nhist = cv.fit(train, **static_fit_config)\n\n[LightGBM] [Info] Start training from score 51.745632\n[10] mape: 0.590690\n[20] mape: 0.251093\n[30] mape: 0.143643\n[40] mape: 0.109723\n[50] mape: 0.102099\n[60] mape: 0.099448\n[70] mape: 0.098349\n[80] mape: 0.098006\n[90] mape: 0.098718\nEarly stopping at round 90\nUsing best iteration: 80\n\n\nBy setting compute_cv_preds we get the predictions from each model on their corresponding validation fold.\n\ncv.cv_preds_\n\n\n\n\n\n\n\n\nunique_id\nds\ny\nBooster\nwindow\n\n\n\n\n0\nH196\n865\n15.5\n15.522924\n0\n\n\n1\nH196\n866\n15.1\n14.985832\n0\n\n\n2\nH196\n867\n14.8\n14.667901\n0\n\n\n3\nH196\n868\n14.4\n14.514592\n0\n\n\n4\nH196\n869\n14.2\n14.035793\n0\n\n\n...\n...\n...\n...\n...\n...\n\n\n187\nH413\n956\n59.0\n77.227905\n1\n\n\n188\nH413\n957\n58.0\n80.589641\n1\n\n\n189\nH413\n958\n53.0\n53.986834\n1\n\n\n190\nH413\n959\n38.0\n36.749786\n1\n\n\n191\nH413\n960\n46.0\n36.281225\n1\n\n\n\n\n384 rows × 5 columns\n\n\n\nThe individual models we trained are saved, so calling predict returns the predictions from every model trained.\n\n\n\nLightGBMCV.predict\n\n LightGBMCV.predict (h:int,\n before_predict_callback:Optional[Callable]=None,\n after_predict_callback:Optional[Callable]=None,\n X_df:Optional[pandas.core.frame.DataFrame]=None)\n\nCompute predictions with each of the trained boosters.\n\n\n\n\n\n\n\n\n\n\nType\nDefault\nDetails\n\n\n\n\nh\nint\n\nForecast horizon.\n\n\nbefore_predict_callback\ntyping.Optional[typing.Callable]\nNone\nFunction to call on the features before computing the predictions. This function will take the input dataframe that will be passed to the model for predicting and should return a dataframe with the same structure. The series identifier is on the index.\n\n\nafter_predict_callback\ntyping.Optional[typing.Callable]\nNone\nFunction to call on the predictions before updating the targets. This function will take a pandas Series with the predictions and should return another one with the same structure. The series identifier is on the index.\n\n\nX_df\ntyping.Optional[pandas.core.frame.DataFrame]\nNone\nDataframe with the future exogenous features. Should have the id column and the time column.\n\n\nReturns\nDataFrame\n\nPredictions for each serie and timestep, with one column per window.\n\n\n\n\npreds = cv.predict(horizon)\npreds\n\n\n\n\n\n\n\n\nunique_id\nds\nBooster0\nBooster1\n\n\n\n\n0\nH196\n961\n15.670252\n15.848888\n\n\n1\nH196\n962\n15.522924\n15.697399\n\n\n2\nH196\n963\n14.985832\n15.166213\n\n\n3\nH196\n964\n14.985832\n14.723238\n\n\n4\nH196\n965\n14.562152\n14.451092\n\n\n...\n...\n...\n...\n...\n\n\n187\nH413\n1004\n70.695242\n65.917620\n\n\n188\nH413\n1005\n66.216580\n62.615788\n\n\n189\nH413\n1006\n63.896573\n67.848598\n\n\n190\nH413\n1007\n46.922797\n50.981950\n\n\n191\nH413\n1008\n45.006541\n42.752819\n\n\n\n\n192 rows × 4 columns\n\n\n\nWe can average these predictions and evaluate them.\n\ndef evaluate_on_valid(preds):\n preds = preds.copy()\n preds['final_prediction'] = preds.drop(columns=['unique_id', 'ds']).mean(1)\n merged = preds.merge(valid, on=['unique_id', 'ds'])\n merged['abs_err'] = abs(merged['final_prediction'] - merged['y']) / merged['y']\n return merged.groupby('unique_id')['abs_err'].mean().mean()\n\n\neval1 = evaluate_on_valid(preds)\neval1\n\n0.11036194712311806\n\n\nNow, since these series are hourly, maybe we can try to remove the daily seasonality by taking the 168th (24 * 7) difference, that is, substract the value at the same hour from one week ago, thus our target will be \\(z_t = y_{t} - y_{t-168}\\). The features will be computed from this target and when we predict they will be automatically re-applied.\n\ncv2 = LightGBMCV(\n freq=1,\n target_transforms=[Differences([24 * 7])],\n lags=[24 * (i+1) for i in range(7)],\n)\nhist2 = cv2.fit(train, **static_fit_config)\n\n[LightGBM] [Info] Start training from score 0.519010\n[10] mape: 0.089024\n[20] mape: 0.090683\n[30] mape: 0.092316\nEarly stopping at round 30\nUsing best iteration: 10\n\n\n\nassert hist2[-1][1] < hist[-1][1]\n\nNice! We achieve a better score in less iterations. Let’s see if this improvement translates to the validation set as well.\n\npreds2 = cv2.predict(horizon)\neval2 = evaluate_on_valid(preds2)\neval2\n\n0.08956665504570135\n\n\n\nassert eval2 < eval1\n\nGreat! Maybe we can try some lag transforms now. We’ll try the seasonal rolling mean that averages the values “every season”, that is, if we set season_length=24 and window_size=7 then we’ll average the value at the same hour for every day of the week.\n\ncv3 = LightGBMCV(\n freq=1,\n target_transforms=[Differences([24 * 7])],\n lags=[24 * (i+1) for i in range(7)],\n lag_transforms={\n 48: [(seasonal_rolling_mean, 24, 7)],\n },\n)\nhist3 = cv3.fit(train, **static_fit_config)\n\n[LightGBM] [Info] Start training from score 0.273641\n[10] mape: 0.086724\n[20] mape: 0.088466\n[30] mape: 0.090536\nEarly stopping at round 30\nUsing best iteration: 10\n\n\nSeems like this is helping as well!\n\nassert hist3[-1][1] < hist2[-1][1]\n\nDoes this reflect on the validation set?\n\npreds3 = cv3.predict(horizon)\neval3 = evaluate_on_valid(preds3)\neval3\n\n0.08961279023129345\n\n\nNice! mlforecast also supports date features, but in this case our time column is made from integers so there aren’t many possibilites here. As you can see this allows you to iterate faster and get better estimates of the forecasting performance you can expect from your model.\nIf you’re doing hyperparameter tuning it’s useful to be able to run a couple of iterations, assess the performance, and determine if this particular configuration isn’t promising and should be discarded. For example, optuna has pruners that you can call with your current score and it decides if the trial should be discarded. We’ll now show how to do that.\nSince the CV requires a bit of setup, like the LightGBM datasets and the internal features, we have this setup method.\n\n\n\nLightGBMCV.setup\n\n LightGBMCV.setup (df:pandas.core.frame.DataFrame, n_windows:int, h:int,\n id_col:str='unique_id', time_col:str='ds',\n target_col:str='y', step_size:Optional[int]=None,\n params:Optional[Dict[str,Any]]=None,\n static_features:Optional[List[str]]=None,\n dropna:bool=True, keep_last_n:Optional[int]=None,\n weights:Optional[Sequence[float]]=None,\n metric:Union[str,Callable]='mape',\n input_size:Optional[int]=None)\n\nInitialize internal data structures to iteratively train the boosters. Use this before calling partial_fit.\n\n\n\n\n\n\n\n\n\n\nType\nDefault\nDetails\n\n\n\n\ndf\nDataFrame\n\nSeries data in long format.\n\n\nn_windows\nint\n\nNumber of windows to evaluate.\n\n\nh\nint\n\nForecast horizon.\n\n\nid_col\nstr\nunique_id\nColumn that identifies each serie.\n\n\ntime_col\nstr\nds\nColumn that identifies each timestep, its values can be timestamps or integers.\n\n\ntarget_col\nstr\ny\nColumn that contains the target.\n\n\nstep_size\ntyping.Optional[int]\nNone\nStep size between each cross validation window. If None it will be equal to h.\n\n\nparams\ntyping.Optional[typing.Dict[str, typing.Any]]\nNone\nParameters to be passed to the LightGBM Boosters.\n\n\nstatic_features\ntyping.Optional[typing.List[str]]\nNone\nNames of the features that are static and will be repeated when forecasting.\n\n\ndropna\nbool\nTrue\nDrop rows with missing values produced by the transformations.\n\n\nkeep_last_n\ntyping.Optional[int]\nNone\nKeep only these many records from each serie for the forecasting step. Can save time and memory if your features allow it.\n\n\nweights\ntyping.Optional[typing.Sequence[float]]\nNone\nWeights to multiply the metric of each window. If None, all windows have the same weight.\n\n\nmetric\ntyping.Union[str, typing.Callable]\nmape\nMetric used to assess the performance of the models and perform early stopping.\n\n\ninput_size\ntyping.Optional[int]\nNone\nMaximum training samples per serie in each window. If None, will use an expanding window.\n\n\nReturns\nLightGBMCV\n\nCV object with internal data structures for partial_fit.\n\n\n\n\ncv4 = LightGBMCV(\n freq=1,\n lags=[24 * (i+1) for i in range(7)],\n)\ncv4.setup(\n train,\n n_windows=2,\n h=horizon,\n params={'verbose': -1},\n)\n\nLightGBMCV(freq=1, lag_features=['lag24', 'lag48', 'lag72', 'lag96', 'lag120', 'lag144', 'lag168'], date_features=[], num_threads=1, bst_threads=8)\n\n\nOnce we have this we can call partial_fit to only train for some iterations and return the score of the forecast window.\n\n\n\nLightGBMCV.partial_fit\n\n LightGBMCV.partial_fit (num_iterations:int,\n before_predict_callback:Optional[Callable]=None,\n after_predict_callback:Optional[Callable]=None)\n\nTrain the boosters for some iterations.\n\n\n\n\n\n\n\n\n\n\nType\nDefault\nDetails\n\n\n\n\nnum_iterations\nint\n\nNumber of boosting iterations to run\n\n\nbefore_predict_callback\ntyping.Optional[typing.Callable]\nNone\nFunction to call on the features before computing the predictions. This function will take the input dataframe that will be passed to the model for predicting and should return a dataframe with the same structure. The series identifier is on the index.\n\n\nafter_predict_callback\ntyping.Optional[typing.Callable]\nNone\nFunction to call on the predictions before updating the targets. This function will take a pandas Series with the predictions and should return another one with the same structure. The series identifier is on the index.\n\n\nReturns\nfloat\n\nWeighted metric after training for num_iterations.\n\n\n\n\nscore = cv4.partial_fit(10)\nscore\n\n[LightGBM] [Info] Start training from score 51.745632\n\n\n0.5906900462828166\n\n\nThis is equal to the first evaluation from our first example.\n\nassert hist[0][1] == score\n\nWe can now use this score to decide if this configuration is promising. If we want to we can train some more iterations.\n\nscore2 = cv4.partial_fit(20)\n\nThis is now equal to our third metric from the first example, since this time we trained for 20 iterations.\n\nassert hist[2][1] == score2\n\n\n\nUsing a custom metric\nThe built-in metrics are MAPE and RMSE, which are computed by serie and then averaged across all series. If you want to do something different or use a different metric entirely, you can define your own metric like the following:\n\ndef weighted_mape(\n y_true: pd.Series,\n y_pred: pd.Series,\n ids: pd.Series,\n dates: pd.Series,\n):\n \"\"\"Weighs the MAPE by the magnitude of the series values\"\"\"\n abs_pct_err = abs(y_true - y_pred) / abs(y_true)\n mape_by_serie = abs_pct_err.groupby(ids).mean()\n totals_per_serie = y_pred.groupby(ids).sum()\n series_weights = totals_per_serie / totals_per_serie.sum()\n return (mape_by_serie * series_weights).sum()\n\n\n_ = LightGBMCV(\n freq=1,\n lags=[24 * (i+1) for i in range(7)],\n).fit(\n train,\n n_windows=2,\n h=horizon,\n params={'verbose': -1},\n metric=weighted_mape,\n)\n\n[LightGBM] [Info] Start training from score 51.745632\n[10] weighted_mape: 0.480353\n[20] weighted_mape: 0.218670\n[30] weighted_mape: 0.161706\n[40] weighted_mape: 0.149992\n[50] weighted_mape: 0.149024\n[60] weighted_mape: 0.148496\nEarly stopping at round 60\nUsing best iteration: 60"
+ },
+ {
+ "objectID": "distributed.models.ray.lgb.html",
+ "href": "distributed.models.ray.lgb.html",
+ "title": "RayLGBMForecast",
+ "section": "",
+ "text": "Wrapper of lightgbm.ray.RayLGBMRegressor that adds a model_ property that contains the fitted booster and is sent to the workers to in the forecasting step.\n\n\nRayLGBMForecast\n\n RayLGBMForecast (boosting_type:str='gbdt', num_leaves:int=31,\n max_depth:int=-1, learning_rate:float=0.1,\n n_estimators:int=100, subsample_for_bin:int=200000, obje\n ctive:Union[str,Callable[[Optional[numpy.ndarray],numpy.\n ndarray],Tuple[numpy.ndarray,numpy.ndarray]],Callable[[O\n ptional[numpy.ndarray],numpy.ndarray,Optional[numpy.ndar\n ray]],Tuple[numpy.ndarray,numpy.ndarray]],Callable[[Opti\n onal[numpy.ndarray],numpy.ndarray,Optional[numpy.ndarray\n ],Optional[numpy.ndarray]],Tuple[numpy.ndarray,numpy.nda\n rray]],NoneType]=None,\n class_weight:Union[Dict,str,NoneType]=None,\n min_split_gain:float=0.0, min_child_weight:float=0.001,\n min_child_samples:int=20, subsample:float=1.0,\n subsample_freq:int=0, colsample_bytree:float=1.0,\n reg_alpha:float=0.0, reg_lambda:float=0.0, random_state:\n Union[int,numpy.random.mtrand.RandomState,NoneType]=None\n , n_jobs:Optional[int]=None,\n importance_type:str='split', **kwargs)\n\nPublicAPI (beta): This API is in beta and may change before becoming stable.\n\n\n\n\nGive us a ⭐ on Github"
+ },
+ {
+ "objectID": "index.html",
+ "href": "index.html",
+ "title": "mlforecast",
+ "section": "",
+ "text": "mlforecast is a framework to perform time series forecasting using machine learning models, with the option to scale to massive amounts of data using remote clusters.\nGive us a ⭐ on Github"
+ },
+ {
+ "objectID": "index.html#install",
+ "href": "index.html#install",
+ "title": "mlforecast",
+ "section": "Install",
+ "text": "Install\n\nPyPI\npip install mlforecast\n\n\nconda-forge\nconda install -c conda-forge mlforecast\nFor more detailed instructions you can refer to the installation page."
+ },
+ {
+ "objectID": "index.html#quick-start",
+ "href": "index.html#quick-start",
+ "title": "mlforecast",
+ "section": "Quick Start",
+ "text": "Quick Start\nMinimal Example\nimport lightgbm as lgb\n\nfrom mlforecast import MLForecast\nfrom sklearn.linear_model import LinearRegression\n\nmlf = MLForecast(\n models = [LinearRegression(), lgb.LGBMRegressor()],\n lags=[1, 12],\n freq = 'M'\n)\nmlf.fit(df)\nmlf.predict(12)\nGet Started with this quick guide.\nFollow this end-to-end walkthrough for best practices.\n\nSample notebooks\n\nm5\nm4\nm4-cv"
+ },
+ {
+ "objectID": "index.html#why",
+ "href": "index.html#why",
+ "title": "mlforecast",
+ "section": "Why?",
+ "text": "Why?\nCurrent Python alternatives for machine learning models are slow, inaccurate and don’t scale well. So we created a library that can be used to forecast in production environments. MLForecast includes efficient feature engineering to train any machine learning model (with fit and predict methods such as sklearn) to fit millions of time series."
+ },
+ {
+ "objectID": "index.html#features",
+ "href": "index.html#features",
+ "title": "mlforecast",
+ "section": "Features",
+ "text": "Features\n\nFastest implementations of feature engineering for time series forecasting in Python.\nOut-of-the-box compatibility with Spark, Dask, and Ray.\nProbabilistic Forecasting with Conformal Prediction.\nSupport for exogenous variables and static covariates.\nFamiliar sklearn syntax: .fit and .predict.\n\nMissing something? Please open an issue or write us in"
+ },
+ {
+ "objectID": "index.html#examples-and-guides",
+ "href": "index.html#examples-and-guides",
+ "title": "mlforecast",
+ "section": "Examples and Guides",
+ "text": "Examples and Guides\n📚 End to End Walkthrough: model training, evaluation and selection for multiple time series.\n🔎 Probabilistic Forecasting: use Conformal Prediction to produce prediciton intervals.\n👩🔬 Cross Validation: robust model’s performance evaluation.\n🔌 Predict Demand Peaks: electricity load forecasting for detecting daily peaks and reducing electric bills.\n📈 Transfer Learning: pretrain a model using a set of time series and then predict another one using that pretrained model.\n🌡️ Distributed Training: use a Dask, Ray or Spark cluster to train models at scale."
+ },
+ {
+ "objectID": "index.html#how-to-use",
+ "href": "index.html#how-to-use",
+ "title": "mlforecast",
+ "section": "How to use",
+ "text": "How to use\nThe following provides a very basic overview, for a more detailed description see the documentation.\n\nData setup\nStore your time series in a pandas dataframe in long format, that is, each row represents an observation for a specific serie and timestamp.\n\nfrom mlforecast.utils import generate_daily_series\n\nseries = generate_daily_series(\n n_series=20,\n max_length=100,\n n_static_features=1,\n static_as_categorical=False,\n with_trend=True\n)\nseries.head()\n\n\n\n\n\n\n\n\nunique_id\nds\ny\nstatic_0\n\n\n\n\n0\nid_00\n2000-01-01\n17.519167\n72\n\n\n1\nid_00\n2000-01-02\n87.799695\n72\n\n\n2\nid_00\n2000-01-03\n177.442975\n72\n\n\n3\nid_00\n2000-01-04\n232.704110\n72\n\n\n4\nid_00\n2000-01-05\n317.510474\n72\n\n\n\n\n\n\n\n\n\nModels\nNext define your models. If you want to use the local interface this can be any regressor that follows the scikit-learn API. For distributed training there are LGBMForecast and XGBForecast.\n\nimport lightgbm as lgb\nimport xgboost as xgb\nfrom sklearn.ensemble import RandomForestRegressor\n\nmodels = [\n lgb.LGBMRegressor(verbosity=-1),\n xgb.XGBRegressor(),\n RandomForestRegressor(random_state=0),\n]\n\n\n\nForecast object\nNow instantiate a MLForecast object with the models and the features that you want to use. The features can be lags, transformations on the lags and date features. The lag transformations are defined as numba jitted functions that transform an array, if they have additional arguments you can either supply a tuple (transform_func, arg1, arg2, …) or define new functions fixing the arguments. You can also define differences to apply to the series before fitting that will be restored when predicting.\n\nfrom mlforecast import MLForecast\nfrom mlforecast.target_transforms import Differences\nfrom numba import njit\nfrom window_ops.expanding import expanding_mean\nfrom window_ops.rolling import rolling_mean\n\n\n@njit\ndef rolling_mean_28(x):\n return rolling_mean(x, window_size=28)\n\n\nfcst = MLForecast(\n models=models,\n freq='D',\n lags=[7, 14],\n lag_transforms={\n 1: [expanding_mean],\n 7: [rolling_mean_28]\n },\n date_features=['dayofweek'],\n target_transforms=[Differences([1])],\n)\n\n\n\nTraining\nTo compute the features and train the models call fit on your Forecast object.\n\nfcst.fit(series)\n\nMLForecast(models=[LGBMRegressor, XGBRegressor, RandomForestRegressor], freq=<Day>, lag_features=['lag7', 'lag14', 'expanding_mean_lag1', 'rolling_mean_28_lag7'], date_features=['dayofweek'], num_threads=1)\n\n\n\n\nPredicting\nTo get the forecasts for the next n days call predict(n) on the forecast object. This will automatically handle the updates required by the features using a recursive strategy.\n\npredictions = fcst.predict(14)\npredictions\n\n\n\n\n\n\n\n\nunique_id\nds\nLGBMRegressor\nXGBRegressor\nRandomForestRegressor\n\n\n\n\n0\nid_00\n2000-04-04\n299.923771\n309.664124\n298.424164\n\n\n1\nid_00\n2000-04-05\n365.424147\n382.150085\n365.816014\n\n\n2\nid_00\n2000-04-06\n432.562441\n453.373779\n436.360620\n\n\n3\nid_00\n2000-04-07\n495.628000\n527.965149\n503.670100\n\n\n4\nid_00\n2000-04-08\n60.786223\n75.762299\n62.176080\n\n\n...\n...\n...\n...\n...\n...\n\n\n275\nid_19\n2000-03-23\n36.266780\n29.889120\n34.799780\n\n\n276\nid_19\n2000-03-24\n44.370984\n34.968884\n39.920982\n\n\n277\nid_19\n2000-03-25\n50.746222\n39.970238\n46.196266\n\n\n278\nid_19\n2000-03-26\n58.906524\n45.125305\n51.653060\n\n\n279\nid_19\n2000-03-27\n63.073949\n50.682716\n56.845384\n\n\n\n\n280 rows × 5 columns\n\n\n\n\n\nVisualize results\n\nfrom utilsforecast.plotting import plot_series\n\n\nfig = plot_series(series, predictions, max_ids=4, plot_random=False)\nfig.savefig('figs/index.png', bbox_inches='tight')"
+ },
+ {
+ "objectID": "index.html#how-to-contribute",
+ "href": "index.html#how-to-contribute",
+ "title": "mlforecast",
+ "section": "How to contribute",
+ "text": "How to contribute\nSee CONTRIBUTING.md."
+ },
+ {
+ "objectID": "distributed.models.spark.xgb.html",
+ "href": "distributed.models.spark.xgb.html",
+ "title": "SparkXGBForecast",
+ "section": "",
+ "text": "Wrapper of xgboost.spark.SparkXGBRegressor that adds an extract_local_model method to get a local version of the trained model and broadcast it to the workers.\n/opt/hostedtoolcache/Python/3.9.18/x64/lib/python3.9/site-packages/fastcore/docscrape.py:225: UserWarning: Unknown section Note\n else: warn(msg)\n/opt/hostedtoolcache/Python/3.9.18/x64/lib/python3.9/site-packages/fastcore/docscrape.py:225: UserWarning: Unknown section Examples\n else: warn(msg)\n\n\nSparkXGBForecast\n\n SparkXGBForecast (features_col:Union[str,List[str]]='features',\n label_col:str='label', prediction_col:str='prediction',\n pred_contrib_col:Optional[str]=None,\n validation_indicator_col:Optional[str]=None,\n weight_col:Optional[str]=None,\n base_margin_col:Optional[str]=None, num_workers:int=1,\n use_gpu:Optional[bool]=None, device:Optional[str]=None,\n force_repartition:bool=False,\n repartition_random_shuffle:bool=False,\n enable_sparse_data_optim:bool=False, **kwargs:Any)\n\nSparkXGBRegressor is a PySpark ML estimator. It implements the XGBoost regression algorithm based on XGBoost python library, and it can be used in PySpark Pipeline and PySpark ML meta algorithms like - :py:class:~pyspark.ml.tuning.CrossValidator/ - :py:class:~pyspark.ml.tuning.TrainValidationSplit/ - :py:class:~pyspark.ml.classification.OneVsRest\nSparkXGBRegressor automatically supports most of the parameters in :py:class:xgboost.XGBRegressor constructor and most of the parameters used in :py:meth:xgboost.XGBRegressor.fit and :py:meth:xgboost.XGBRegressor.predict method.\nTo enable GPU support, set device to cuda or gpu.\nSparkXGBRegressor doesn’t support setting base_margin explicitly as well, but support another param called base_margin_col. see doc below for more details.\nSparkXGBRegressor doesn’t support validate_features and output_margin param.\nSparkXGBRegressor doesn’t support setting nthread xgboost param, instead, the nthread param for each xgboost worker will be set equal to spark.task.cpus config value.\n\n\n\n\nGive us a ⭐ on Github"
+ },
+ {
+ "objectID": "distributed.models.ray.xgb.html",
+ "href": "distributed.models.ray.xgb.html",
+ "title": "RayXGBForecast",
+ "section": "",
+ "text": "Wrapper of xgboost.ray.RayXGBRegressor that adds a model_ property that contains the fitted model and is sent to the workers in the forecasting step.\n\n\nRayXGBForecast\n\n RayXGBForecast (objective:Union[str,Callable[[numpy.ndarray,numpy.ndarray\n ],Tuple[numpy.ndarray,numpy.ndarray]],NoneType]='reg:squa\n rederror', **kwargs:Any)\n\nImplementation of the scikit-learn API for Ray-distributed XGBoost regression. See :doc:/python/sklearn_estimator for more information.\n\n\n\n\n\n\n\n\n\n\nType\nDefault\nDetails\n\n\n\n\nobjective\ntyping.Union[str, typing.Callable[[numpy.ndarray, numpy.ndarray], typing.Tuple[numpy.ndarray, numpy.ndarray]], NoneType]\nreg:squarederror\nSpecify the learning task and the corresponding learning objective ora custom objective function to be used (see note below).\n\n\nkwargs\ntyping.Any\n\nKeyword arguments for XGBoost Booster object. Full documentation of parameterscan be found :doc:here </parameter>.Attempting to set a parameter via the constructor args and **kwargsdict simultaneously will result in a TypeError... note:: **kwargs unsupported by scikit-learn **kwargs is unsupported by scikit-learn. We do not guarantee that parameters passed via this argument will interact properly with scikit-learn... note:: Custom objective function A custom objective function can be provided for the objective parameter. In this case, it should have the signature objective(y_true, y_pred) -> grad, hess: y_true: array_like of shape [n_samples] The target values y_pred: array_like of shape [n_samples] The predicted values grad: array_like of shape [n_samples] The value of the gradient for each sample point. hess: array_like of shape [n_samples] The value of the second derivative for each sample point\n\n\nReturns\nNone\n\n\n\n\n\n\n\n\n\nGive us a ⭐ on Github"
+ },
+ {
+ "objectID": "target_transforms.html",
+ "href": "target_transforms.html",
+ "title": "Target transforms",
+ "section": "",
+ "text": "import pandas as pd\nfrom fastcore.test import test_fail\nfrom sklearn.linear_model import LinearRegression\nfrom sklearn.preprocessing import PowerTransformer\n\nfrom mlforecast import MLForecast\nfrom mlforecast.utils import generate_daily_series\n\n\n\nBaseTargetTransform\n\n BaseTargetTransform ()\n\nBase class used for target transformations.\n\n\n\nDifferences\n\n Differences (differences:Iterable[int])\n\nSubtracts previous values of the serie. Can be used to remove trend or seasonalities.\n\nseries = generate_daily_series(10, min_length=50, max_length=100)\n\n\ndiffs = Differences([1, 2, 5])\ndiffs.set_column_names('unique_id', 'ds', 'y')\n\n# differences are applied correctly\ntransformed = diffs.fit_transform(series)\nassert diffs.fitted_ == []\nexpected = series.copy()\nfor d in diffs.differences:\n expected['y'] -= expected.groupby('unique_id')['y'].shift(d)\npd.testing.assert_frame_equal(transformed, expected)\n\n# fitted differences are restored correctly\ndiffs.store_fitted = True\ntransformed = diffs.fit_transform(series)\nkeep_mask = transformed['y'].notnull()\nrestored = diffs.inverse_transform_fitted(\n transformed,\n transformed.groupby('unique_id', observed=True).size().values,\n)\npd.testing.assert_frame_equal(series[keep_mask], restored[keep_mask])\nrestored_subs = diffs.inverse_transform_fitted(\n transformed[keep_mask],\n transformed[keep_mask].groupby('unique_id', observed=True).size().values,\n)\npd.testing.assert_frame_equal(series[keep_mask], restored_subs)\n\n# short series\ntest_fail(lambda: diffs.fit_transform(series.head(2)), contains=\"too short for the differences: ['id_0']\")\n\n\n\n\nLocalStandardScaler\n\n LocalStandardScaler ()\n\nStandardizes each serie by subtracting its mean and dividing by its standard deviation.\n\nsc = LocalStandardScaler()\nsc.set_column_names('unique_id', 'ds', 'y')\nsizes = series.groupby('unique_id', observed=True).size().values\ntransformed = sc.fit_transform(series)\npd.testing.assert_frame_equal(\n sc.inverse_transform_fitted(transformed, sizes),\n series,\n)\n\ndef filter_df(df):\n return (\n df[df['unique_id'].isin(['id_0', 'id_7'])]\n .groupby('unique_id', observed=True)\n .head(10)\n )\n\nsubset = filter_df(series)\ntransformed_subset = filter_df(transformed)\nsc.idxs = [0, 7]\npd.testing.assert_frame_equal(\n sc.inverse_transform(transformed_subset),\n subset\n)\n\n\n\n\nGlobalSklearnTransformer\n\n GlobalSklearnTransformer (transformer:sklearn.base.TransformerMixin)\n\nApplies the same scikit-learn transformer to all series.\n\nsk_boxcox = PowerTransformer(method='box-cox', standardize=False)\nboxcox_global = GlobalSklearnTransformer(sk_boxcox)\nsingle_difference = Differences([1])\nseries = generate_daily_series(10)\nfcst = MLForecast(\n models=[LinearRegression()],\n freq='D',\n lags=[1, 2],\n target_transforms=[boxcox_global, single_difference]\n)\nprep = fcst.preprocess(series, dropna=False)\nexpected = (\n pd.Series(\n sk_boxcox.fit_transform(series[['y']])[:, 0], index=series['unique_id']\n ).groupby('unique_id')\n .diff()\n .values\n)\nnp.testing.assert_allclose(prep['y'].values, expected)\n\n\n\n\n\nGive us a ⭐ on Github"
+ },
+ {
+ "objectID": "distributed.models.dask.xgb.html",
+ "href": "distributed.models.dask.xgb.html",
+ "title": "DaskXGBForecast",
+ "section": "",
+ "text": "Wrapper of xgboost.dask.DaskXGBRegressor that adds a model_ property that contains the fitted model and is sent to the workers in the forecasting step.\n\n\nDaskXGBForecast\n\n DaskXGBForecast (max_depth:Optional[int]=None,\n max_leaves:Optional[int]=None,\n max_bin:Optional[int]=None,\n grow_policy:Optional[str]=None,\n learning_rate:Optional[float]=None,\n n_estimators:Optional[int]=None,\n verbosity:Optional[int]=None, objective:Union[str,Callab\n le[[numpy.ndarray,numpy.ndarray],Tuple[numpy.ndarray,num\n py.ndarray]],NoneType]=None, booster:Optional[str]=None,\n tree_method:Optional[str]=None,\n n_jobs:Optional[int]=None, gamma:Optional[float]=None,\n min_child_weight:Optional[float]=None,\n max_delta_step:Optional[float]=None,\n subsample:Optional[float]=None,\n sampling_method:Optional[str]=None,\n colsample_bytree:Optional[float]=None,\n colsample_bylevel:Optional[float]=None,\n colsample_bynode:Optional[float]=None,\n reg_alpha:Optional[float]=None,\n reg_lambda:Optional[float]=None,\n scale_pos_weight:Optional[float]=None,\n base_score:Optional[float]=None, random_state:Union[nump\n y.random.mtrand.RandomState,int,NoneType]=None,\n missing:float=nan, num_parallel_tree:Optional[int]=None,\n monotone_constraints:Union[Dict[str,int],str,NoneType]=N\n one, interaction_constraints:Union[str,Sequence[Sequence\n [str]],NoneType]=None,\n importance_type:Optional[str]=None,\n device:Optional[str]=None,\n validate_parameters:Optional[bool]=None,\n enable_categorical:bool=False,\n feature_types:Optional[Sequence[str]]=None,\n max_cat_to_onehot:Optional[int]=None,\n max_cat_threshold:Optional[int]=None,\n multi_strategy:Optional[str]=None,\n eval_metric:Union[str,List[str],Callable,NoneType]=None,\n early_stopping_rounds:Optional[int]=None, callbacks:Opti\n onal[List[xgboost.callback.TrainingCallback]]=None,\n **kwargs:Any)\n\nImplementation of the Scikit-Learn API for XGBoost. See :doc:/python/sklearn_estimator for more information.\n\n\n\n\n\n\n\n\n\n\nType\nDefault\nDetails\n\n\n\n\nmax_depth\ntyping.Optional[int]\nNone\nMaximum tree depth for base learners.\n\n\nmax_leaves\ntyping.Optional[int]\nNone\nMaximum number of leaves; 0 indicates no limit.\n\n\nmax_bin\ntyping.Optional[int]\nNone\nIf using histogram-based algorithm, maximum number of bins per feature\n\n\ngrow_policy\ntyping.Optional[str]\nNone\nTree growing policy. 0: favor splitting at nodes closest to the node, i.e. growdepth-wise. 1: favor splitting at nodes with highest loss change.\n\n\nlearning_rate\ntyping.Optional[float]\nNone\nBoosting learning rate (xgb’s “eta”)\n\n\nn_estimators\ntyping.Optional[int]\nNone\nNumber of gradient boosted trees. Equivalent to number of boostingrounds.\n\n\nverbosity\ntyping.Optional[int]\nNone\nThe degree of verbosity. Valid values are 0 (silent) - 3 (debug).\n\n\nobjective\ntyping.Union[str, typing.Callable[[numpy.ndarray, numpy.ndarray], typing.Tuple[numpy.ndarray, numpy.ndarray]], NoneType]\nNone\nSpecify the learning task and the corresponding learning objective ora custom objective function to be used (see note below).\n\n\nbooster\ntyping.Optional[str]\nNone\n\n\n\ntree_method\ntyping.Optional[str]\nNone\n\n\n\nn_jobs\ntyping.Optional[int]\nNone\nNumber of parallel threads used to run xgboost. When used with otherScikit-Learn algorithms like grid search, you may choose which algorithm toparallelize and balance the threads. Creating thread contention willsignificantly slow down both algorithms.\n\n\ngamma\ntyping.Optional[float]\nNone\n(min_split_loss) Minimum loss reduction required to make a further partition on aleaf node of the tree.\n\n\nmin_child_weight\ntyping.Optional[float]\nNone\nMinimum sum of instance weight(hessian) needed in a child.\n\n\nmax_delta_step\ntyping.Optional[float]\nNone\nMaximum delta step we allow each tree’s weight estimation to be.\n\n\nsubsample\ntyping.Optional[float]\nNone\nSubsample ratio of the training instance.\n\n\nsampling_method\ntyping.Optional[str]\nNone\nSampling method. Used only by the GPU version of hist tree method. - uniform: select random training instances uniformly. - gradient_based select random training instances with higher probability when the gradient and hessian are larger. (cf. CatBoost)\n\n\ncolsample_bytree\ntyping.Optional[float]\nNone\nSubsample ratio of columns when constructing each tree.\n\n\ncolsample_bylevel\ntyping.Optional[float]\nNone\nSubsample ratio of columns for each level.\n\n\ncolsample_bynode\ntyping.Optional[float]\nNone\nSubsample ratio of columns for each split.\n\n\nreg_alpha\ntyping.Optional[float]\nNone\nL1 regularization term on weights (xgb’s alpha).\n\n\nreg_lambda\ntyping.Optional[float]\nNone\nL2 regularization term on weights (xgb’s lambda).\n\n\nscale_pos_weight\ntyping.Optional[float]\nNone\nBalancing of positive and negative weights.\n\n\nbase_score\ntyping.Optional[float]\nNone\nThe initial prediction score of all instances, global bias.\n\n\nrandom_state\ntyping.Union[numpy.random.mtrand.RandomState, int, NoneType]\nNone\nRandom number seed... note:: Using gblinear booster with shotgun updater is nondeterministic as it uses Hogwild algorithm.\n\n\nmissing\nfloat\nnan\nValue in the data which needs to be present as a missing value.\n\n\nnum_parallel_tree\ntyping.Optional[int]\nNone\n\n\n\nmonotone_constraints\ntyping.Union[typing.Dict[str, int], str, NoneType]\nNone\nConstraint of variable monotonicity. See :doc:tutorial </tutorials/monotonic>for more information.\n\n\ninteraction_constraints\ntyping.Union[str, typing.Sequence[typing.Sequence[str]], NoneType]\nNone\nConstraints for interaction representing permitted interactions. Theconstraints must be specified in the form of a nested list, e.g. [[0, 1], [2,<br>3, 4]], where each inner list is a group of indices of features that areallowed to interact with each other. See :doc:tutorial<br></tutorials/feature_interaction_constraint> for more information\n\n\nimportance_type\ntyping.Optional[str]\nNone\n\n\n\ndevice\ntyping.Optional[str]\nNone\n.. versionadded:: 2.0.0Device ordinal, available options are cpu, cuda, and gpu.\n\n\nvalidate_parameters\ntyping.Optional[bool]\nNone\nGive warnings for unknown parameter.\n\n\nenable_categorical\nbool\nFalse\n.. versionadded:: 1.5.0.. note:: This parameter is experimentalExperimental support for categorical data. When enabled, cudf/pandas.DataFrameshould be used to specify categorical data type. Also, JSON/UBJSONserialization format is required.\n\n\nfeature_types\ntyping.Optional[typing.Sequence[str]]\nNone\n.. versionadded:: 1.7.0Used for specifying feature types without constructing a dataframe. See:py:class:DMatrix for details.\n\n\nmax_cat_to_onehot\ntyping.Optional[int]\nNone\n.. versionadded:: 1.6.0.. note:: This parameter is experimentalA threshold for deciding whether XGBoost should use one-hot encoding based splitfor categorical data. When number of categories is lesser than the thresholdthen one-hot encoding is chosen, otherwise the categories will be partitionedinto children nodes. Also, enable_categorical needs to be set to havecategorical feature support. See :doc:Categorical Data<br></tutorials/categorical> and :ref:cat-param for details.\n\n\nmax_cat_threshold\ntyping.Optional[int]\nNone\n.. versionadded:: 1.7.0.. note:: This parameter is experimentalMaximum number of categories considered for each split. Used only bypartition-based splits for preventing over-fitting. Also, enable_categoricalneeds to be set to have categorical feature support. See :doc:Categorical Data<br></tutorials/categorical> and :ref:cat-param for details.\n\n\nmulti_strategy\ntyping.Optional[str]\nNone\n.. versionadded:: 2.0.0.. note:: This parameter is working-in-progress.The strategy used for training multi-target models, including multi-targetregression and multi-class classification. See :doc:/tutorials/multioutput formore information.- one_output_per_tree: One model for each target.- multi_output_tree: Use multi-target trees.\n\n\neval_metric\ntyping.Union[str, typing.List[str], typing.Callable, NoneType]\nNone\n.. versionadded:: 1.6.0Metric used for monitoring the training result and early stopping. It can be astring or list of strings as names of predefined metric in XGBoost (Seedoc/parameter.rst), one of the metrics in :py:mod:sklearn.metrics, or any otheruser defined metric that looks like sklearn.metrics.If custom objective is also provided, then custom metric should implement thecorresponding reverse link function.Unlike the scoring parameter commonly used in scikit-learn, when a callableobject is provided, it’s assumed to be a cost function and by default XGBoost willminimize the result during early stopping.For advanced usage on Early stopping like directly choosing to maximize instead ofminimize, see :py:obj:xgboost.callback.EarlyStopping.See :doc:Custom Objective and Evaluation Metric </tutorials/custom_metric_obj>for more... note:: This parameter replaces eval_metric in :py:meth:fit method. The old one receives un-transformed prediction regardless of whether custom objective is being used... code-block:: python from sklearn.datasets import load_diabetes from sklearn.metrics import mean_absolute_error X, y = load_diabetes(return_X_y=True) reg = xgb.XGBRegressor( tree_method=“hist”, eval_metric=mean_absolute_error, ) reg.fit(X, y, eval_set=[(X, y)])\n\n\nearly_stopping_rounds\ntyping.Optional[int]\nNone\n.. versionadded:: 1.6.0- Activates early stopping. Validation metric needs to improve at least once in every early_stopping_rounds round(s) to continue training. Requires at least one item in eval_set in :py:meth:fit.- If early stopping occurs, the model will have two additional attributes: :py:attr:best_score and :py:attr:best_iteration. These are used by the :py:meth:predict and :py:meth:apply methods to determine the optimal number of trees during inference. If users want to access the full model (including trees built after early stopping), they can specify the iteration_range in these inference methods. In addition, other utilities like model plotting can also use the entire model.- If you prefer to discard the trees after best_iteration, consider using the callback function :py:class:xgboost.callback.EarlyStopping.- If there’s more than one item in eval_set, the last entry will be used for early stopping. If there’s more than one metric in eval_metric, the last metric will be used for early stopping... note:: This parameter replaces early_stopping_rounds in :py:meth:fit method.\n\n\ncallbacks\ntyping.Optional[typing.List[xgboost.callback.TrainingCallback]]\nNone\nList of callback functions that are applied at end of each iteration.It is possible to use predefined callbacks by using:ref:Callback API <callback_api>... note:: States in callback are not preserved during training, which means callback objects can not be reused for multiple training sessions without reinitialization or deepcopy... code-block:: python for params in parameters_grid: # be sure to (re)initialize the callbacks before each run callbacks = [xgb.callback.LearningRateScheduler(custom_rates)] reg = xgboost.XGBRegressor(**params, callbacks=callbacks) reg.fit(X, y)\n\n\nkwargs\ntyping.Any\n\nKeyword arguments for XGBoost Booster object. Full documentation of parameterscan be found :doc:here </parameter>.Attempting to set a parameter via the constructor args and **kwargsdict simultaneously will result in a TypeError... note:: **kwargs unsupported by scikit-learn **kwargs is unsupported by scikit-learn. We do not guarantee that parameters passed via this argument will interact properly with scikit-learn.\n\n\nReturns\nNone\n\n\n\n\n\n\n\n\n\nGive us a ⭐ on Github"
+ },
+ {
+ "objectID": "distributed.models.spark.lgb.html",
+ "href": "distributed.models.spark.lgb.html",
+ "title": "SparkLGBMForecast",
+ "section": "",
+ "text": "Wrapper of synapse.ml.lightgbm.LightGBMRegressor that adds an extract_local_model method to get a local version of the trained model and broadcast it to the workers.\n\n\nSparkLGBMForecast\n\n SparkLGBMForecast ()\n\nInitialize self. See help(type(self)) for accurate signature.\n\n\n\n\nGive us a ⭐ on Github"
+ },
+ {
+ "objectID": "grouped_array.html",
+ "href": "grouped_array.html",
+ "title": "mlforecast",
+ "section": "",
+ "text": "GroupedArray\n\n GroupedArray (data:numpy.ndarray, indptr:numpy.ndarray)\n\nArray made up of different groups. Can be thought of (and iterated) as a list of arrays.\nAll the data is stored in a single 1d array data. The indices for the group boundaries are stored in another 1d array indptr.\n\nfrom fastcore.test import test_eq, test_fail\n\n\n# The `GroupedArray` is used internally for storing the series values and performing transformations.\ndata = np.arange(10, dtype=np.float32)\nindptr = np.array([0, 2, 10]) # group 1: [0, 1], group 2: [2..9]\nga = GroupedArray(data, indptr)\ntest_eq(len(ga), 2)\ntest_eq(str(ga), 'GroupedArray(ndata=10, ngroups=2)')\n\n\n# Iterate through the groups\nga_iter = iter(ga)\nnp.testing.assert_equal(next(ga_iter), np.array([0, 1]))\nnp.testing.assert_equal(next(ga_iter), np.arange(2, 10))\n\n\n# Take the last two observations from every group\nlast_2 = ga.take_from_groups(slice(-2, None))\nnp.testing.assert_equal(last_2.data, np.array([0, 1, 8, 9]))\nnp.testing.assert_equal(last_2.indptr, np.array([0, 2, 4]))\n\n\n# Take the last four observations from every group. Note that since group 1 only has two elements, only these are returned.\nlast_4 = ga.take_from_groups(slice(-4, None))\nnp.testing.assert_equal(last_4.data, np.array([0, 1, 6, 7, 8, 9]))\nnp.testing.assert_equal(last_4.indptr, np.array([0, 2, 6]))\n\n\n# Select a specific subset of groups\nindptr = np.array([0, 2, 4, 7, 10])\nga2 = GroupedArray(data, indptr)\nsubset = ga2.take([0, 2])\nnp.testing.assert_allclose(subset[0].data, ga2[0].data)\nnp.testing.assert_allclose(subset[1].data, ga2[2].data)\n\n\n# The groups are [0, 1], [2, ..., 9]. expand_target(2) should take rolling pairs of them and fill with nans when there aren't enough\nnp.testing.assert_equal(\n ga.expand_target(2),\n np.array([\n [0, 1],\n [1, np.nan],\n [2, 3],\n [3, 4],\n [4, 5],\n [5, 6],\n [6, 7],\n [7, 8],\n [8, 9],\n [9, np.nan]\n ])\n)\n\n\n# try to append new values that don't match the number of groups\ntest_fail(lambda: ga.append(np.array([1., 2., 3.])), contains='new must be of size 2')\n\n\n# __setitem__\nnew_vals = np.array([10, 11])\nga[0] = new_vals\nnp.testing.assert_equal(ga.data, np.append(new_vals, np.arange(2, 10)))\n\n\n\n\n\nGive us a ⭐ on Github"
+ },
+ {
+ "objectID": "core.html",
+ "href": "core.html",
+ "title": "Core",
+ "section": "",
+ "text": "import copy\n\nfrom nbdev import show_doc\nfrom fastcore.test import test_eq, test_fail, test_warns\nfrom window_ops.expanding import expanding_mean\nfrom window_ops.rolling import rolling_mean\nfrom window_ops.shift import shift_array\n\nfrom mlforecast.target_transforms import Differences, LocalStandardScaler\nfrom mlforecast.utils import generate_daily_series, generate_prices_for_series\nGive us a ⭐ on Github"
+ },
+ {
+ "objectID": "core.html#data-format",
+ "href": "core.html#data-format",
+ "title": "Core",
+ "section": "Data format",
+ "text": "Data format\nThe required input format is a dataframe with at least the following columns: * unique_id with a unique identifier for each time serie * ds with the datestamp and a column * y with the values of the serie.\nEvery other column is considered a static feature unless stated otherwise in TimeSeries.fit\n\nseries = generate_daily_series(20, n_static_features=2)\nseries\n\n\n\n\n\n\n\n\nunique_id\nds\ny\nstatic_0\nstatic_1\n\n\n\n\n0\nid_00\n2000-01-01\n7.404529\n27\n53\n\n\n1\nid_00\n2000-01-02\n35.952624\n27\n53\n\n\n2\nid_00\n2000-01-03\n68.958353\n27\n53\n\n\n3\nid_00\n2000-01-04\n84.994505\n27\n53\n\n\n4\nid_00\n2000-01-05\n113.219810\n27\n53\n\n\n...\n...\n...\n...\n...\n...\n\n\n4869\nid_19\n2000-03-25\n400.606807\n97\n45\n\n\n4870\nid_19\n2000-03-26\n538.794824\n97\n45\n\n\n4871\nid_19\n2000-03-27\n620.202104\n97\n45\n\n\n4872\nid_19\n2000-03-28\n20.625426\n97\n45\n\n\n4873\nid_19\n2000-03-29\n141.513169\n97\n45\n\n\n\n\n4874 rows × 5 columns\n\n\n\nFor simplicity we’ll just take one time serie here.\n\nuids = series['unique_id'].unique()\nserie = series[series['unique_id'].eq(uids[0])]\nserie\n\n\n\n\n\n\n\n\nunique_id\nds\ny\nstatic_0\nstatic_1\n\n\n\n\n0\nid_00\n2000-01-01\n7.404529\n27\n53\n\n\n1\nid_00\n2000-01-02\n35.952624\n27\n53\n\n\n2\nid_00\n2000-01-03\n68.958353\n27\n53\n\n\n3\nid_00\n2000-01-04\n84.994505\n27\n53\n\n\n4\nid_00\n2000-01-05\n113.219810\n27\n53\n\n\n...\n...\n...\n...\n...\n...\n\n\n217\nid_00\n2000-08-05\n13.263188\n27\n53\n\n\n218\nid_00\n2000-08-06\n38.231981\n27\n53\n\n\n219\nid_00\n2000-08-07\n59.555183\n27\n53\n\n\n220\nid_00\n2000-08-08\n86.986368\n27\n53\n\n\n221\nid_00\n2000-08-09\n119.254810\n27\n53\n\n\n\n\n222 rows × 5 columns\n\n\n\n\n\nTimeSeries\n\n TimeSeries (freq:Union[int,str,pandas._libs.tslibs.offsets.BaseOffset,Non\n eType]=None, lags:Optional[Iterable[int]]=None, lag_transform\n s:Optional[Dict[int,List[Union[Callable,Tuple[Callable,Any]]]\n ]]=None,\n date_features:Optional[Iterable[Union[str,Callable]]]=None,\n num_threads:int=1, target_transforms:Optional[List[mlforecast\n .target_transforms.BaseTargetTransform]]=None)\n\nUtility class for storing and transforming time series data.\nThe TimeSeries class takes care of defining the transformations to be performed (lags, lag_transforms and date_features). The transformations can be computed using multithreading if num_threads > 1.\n\ndef month_start_or_end(dates):\n return dates.is_month_start | dates.is_month_end\n\nflow_config = dict(\n freq='W-THU',\n lags=[7],\n lag_transforms={\n 1: [expanding_mean, (rolling_mean, 7)]\n },\n date_features=['dayofweek', 'week', month_start_or_end]\n)\n\nts = TimeSeries(**flow_config)\nts\n\nTimeSeries(freq=<Week: weekday=3>, transforms=['lag7', 'expanding_mean_lag1', 'rolling_mean_lag1_window_size7'], date_features=['dayofweek', 'week', 'month_start_or_end'], num_threads=1)\n\n\nThe frequency is converted to an offset.\n\ntest_eq(ts.freq, pd.tseries.frequencies.to_offset(flow_config['freq']))\n\nThe date features are stored as they were passed to the constructor.\n\ntest_eq(ts.date_features, flow_config['date_features'])\n\nThe transformations are stored as a dictionary where the key is the name of the transformation (name of the column in the dataframe with the computed features), which is built using build_transform_name and the value is a tuple where the first element is the lag it is applied to, then the function and then the function arguments.\n\ntest_eq(\n ts.transforms, \n {\n 'lag7': (7, _identity),\n 'expanding_mean_lag1': (1, expanding_mean), \n 'rolling_mean_lag1_window_size7': (1, rolling_mean, 7)\n \n }\n)\n\nNote that for lags we define the transformation as the identity function applied to its corresponding lag. This is because _transform_series takes the lag as an argument and shifts the array before computing the transformation."
+ },
+ {
+ "objectID": "core.html#timeseries.fit_transform",
+ "href": "core.html#timeseries.fit_transform",
+ "title": "Core",
+ "section": "TimeSeries.fit_transform",
+ "text": "TimeSeries.fit_transform\n\n TimeSeries.fit_transform (data:pandas.core.frame.DataFrame, id_col:str,\n time_col:str, target_col:str,\n static_features:Optional[List[str]]=None,\n dropna:bool=True,\n keep_last_n:Optional[int]=None,\n max_horizon:Optional[int]=None,\n return_X_y:bool=False)\n\nAdd the features to data and save the required information for the predictions step.\nIf not all features are static, specify which ones are in static_features. If you don’t want to drop rows with null values after the transformations set dropna=False If keep_last_n is not None then that number of observations is kept across all series for updates.\n\nflow_config = dict(\n freq='D',\n lags=[7, 14],\n lag_transforms={\n 2: [\n (rolling_mean, 7),\n (rolling_mean, 14),\n ]\n },\n date_features=['dayofweek', 'month', 'year'],\n num_threads=2\n)\n\nts = TimeSeries(**flow_config)\n_ = ts.fit_transform(series, id_col='unique_id', time_col='ds', target_col='y')\n\nThe series values are stored as a GroupedArray in an attribute ga. If the data type of the series values is an int then it is converted to np.float32, this is because lags generate np.nans so we need a float data type for them.\n\nnp.testing.assert_equal(ts.ga.data, series.y.values)\n\nThe series ids are stored in an uids attribute.\n\ntest_eq(ts.uids, series['unique_id'].unique())\n\nFor each time serie, the last observed date is stored so that predictions start from the last date + the frequency.\n\ntest_eq(ts.last_dates, series.groupby('unique_id')['ds'].max().values)\n\nThe last row of every serie without the y and ds columns are taken as static features.\n\npd.testing.assert_frame_equal(\n ts.static_features_,\n series.groupby('unique_id').tail(1).drop(columns=['ds', 'y']).reset_index(drop=True),\n)\n\nIf you pass static_features to TimeSeries.fit_transform then only these are kept.\n\nts.fit_transform(series, id_col='unique_id', time_col='ds', target_col='y', static_features=['static_0'])\n\npd.testing.assert_frame_equal(\n ts.static_features_,\n series.groupby('unique_id').tail(1)[['unique_id', 'static_0']].reset_index(drop=True),\n)\n\nYou can also specify keep_last_n in TimeSeries.fit_transform, which means that after computing the features for training we want to keep only the last n samples of each time serie for computing the updates. This saves both memory and time, since the updates are performed by running the transformation functions on all time series again and keeping only the last value (the update).\nIf you have very long time series and your updates only require a small sample it’s recommended that you set keep_last_n to the minimum number of samples required to compute the updates, which in this case is 15 since we have a rolling mean of size 14 over the lag 2 and in the first update the lag 2 becomes the lag 1. This is because in the first update the lag 1 is the last value of the series (or the lag 0), the lag 2 is the lag 1 and so on.\n\nkeep_last_n = 15\n\nts = TimeSeries(**flow_config)\ndf = ts.fit_transform(series, id_col='unique_id', time_col='ds', target_col='y', keep_last_n=keep_last_n)\nts._uids = ts.uids.tolist()\nts._idxs = np.arange(len(ts.ga))\nts._predict_setup()\n\nexpected_lags = ['lag7', 'lag14']\nexpected_transforms = ['rolling_mean_lag2_window_size7', \n 'rolling_mean_lag2_window_size14']\nexpected_date_features = ['dayofweek', 'month', 'year']\n\ntest_eq(ts.features, expected_lags + expected_transforms + expected_date_features)\ntest_eq(ts.static_features_.columns.tolist() + ts.features, df.columns.drop(['ds', 'y']).tolist())\n# we dropped 2 rows because of the lag 2 and 13 more to have the window of size 14\ntest_eq(df.shape[0], series.shape[0] - (2 + 13) * ts.ga.ngroups)\ntest_eq(ts.ga.data.size, ts.ga.ngroups * keep_last_n)\n\nTimeSeries.fit_transform requires that the y column doesn’t have any null values. This is because the transformations could propagate them forward, so if you have null values in the y column you’ll get an error.\n\nseries_with_nulls = series.copy()\nseries_with_nulls.loc[1, 'y'] = np.nan\ntest_fail(\n lambda: ts.fit_transform(series_with_nulls, id_col='unique_id', time_col='ds', target_col='y'),\n contains='y column contains null values'\n)"
+ },
+ {
+ "objectID": "core.html#timeseries.predict",
+ "href": "core.html#timeseries.predict",
+ "title": "Core",
+ "section": "TimeSeries.predict",
+ "text": "TimeSeries.predict\n\n TimeSeries.predict (models:Dict[str,Union[sklearn.base.BaseEstimator,List\n [sklearn.base.BaseEstimator]]], horizon:int,\n before_predict_callback:Optional[Callable]=None,\n after_predict_callback:Optional[Callable]=None,\n X_df:Optional[pandas.core.frame.DataFrame]=None,\n ids:Optional[List[str]]=None)\n\nOnce we have a trained model we can use TimeSeries.predict passing the model and the horizon to get the predictions back.\n\nclass DummyModel:\n def predict(self, X: pd.DataFrame) -> np.ndarray:\n return X['lag7'].values\n\nhorizon = 7\nmodel = DummyModel()\nts = TimeSeries(**flow_config)\nts.fit_transform(series, id_col='unique_id', time_col='ds', target_col='y')\npredictions = ts.predict({'DummyModel': model}, horizon)\n\ngrouped_series = series.groupby('unique_id')\nexpected_preds = grouped_series['y'].tail(7) # the model predicts the lag-7\nlast_dates = grouped_series['ds'].max()\nexpected_dsmin = last_dates + ts.freq\nexpected_dsmax = last_dates + horizon * ts.freq\ngrouped_preds = predictions.groupby('unique_id')\n\nnp.testing.assert_allclose(predictions['DummyModel'], expected_preds)\npd.testing.assert_series_equal(grouped_preds['ds'].min(), expected_dsmin)\npd.testing.assert_series_equal(grouped_preds['ds'].max(), expected_dsmax)\n\nIf we have dynamic features we can pass them to X_df.\n\nclass PredictPrice:\n def predict(self, X):\n return X['price']\n\nseries = generate_daily_series(20, n_static_features=2, equal_ends=True)\ndynamic_series = series.rename(columns={'static_1': 'product_id'})\nprices_catalog = generate_prices_for_series(dynamic_series)\nseries_with_prices = dynamic_series.merge(prices_catalog, how='left')\n\nmodel = PredictPrice()\nts = TimeSeries(**flow_config)\nts.fit_transform(\n series_with_prices,\n id_col='unique_id',\n time_col='ds',\n target_col='y',\n static_features=['static_0', 'product_id'],\n)\npredictions = ts.predict({'PredictPrice': model}, horizon=1, X_df=prices_catalog)\npd.testing.assert_frame_equal(\n predictions.rename(columns={'PredictPrice': 'price'}),\n prices_catalog.merge(predictions[['unique_id', 'ds']])[['unique_id', 'ds', 'price']]\n)"
+ },
+ {
+ "objectID": "core.html#timeseries.update",
+ "href": "core.html#timeseries.update",
+ "title": "Core",
+ "section": "TimeSeries.update",
+ "text": "TimeSeries.update\n\n TimeSeries.update (df:pandas.core.frame.DataFrame)\n\nUpdate the values of the stored series."
+ },
+ {
+ "objectID": "utils.html",
+ "href": "utils.html",
+ "title": "Utils",
+ "section": "",
+ "text": "from fastcore.test import test_eq, test_fail, test_warns\n\n\n\ngenerate_daily_series\n\n generate_daily_series (n_series:int, min_length:int=50,\n max_length:int=500, n_static_features:int=0,\n equal_ends:bool=False,\n static_as_categorical:bool=True,\n with_trend:bool=False, seed:int=0)\n\nGenerates n_series of different lengths in the interval [min_length, max_length].\nIf n_static_features > 0, then each serie gets static features with random values. If equal_ends == True then all series end at the same date.\nGenerate 20 series with lengths between 100 and 1,000.\n\nn_series = 20\nmin_length = 100\nmax_length = 1000\n\nseries = generate_daily_series(n_series, min_length, max_length)\nseries\n\n\n\n\n\n\n\n\nunique_id\nds\ny\n\n\n\n\n0\nid_00\n2000-01-01\n0.395863\n\n\n1\nid_00\n2000-01-02\n1.264447\n\n\n2\nid_00\n2000-01-03\n2.284022\n\n\n3\nid_00\n2000-01-04\n3.462798\n\n\n4\nid_00\n2000-01-05\n4.035518\n\n\n...\n...\n...\n...\n\n\n12446\nid_19\n2002-03-11\n0.309275\n\n\n12447\nid_19\n2002-03-12\n1.189464\n\n\n12448\nid_19\n2002-03-13\n2.325032\n\n\n12449\nid_19\n2002-03-14\n3.333198\n\n\n12450\nid_19\n2002-03-15\n4.306117\n\n\n\n\n12451 rows × 3 columns\n\n\n\nWe can also add static features to each serie (these can be things like product_id or store_id). Only the first static feature (static_0) is relevant to the target.\n\nn_static_features = 2\n\nseries_with_statics = generate_daily_series(n_series, min_length, max_length, n_static_features)\nseries_with_statics\n\n\n\n\n\n\n\n\nunique_id\nds\ny\nstatic_0\nstatic_1\n\n\n\n\n0\nid_00\n2000-01-01\n7.521388\n18\n10\n\n\n1\nid_00\n2000-01-02\n24.024502\n18\n10\n\n\n2\nid_00\n2000-01-03\n43.396423\n18\n10\n\n\n3\nid_00\n2000-01-04\n65.793168\n18\n10\n\n\n4\nid_00\n2000-01-05\n76.674843\n18\n10\n\n\n...\n...\n...\n...\n...\n...\n\n\n12446\nid_19\n2002-03-11\n27.834771\n89\n42\n\n\n12447\nid_19\n2002-03-12\n107.051746\n89\n42\n\n\n12448\nid_19\n2002-03-13\n209.252845\n89\n42\n\n\n12449\nid_19\n2002-03-14\n299.987801\n89\n42\n\n\n12450\nid_19\n2002-03-15\n387.550536\n89\n42\n\n\n\n\n12451 rows × 5 columns\n\n\n\n\nfor i in range(n_static_features):\n assert all(series_with_statics.groupby('unique_id')[f'static_{i}'].nunique() == 1)\n\nIf equal_ends=False (the default) then every serie has a different end date.\n\nassert series_with_statics.groupby('unique_id')['ds'].max().nunique() > 1\n\nWe can have all of them end at the same date by specifying equal_ends=True.\n\nseries_equal_ends = generate_daily_series(n_series, min_length, max_length, equal_ends=True)\n\nassert series_equal_ends.groupby('unique_id')['ds'].max().nunique() == 1\n\n\n\n\ngenerate_prices_for_series\n\n generate_prices_for_series (series:pandas.core.frame.DataFrame,\n horizon:int=7, seed:int=0)\n\n\nseries_for_prices = generate_daily_series(20, n_static_features=2, equal_ends=True)\nseries_for_prices.rename(columns={'static_1': 'product_id'}, inplace=True)\nprices_catalog = generate_prices_for_series(series_for_prices, horizon=7)\nprices_catalog\n\n\n\n\n\n\n\n\nds\nunique_id\nprice\n\n\n\n\n0\n2000-10-05\nid_00\n0.548814\n\n\n1\n2000-10-06\nid_00\n0.715189\n\n\n2\n2000-10-07\nid_00\n0.602763\n\n\n3\n2000-10-08\nid_00\n0.544883\n\n\n4\n2000-10-09\nid_00\n0.423655\n\n\n...\n...\n...\n...\n\n\n5009\n2001-05-17\nid_19\n0.288027\n\n\n5010\n2001-05-18\nid_19\n0.846305\n\n\n5011\n2001-05-19\nid_19\n0.791284\n\n\n5012\n2001-05-20\nid_19\n0.578636\n\n\n5013\n2001-05-21\nid_19\n0.288589\n\n\n\n\n5014 rows × 3 columns\n\n\n\n\ntest_eq(set(prices_catalog['unique_id']), set(series_for_prices['unique_id']))\ntest_fail(lambda: generate_prices_for_series(series), contains='equal ends')\n\n\n\n\nbacktest_splits\n\n backtest_splits (df:pandas.core.frame.DataFrame, n_windows:int, h:int,\n id_col:str, time_col:str,\n freq:Union[pandas._libs.tslibs.offsets.BaseOffset,int],\n step_size:Optional[int]=None,\n input_size:Optional[int]=None)\n\n\n\n\nPredictionIntervals\n\n PredictionIntervals (n_windows:int=2, h:int=1,\n method:str='conformal_distribution')\n\nClass for storing prediction intervals metadata information.\n\n\n\n\nGive us a ⭐ on Github"
+ },
+ {
+ "objectID": "distributed.models.dask.lgb.html",
+ "href": "distributed.models.dask.lgb.html",
+ "title": "DaskLGBMForecast",
+ "section": "",
+ "text": "Wrapper of lightgbm.dask.DaskLGBMRegressor that adds a model_ property that contains the fitted booster and is sent to the workers to in the forecasting step.\n\n\nDaskLGBMForecast\n\n DaskLGBMForecast (boosting_type:str='gbdt', num_leaves:int=31,\n max_depth:int=-1, learning_rate:float=0.1,\n n_estimators:int=100, subsample_for_bin:int=200000, obj\n ective:Union[str,Callable[[Optional[numpy.ndarray],nump\n y.ndarray],Tuple[numpy.ndarray,numpy.ndarray]],Callable\n [[Optional[numpy.ndarray],numpy.ndarray,Optional[numpy.\n ndarray]],Tuple[numpy.ndarray,numpy.ndarray]],Callable[\n [Optional[numpy.ndarray],numpy.ndarray,Optional[numpy.n\n darray],Optional[numpy.ndarray]],Tuple[numpy.ndarray,nu\n mpy.ndarray]],NoneType]=None,\n class_weight:Union[dict,str,NoneType]=None,\n min_split_gain:float=0.0, min_child_weight:float=0.001,\n min_child_samples:int=20, subsample:float=1.0,\n subsample_freq:int=0, colsample_bytree:float=1.0,\n reg_alpha:float=0.0, reg_lambda:float=0.0, random_state\n :Union[int,numpy.random.mtrand.RandomState,NoneType]=No\n ne, n_jobs:Optional[int]=None,\n importance_type:str='split',\n client:Optional[distributed.client.Client]=None,\n **kwargs:Any)\n\nDistributed version of lightgbm.LGBMRegressor.\n\n\n\n\nGive us a ⭐ on Github"
+ },
+ {
+ "objectID": "docs/how-to-guides/target_transforms_guide.html",
+ "href": "docs/how-to-guides/target_transforms_guide.html",
+ "title": "Target transformations",
+ "section": "",
+ "text": "Since mlforecast uses a single global model it can be helpful to apply some transformations to the target to ensure that all series have similar distributions. They can also help remove trend for models that can’t deal with it out of the box.\nGive us a ⭐ on Github"
+ },
+ {
+ "objectID": "docs/how-to-guides/target_transforms_guide.html#data-setup",
+ "href": "docs/how-to-guides/target_transforms_guide.html#data-setup",
+ "title": "Target transformations",
+ "section": "Data setup",
+ "text": "Data setup\nFor this example we’ll use a single serie from the M4 dataset.\n\nimport matplotlib.pyplot as plt\nimport numpy as np\nimport pandas as pd\nfrom datasetsforecast.m4 import M4\nfrom sklearn.base import BaseEstimator\n\nfrom mlforecast import MLForecast\nfrom mlforecast.target_transforms import Differences, LocalStandardScaler\n\n\ndata_path = 'data'\nawait M4.async_download(data_path, group='Hourly')\ndf, *_ = M4.load(data_path, 'Hourly')\ndf['ds'] = df['ds'].astype('int32')\nserie = df[df['unique_id'].eq('H196')]"
+ },
+ {
+ "objectID": "docs/how-to-guides/target_transforms_guide.html#local-transformations",
+ "href": "docs/how-to-guides/target_transforms_guide.html#local-transformations",
+ "title": "Target transformations",
+ "section": "Local transformations",
+ "text": "Local transformations\n\nTransformations applied per serie\n\n\nDifferences\nWe’ll take a look at our serie to see possible differences that would help our models.\n\ndef plot(series, fname):\n n_series = len(series)\n fig, ax = plt.subplots(ncols=n_series, figsize=(7 * n_series, 6), squeeze=False)\n for (title, serie), axi in zip(series.items(), ax.flat):\n serie.set_index('ds')['y'].plot(title=title, ax=axi)\n fig.savefig(f'../../figs/{fname}', bbox_inches='tight')\n plt.close()\n\n\nplot({'original': serie}, 'target_transforms__eda.png')\n\n\nWe can see that our data has a trend as well as a clear seasonality. We can try removing the trend first.\n\nfcst = MLForecast(\n models=[],\n freq=1,\n target_transforms=[Differences([1])],\n)\nwithout_trend = fcst.preprocess(serie)\nplot({'original': serie, 'without trend': without_trend}, 'target_transforms__diff1.png')\n\n\nThe trend is gone, we can now try taking the 24 difference (subtract the value at the same hour in the previous day).\n\nfcst = MLForecast(\n models=[],\n freq=1,\n target_transforms=[Differences([1, 24])],\n)\nwithout_trend_and_seasonality = fcst.preprocess(serie)\nplot({'original': serie, 'without trend and seasonality': without_trend_and_seasonality}, 'target_transforms__diff2.png')\n\n\n\n\nLocalStandardScaler\nWe see that our serie is random noise now. Suppose we also want to standardize it, i.e. make it have a mean of 0 and variance of 1. We can add the LocalStandardScaler transformation after these differences.\n\nfcst = MLForecast(\n models=[],\n freq=1,\n target_transforms=[Differences([1, 24]), LocalStandardScaler()],\n)\nstandardized = fcst.preprocess(serie)\nplot({'original': serie, 'standardized': standardized}, 'target_transforms__standardized.png')\nstandardized['y'].agg(['mean', 'var']).round(2)\n\nmean -0.0\nvar 1.0\nName: y, dtype: float64\n\n\n\nNow that we’ve captured the components of the serie (trend + seasonality), we could try forecasting it with a model that always predicts 0, which will basically project the trend and seasonality.\n\nclass Zeros(BaseEstimator):\n def fit(self, X, y=None):\n return self\n\n def predict(self, X, y=None):\n return np.zeros(X.shape[0])\n\nfcst = MLForecast(\n models={'zeros_model': Zeros()},\n freq=1,\n target_transforms=[Differences([1, 24]), LocalStandardScaler()],\n)\npreds = fcst.fit(serie).predict(48)\nfig, ax = plt.subplots()\npd.concat([serie.tail(24 * 10), preds]).set_index('ds').plot(ax=ax)\nplt.close()\nfig.savefig('target_transforms__zeros.png')"
+ },
+ {
+ "objectID": "docs/how-to-guides/target_transforms_guide.html#global-transformations",
+ "href": "docs/how-to-guides/target_transforms_guide.html#global-transformations",
+ "title": "Target transformations",
+ "section": "Global transformations",
+ "text": "Global transformations\n\nTransformations applied to all series\n\n\nGlobalSklearnTransformer\nThere are some transformations that don’t require to learn any parameters, such as applying logarithm for example. These can be easily defined using the GlobalSklearnTransformer, which takes a scikit-learn compatible transformer and applies it to all series. Here’s an example on how to define a transformation that applies logarithm to each value of the series + 1, which can help avoid computing the log of 0.\n\nimport numpy as np\nfrom sklearn.preprocessing import FunctionTransformer\n\nfrom mlforecast.target_transforms import GlobalSklearnTransformer\n\nsk_log1p = FunctionTransformer(func=np.log1p, inverse_func=np.expm1)\nfcst = MLForecast(\n models={'zeros_model': Zeros()},\n freq=1,\n target_transforms=[GlobalSklearnTransformer(sk_log1p)],\n)\nlog1p_transformed = fcst.preprocess(serie)\nplot({'original': serie, 'Log transformed': log1p_transformed}, 'target_transforms__log.png')\n\n\nWe can also combine this with local transformations. For example we can apply log first and then differencing.\n\nfcst = MLForecast(\n models=[],\n freq=1,\n target_transforms=[GlobalSklearnTransformer(sk_log1p), Differences([1, 24])],\n)\nlog_diffs = fcst.preprocess(serie)\nplot({'original': serie, 'Log + Differences': log_diffs}, 'target_transforms__log_diffs.png')"
+ },
+ {
+ "objectID": "docs/how-to-guides/target_transforms_guide.html#custom-transformations",
+ "href": "docs/how-to-guides/target_transforms_guide.html#custom-transformations",
+ "title": "Target transformations",
+ "section": "Custom transformations",
+ "text": "Custom transformations\n\nImplementing your own target transformations\n\nIn order to implement your own target transformation you have to define a class that inherits from mlforecast.target_transforms.BaseTargetTransform (this takes care of setting the column names as the id_col, time_col and target_col attributes) and implement the fit_transform and inverse_transform methods. Here’s an example on how to define a min-max scaler.\n\nfrom mlforecast.target_transforms import BaseTargetTransform\n\n\nclass LocalMinMaxScaler(BaseTargetTransform):\n \"\"\"Scales each serie to be in the [0, 1] interval.\"\"\"\n def fit_transform(self, df: pd.DataFrame) -> pd.DataFrame:\n self.stats_ = df.groupby(self.id_col)[self.target_col].agg(['min', 'max'])\n df = df.merge(self.stats_, on=self.id_col)\n df[self.target_col] = (df[self.target_col] - df['min']) / (df['max'] - df['min'])\n df = df.drop(columns=['min', 'max'])\n return df\n\n def inverse_transform(self, df: pd.DataFrame) -> pd.DataFrame:\n df = df.merge(self.stats_, on=self.id_col)\n for col in df.columns.drop([self.id_col, self.time_col, 'min', 'max']):\n df[col] = df[col] * (df['max'] - df['min']) + df['min']\n df = df.drop(columns=['min', 'max'])\n return df\n\nAnd now you can pass an instance of this class to the target_transforms argument.\n\nfcst = MLForecast(\n models=[],\n freq=1,\n target_transforms=[LocalMinMaxScaler()],\n)\nminmax_scaled = fcst.preprocess(serie)\nplot({'original': serie, 'min-max scaled': minmax_scaled}, 'target_transforms__minmax.png')"
+ },
+ {
+ "objectID": "docs/how-to-guides/prediction_intervals.html",
+ "href": "docs/how-to-guides/prediction_intervals.html",
+ "title": "Probabilistic forecasting",
+ "section": "",
+ "text": "Prerequesites\n\n\n\n\n\nThis tutorial assumes basic familiarity with MLForecast. For a minimal example visit the Quick Start\nGive us a ⭐ on Github"
+ },
+ {
+ "objectID": "docs/how-to-guides/prediction_intervals.html#introduction",
+ "href": "docs/how-to-guides/prediction_intervals.html#introduction",
+ "title": "Probabilistic forecasting",
+ "section": "Introduction",
+ "text": "Introduction\nWhen we generate a forecast, we usually produce a single value known as the point forecast. This value, however, doesn’t tell us anything about the uncertainty associated with the forecast. To have a measure of this uncertainty, we need prediction intervals.\nA prediction interval is a range of values that the forecast can take with a given probability. Hence, a 95% prediction interval should contain a range of values that include the actual future value with probability 95%. Probabilistic forecasting aims to generate the full forecast distribution. Point forecasting, on the other hand, usually returns the mean or the median or said distribution. However, in real-world scenarios, it is better to forecast not only the most probable future outcome, but many alternative outcomes as well.\nWith MLForecast you can train sklearn models to generate point forecasts. It also takes the advantages of ConformalPrediction to generate the same point forecasts and adds them prediction intervals. By the end of this tutorial, you’ll have a good understanding of how to add probabilistic intervals to sklearn models for time series forecasting. Furthermore, you’ll also learn how to generate plots with the historical data, the point forecasts, and the prediction intervals.\n\n\n\n\n\n\nImportant\n\n\n\nAlthough the terms are often confused, prediction intervals are not the same as confidence intervals.\n\n\n\n\n\n\n\n\nWarning\n\n\n\nIn practice, most prediction intervals are too narrow since models do not account for all sources of uncertainty. A discussion about this can be found here.\n\n\nOutline:\n\nInstall libraries\nLoad and explore the data\nTrain models\nPlot prediction intervals\n\n\n\n\n\n\n\nTip\n\n\n\nYou can use Colab to run this Notebook interactively"
+ },
+ {
+ "objectID": "docs/how-to-guides/prediction_intervals.html#install-libraries",
+ "href": "docs/how-to-guides/prediction_intervals.html#install-libraries",
+ "title": "Probabilistic forecasting",
+ "section": "Install libraries",
+ "text": "Install libraries\nInstall the necessary packages using pip install mlforecast utilsforecast"
+ },
+ {
+ "objectID": "docs/how-to-guides/prediction_intervals.html#load-and-explore-the-data",
+ "href": "docs/how-to-guides/prediction_intervals.html#load-and-explore-the-data",
+ "title": "Probabilistic forecasting",
+ "section": "Load and explore the data",
+ "text": "Load and explore the data\nFor this example, we’ll use the hourly dataset from the M4 Competition. We first need to download the data from a URL and then load it as a pandas dataframe. Notice that we’ll load the train and the test data separately. We’ll also rename the y column of the test data as y_test.\n\nimport pandas as pd\nfrom utilsforecast.plotting import plot_series\n\n\ntrain = pd.read_csv('https://auto-arima-results.s3.amazonaws.com/M4-Hourly.csv')\ntest = pd.read_csv('https://auto-arima-results.s3.amazonaws.com/M4-Hourly-test.csv')\n\n\ntrain.head()\n\n\n\n\n\n\n\n\nunique_id\nds\ny\n\n\n\n\n0\nH1\n1\n605.0\n\n\n1\nH1\n2\n586.0\n\n\n2\nH1\n3\n586.0\n\n\n3\nH1\n4\n559.0\n\n\n4\nH1\n5\n511.0\n\n\n\n\n\n\n\n\ntest.head()\n\n\n\n\n\n\n\n\nunique_id\nds\ny\n\n\n\n\n0\nH1\n701\n619.0\n\n\n1\nH1\n702\n565.0\n\n\n2\nH1\n703\n532.0\n\n\n3\nH1\n704\n495.0\n\n\n4\nH1\n705\n481.0\n\n\n\n\n\n\n\nSince the goal of this notebook is to generate prediction intervals, we’ll only use the first 8 series of the dataset to reduce the total computational time.\n\nn_series = 8 \nuids = train['unique_id'].unique()[:n_series] # select first n_series of the dataset\ntrain = train.query('unique_id in @uids')\ntest = test.query('unique_id in @uids')\n\nWe can plot these series using the plot_series function from the utilsforecast library. This function has multiple parameters, and the required ones to generate the plots in this notebook are explained below.\n\ndf: A pandas dataframe with columns [unique_id, ds, y].\nforecasts_df: A pandas dataframe with columns [unique_id, ds] and models.\nplot_random: bool = True. Plots the time series randomly.\nmodels: List[str]. A list with the models we want to plot.\nlevel: List[float]. A list with the prediction intervals we want to plot.\nengine: str = matplotlib. It can also be plotly. plotly generates interactive plots, while matplotlib generates static plots.\n\n\nfig = plot_series(train, test.rename(columns={'y': 'y_test'}), models=['y_test'], plot_random=False)"
+ },
+ {
+ "objectID": "docs/how-to-guides/prediction_intervals.html#train-models",
+ "href": "docs/how-to-guides/prediction_intervals.html#train-models",
+ "title": "Probabilistic forecasting",
+ "section": "Train models",
+ "text": "Train models\nMLForecast can train multiple models that follow the sklearn syntax (fit and predict) on different time series efficiently.\nFor this example, we’ll use the following sklearn baseline models:\n\nLasso\nLinearRegression\nRidge\nK-Nearest Neighbors\nMultilayer Perceptron (NeuralNetwork)\n\nTo use these models, we first need to import them from sklearn and then we need to instantiate them.\n\nfrom mlforecast import MLForecast\nfrom mlforecast.target_transforms import Differences\nfrom mlforecast.utils import PredictionIntervals\nfrom sklearn.linear_model import Lasso, LinearRegression, Ridge\nfrom sklearn.neighbors import KNeighborsRegressor\nfrom sklearn.neural_network import MLPRegressor\n\n\n# Create a list of models and instantiation parameters \nmodels = [\n KNeighborsRegressor(),\n Lasso(),\n LinearRegression(),\n MLPRegressor(),\n Ridge(),\n]\n\nTo instantiate a new MLForecast object, we need the following parameters:\n\nmodels: The list of models defined in the previous step.\n\ntarget_transforms: Transformations to apply to the target before computing the features. These are restored at the forecasting step.\nlags: Lags of the target to use as features.\n\n\nmlf = MLForecast(\n models=[Ridge(), Lasso(), LinearRegression(), KNeighborsRegressor(), MLPRegressor(random_state=0)],\n target_transforms=[Differences([1])],\n lags=[24 * (i+1) for i in range(7)],\n)\n\nNow we’re ready to generate the point forecasts and the prediction intervals. To do this, we’ll use the fit method, which takes the following arguments:\n\ndata: Series data in long format.\nid_col: Column that identifies each series. In our case, unique_id.\ntime_col: Column that identifies each timestep, its values can be timestamps or integers. In our case, ds.\ntarget_col: Column that contains the target. In our case, y.\nprediction_intervals: A PredicitonIntervals class. The class takes two parameters: n_windows and h. n_windows represents the number of cross-validation windows used to calibrate the intervals and h is the forecast horizon. The strategy will adjust the intervals for each horizon step, resulting in different widths for each step.\n\n\nmlf.fit(\n train,\n prediction_intervals=PredictionIntervals(n_windows=10, h=48),\n);\n\nAfter fitting the models, we will call the predict method to generate forecasts with prediction intervals. The method takes the following arguments:\n\nhorizon: An integer that represent the forecasting horizon. In this case, we’ll forecast the next 48 hours.\nlevel: A list of floats with the confidence levels of the prediction intervals. For example, level=[95] means that the range of values should include the actual future value with probability 95%.\n\n\nlevels = [50, 80, 95]\nforecasts = mlf.predict(48, level=levels)\nforecasts.head()\n\n\n\n\n\n\n\n\nunique_id\nds\nRidge\nLasso\nLinearRegression\nKNeighborsRegressor\nMLPRegressor\nRidge-lo-95\nRidge-lo-80\nRidge-lo-50\n...\nKNeighborsRegressor-lo-50\nKNeighborsRegressor-hi-50\nKNeighborsRegressor-hi-80\nKNeighborsRegressor-hi-95\nMLPRegressor-lo-95\nMLPRegressor-lo-80\nMLPRegressor-lo-50\nMLPRegressor-hi-50\nMLPRegressor-hi-80\nMLPRegressor-hi-95\n\n\n\n\n0\nH1\n701\n612.418170\n612.418079\n612.418170\n615.2\n612.651532\n590.473256\n594.326570\n603.409944\n...\n609.45\n620.95\n627.20\n631.310\n584.736193\n591.084898\n597.462107\n627.840957\n634.218166\n640.566870\n\n\n1\nH1\n702\n552.309298\n552.308073\n552.309298\n551.6\n548.791801\n498.721501\n518.433843\n532.710850\n...\n535.85\n567.35\n569.16\n597.525\n497.308756\n500.417799\n515.452396\n582.131207\n597.165804\n600.274847\n\n\n2\nH1\n703\n494.943384\n494.943367\n494.943384\n509.6\n490.226796\n448.253304\n463.266064\n475.006125\n...\n492.70\n526.50\n530.92\n544.180\n424.587658\n436.042788\n448.682502\n531.771091\n544.410804\n555.865935\n\n\n3\nH1\n704\n462.815779\n462.815363\n462.815779\n474.6\n459.619069\n409.975219\n422.243593\n436.128272\n...\n451.80\n497.40\n510.26\n525.500\n379.291083\n392.580306\n413.353178\n505.884959\n526.657832\n539.947054\n\n\n4\nH1\n705\n440.141034\n440.140586\n440.141034\n451.6\n438.091712\n377.999588\n392.523016\n413.474795\n...\n427.40\n475.80\n488.96\n503.945\n348.618034\n362.503767\n386.303325\n489.880099\n513.679657\n527.565389\n\n\n\n\n5 rows × 37 columns\n\n\n\n\ntest = test.merge(forecasts, how='left', on=['unique_id', 'ds'])"
+ },
+ {
+ "objectID": "docs/how-to-guides/prediction_intervals.html#plot-prediction-intervals",
+ "href": "docs/how-to-guides/prediction_intervals.html#plot-prediction-intervals",
+ "title": "Probabilistic forecasting",
+ "section": "Plot prediction intervals",
+ "text": "Plot prediction intervals\nTo plot the point and the prediction intervals, we’ll use the plot_series function again. Notice that now we also need to specify the model and the levels that we want to plot.\n\nKNeighborsRegressor\n\nfig = plot_series(\n train, \n test, \n plot_random=False, \n models=['KNeighborsRegressor'], \n level=levels, \n max_insample_length=48\n)\n\n\n\n\nLasso\n\nfig = plot_series(\n train, \n test, \n plot_random=False, \n models=['Lasso'],\n level=levels, \n max_insample_length=48\n)\n\n\n\n\nLineaRegression\n\nfig = plot_series(\n train, \n test, \n plot_random=False, \n models=['LinearRegression'],\n level=levels, \n max_insample_length=48\n)\n\n\n\n\nMLPRegressor\n\nfig = plot_series(\n train, \n test, \n plot_random=False, \n models=['MLPRegressor'],\n level=levels, \n max_insample_length=48\n)\n\n\n\n\nRidge\n\nfig = plot_series(\n train, \n test, \n plot_random=False, \n models=['Ridge'],\n level=levels, \n max_insample_length=48\n)\n\n\nFrom these plots, we can conclude that the uncertainty around each forecast varies according to the model that is being used. For the same time series, one model can predict a wider range of possible future values than others."
+ },
+ {
+ "objectID": "docs/how-to-guides/prediction_intervals.html#references",
+ "href": "docs/how-to-guides/prediction_intervals.html#references",
+ "title": "Probabilistic forecasting",
+ "section": "References",
+ "text": "References\n\nKamile Stankeviciute, Ahmed M. Alaa and Mihaela van der Schaar (2021). “Conformal Time-Series Forecasting”\nRob J. Hyndman and George Athanasopoulos (2018). “Forecasting principles and practice, The Statistical Forecasting Perspective”."
+ },
+ {
+ "objectID": "docs/how-to-guides/exogenous_features.html",
+ "href": "docs/how-to-guides/exogenous_features.html",
+ "title": "Exogenous features",
+ "section": "",
+ "text": "import lightgbm as lgb\nimport pandas as pd\nfrom mlforecast import MLForecast\nfrom mlforecast.utils import generate_daily_series, generate_prices_for_series\nfrom window_ops.expanding import expanding_mean\nfrom window_ops.rolling import rolling_mean\nGive us a ⭐ on Github"
+ },
+ {
+ "objectID": "docs/how-to-guides/exogenous_features.html#data-setup",
+ "href": "docs/how-to-guides/exogenous_features.html#data-setup",
+ "title": "Exogenous features",
+ "section": "Data setup",
+ "text": "Data setup\n\nseries = generate_daily_series(\n 100, equal_ends=True, n_static_features=2\n).rename(columns={'static_1': 'product_id'})\nseries.head()\n\n\n\n\n\n\n\n\nunique_id\nds\ny\nstatic_0\nproduct_id\n\n\n\n\n0\nid_00\n2000-10-05\n39.811983\n79\n45\n\n\n1\nid_00\n2000-10-06\n103.274013\n79\n45\n\n\n2\nid_00\n2000-10-07\n176.574744\n79\n45\n\n\n3\nid_00\n2000-10-08\n258.987900\n79\n45\n\n\n4\nid_00\n2000-10-09\n344.940404\n79\n45\n\n\n\n\n\n\n\nIn mlforecast the required columns are the series identifier, time and target. Any extra columns you have, like static_0 and product_id here are considered to be static and are replicated when constructing the features for the next timestamp. You can disable this by passing static_features to MLForecast.preprocess or MLForecast.fit, which will only keep the columns you define there as static. Keep in mind that all features in your input dataframe will be used for training, so you’ll have to provide the future values of exogenous features to MLForecast.predict through the X_df argument.\nConsider the following example. Suppose that we have a prices catalog for each id and date.\n\nprices_catalog = generate_prices_for_series(series)\nprices_catalog.head()\n\n\n\n\n\n\n\n\nds\nunique_id\nprice\n\n\n\n\n0\n2000-10-05\nid_00\n0.548814\n\n\n1\n2000-10-06\nid_00\n0.715189\n\n\n2\n2000-10-07\nid_00\n0.602763\n\n\n3\n2000-10-08\nid_00\n0.544883\n\n\n4\n2000-10-09\nid_00\n0.423655\n\n\n\n\n\n\n\nAnd that you have already merged these prices into your series dataframe.\n\nseries_with_prices = series.merge(prices_catalog, how='left')\nseries_with_prices.head()\n\n\n\n\n\n\n\n\nunique_id\nds\ny\nstatic_0\nproduct_id\nprice\n\n\n\n\n0\nid_00\n2000-10-05\n39.811983\n79\n45\n0.548814\n\n\n1\nid_00\n2000-10-06\n103.274013\n79\n45\n0.715189\n\n\n2\nid_00\n2000-10-07\n176.574744\n79\n45\n0.602763\n\n\n3\nid_00\n2000-10-08\n258.987900\n79\n45\n0.544883\n\n\n4\nid_00\n2000-10-09\n344.940404\n79\n45\n0.423655\n\n\n\n\n\n\n\nThis dataframe will be passed to MLForecast.fit (or MLForecast.preprocess). However, since the price is dynamic we have to tell that method that only static_0 and product_id are static.\n\nfcst = MLForecast(\n models=lgb.LGBMRegressor(n_jobs=1, random_state=0, verbosity=-1),\n freq='D',\n lags=[7],\n lag_transforms={\n 1: [expanding_mean],\n 7: [(rolling_mean, 14)]\n },\n date_features=['dayofweek', 'month'],\n num_threads=2,\n)\nfcst.fit(series_with_prices, static_features=['static_0', 'product_id'])\n\nMLForecast(models=[LGBMRegressor], freq=<Day>, lag_features=['lag7', 'expanding_mean_lag1', 'rolling_mean_lag7_window_size14'], date_features=['dayofweek', 'month'], num_threads=2)\n\n\nThe features used for training are stored in MLForecast.ts.features_order_. As you can see price was used for training.\n\nfcst.ts.features_order_\n\n['static_0',\n 'product_id',\n 'price',\n 'lag7',\n 'expanding_mean_lag1',\n 'rolling_mean_lag7_window_size14',\n 'dayofweek',\n 'month']\n\n\nSo in order to update the price in each timestep we just call MLForecast.predict with our forecast horizon and pass the prices catalog through X_df.\n\npreds = fcst.predict(h=7, X_df=prices_catalog)\npreds.head()\n\n\n\n\n\n\n\n\nunique_id\nds\nLGBMRegressor\n\n\n\n\n0\nid_00\n2001-05-15\n418.930093\n\n\n1\nid_00\n2001-05-16\n499.487368\n\n\n2\nid_00\n2001-05-17\n20.321885\n\n\n3\nid_00\n2001-05-18\n102.310778\n\n\n4\nid_00\n2001-05-19\n185.340281"
+ },
+ {
+ "objectID": "docs/how-to-guides/predict_callbacks.html",
+ "href": "docs/how-to-guides/predict_callbacks.html",
+ "title": "Predict callbacks",
+ "section": "",
+ "text": "If you want to do something to the input before predicting or something to the output before it gets used to update the target (and thus the next features that rely on lags), you can pass a function to run at any of these times.\nHere are a couple of examples:\nimport lightgbm as lgb\nimport numpy as np\nfrom IPython.display import display\n\nfrom mlforecast import MLForecast\nfrom mlforecast.utils import generate_daily_series\nseries = generate_daily_series(1)\nGive us a ⭐ on Github"
+ },
+ {
+ "objectID": "docs/how-to-guides/predict_callbacks.html#before-predicting",
+ "href": "docs/how-to-guides/predict_callbacks.html#before-predicting",
+ "title": "Predict callbacks",
+ "section": "Before predicting",
+ "text": "Before predicting\n\nInspecting the input\nWe can define a function that displays our input dataframe before predicting.\n\ndef inspect_input(new_x):\n \"\"\"Displays the model inputs to inspect them\"\"\"\n display(new_x)\n return new_x\n\nAnd now we can pass this function to the before_predict_callback argument of MLForecast.predict.\n\nfcst = MLForecast(lgb.LGBMRegressor(verbosity=-1), freq='D', lags=[1, 2])\nfcst.fit(series, static_features=['unique_id'])\npreds = fcst.predict(2, before_predict_callback=inspect_input)\npreds\n\n\n\n\n\n\n\n\nunique_id\nlag1\nlag2\n\n\n\n\n0\nid_0\n4.15593\n3.000028\n\n\n\n\n\n\n\n\n\n\n\n\n\n\nunique_id\nlag1\nlag2\n\n\n\n\n0\nid_0\n5.250205\n4.15593\n\n\n\n\n\n\n\n\n\n\n\n\n\n\nunique_id\nds\nLGBMRegressor\n\n\n\n\n0\nid_0\n2000-08-10\n5.250205\n\n\n1\nid_0\n2000-08-11\n6.241739\n\n\n\n\n\n\n\n\n\nSaving the input features\nSaving the features that are sent as input to the model in each timestamp can be helpful, for example to estimate SHAP values. This can be easily achieved with the SaveFeatures callback.\n\nfrom mlforecast.callbacks import SaveFeatures\n\n\nfcst = MLForecast(lgb.LGBMRegressor(verbosity=-1), freq='D', lags=[1])\nfcst.fit(series, static_features=['unique_id'])\nsave_features_cbk = SaveFeatures()\nfcst.predict(2, before_predict_callback=save_features_cbk);\n\nOnce we’ve called predict we can just retrieve the features.\n\nsave_features_cbk.get_features()\n\n\n\n\n\n\n\n\nunique_id\nlag1\n\n\n\n\n0\nid_0\n4.155930\n\n\n0\nid_0\n5.281643"
+ },
+ {
+ "objectID": "docs/how-to-guides/predict_callbacks.html#after-predicting",
+ "href": "docs/how-to-guides/predict_callbacks.html#after-predicting",
+ "title": "Predict callbacks",
+ "section": "After predicting",
+ "text": "After predicting\nWhen predicting with the recursive strategy (the default) the predictions for each timestamp are used to update the target and recompute the features. If you want to do something to these predictions before that happens you can use the after_predict_callback argument of MLForecast.predict.\n\nIncreasing predictions values\nSuppose we know that our model always underestimates and we want to prevent that from happening by making our predictions 10% higher. We can achieve that with the following:\n\ndef increase_predictions(predictions):\n \"\"\"Increases all predictions by 10%\"\"\"\n return 1.1 * predictions\n\n\nfcst = MLForecast(\n {'model': lgb.LGBMRegressor(verbosity=-1)},\n freq='D',\n date_features=['dayofweek'],\n)\nfcst.fit(series)\noriginal_preds = fcst.predict(2)\nscaled_preds = fcst.predict(2, after_predict_callback=increase_predictions)\nnp.testing.assert_array_less(\n original_preds['model'].values,\n scaled_preds['model'].values,\n)"
+ },
+ {
+ "objectID": "docs/how-to-guides/transfer_learning.html",
+ "href": "docs/how-to-guides/transfer_learning.html",
+ "title": "Transfer Learning",
+ "section": "",
+ "text": "Transfer learning refers to the process of pre-training a flexible model on a large dataset and using it later on other data with little to no training. It is one of the most outstanding 🚀 achievements in Machine Learning and has many practical applications.\nFor time series forecasting, the technique allows you to get lightning-fast predictions ⚡ bypassing the tradeoff between accuracy and speed (more than 30 times faster than our already fast AutoARIMA for a similar accuracy).\nThis notebook shows how to generate a pre-trained model to forecast new time series never seen by the model.\nTable of Contents\nYou can run these experiments with Google Colab.\nGive us a ⭐ on Github"
+ },
+ {
+ "objectID": "docs/how-to-guides/transfer_learning.html#installing-libraries",
+ "href": "docs/how-to-guides/transfer_learning.html#installing-libraries",
+ "title": "Transfer Learning",
+ "section": "Installing Libraries",
+ "text": "Installing Libraries\n\n# !pip install mlforecast datasetsforecast utilsforecast s3fs\n\n\nimport lightgbm as lgb\nimport numpy as np\nimport pandas as pd\nfrom datasetsforecast.m3 import M3\nfrom sklearn.metrics import mean_absolute_error\nfrom utilsforecast.plotting import plot_series\n\nfrom mlforecast import MLForecast\nfrom mlforecast.target_transforms import Differences"
+ },
+ {
+ "objectID": "docs/how-to-guides/transfer_learning.html#load-m3-data",
+ "href": "docs/how-to-guides/transfer_learning.html#load-m3-data",
+ "title": "Transfer Learning",
+ "section": "Load M3 Data",
+ "text": "Load M3 Data\nThe M3 class will automatically download the complete M3 dataset and process it.\nIt return three Dataframes: Y_df contains the values for the target variables, X_df contains exogenous calendar features and S_df contains static features for each time-series. For this example we will only use Y_df.\nIf you want to use your own data just replace Y_df. Be sure to use a long format and have a simmilar structure than our data set.\n\nY_df_M3, _, _ = M3.load(directory='./', group='Monthly')\n\nIn this tutorial we are only using 1_000 series to speed up computations. Remove the filter to use the whole dataset.\n\nfig = plot_series(Y_df_M3)"
+ },
+ {
+ "objectID": "docs/how-to-guides/transfer_learning.html#model-training",
+ "href": "docs/how-to-guides/transfer_learning.html#model-training",
+ "title": "Transfer Learning",
+ "section": "Model Training",
+ "text": "Model Training\nUsing the MLForecast.fit method you can train a set of models to your dataset. You can modify the hyperparameters of the model to get a better accuracy, in this case we will use the default hyperparameters of lgb.LGBMRegressor.\n\nmodels = [lgb.LGBMRegressor(verbosity=-1)]\n\nThe MLForecast object has the following parameters:\n\nmodels: a list of sklearn-like (fit and predict) models.\nfreq: a string indicating the frequency of the data. See panda’s available frequencies.\ndifferences: Differences to take of the target before computing the features. These are restored at the forecasting step.\nlags: Lags of the target to use as features.\n\nIn this example, we are only using differences and lags to produce features. See the full documentation to see all available features.\nAny settings are passed into the constructor. Then you call its fit method and pass in the historical data frame Y_df_M3.\n\nfcst = MLForecast(\n models=models, \n lags=range(1, 13),\n freq='MS',\n target_transforms=[Differences([1, 12])],\n)\nfcst.fit(Y_df_M3);"
+ },
+ {
+ "objectID": "docs/how-to-guides/transfer_learning.html#transfer-m3-to-airpassengers",
+ "href": "docs/how-to-guides/transfer_learning.html#transfer-m3-to-airpassengers",
+ "title": "Transfer Learning",
+ "section": "Transfer M3 to AirPassengers",
+ "text": "Transfer M3 to AirPassengers\nNow we can transfer the trained model to forecast AirPassengers with the MLForecast.predict method, we just have to pass the new dataframe to the new_data argument.\n\nY_df = pd.read_csv('https://datasets-nixtla.s3.amazonaws.com/air-passengers.csv', parse_dates=['ds'])\n\n# We define the train df. \nY_train_df = Y_df[Y_df.ds<='1959-12-31'] # 132 train\nY_test_df = Y_df[Y_df.ds>'1959-12-31'] # 12 test\n\n\nY_hat_df = fcst.predict(h=12, new_df=Y_train_df)\nY_hat_df.head()\n\n\n\n\n\n\n\n\nunique_id\nds\nLGBMRegressor\n\n\n\n\n0\nAirPassengers\n1960-01-01\n422.740096\n\n\n1\nAirPassengers\n1960-02-01\n399.480193\n\n\n2\nAirPassengers\n1960-03-01\n458.220289\n\n\n3\nAirPassengers\n1960-04-01\n442.960385\n\n\n4\nAirPassengers\n1960-05-01\n461.700482\n\n\n\n\n\n\n\n\nY_hat_df = Y_test_df.merge(Y_hat_df, how='left', on=['unique_id', 'ds'])\n\n\nfig = plot_series(Y_train_df, Y_hat_df)"
+ },
+ {
+ "objectID": "docs/how-to-guides/transfer_learning.html#evaluate-results",
+ "href": "docs/how-to-guides/transfer_learning.html#evaluate-results",
+ "title": "Transfer Learning",
+ "section": "Evaluate Results",
+ "text": "Evaluate Results\nWe evaluate the forecasts of the pre-trained model with the Mean Absolute Error (mae).\n\\[\n\\qquad MAE = \\frac{1}{Horizon} \\sum_{\\tau} |y_{\\tau} - \\hat{y}_{\\tau}|\\qquad\n\\]\n\ny_true = Y_test_df.y.values\ny_hat = Y_hat_df['LGBMRegressor'].values\n\n\nprint(f'LGBMRegressor MAE: {mean_absolute_error(y_hat, y_true):.3f}')\nprint('ETS MAE: 16.222')\nprint('AutoARIMA MAE: 18.551')\n\nLGBMRegressor MAE: 13.560\nETS MAE: 16.222\nAutoARIMA MAE: 18.551"
+ },
+ {
+ "objectID": "docs/tutorials/electricity_peak_forecasting.html",
+ "href": "docs/tutorials/electricity_peak_forecasting.html",
+ "title": "Detect Demand Peaks",
+ "section": "",
+ "text": "Predicting peaks in different markets is useful. In the electricity market, consuming electricity at peak demand is penalized with higher tarifs. When an individual or company consumes electricity when its most demanded, regulators calls that a coincident peak (CP).\nIn the Texas electricity market (ERCOT), the peak is the monthly 15-minute interval when the ERCOT Grid is at a point of highest capacity. The peak is caused by all consumers’ combined demand on the electrical grid. The coincident peak demand is an important factor used by ERCOT to determine final electricity consumption bills. ERCOT registers the CP demand of each client for 4 months, between June and September, and uses this to adjust electricity prices. Clients can therefore save on electricity bills by reducing the coincident peak demand.\nIn this example we will train a LightGBM model on historic load data to forecast day-ahead peaks on September 2022. Multiple seasonality is traditionally present in low sampled electricity data. Demand exhibits daily and weekly seasonality, with clear patterns for specific hours of the day such as 6:00pm vs 3:00am or for specific days such as Sunday vs Friday.\nFirst, we will load ERCOT historic demand, then we will use the MLForecast.cross_validation method to fit the LightGBM model and forecast daily load during September. Finally, we show how to use the forecasts to detect the coincident peak.\nOutline\n\nInstall libraries\nLoad and explore the data\nFit LightGBM model and forecast\nPeak detection\n\n\n\n\n\n\n\nTip\n\n\n\nYou can use Colab to run this Notebook interactively\nGive us a ⭐ on Github"
+ },
+ {
+ "objectID": "docs/tutorials/electricity_peak_forecasting.html#introduction",
+ "href": "docs/tutorials/electricity_peak_forecasting.html#introduction",
+ "title": "Detect Demand Peaks",
+ "section": "",
+ "text": "Predicting peaks in different markets is useful. In the electricity market, consuming electricity at peak demand is penalized with higher tarifs. When an individual or company consumes electricity when its most demanded, regulators calls that a coincident peak (CP).\nIn the Texas electricity market (ERCOT), the peak is the monthly 15-minute interval when the ERCOT Grid is at a point of highest capacity. The peak is caused by all consumers’ combined demand on the electrical grid. The coincident peak demand is an important factor used by ERCOT to determine final electricity consumption bills. ERCOT registers the CP demand of each client for 4 months, between June and September, and uses this to adjust electricity prices. Clients can therefore save on electricity bills by reducing the coincident peak demand.\nIn this example we will train a LightGBM model on historic load data to forecast day-ahead peaks on September 2022. Multiple seasonality is traditionally present in low sampled electricity data. Demand exhibits daily and weekly seasonality, with clear patterns for specific hours of the day such as 6:00pm vs 3:00am or for specific days such as Sunday vs Friday.\nFirst, we will load ERCOT historic demand, then we will use the MLForecast.cross_validation method to fit the LightGBM model and forecast daily load during September. Finally, we show how to use the forecasts to detect the coincident peak.\nOutline\n\nInstall libraries\nLoad and explore the data\nFit LightGBM model and forecast\nPeak detection\n\n\n\n\n\n\n\nTip\n\n\n\nYou can use Colab to run this Notebook interactively"
+ },
+ {
+ "objectID": "docs/tutorials/electricity_peak_forecasting.html#libraries",
+ "href": "docs/tutorials/electricity_peak_forecasting.html#libraries",
+ "title": "Detect Demand Peaks",
+ "section": "Libraries",
+ "text": "Libraries\nWe assume you have MLForecast already installed. Check this guide for instructions on how to install MLForecast.\nInstall the necessary packages using pip install mlforecast.\nAlso we have to install LightGBM using pip install lightgbm."
+ },
+ {
+ "objectID": "docs/tutorials/electricity_peak_forecasting.html#load-data",
+ "href": "docs/tutorials/electricity_peak_forecasting.html#load-data",
+ "title": "Detect Demand Peaks",
+ "section": "Load Data",
+ "text": "Load Data\nThe input to MLForecast is always a data frame in long format with three columns: unique_id, ds and y:\n\nThe unique_id (string, int or category) represents an identifier for the series.\nThe ds (datestamp or int) column should be either an integer indexing time or a datestamp ideally like YYYY-MM-DD for a date or YYYY-MM-DD HH:MM:SS for a timestamp.\nThe y (numeric) represents the measurement we wish to forecast. We will rename the\n\nFirst, read the 2022 historic total demand of the ERCOT market. We processed the original data (available here), by adding the missing hour due to daylight saving time, parsing the date to datetime format, and filtering columns of interest.\n\nimport numpy as np\nimport pandas as pd\nfrom utilsforecast.plotting import plot_series\n\n\n# Load data\nY_df = pd.read_csv('https://datasets-nixtla.s3.amazonaws.com/ERCOT-clean.csv', parse_dates=['ds'])\nY_df = Y_df.query(\"ds >= '2022-01-01' & ds <= '2022-10-01'\")\n\n\nfig = plot_series(Y_df)\n\n\nWe observe that the time series exhibits seasonal patterns. Moreover, the time series contains 6,552 observations, so it is necessary to use computationally efficient methods to deploy them in production."
+ },
+ {
+ "objectID": "docs/tutorials/electricity_peak_forecasting.html#fit-and-forecast-lightgbm-model",
+ "href": "docs/tutorials/electricity_peak_forecasting.html#fit-and-forecast-lightgbm-model",
+ "title": "Detect Demand Peaks",
+ "section": "Fit and Forecast LightGBM model",
+ "text": "Fit and Forecast LightGBM model\nImport the MLForecast class and the models you need.\n\nimport lightgbm as lgb\n\nfrom mlforecast import MLForecast\nfrom mlforecast.target_transforms import Differences\n\nFirst, instantiate the model and define the parameters.\n\n\n\n\n\n\nTip\n\n\n\nIn this example we are using the default parameters of the lgb.LGBMRegressor model, but you can change them to improve the forecasting performance.\n\n\n\nmodels = [\n lgb.LGBMRegressor(verbosity=-1) # you can include more models here\n]\n\nWe fit the model by instantiating a MLForecast object with the following required parameters:\n\nmodels: a list of sklearn-like (fit and predict) models.\nfreq: a string indicating the frequency of the data. (See panda’s available frequencies.)\ntarget_transforms: Transformations to apply to the target before computing the features. These are restored at the forecasting step.\nlags: Lags of the target to use as features.\n\n\n# Instantiate MLForecast class as mlf\nmlf = MLForecast(\n models=models,\n freq='H', \n target_transforms=[Differences([24])],\n lags=range(1, 25)\n)\n\n\n\n\n\n\n\nTip\n\n\n\nIn this example, we are only using differences and lags to produce features. See the full documentation to see all available features.\n\n\nThe cross_validation method allows the user to simulate multiple historic forecasts, greatly simplifying pipelines by replacing for loops with fit and predict methods. This method re-trains the model and forecast each window. See this tutorial for an animation of how the windows are defined.\nUse the cross_validation method to produce all the daily forecasts for September. To produce daily forecasts set the forecasting horizon window_size as 24. In this example we are simulating deploying the pipeline during September, so set the number of windows as 30 (one for each day). Finally, the step size between windows is 24 (equal to the window_size). This ensure to only produce one forecast per day.\nAdditionally,\n\nid_col: identifies each time series.\ntime_col: indetifies the temporal column of the time series.\ntarget_col: identifies the column to model.\n\n\ncrossvalidation_df = mlf.cross_validation(\n df=Y_df,\n h=24,\n n_windows=30,\n)\n\n\ncrossvalidation_df.head()\n\n\n\n\n\n\n\n\nunique_id\nds\ncutoff\ny\nLGBMRegressor\n\n\n\n\n0\nERCOT\n2022-09-01 00:00:00\n2022-08-31 23:00:00\n45482.471757\n45685.265537\n\n\n1\nERCOT\n2022-09-01 01:00:00\n2022-08-31 23:00:00\n43602.658043\n43779.819515\n\n\n2\nERCOT\n2022-09-01 02:00:00\n2022-08-31 23:00:00\n42284.817342\n42672.470923\n\n\n3\nERCOT\n2022-09-01 03:00:00\n2022-08-31 23:00:00\n41663.156771\n42091.768192\n\n\n4\nERCOT\n2022-09-01 04:00:00\n2022-08-31 23:00:00\n41710.621904\n42481.403168\n\n\n\n\n\n\n\n\n\n\n\n\n\nImportant\n\n\n\nWhen using cross_validation make sure the forecasts are produced at the desired timestamps. Check the cutoff column which specifices the last timestamp before the forecasting window."
+ },
+ {
+ "objectID": "docs/tutorials/electricity_peak_forecasting.html#peak-detection",
+ "href": "docs/tutorials/electricity_peak_forecasting.html#peak-detection",
+ "title": "Detect Demand Peaks",
+ "section": "Peak Detection",
+ "text": "Peak Detection\nFinally, we use the forecasts in crossvaldation_df to detect the daily hourly demand peaks. For each day, we set the detected peaks as the highest forecasts. In this case, we want to predict one peak (npeaks); depending on your setting and goals, this parameter might change. For example, the number of peaks can correspond to how many hours a battery can be discharged to reduce demand.\n\nnpeaks = 1 # Number of peaks\n\nFor the ERCOT 4CP detection task we are interested in correctly predicting the highest monthly load. Next, we filter the day in September with the highest hourly demand and predict the peak.\n\ncrossvalidation_df = crossvalidation_df.reset_index()[['ds','y','LGBMRegressor']]\nmax_day = crossvalidation_df.iloc[crossvalidation_df['y'].argmax()].ds.day # Day with maximum load\ncv_df_day = crossvalidation_df.query('ds.dt.day == @max_day')\nmax_hour = cv_df_day['y'].argmax()\npeaks = cv_df_day['LGBMRegressor'].argsort().iloc[-npeaks:].values # Predicted peaks\n\nIn the following plot we see how the LightGBM model is able to correctly detect the coincident peak for September 2022.\n\nimport matplotlib.pyplot as plt\n\n\nfig, ax = plt.subplots(figsize=(10, 5))\nax.axvline(cv_df_day.iloc[max_hour]['ds'], color='black', label='True Peak')\nax.scatter(cv_df_day.iloc[peaks]['ds'], cv_df_day.iloc[peaks]['LGBMRegressor'], color='green', label=f'Predicted Top-{npeaks}')\nax.plot(cv_df_day['ds'], cv_df_day['y'], label='y', color='blue')\nax.plot(cv_df_day['ds'], cv_df_day['LGBMRegressor'], label='Forecast', color='red')\nax.set(xlabel='Time', ylabel='Load (MW)')\nax.grid()\nax.legend()\nfig.savefig('../../figs/electricity_peak_forecasting__predicted_peak.png', bbox_inches='tight')\nplt.close()\n\n\n\n\n\n\n\n\nImportant\n\n\n\nIn this example we only include September. However, MLForecast and LightGBM can correctly predict the peaks for the 4 months of 2022. You can try this by increasing the n_windows parameter of cross_validation or filtering the Y_df dataset."
+ },
+ {
+ "objectID": "docs/tutorials/electricity_peak_forecasting.html#next-steps",
+ "href": "docs/tutorials/electricity_peak_forecasting.html#next-steps",
+ "title": "Detect Demand Peaks",
+ "section": "Next steps",
+ "text": "Next steps\nMLForecast and LightGBM in particular are good benchmarking models for peak detection. However, it might be useful to explore further and newer forecasting algorithms or perform hyperparameter optimization."
+ },
+ {
+ "objectID": "docs/tutorials/electricity_load_forecasting.html",
+ "href": "docs/tutorials/electricity_load_forecasting.html",
+ "title": "Electricity Load Forecast",
+ "section": "",
+ "text": "Some time series are generated from very low frequency data. These data generally exhibit multiple seasonalities. For example, hourly data may exhibit repeated patterns every hour (every 24 observations) or every day (every 24 * 7, hours per day, observations). This is the case for electricity load. Electricity load may vary hourly, e.g., during the evenings electricity consumption may be expected to increase. But also, the electricity load varies by week. Perhaps on weekends there is an increase in electrical activity.\nIn this example we will show how to model the two seasonalities of the time series to generate accurate forecasts in a short time. We will use hourly PJM electricity load data. The original data can be found here.\nGive us a ⭐ on Github"
+ },
+ {
+ "objectID": "docs/tutorials/electricity_load_forecasting.html#introduction",
+ "href": "docs/tutorials/electricity_load_forecasting.html#introduction",
+ "title": "Electricity Load Forecast",
+ "section": "",
+ "text": "Some time series are generated from very low frequency data. These data generally exhibit multiple seasonalities. For example, hourly data may exhibit repeated patterns every hour (every 24 observations) or every day (every 24 * 7, hours per day, observations). This is the case for electricity load. Electricity load may vary hourly, e.g., during the evenings electricity consumption may be expected to increase. But also, the electricity load varies by week. Perhaps on weekends there is an increase in electrical activity.\nIn this example we will show how to model the two seasonalities of the time series to generate accurate forecasts in a short time. We will use hourly PJM electricity load data. The original data can be found here."
+ },
+ {
+ "objectID": "docs/tutorials/electricity_load_forecasting.html#libraries",
+ "href": "docs/tutorials/electricity_load_forecasting.html#libraries",
+ "title": "Electricity Load Forecast",
+ "section": "Libraries",
+ "text": "Libraries\nIn this example we will use the following libraries:\n\nmlforecast. Accurate and ⚡️ fast forecasting withc lassical machine learning models.\nprophet. Benchmark model developed by Facebook.\nutilsforecast. Library with different functions for forecasting evaluation.\n\nIf you have already installed the libraries you can skip the next cell, if not be sure to run it.\n\n# %%capture\n# !pip install prophet\n# !pip install -U mlforecast\n# !pip install -U utilsforecast"
+ },
+ {
+ "objectID": "docs/tutorials/electricity_load_forecasting.html#forecast-using-multiple-seasonalities",
+ "href": "docs/tutorials/electricity_load_forecasting.html#forecast-using-multiple-seasonalities",
+ "title": "Electricity Load Forecast",
+ "section": "Forecast using Multiple Seasonalities",
+ "text": "Forecast using Multiple Seasonalities\n\nElectricity Load Data\nAccording to the dataset’s page,\n\nPJM Interconnection LLC (PJM) is a regional transmission organization (RTO) in the United States. It is part of the Eastern Interconnection grid operating an electric transmission system serving all or parts of Delaware, Illinois, Indiana, Kentucky, Maryland, Michigan, New Jersey, North Carolina, Ohio, Pennsylvania, Tennessee, Virginia, West Virginia, and the District of Columbia. The hourly power consumption data comes from PJM’s website and are in megawatts (MW).\n\nLet’s take a look to the data.\n\nimport matplotlib.pyplot as plt\nimport numpy as np\nimport pandas as pd\nfrom utilsforecast.plotting import plot_series\n\n\npd.plotting.register_matplotlib_converters()\nplt.rc(\"figure\", figsize=(10, 8))\nplt.rc(\"font\", size=10)\n\n\ndata_url = 'https://raw.githubusercontent.com/panambY/Hourly_Energy_Consumption/master/data/PJM_Load_hourly.csv'\ndf = pd.read_csv(data_url, parse_dates=['Datetime'])\ndf.columns = ['ds', 'y']\ndf.insert(0, 'unique_id', 'PJM_Load_hourly')\ndf['ds'] = pd.to_datetime(df['ds'])\ndf = df.sort_values(['unique_id', 'ds']).reset_index(drop=True)\nprint(f'Shape of the data {df.shape}')\ndf.tail()\n\nShape of the data (32896, 3)\n\n\n\n\n\n\n\n\n\nunique_id\nds\ny\n\n\n\n\n32891\nPJM_Load_hourly\n2001-12-31 20:00:00\n36392.0\n\n\n32892\nPJM_Load_hourly\n2001-12-31 21:00:00\n35082.0\n\n\n32893\nPJM_Load_hourly\n2001-12-31 22:00:00\n33890.0\n\n\n32894\nPJM_Load_hourly\n2001-12-31 23:00:00\n32590.0\n\n\n32895\nPJM_Load_hourly\n2002-01-01 00:00:00\n31569.0\n\n\n\n\n\n\n\n\nfig = plot_series(df)\n\n\nWe clearly observe that the time series exhibits seasonal patterns. Moreover, the time series contains 32,896 observations, so it is necessary to use very computationally efficient methods to display them in production.\nWe are going to split our series in order to create a train and test set. The model will be tested using the last 24 hours of the timeseries.\n\nthreshold_time = df['ds'].max() - pd.Timedelta(hours=24)\n\n# Split the dataframe\ndf_train = df[df['ds'] <= threshold_time]\ndf_last_24_hours = df[df['ds'] > threshold_time]\n\n\n\nAnalizing Seasonalities\nFirst we must visualize the seasonalities of the model. As mentioned before, the electricity load presents seasonalities every 24 hours (Hourly) and every 24 * 7 (Daily) hours. Therefore, we will use [24, 24 * 7] as the seasonalities for the model. In order to analize how they affect our series we are going to use the Difference method.\n\nfrom mlforecast import MLForecast\nfrom mlforecast.target_transforms import Differences\n\nWe can use the MLForecast.preprocess method to explore different transformations. It looks like these series have a strong seasonality on the hour of the day, so we can subtract the value from the same hour in the previous day to remove it. This can be done with the mlforecast.target_transforms.Differences transformer, which we pass through target_transforms.\nIn order to analize the trends individually and combined we are going to plot them individually and combined. Therefore, we can compare them against the original series. We can use the next function for that.\n\ndef plot_differences(df, differences,fname):\n prep = [df]\n # Plot individual Differences\n for d in differences:\n fcst = MLForecast(\n models=[], # we're not interested in modeling yet\n freq='H', # our series have hourly frequency \n target_transforms=[Differences([d])],\n )\n df_ = fcst.preprocess(df)\n df_['unique_id'] = df_['unique_id'] + f'_{d}'\n prep.append(df_)\n \n # Plot combined Differences\n fcst = MLForecast(\n models=[], # we're not interested in modeling yet\n freq='H', # our series have hourly frequency \n target_transforms=[Differences([24, 24*7])],\n )\n df_ = fcst.preprocess(df)\n df_['unique_id'] = df_['unique_id'] + f'_all_diff'\n prep.append(df_)\n prep = pd.concat(prep, ignore_index=True)\n #return prep\n n_series = len(prep['unique_id'].unique())\n fig, ax = plt.subplots(nrows=n_series, figsize=(7 * n_series, 10*n_series), squeeze=False)\n for title, axi in zip(prep['unique_id'].unique(), ax.flat):\n df_ = prep[prep['unique_id'] == title]\n df_.set_index('ds')['y'].plot(title=title, ax=axi)\n fig.savefig(f'../../figs/{fname}', bbox_inches='tight')\n plt.close()\n\nSince the seasonalities are present at 24 hours (daily) and 24*7 (weekly) we are going to substract them from the serie using Differences([24, 24*7]) and plot them.\n\nplot_differences(df=df_train, differences=[24, 24*7], fname='load_forecasting__differences.png')\n\n\nAs we can see when we extract the 24 difference (daily) in PJM_Load_hourly_24 the series seem to stabilize sisnce the peaks seem more uniform in comparison with the original series PJM_Load_hourly.\nWhen we extrac the 24*7 (weekly) PJM_Load_hourly_168 difference we can see there is more periodicity in the peaks in comparison with the original series.\nFinally we can see the result from the combined result from substracting all the differences PJM_Load_hourly_all_diff.\nFor modeling we are going to use both difference for the forecasting, therefore we are setting the argument target_transforms from the MLForecast object equal to [Differences([24, 24*7])], if we wanted to include a yearly difference we would need to add the term 24*365.\n\nfcst = MLForecast(\n models=[], # we're not interested in modeling yet\n freq='H', # our series have hourly frequency \n target_transforms=[Differences([24, 24*7])],\n)\nprep = fcst.preprocess(df_train)\nprep\n\n\n\n\n\n\n\n\nunique_id\nds\ny\n\n\n\n\n192\nPJM_Load_hourly\n1998-04-09 02:00:00\n831.0\n\n\n193\nPJM_Load_hourly\n1998-04-09 03:00:00\n918.0\n\n\n194\nPJM_Load_hourly\n1998-04-09 04:00:00\n760.0\n\n\n195\nPJM_Load_hourly\n1998-04-09 05:00:00\n849.0\n\n\n196\nPJM_Load_hourly\n1998-04-09 06:00:00\n710.0\n\n\n...\n...\n...\n...\n\n\n32867\nPJM_Load_hourly\n2001-12-30 20:00:00\n3417.0\n\n\n32868\nPJM_Load_hourly\n2001-12-30 21:00:00\n3596.0\n\n\n32869\nPJM_Load_hourly\n2001-12-30 22:00:00\n3501.0\n\n\n32870\nPJM_Load_hourly\n2001-12-30 23:00:00\n3939.0\n\n\n32871\nPJM_Load_hourly\n2001-12-31 00:00:00\n4235.0\n\n\n\n\n32680 rows × 3 columns\n\n\n\n\nfig = plot_series(prep)\n\n\n\n\nModel Selection with Cross-Validation\nWe can test many models simoultaneously using MLForecast cross_validation. We can import lightgbm and scikit-learn models and try different combinations of them, alongside different target transformations (as the ones we created previously) and historical variables.\nYou can see an in-depth tutorial on how to use MLForecast Cross Validation methods here\n\nimport lightgbm as lgb\nfrom mlforecast.target_transforms import Differences\nfrom window_ops.expanding import expanding_mean\nfrom window_ops.rolling import rolling_mean\n\nfrom sklearn.base import BaseEstimator\nfrom sklearn.linear_model import Lasso, LinearRegression, Ridge\nfrom sklearn.neighbors import KNeighborsRegressor\nfrom sklearn.neural_network import MLPRegressor\nfrom sklearn.ensemble import RandomForestRegressor\n\nWe can create a benchmark Naive model that uses the electricity load of the last hour as prediction lag1 as showed in the next cell. You can create your own models and try them with MLForecast using the same structure.\n\nclass Naive(BaseEstimator):\n def fit(self, X, y):\n return self\n\n def predict(self, X):\n return X['lag1']\n\nNow let’s try differen models from the scikit-learn library: Lasso, LinearRegression, Ridge, KNN, MLP and Random Forest alongside the LightGBM. You can add any model to the dictionary to train and compare them by adding them to the dictionary (models) as shown.\n\n# Model dictionary\nmodels ={\n 'naive': Naive(),\n 'lgbm': lgb.LGBMRegressor(verbosity=-1),\n 'lasso': Lasso(),\n 'lin_reg': LinearRegression(),\n 'ridge': Ridge(),\n 'knn': KNeighborsRegressor(),\n 'mlp': MLPRegressor(), \n 'rf': RandomForestRegressor()\n }\n\nThe we can instanciate the MLForecast class with the models we want to try along side target_transforms, lags, lag_transforms, and date_features. All this features are applied to the models we selected.\nIn this case we use the 1st, 12th and 24th lag, which are passed as a list. Potentially you could pass a range.\nlags=[1,12,24]\nLag transforms are defined as a dictionary where the keys are the lags and the values are lists of functions that transform an array. These must be numba jitted functions (so that computing the features doesn’t become a bottleneck). There are some implemented in the window-ops package but you can also implement your own.\nFor this example we applied an expanding mean to the first lag, and a rolling mean to the 24th lag.\n lag_transforms={ \n 1: [expanding_mean],\n 24: [(rolling_mean, 48)],\n }\nFor using the date features you need to be sure that your time column is made of timestamps. Then it might make sense to extract features like week, dayofweek, quarter, etc. You can do that by passing a list of strings with pandas time/date components. You can also pass functions that will take the time column as input, as we’ll show here.\nHere we add month, hour and dayofweek features:\n date_features=['month', 'hour', 'dayofweek']\n\n\nmlf = MLForecast(\n models = models, \n freq='H', # our series have hourly frequency \n target_transforms=[Differences([24, 24*7])],\n lags=[1,12,24], # Lags to be used as features\n lag_transforms={ \n 1: [expanding_mean],\n 24: [(rolling_mean, 48)],\n },\n date_features=['month', 'hour', 'dayofweek']\n)\n\nNow we use the cross_validation method to train and evalaute the models. + df: Receives the training data + h: Forecast horizon + n_windows: The number of folds we want to predict\nYou can specify the names of the time series id, time and target columns. + id_col:Column that identifies each serie ( Default unique_id ) + time_col: Column that identifies each timestep, its values can be timestamps or integer( Default ds ) + target_col:Column that contains the target ( Default y )\n\ncrossvalidation_df = mlf.cross_validation(\n df=df_train,\n h=24,\n n_windows=4,\n refit=False,\n)\ncrossvalidation_df.head()\n\n\n\n\n\n\n\n\nunique_id\nds\ncutoff\ny\nnaive\nlgbm\nlasso\nlin_reg\nridge\nknn\nmlp\nrf\n\n\n\n\n0\nPJM_Load_hourly\n2001-12-27 01:00:00\n2001-12-27\n28332.0\n28837.0\n28526.505572\n28703.185712\n28702.625949\n28702.625956\n28479.0\n28834.839479\n28277.94\n\n\n1\nPJM_Load_hourly\n2001-12-27 02:00:00\n2001-12-27\n27329.0\n27969.0\n27467.860847\n27693.502318\n27692.395954\n27692.395969\n27521.6\n27940.414699\n27307.90\n\n\n2\nPJM_Load_hourly\n2001-12-27 03:00:00\n2001-12-27\n26986.0\n27435.0\n26605.710615\n26991.795124\n26990.157567\n26990.157589\n26451.6\n27370.170779\n26599.23\n\n\n3\nPJM_Load_hourly\n2001-12-27 04:00:00\n2001-12-27\n27009.0\n27401.0\n26284.065138\n26789.418399\n26787.262262\n26787.262291\n26388.4\n27294.232077\n26366.54\n\n\n4\nPJM_Load_hourly\n2001-12-27 05:00:00\n2001-12-27\n27555.0\n28169.0\n26823.617078\n27369.643789\n27366.983075\n27366.983111\n26779.6\n28014.090505\n27095.55\n\n\n\n\n\n\n\nNow we can plot each model and window (fold) to see how it behaves\n\ndef plot_cv(df, df_cv, uid, fname, last_n=24 * 14, models={}):\n cutoffs = df_cv.query('unique_id == @uid')['cutoff'].unique()\n fig, ax = plt.subplots(nrows=len(cutoffs), ncols=1, figsize=(14, 14), gridspec_kw=dict(hspace=0.8))\n for cutoff, axi in zip(cutoffs, ax.flat):\n max_date = df_cv.query('unique_id == @uid & cutoff == @cutoff')['ds'].max()\n df[df['ds'] < max_date].query('unique_id == @uid').tail(last_n).set_index('ds').plot(ax=axi, title=uid, y='y')\n for m in models.keys():\n df_cv.query('unique_id == @uid & cutoff == @cutoff').set_index('ds').plot(ax=axi, title=uid, y=m) \n fig.savefig(f'../../figs/{fname}', bbox_inches='tight')\n plt.close()\n\n\nplot_cv(df_train, crossvalidation_df, 'PJM_Load_hourly', 'load_forecasting__predictions.png', models=models)\n\n\nVisually examining the forecasts can give us some idea of how the model is behaving, yet in order to asses the performace we need to evaluate them trough metrics. For that we use the utilsforecast library that contains many useful metrics and an evaluate function.\n\nfrom utilsforecast.losses import *\nfrom utilsforecast.evaluation import evaluate\n\n\n# Metrics to be used for evaluation\nmetrics = [\n mae,\n rmse,\n mape,\n smape\n ]\n\n\n# Function to evaluate the crossvalidation\ndef evaluate_crossvalidation(crossvalidation_df, metrics, models):\n evaluations = []\n for c in crossvalidation_df['cutoff'].unique():\n df_cv = crossvalidation_df.query('cutoff == @c')\n evaluation = evaluate(\n df = df_cv,\n metrics=metrics,\n models=list(models.keys())\n )\n evaluations.append(evaluation)\n evaluations = pd.concat(evaluations, ignore_index=True).drop(columns='unique_id')\n evaluations = evaluations.groupby('metric').mean()\n return evaluations.style.background_gradient(cmap='RdYlGn_r', axis=1)\n\n\nevaluate_crossvalidation(crossvalidation_df, metrics, models)\n\n\n\n\n\n\n \nnaive\nlgbm\nlasso\nlin_reg\nridge\nknn\nmlp\nrf\n\n\nmetric\n \n \n \n \n \n \n \n \n\n\n\n\nmae\n1631.395833\n971.536200\n1003.796433\n1007.998597\n1007.998547\n1248.145833\n1268.841369\n1219.286771\n\n\nmape\n0.049759\n0.030966\n0.031760\n0.031888\n0.031888\n0.038721\n0.039149\n0.037969\n\n\nrmse\n1871.398919\n1129.713256\n1148.616156\n1153.262719\n1153.262664\n1451.964390\n1463.836007\n1409.549197\n\n\nsmape\n0.024786\n0.015886\n0.016269\n0.016338\n0.016338\n0.019549\n0.019704\n0.019252\n\n\n\n\n\nWe can se that the model lgbm has top performance in most metrics folowed by the lasso regression. Both models perform way better than the naive.\n\n\nTest Evaluation\nNow we are going to evaluate their perfonce in the test set. We can use both of them for forecasting the test alongside some prediction intervals. For that we can use the PredictionIntervals function in mlforecast.utils.\nYou can see an in-depth tutotorial of Probabilistic Forecasting here\n\nfrom mlforecast.utils import PredictionIntervals\n\n\nmodels_evaluation ={\n 'lgbm': lgb.LGBMRegressor(verbosity=-1),\n 'lasso': Lasso(),\n }\n\nmlf_evaluation = MLForecast(\n models = models_evaluation, \n freq='H', # our series have hourly frequency \n target_transforms=[Differences([24, 24*7])],\n lags=[1,12,24], \n lag_transforms={ \n 1: [expanding_mean],\n 24: [(rolling_mean, 48)],\n },\n date_features=['month', 'hour', 'dayofweek']\n)\n\nNow we’re ready to generate the point forecasts and the prediction intervals. To do this, we’ll use the fit method, which takes the following arguments:\n\ndf: Series data in long format.\nid_col: Column that identifies each series. In our case, unique_id.\ntime_col: Column that identifies each timestep, its values can be timestamps or integers. In our case, ds.\ntarget_col: Column that contains the target. In our case, y.\n\nThe PredictionIntervals function is used to compute prediction intervals for the models using Conformal Prediction. The function takes the following arguments: + n_windows: represents the number of cross-validation windows used to calibrate the intervals + h: the forecast horizon\n\nmlf_evaluation.fit(\n df = df_train,\n prediction_intervals=PredictionIntervals(n_windows=4, h=24)\n)\n\nMLForecast(models=[lgbm, lasso], freq=<Hour>, lag_features=['lag1', 'lag12', 'lag24', 'expanding_mean_lag1', 'rolling_mean_lag24_window_size48'], date_features=['month', 'hour', 'dayofweek'], num_threads=1)\n\n\nNow that the model has been trained we are going to forecast the next 24 hours using the predict method so we can compare them to our test data. Additionally, we are going to create prediction intervals at levels [90,95].\n\nlevels = [90, 95] # Levels for prediction intervals\nforecasts = mlf_evaluation.predict(24, level=levels)\nforecasts.head()\n\n\n\n\n\n\n\n\nunique_id\nds\nlgbm\nlasso\nlgbm-lo-95\nlgbm-lo-90\nlgbm-hi-90\nlgbm-hi-95\nlasso-lo-95\nlasso-lo-90\nlasso-hi-90\nlasso-hi-95\n\n\n\n\n0\nPJM_Load_hourly\n2001-12-31 01:00:00\n28847.573176\n29124.085976\n28544.593464\n28567.603130\n29127.543222\n29150.552888\n28762.752269\n28772.604275\n29475.567677\n29485.419682\n\n\n1\nPJM_Load_hourly\n2001-12-31 02:00:00\n27862.589195\n28365.330749\n27042.311414\n27128.839888\n28596.338503\n28682.866977\n27528.548959\n27619.065224\n29111.596275\n29202.112539\n\n\n2\nPJM_Load_hourly\n2001-12-31 03:00:00\n27044.418960\n27712.161676\n25596.659896\n25688.230426\n28400.607493\n28492.178023\n26236.955369\n26338.087102\n29086.236251\n29187.367984\n\n\n3\nPJM_Load_hourly\n2001-12-31 04:00:00\n26976.104125\n27661.572733\n25249.961527\n25286.024722\n28666.183529\n28702.246724\n25911.133521\n25959.815715\n29363.329750\n29412.011944\n\n\n4\nPJM_Load_hourly\n2001-12-31 05:00:00\n26694.246238\n27393.922370\n25044.220845\n25051.548832\n28336.943644\n28344.271631\n25751.547897\n25762.524815\n29025.319924\n29036.296843\n\n\n\n\n\n\n\nThe predict method returns a DataFrame witht the predictions for each model (lasso and lgbm) along side the prediction tresholds. The high-threshold is indicated by the keyword hi, the low-threshold by the keyword lo, and the level by the number in the column names.\n\ntest = df_last_24_hours.merge(forecasts, how='left', on=['unique_id', 'ds'])\ntest.head()\n\n\n\n\n\n\n\n\nunique_id\nds\ny\nlgbm\nlasso\nlgbm-lo-95\nlgbm-lo-90\nlgbm-hi-90\nlgbm-hi-95\nlasso-lo-95\nlasso-lo-90\nlasso-hi-90\nlasso-hi-95\n\n\n\n\n0\nPJM_Load_hourly\n2001-12-31 01:00:00\n29001.0\n28847.573176\n29124.085976\n28544.593464\n28567.603130\n29127.543222\n29150.552888\n28762.752269\n28772.604275\n29475.567677\n29485.419682\n\n\n1\nPJM_Load_hourly\n2001-12-31 02:00:00\n28138.0\n27862.589195\n28365.330749\n27042.311414\n27128.839888\n28596.338503\n28682.866977\n27528.548959\n27619.065224\n29111.596275\n29202.112539\n\n\n2\nPJM_Load_hourly\n2001-12-31 03:00:00\n27830.0\n27044.418960\n27712.161676\n25596.659896\n25688.230426\n28400.607493\n28492.178023\n26236.955369\n26338.087102\n29086.236251\n29187.367984\n\n\n3\nPJM_Load_hourly\n2001-12-31 04:00:00\n27874.0\n26976.104125\n27661.572733\n25249.961527\n25286.024722\n28666.183529\n28702.246724\n25911.133521\n25959.815715\n29363.329750\n29412.011944\n\n\n4\nPJM_Load_hourly\n2001-12-31 05:00:00\n28427.0\n26694.246238\n27393.922370\n25044.220845\n25051.548832\n28336.943644\n28344.271631\n25751.547897\n25762.524815\n29025.319924\n29036.296843\n\n\n\n\n\n\n\nNow we can evaluate the metrics and performance in the test set.\n\nevaluate(\n df = test,\n metrics=metrics,\n models=list(models_evaluation.keys())\n )\n\n\n\n\n\n\n\n\nunique_id\nmetric\nlgbm\nlasso\n\n\n\n\n0\nPJM_Load_hourly\nmae\n1092.050817\n899.979743\n\n\n1\nPJM_Load_hourly\nrmse\n1340.422762\n1163.695525\n\n\n2\nPJM_Load_hourly\nmape\n0.033600\n0.027688\n\n\n3\nPJM_Load_hourly\nsmape\n0.017137\n0.013812\n\n\n\n\n\n\n\nWe can see that the lasso regression performed slighty better than the LightGBM for the test set. Additonally, we can also plot the forecasts alongside their prediction intervals. For that we can use the plot_series method available in utilsforecast.plotting.\nWe can plot one or many models at once alongside their coinfidence intervals.\n\nfig = plot_series(\n df_train, \n test, \n models=['lasso', 'lgbm'],\n plot_random=False, \n level=levels, \n max_insample_length=24\n)\n\n\n\n\nComparison with Prophet\nOne of the most widely used models for time series forecasting is Prophet. This model is known for its ability to model different seasonalities (weekly, daily yearly). We will use this model as a benchmark to see if the lgbm alongside MLForecast adds value for this time series.\n\nfrom prophet import Prophet\nfrom time import time\n\n\n# create prophet model\nprophet = Prophet(interval_width=0.9)\ninit = time()\nprophet.fit(df_train)\n# produce forecasts\nfuture = prophet.make_future_dataframe(periods=len(df_last_24_hours), freq='H', include_history=False)\nforecast_prophet = prophet.predict(future)\nend = time()\n# data wrangling\nforecast_prophet = forecast_prophet[['ds', 'yhat', 'yhat_lower', 'yhat_upper']]\nforecast_prophet.columns = ['ds', 'Prophet', 'Prophet-lo-90', 'Prophet-hi-90']\nforecast_prophet.insert(0, 'unique_id', 'PJM_Load_hourly')\nforecast_prophet.head()\n\n\n\n\n\n\n\n\nunique_id\nds\nProphet\nProphet-lo-90\nProphet-hi-90\n\n\n\n\n0\nPJM_Load_hourly\n2001-12-31 01:00:00\n25294.246960\n20452.615295\n30151.111598\n\n\n1\nPJM_Load_hourly\n2001-12-31 02:00:00\n24000.725423\n18954.861631\n28946.645807\n\n\n2\nPJM_Load_hourly\n2001-12-31 03:00:00\n23324.771966\n18562.568378\n28009.837383\n\n\n3\nPJM_Load_hourly\n2001-12-31 04:00:00\n23332.519871\n18706.835864\n28253.861051\n\n\n4\nPJM_Load_hourly\n2001-12-31 05:00:00\n24107.126827\n18966.217684\n28907.516733\n\n\n\n\n\n\n\n\ntime_prophet = (end - init) \nprint(f'Prophet Time: {time_prophet:.2f} seconds')\n\nProphet Time: 36.90 seconds\n\n\n\nmodels_comparison ={\n 'lgbm': lgb.LGBMRegressor(verbosity=-1)\n }\n\nmlf_comparison = MLForecast(\n models = models_comparison, \n freq='H', # our series have hourly frequency \n target_transforms=[Differences([24, 24*7])],\n lags=[1,12,24],\n lag_transforms={ \n 1: [expanding_mean],\n 24: [(rolling_mean, 48)],\n },\n date_features=['month', 'hour', 'dayofweek']\n)\n\ninit = time()\nmlf_comparison.fit(\n df = df_train,\n prediction_intervals=PredictionIntervals(n_windows=4, h=24)\n)\n\nlevels = [90]\nforecasts_comparison = mlf_comparison.predict(24, level=levels)\nend = time()\nforecasts_comparison.head()\n\n\n\n\n\n\n\n\nunique_id\nds\nlgbm\nlgbm-lo-90\nlgbm-hi-90\n\n\n\n\n0\nPJM_Load_hourly\n2001-12-31 01:00:00\n28847.573176\n28567.603130\n29127.543222\n\n\n1\nPJM_Load_hourly\n2001-12-31 02:00:00\n27862.589195\n27128.839888\n28596.338503\n\n\n2\nPJM_Load_hourly\n2001-12-31 03:00:00\n27044.418960\n25688.230426\n28400.607493\n\n\n3\nPJM_Load_hourly\n2001-12-31 04:00:00\n26976.104125\n25286.024722\n28666.183529\n\n\n4\nPJM_Load_hourly\n2001-12-31 05:00:00\n26694.246238\n25051.548832\n28336.943644\n\n\n\n\n\n\n\n\ntime_lgbm = (end - init)\nprint(f'LGBM Time: {time_lgbm:.2f} seconds')\n\nLGBM Time: 1.34 seconds\n\n\n\nmetrics_comparison = df_last_24_hours.merge(forecasts_comparison, how='left', on=['unique_id', 'ds']).merge(\n forecast_prophet, how='left', on=['unique_id', 'ds'])\nmetrics_comparison = evaluate(\n df = metrics_comparison,\n metrics=metrics,\n models=['Prophet', 'lgbm']\n )\nmetrics_comparison.reset_index(drop=True).style.background_gradient(cmap='RdYlGn_r', axis=1)\n\n\n\n\n\n\n \nunique_id\nmetric\nProphet\nlgbm\n\n\n\n\n0\nPJM_Load_hourly\nmae\n2282.966977\n1092.050817\n\n\n1\nPJM_Load_hourly\nrmse\n2721.817203\n1340.422762\n\n\n2\nPJM_Load_hourly\nmape\n0.073750\n0.033600\n\n\n3\nPJM_Load_hourly\nsmape\n0.038633\n0.017137\n\n\n\n\n\nAs we can see lgbm had consistently better metrics than prophet.\n\nmetrics_comparison['improvement'] = metrics_comparison['Prophet'] / metrics_comparison['lgbm']\nmetrics_comparison['improvement'] = metrics_comparison['improvement'].apply(lambda x: f'{x:.2f}')\nmetrics_comparison.set_index('metric')[['improvement']]\n\n\n\n\n\n\n\n\nimprovement\n\n\nmetric\n\n\n\n\n\nmae\n2.09\n\n\nrmse\n2.03\n\n\nmape\n2.19\n\n\nsmape\n2.25\n\n\n\n\n\n\n\n\nprint(f'lgbm with MLForecast has a speedup of {time_prophet/time_lgbm:.2f} compared with prophet')\n\nlgbm with MLForecast has a speedup of 27.62 compared with prophet\n\n\nWe can see that lgbm with MLForecast was able to provide metrics at least twice as good as Prophet as seen in the column improvement above, and way faster."
+ },
+ {
+ "objectID": "docs/getting-started/install.html",
+ "href": "docs/getting-started/install.html",
+ "title": "Install",
+ "section": "",
+ "text": "To install the latest release of mlforecast from PyPI you just have to run the following in a terminal:\npip install mlforecast\n\n\n\nIf you want a specific version you can include a filter, for example:\n\npip install \"mlforecast==0.3.0\" to install the 0.3.0 version\npip install \"mlforecast<0.4.0\" to install any version prior to 0.4.0\n\n\n\n\n\n\n\nThe mlforecast package is also published to conda-forge, which you can install by running the following in a terminal:\nconda install -c conda-forge mlforecast\nNote that this happens about a day later after it is published to PyPI, so you may have to wait to get the latest release.\n\n\n\nIf you want a specific version you can include a filter, for example:\n\nconda install -c conda-forge \"mlforecast==0.3.0\" to install the 0.3.0 version\nconda install -c conda-forge \"mlforecast<0.4.0\" to install any version prior to 0.4.0\n\n\n\n\n\nIf you want to perform distributed training you can use either dask, ray or spark. Once you know which framework you want to use you can include its extra:\n\ndask: pip install \"mlforecast[dask]\"\nray: pip install \"mlforecast[ray]\"\nspark: pip install \"mlforecast[spark]\"\nGive us a ⭐ on Github"
+ },
+ {
+ "objectID": "docs/getting-started/install.html#released-versions",
+ "href": "docs/getting-started/install.html#released-versions",
+ "title": "Install",
+ "section": "",
+ "text": "To install the latest release of mlforecast from PyPI you just have to run the following in a terminal:\npip install mlforecast\n\n\n\nIf you want a specific version you can include a filter, for example:\n\npip install \"mlforecast==0.3.0\" to install the 0.3.0 version\npip install \"mlforecast<0.4.0\" to install any version prior to 0.4.0\n\n\n\n\n\n\n\nThe mlforecast package is also published to conda-forge, which you can install by running the following in a terminal:\nconda install -c conda-forge mlforecast\nNote that this happens about a day later after it is published to PyPI, so you may have to wait to get the latest release.\n\n\n\nIf you want a specific version you can include a filter, for example:\n\nconda install -c conda-forge \"mlforecast==0.3.0\" to install the 0.3.0 version\nconda install -c conda-forge \"mlforecast<0.4.0\" to install any version prior to 0.4.0\n\n\n\n\n\nIf you want to perform distributed training you can use either dask, ray or spark. Once you know which framework you want to use you can include its extra:\n\ndask: pip install \"mlforecast[dask]\"\nray: pip install \"mlforecast[ray]\"\nspark: pip install \"mlforecast[spark]\""
+ },
+ {
+ "objectID": "docs/getting-started/install.html#development-version",
+ "href": "docs/getting-started/install.html#development-version",
+ "title": "Install",
+ "section": "Development version",
+ "text": "Development version\nIf you want to try out a new feature that hasn’t made it into a release yet you have the following options:\n\nInstall from github: pip install git+https://github.com/Nixtla/mlforecast\nClone and install:\n\ngit clone https://github.com/Nixtla/mlforecast\npip install mlforecast\n\n\nwhich will install the version from the current main branch."
+ },
+ {
+ "objectID": "docs/getting-started/quick_start_distributed.html",
+ "href": "docs/getting-started/quick_start_distributed.html",
+ "title": "Quick start (distributed)",
+ "section": "",
+ "text": "The DistributedMLForecast class is a high level abstraction that encapsulates all the steps in the pipeline (preprocessing, fitting the model and computing predictions) and applies them in a distributed way.\nThe different things that you need to use DistributedMLForecast (as opposed to MLForecast) are:\nimport matplotlib.pyplot as plt\nimport numpy as np\nimport pandas as pd\nfrom window_ops.expanding import expanding_mean\nfrom window_ops.rolling import rolling_mean\n\nfrom mlforecast.distributed import DistributedMLForecast\nfrom mlforecast.target_transforms import Differences\nfrom mlforecast.utils import backtest_splits, generate_daily_series, generate_prices_for_series\nGive us a ⭐ on Github"
+ },
+ {
+ "objectID": "docs/getting-started/quick_start_distributed.html#dask",
+ "href": "docs/getting-started/quick_start_distributed.html#dask",
+ "title": "Quick start (distributed)",
+ "section": "Dask",
+ "text": "Dask\n\nimport dask.dataframe as dd\nfrom dask.distributed import Client\n\n\nClient setup\n\nclient = Client(n_workers=2, threads_per_worker=1)\n\nHere we define a client that connects to a dask.distributed.LocalCluster, however it could be any other kind of cluster.\n\n\nData setup\nFor dask, the data must be a dask.dataframe.DataFrame. You need to make sure that each time serie is only in one partition and it is recommended that you have as many partitions as you have workers. If you have more partitions than workers make sure to set num_threads=1 to avoid having nested parallelism.\nThe required input format is the same as for MLForecast, except that it’s a dask.dataframe.DataFrame instead of a pandas.Dataframe.\n\nseries = generate_daily_series(100, n_static_features=2, equal_ends=True, static_as_categorical=False)\nnpartitions = 10\npartitioned_series = dd.from_pandas(series.set_index('unique_id'), npartitions=npartitions) # make sure we split by the id_col\npartitioned_series = partitioned_series.map_partitions(lambda df: df.reset_index())\npartitioned_series['unique_id'] = partitioned_series['unique_id'].astype(str) # can't handle categoricals atm\npartitioned_series\n\nDask DataFrame Structure:\n\n\n\n\n\n\n\nunique_id\nds\ny\nstatic_0\nstatic_1\n\n\nnpartitions=10\n\n\n\n\n\n\n\n\n\nid_00\nobject\ndatetime64[ns]\nfloat64\nint64\nint64\n\n\nid_10\n...\n...\n...\n...\n...\n\n\n...\n...\n...\n...\n...\n...\n\n\nid_89\n...\n...\n...\n...\n...\n\n\nid_99\n...\n...\n...\n...\n...\n\n\n\n\n\nDask Name: assign, 5 graph layers\n\n\n\n\nModels\nIn order to perform distributed forecasting, we need to use a model that is able to train in a distributed way using dask. The current implementations are in DaskLGBMForecast and DaskXGBForecast which are just wrappers around the native implementations.\n\nfrom mlforecast.distributed.models.dask.lgb import DaskLGBMForecast\nfrom mlforecast.distributed.models.dask.xgb import DaskXGBForecast\n\n\nmodels = [DaskXGBForecast(random_state=0), DaskLGBMForecast(random_state=0)]\n\n\n\nTraining\nOnce we have our models we instantiate a DistributedMLForecast object defining our features. We can then call fit on this object passing our dask dataframe.\n\nfcst = DistributedMLForecast(\n models=models,\n freq='D',\n lags=[7],\n lag_transforms={\n 1: [expanding_mean],\n 7: [(rolling_mean, 14)]\n },\n date_features=['dayofweek', 'month'],\n num_threads=1,\n engine=client,\n)\nfcst.fit(partitioned_series)\n\nOnce we have our fitted models we can compute the predictions for the next 7 timesteps.\n\n\nForecasting\n\npreds = fcst.predict(7)\npreds.compute().head()\n\n\n\n\n\n\n\n\nunique_id\nds\nDaskXGBForecast\nDaskLGBMForecast\n\n\n\n\n0\nid_00\n2001-05-15\n421.880432\n421.607612\n\n\n1\nid_00\n2001-05-16\n503.725891\n501.287188\n\n\n2\nid_00\n2001-05-17\n21.745794\n20.218114\n\n\n3\nid_00\n2001-05-18\n103.609970\n103.614699\n\n\n4\nid_00\n2001-05-19\n184.415192\n184.327408\n\n\n\n\n\n\n\n\n\nCross validation\n\ncv_res = fcst.cross_validation(\n partitioned_series,\n n_windows=2,\n h=14,\n)\ncv_res\n\n\ncv_res.compute().head()\n\n\n\n\n\n\n\n\nunique_id\nds\nDaskXGBForecast\nDaskLGBMForecast\ncutoff\ny\n\n\n\n\n0\nid_00\n2001-04-17\n416.596832\n414.746359\n2001-04-16\n404.993322\n\n\n1\nid_00\n2001-04-18\n501.541565\n495.292467\n2001-04-16\n508.883226\n\n\n2\nid_00\n2001-04-19\n22.663921\n20.404674\n2001-04-16\n1.218119\n\n\n3\nid_00\n2001-04-20\n100.937668\n100.720564\n2001-04-16\n109.879770\n\n\n4\nid_00\n2001-04-21\n184.458405\n188.645839\n2001-04-16\n163.703847\n\n\n\n\n\n\n\n\nclient.close()"
+ },
+ {
+ "objectID": "docs/getting-started/quick_start_distributed.html#spark",
+ "href": "docs/getting-started/quick_start_distributed.html#spark",
+ "title": "Quick start (distributed)",
+ "section": "Spark",
+ "text": "Spark\n\nSession setup\n\nfrom pyspark.sql import SparkSession\n\n\nspark = (\n SparkSession\n .builder\n .config(\"spark.jars.packages\", \"com.microsoft.azure:synapseml_2.12:0.10.2\")\n .config(\"spark.jars.repositories\", \"https://mmlspark.azureedge.net/maven\")\n .getOrCreate()\n)\n\n\n\nData setup\nFor spark, the data must be a pyspark DataFrame. You need to make sure that each time serie is only in one partition (which you can do using repartitionByRange, for example) and it is recommended that you have as many partitions as you have workers. If you have more partitions than workers make sure to set num_threads=1 to avoid having nested parallelism.\nThe required input format is the same as for MLForecast, i.e. it should have at least an id column, a time column and a target column.\n\nnumPartitions = 4\nseries = generate_daily_series(100, n_static_features=2, equal_ends=True, static_as_categorical=False)\nspark_series = spark.createDataFrame(series).repartitionByRange(numPartitions, 'unique_id')\n\n\n\nModels\nIn order to perform distributed forecasting, we need to use a model that is able to train in a distributed way using spark. The current implementations are in SparkLGBMForecast and SparkXGBForecast which are just wrappers around the native implementations.\n\nfrom mlforecast.distributed.models.spark.lgb import SparkLGBMForecast\n\nmodels = [SparkLGBMForecast()]\ntry:\n from xgboost.spark import SparkXGBRegressor\n from mlforecast.distributed.models.spark.xgb import SparkXGBForecast\n models.append(SparkXGBForecast())\nexcept ModuleNotFoundError: # py < 38\n pass\n\n\n\nTraining\n\nfcst = DistributedMLForecast(\n models,\n freq='D',\n lags=[1],\n lag_transforms={\n 1: [expanding_mean]\n },\n date_features=['dayofweek'],\n)\nfcst.fit(\n spark_series,\n static_features=['static_0', 'static_1'],\n)\n\n\n\nForecasting\n\npreds = fcst.predict(14)\n\n\npreds.toPandas().head()\n\n/hdd/miniforge3/envs/mlforecast/lib/python3.10/site-packages/pyspark/sql/pandas/conversion.py:251: FutureWarning: Passing unit-less datetime64 dtype to .astype is deprecated and will raise in a future version. Pass 'datetime64[ns]' instead\n series = series.astype(t, copy=False)\n\n\n\n\n\n\n\n\n\nunique_id\nds\nSparkLGBMForecast\nSparkXGBForecast\n\n\n\n\n0\nid_00\n2001-05-15\n422.139843\n421.606537\n\n\n1\nid_00\n2001-05-16\n497.180212\n505.575836\n\n\n2\nid_00\n2001-05-17\n13.062478\n15.462178\n\n\n3\nid_00\n2001-05-18\n100.601041\n102.123245\n\n\n4\nid_00\n2001-05-19\n180.707848\n182.308197\n\n\n\n\n\n\n\n\n\nCross validation\n\ncv_res = fcst.cross_validation(\n spark_series,\n n_windows=2,\n h=14,\n).toPandas()\n\n\ncv_res.head()\n\n\n\n\n\n\n\n\nunique_id\nds\nSparkLGBMForecast\nSparkXGBForecast\ncutoff\ny\n\n\n\n\n0\nid_21\n2001-04-28\n92.870945\n58.219631\n2001-04-16\n58.044008\n\n\n1\nid_04\n2001-04-23\n114.502043\n117.134018\n2001-04-16\n118.452839\n\n\n2\nid_18\n2001-04-17\n82.597659\n86.992348\n2001-04-16\n80.416969\n\n\n3\nid_20\n2001-04-21\n230.254435\n237.235214\n2001-04-16\n230.352460\n\n\n4\nid_23\n2001-04-17\n426.386288\n419.380524\n2001-04-16\n432.918548\n\n\n\n\n\n\n\n\nspark.stop()"
+ },
+ {
+ "objectID": "docs/getting-started/quick_start_distributed.html#ray",
+ "href": "docs/getting-started/quick_start_distributed.html#ray",
+ "title": "Quick start (distributed)",
+ "section": "Ray",
+ "text": "Ray\n\nSession setup\n\nimport ray\nfrom ray.cluster_utils import Cluster\n\n\nray_cluster = Cluster(\n initialize_head=True,\n head_node_args={\"num_cpus\": 2}\n)\nray.init(address=ray_cluster.address, ignore_reinit_error=True)\n# add mock node to simulate a cluster\nmock_node = ray_cluster.add_node(num_cpus=2)\n\n\n\nData setup\nFor ray, the data must be a ray DataFrame. It is recommended that you have as many partitions as you have workers. If you have more partitions than workers make sure to set num_threads=1 to avoid having nested parallelism.\nThe required input format is the same as for MLForecast, i.e. it should have at least an id column, a time column and a target column.\n\nseries = generate_daily_series(100, n_static_features=2, equal_ends=True, static_as_categorical=False)\n# we need noncategory unique_id\nseries['unique_id'] = series['unique_id'].astype(str)\nray_series = ray.data.from_pandas(series)\n\n\n\nModels\nThe ray integration allows to include lightgbm (RayLGBMRegressor), and xgboost (RayXGBRegressor).\n\nfrom mlforecast.distributed.models.ray.lgb import RayLGBMForecast\nfrom mlforecast.distributed.models.ray.xgb import RayXGBForecast\n\n\nmodels = [\n RayLGBMForecast(),\n RayXGBForecast(),\n]\n\n\n\nTraining\nTo control the number of partitions to use using Ray, we have to include num_partitions to DistributedMLForecast.\n\nnum_partitions = 4\n\n\nfcst = DistributedMLForecast(\n models,\n freq='D',\n lags=[1],\n lag_transforms={\n 1: [expanding_mean]\n },\n date_features=['dayofweek'],\n num_partitions=num_partitions, # Use num_partitions to reduce overhead\n)\nfcst.fit(\n ray_series,\n static_features=['static_0', 'static_1'],\n)\n\n\n\nForecasting\n\npreds = fcst.predict(14).to_pandas()\n\n\npreds.head()\n\n\n\n\n\n\n\n\nunique_id\nds\nRayLGBMForecast\nRayXGBForecast\n\n\n\n\n0\nid_00\n2001-05-15\n422.139843\n419.180908\n\n\n1\nid_00\n2001-05-16\n497.180212\n502.074249\n\n\n2\nid_00\n2001-05-17\n13.062478\n16.981802\n\n\n3\nid_00\n2001-05-18\n100.601041\n102.311279\n\n\n4\nid_00\n2001-05-19\n180.707848\n181.406143\n\n\n\n\n\n\n\n\n\nCross validation\n\ncv_res = fcst.cross_validation(\n ray_series,\n n_windows=2,\n h=14,\n).to_pandas()\n\n\ncv_res.head()\n\n\n\n\n\n\n\n\nunique_id\nds\nRayLGBMForecast\nRayXGBForecast\ncutoff\ny\n\n\n\n\n0\nid_10\n2001-04-17\n29.912521\n27.272474\n2001-04-16\n34.156241\n\n\n1\nid_10\n2001-04-18\n57.589800\n50.432514\n2001-04-16\n53.208507\n\n\n2\nid_10\n2001-04-19\n76.750741\n75.065681\n2001-04-16\n71.920677\n\n\n3\nid_10\n2001-04-20\n101.297954\n98.846138\n2001-04-16\n102.347087\n\n\n4\nid_10\n2001-04-21\n117.002167\n120.924911\n2001-04-16\n119.019914\n\n\n\n\n\n\n\n\nray.shutdown()"
+ }
+]
\ No newline at end of file
diff --git a/site_libs/bootstrap/bootstrap-icons.css b/site_libs/bootstrap/bootstrap-icons.css
new file mode 100644
index 00000000..94f19404
--- /dev/null
+++ b/site_libs/bootstrap/bootstrap-icons.css
@@ -0,0 +1,2018 @@
+@font-face {
+ font-display: block;
+ font-family: "bootstrap-icons";
+ src:
+url("./bootstrap-icons.woff?2ab2cbbe07fcebb53bdaa7313bb290f2") format("woff");
+}
+
+.bi::before,
+[class^="bi-"]::before,
+[class*=" bi-"]::before {
+ display: inline-block;
+ font-family: bootstrap-icons !important;
+ font-style: normal;
+ font-weight: normal !important;
+ font-variant: normal;
+ text-transform: none;
+ line-height: 1;
+ vertical-align: -.125em;
+ -webkit-font-smoothing: antialiased;
+ -moz-osx-font-smoothing: grayscale;
+}
+
+.bi-123::before { content: "\f67f"; }
+.bi-alarm-fill::before { content: "\f101"; }
+.bi-alarm::before { content: "\f102"; }
+.bi-align-bottom::before { content: "\f103"; }
+.bi-align-center::before { content: "\f104"; }
+.bi-align-end::before { content: "\f105"; }
+.bi-align-middle::before { content: "\f106"; }
+.bi-align-start::before { content: "\f107"; }
+.bi-align-top::before { content: "\f108"; }
+.bi-alt::before { content: "\f109"; }
+.bi-app-indicator::before { content: "\f10a"; }
+.bi-app::before { content: "\f10b"; }
+.bi-archive-fill::before { content: "\f10c"; }
+.bi-archive::before { content: "\f10d"; }
+.bi-arrow-90deg-down::before { content: "\f10e"; }
+.bi-arrow-90deg-left::before { content: "\f10f"; }
+.bi-arrow-90deg-right::before { content: "\f110"; }
+.bi-arrow-90deg-up::before { content: "\f111"; }
+.bi-arrow-bar-down::before { content: "\f112"; }
+.bi-arrow-bar-left::before { content: "\f113"; }
+.bi-arrow-bar-right::before { content: "\f114"; }
+.bi-arrow-bar-up::before { content: "\f115"; }
+.bi-arrow-clockwise::before { content: "\f116"; }
+.bi-arrow-counterclockwise::before { content: "\f117"; }
+.bi-arrow-down-circle-fill::before { content: "\f118"; }
+.bi-arrow-down-circle::before { content: "\f119"; }
+.bi-arrow-down-left-circle-fill::before { content: "\f11a"; }
+.bi-arrow-down-left-circle::before { content: "\f11b"; }
+.bi-arrow-down-left-square-fill::before { content: "\f11c"; }
+.bi-arrow-down-left-square::before { content: "\f11d"; }
+.bi-arrow-down-left::before { content: "\f11e"; }
+.bi-arrow-down-right-circle-fill::before { content: "\f11f"; }
+.bi-arrow-down-right-circle::before { content: "\f120"; }
+.bi-arrow-down-right-square-fill::before { content: "\f121"; }
+.bi-arrow-down-right-square::before { content: "\f122"; }
+.bi-arrow-down-right::before { content: "\f123"; }
+.bi-arrow-down-short::before { content: "\f124"; }
+.bi-arrow-down-square-fill::before { content: "\f125"; }
+.bi-arrow-down-square::before { content: "\f126"; }
+.bi-arrow-down-up::before { content: "\f127"; }
+.bi-arrow-down::before { content: "\f128"; }
+.bi-arrow-left-circle-fill::before { content: "\f129"; }
+.bi-arrow-left-circle::before { content: "\f12a"; }
+.bi-arrow-left-right::before { content: "\f12b"; }
+.bi-arrow-left-short::before { content: "\f12c"; }
+.bi-arrow-left-square-fill::before { content: "\f12d"; }
+.bi-arrow-left-square::before { content: "\f12e"; }
+.bi-arrow-left::before { content: "\f12f"; }
+.bi-arrow-repeat::before { content: "\f130"; }
+.bi-arrow-return-left::before { content: "\f131"; }
+.bi-arrow-return-right::before { content: "\f132"; }
+.bi-arrow-right-circle-fill::before { content: "\f133"; }
+.bi-arrow-right-circle::before { content: "\f134"; }
+.bi-arrow-right-short::before { content: "\f135"; }
+.bi-arrow-right-square-fill::before { content: "\f136"; }
+.bi-arrow-right-square::before { content: "\f137"; }
+.bi-arrow-right::before { content: "\f138"; }
+.bi-arrow-up-circle-fill::before { content: "\f139"; }
+.bi-arrow-up-circle::before { content: "\f13a"; }
+.bi-arrow-up-left-circle-fill::before { content: "\f13b"; }
+.bi-arrow-up-left-circle::before { content: "\f13c"; }
+.bi-arrow-up-left-square-fill::before { content: "\f13d"; }
+.bi-arrow-up-left-square::before { content: "\f13e"; }
+.bi-arrow-up-left::before { content: "\f13f"; }
+.bi-arrow-up-right-circle-fill::before { content: "\f140"; }
+.bi-arrow-up-right-circle::before { content: "\f141"; }
+.bi-arrow-up-right-square-fill::before { content: "\f142"; }
+.bi-arrow-up-right-square::before { content: "\f143"; }
+.bi-arrow-up-right::before { content: "\f144"; }
+.bi-arrow-up-short::before { content: "\f145"; }
+.bi-arrow-up-square-fill::before { content: "\f146"; }
+.bi-arrow-up-square::before { content: "\f147"; }
+.bi-arrow-up::before { content: "\f148"; }
+.bi-arrows-angle-contract::before { content: "\f149"; }
+.bi-arrows-angle-expand::before { content: "\f14a"; }
+.bi-arrows-collapse::before { content: "\f14b"; }
+.bi-arrows-expand::before { content: "\f14c"; }
+.bi-arrows-fullscreen::before { content: "\f14d"; }
+.bi-arrows-move::before { content: "\f14e"; }
+.bi-aspect-ratio-fill::before { content: "\f14f"; }
+.bi-aspect-ratio::before { content: "\f150"; }
+.bi-asterisk::before { content: "\f151"; }
+.bi-at::before { content: "\f152"; }
+.bi-award-fill::before { content: "\f153"; }
+.bi-award::before { content: "\f154"; }
+.bi-back::before { content: "\f155"; }
+.bi-backspace-fill::before { content: "\f156"; }
+.bi-backspace-reverse-fill::before { content: "\f157"; }
+.bi-backspace-reverse::before { content: "\f158"; }
+.bi-backspace::before { content: "\f159"; }
+.bi-badge-3d-fill::before { content: "\f15a"; }
+.bi-badge-3d::before { content: "\f15b"; }
+.bi-badge-4k-fill::before { content: "\f15c"; }
+.bi-badge-4k::before { content: "\f15d"; }
+.bi-badge-8k-fill::before { content: "\f15e"; }
+.bi-badge-8k::before { content: "\f15f"; }
+.bi-badge-ad-fill::before { content: "\f160"; }
+.bi-badge-ad::before { content: "\f161"; }
+.bi-badge-ar-fill::before { content: "\f162"; }
+.bi-badge-ar::before { content: "\f163"; }
+.bi-badge-cc-fill::before { content: "\f164"; }
+.bi-badge-cc::before { content: "\f165"; }
+.bi-badge-hd-fill::before { content: "\f166"; }
+.bi-badge-hd::before { content: "\f167"; }
+.bi-badge-tm-fill::before { content: "\f168"; }
+.bi-badge-tm::before { content: "\f169"; }
+.bi-badge-vo-fill::before { content: "\f16a"; }
+.bi-badge-vo::before { content: "\f16b"; }
+.bi-badge-vr-fill::before { content: "\f16c"; }
+.bi-badge-vr::before { content: "\f16d"; }
+.bi-badge-wc-fill::before { content: "\f16e"; }
+.bi-badge-wc::before { content: "\f16f"; }
+.bi-bag-check-fill::before { content: "\f170"; }
+.bi-bag-check::before { content: "\f171"; }
+.bi-bag-dash-fill::before { content: "\f172"; }
+.bi-bag-dash::before { content: "\f173"; }
+.bi-bag-fill::before { content: "\f174"; }
+.bi-bag-plus-fill::before { content: "\f175"; }
+.bi-bag-plus::before { content: "\f176"; }
+.bi-bag-x-fill::before { content: "\f177"; }
+.bi-bag-x::before { content: "\f178"; }
+.bi-bag::before { content: "\f179"; }
+.bi-bar-chart-fill::before { content: "\f17a"; }
+.bi-bar-chart-line-fill::before { content: "\f17b"; }
+.bi-bar-chart-line::before { content: "\f17c"; }
+.bi-bar-chart-steps::before { content: "\f17d"; }
+.bi-bar-chart::before { content: "\f17e"; }
+.bi-basket-fill::before { content: "\f17f"; }
+.bi-basket::before { content: "\f180"; }
+.bi-basket2-fill::before { content: "\f181"; }
+.bi-basket2::before { content: "\f182"; }
+.bi-basket3-fill::before { content: "\f183"; }
+.bi-basket3::before { content: "\f184"; }
+.bi-battery-charging::before { content: "\f185"; }
+.bi-battery-full::before { content: "\f186"; }
+.bi-battery-half::before { content: "\f187"; }
+.bi-battery::before { content: "\f188"; }
+.bi-bell-fill::before { content: "\f189"; }
+.bi-bell::before { content: "\f18a"; }
+.bi-bezier::before { content: "\f18b"; }
+.bi-bezier2::before { content: "\f18c"; }
+.bi-bicycle::before { content: "\f18d"; }
+.bi-binoculars-fill::before { content: "\f18e"; }
+.bi-binoculars::before { content: "\f18f"; }
+.bi-blockquote-left::before { content: "\f190"; }
+.bi-blockquote-right::before { content: "\f191"; }
+.bi-book-fill::before { content: "\f192"; }
+.bi-book-half::before { content: "\f193"; }
+.bi-book::before { content: "\f194"; }
+.bi-bookmark-check-fill::before { content: "\f195"; }
+.bi-bookmark-check::before { content: "\f196"; }
+.bi-bookmark-dash-fill::before { content: "\f197"; }
+.bi-bookmark-dash::before { content: "\f198"; }
+.bi-bookmark-fill::before { content: "\f199"; }
+.bi-bookmark-heart-fill::before { content: "\f19a"; }
+.bi-bookmark-heart::before { content: "\f19b"; }
+.bi-bookmark-plus-fill::before { content: "\f19c"; }
+.bi-bookmark-plus::before { content: "\f19d"; }
+.bi-bookmark-star-fill::before { content: "\f19e"; }
+.bi-bookmark-star::before { content: "\f19f"; }
+.bi-bookmark-x-fill::before { content: "\f1a0"; }
+.bi-bookmark-x::before { content: "\f1a1"; }
+.bi-bookmark::before { content: "\f1a2"; }
+.bi-bookmarks-fill::before { content: "\f1a3"; }
+.bi-bookmarks::before { content: "\f1a4"; }
+.bi-bookshelf::before { content: "\f1a5"; }
+.bi-bootstrap-fill::before { content: "\f1a6"; }
+.bi-bootstrap-reboot::before { content: "\f1a7"; }
+.bi-bootstrap::before { content: "\f1a8"; }
+.bi-border-all::before { content: "\f1a9"; }
+.bi-border-bottom::before { content: "\f1aa"; }
+.bi-border-center::before { content: "\f1ab"; }
+.bi-border-inner::before { content: "\f1ac"; }
+.bi-border-left::before { content: "\f1ad"; }
+.bi-border-middle::before { content: "\f1ae"; }
+.bi-border-outer::before { content: "\f1af"; }
+.bi-border-right::before { content: "\f1b0"; }
+.bi-border-style::before { content: "\f1b1"; }
+.bi-border-top::before { content: "\f1b2"; }
+.bi-border-width::before { content: "\f1b3"; }
+.bi-border::before { content: "\f1b4"; }
+.bi-bounding-box-circles::before { content: "\f1b5"; }
+.bi-bounding-box::before { content: "\f1b6"; }
+.bi-box-arrow-down-left::before { content: "\f1b7"; }
+.bi-box-arrow-down-right::before { content: "\f1b8"; }
+.bi-box-arrow-down::before { content: "\f1b9"; }
+.bi-box-arrow-in-down-left::before { content: "\f1ba"; }
+.bi-box-arrow-in-down-right::before { content: "\f1bb"; }
+.bi-box-arrow-in-down::before { content: "\f1bc"; }
+.bi-box-arrow-in-left::before { content: "\f1bd"; }
+.bi-box-arrow-in-right::before { content: "\f1be"; }
+.bi-box-arrow-in-up-left::before { content: "\f1bf"; }
+.bi-box-arrow-in-up-right::before { content: "\f1c0"; }
+.bi-box-arrow-in-up::before { content: "\f1c1"; }
+.bi-box-arrow-left::before { content: "\f1c2"; }
+.bi-box-arrow-right::before { content: "\f1c3"; }
+.bi-box-arrow-up-left::before { content: "\f1c4"; }
+.bi-box-arrow-up-right::before { content: "\f1c5"; }
+.bi-box-arrow-up::before { content: "\f1c6"; }
+.bi-box-seam::before { content: "\f1c7"; }
+.bi-box::before { content: "\f1c8"; }
+.bi-braces::before { content: "\f1c9"; }
+.bi-bricks::before { content: "\f1ca"; }
+.bi-briefcase-fill::before { content: "\f1cb"; }
+.bi-briefcase::before { content: "\f1cc"; }
+.bi-brightness-alt-high-fill::before { content: "\f1cd"; }
+.bi-brightness-alt-high::before { content: "\f1ce"; }
+.bi-brightness-alt-low-fill::before { content: "\f1cf"; }
+.bi-brightness-alt-low::before { content: "\f1d0"; }
+.bi-brightness-high-fill::before { content: "\f1d1"; }
+.bi-brightness-high::before { content: "\f1d2"; }
+.bi-brightness-low-fill::before { content: "\f1d3"; }
+.bi-brightness-low::before { content: "\f1d4"; }
+.bi-broadcast-pin::before { content: "\f1d5"; }
+.bi-broadcast::before { content: "\f1d6"; }
+.bi-brush-fill::before { content: "\f1d7"; }
+.bi-brush::before { content: "\f1d8"; }
+.bi-bucket-fill::before { content: "\f1d9"; }
+.bi-bucket::before { content: "\f1da"; }
+.bi-bug-fill::before { content: "\f1db"; }
+.bi-bug::before { content: "\f1dc"; }
+.bi-building::before { content: "\f1dd"; }
+.bi-bullseye::before { content: "\f1de"; }
+.bi-calculator-fill::before { content: "\f1df"; }
+.bi-calculator::before { content: "\f1e0"; }
+.bi-calendar-check-fill::before { content: "\f1e1"; }
+.bi-calendar-check::before { content: "\f1e2"; }
+.bi-calendar-date-fill::before { content: "\f1e3"; }
+.bi-calendar-date::before { content: "\f1e4"; }
+.bi-calendar-day-fill::before { content: "\f1e5"; }
+.bi-calendar-day::before { content: "\f1e6"; }
+.bi-calendar-event-fill::before { content: "\f1e7"; }
+.bi-calendar-event::before { content: "\f1e8"; }
+.bi-calendar-fill::before { content: "\f1e9"; }
+.bi-calendar-minus-fill::before { content: "\f1ea"; }
+.bi-calendar-minus::before { content: "\f1eb"; }
+.bi-calendar-month-fill::before { content: "\f1ec"; }
+.bi-calendar-month::before { content: "\f1ed"; }
+.bi-calendar-plus-fill::before { content: "\f1ee"; }
+.bi-calendar-plus::before { content: "\f1ef"; }
+.bi-calendar-range-fill::before { content: "\f1f0"; }
+.bi-calendar-range::before { content: "\f1f1"; }
+.bi-calendar-week-fill::before { content: "\f1f2"; }
+.bi-calendar-week::before { content: "\f1f3"; }
+.bi-calendar-x-fill::before { content: "\f1f4"; }
+.bi-calendar-x::before { content: "\f1f5"; }
+.bi-calendar::before { content: "\f1f6"; }
+.bi-calendar2-check-fill::before { content: "\f1f7"; }
+.bi-calendar2-check::before { content: "\f1f8"; }
+.bi-calendar2-date-fill::before { content: "\f1f9"; }
+.bi-calendar2-date::before { content: "\f1fa"; }
+.bi-calendar2-day-fill::before { content: "\f1fb"; }
+.bi-calendar2-day::before { content: "\f1fc"; }
+.bi-calendar2-event-fill::before { content: "\f1fd"; }
+.bi-calendar2-event::before { content: "\f1fe"; }
+.bi-calendar2-fill::before { content: "\f1ff"; }
+.bi-calendar2-minus-fill::before { content: "\f200"; }
+.bi-calendar2-minus::before { content: "\f201"; }
+.bi-calendar2-month-fill::before { content: "\f202"; }
+.bi-calendar2-month::before { content: "\f203"; }
+.bi-calendar2-plus-fill::before { content: "\f204"; }
+.bi-calendar2-plus::before { content: "\f205"; }
+.bi-calendar2-range-fill::before { content: "\f206"; }
+.bi-calendar2-range::before { content: "\f207"; }
+.bi-calendar2-week-fill::before { content: "\f208"; }
+.bi-calendar2-week::before { content: "\f209"; }
+.bi-calendar2-x-fill::before { content: "\f20a"; }
+.bi-calendar2-x::before { content: "\f20b"; }
+.bi-calendar2::before { content: "\f20c"; }
+.bi-calendar3-event-fill::before { content: "\f20d"; }
+.bi-calendar3-event::before { content: "\f20e"; }
+.bi-calendar3-fill::before { content: "\f20f"; }
+.bi-calendar3-range-fill::before { content: "\f210"; }
+.bi-calendar3-range::before { content: "\f211"; }
+.bi-calendar3-week-fill::before { content: "\f212"; }
+.bi-calendar3-week::before { content: "\f213"; }
+.bi-calendar3::before { content: "\f214"; }
+.bi-calendar4-event::before { content: "\f215"; }
+.bi-calendar4-range::before { content: "\f216"; }
+.bi-calendar4-week::before { content: "\f217"; }
+.bi-calendar4::before { content: "\f218"; }
+.bi-camera-fill::before { content: "\f219"; }
+.bi-camera-reels-fill::before { content: "\f21a"; }
+.bi-camera-reels::before { content: "\f21b"; }
+.bi-camera-video-fill::before { content: "\f21c"; }
+.bi-camera-video-off-fill::before { content: "\f21d"; }
+.bi-camera-video-off::before { content: "\f21e"; }
+.bi-camera-video::before { content: "\f21f"; }
+.bi-camera::before { content: "\f220"; }
+.bi-camera2::before { content: "\f221"; }
+.bi-capslock-fill::before { content: "\f222"; }
+.bi-capslock::before { content: "\f223"; }
+.bi-card-checklist::before { content: "\f224"; }
+.bi-card-heading::before { content: "\f225"; }
+.bi-card-image::before { content: "\f226"; }
+.bi-card-list::before { content: "\f227"; }
+.bi-card-text::before { content: "\f228"; }
+.bi-caret-down-fill::before { content: "\f229"; }
+.bi-caret-down-square-fill::before { content: "\f22a"; }
+.bi-caret-down-square::before { content: "\f22b"; }
+.bi-caret-down::before { content: "\f22c"; }
+.bi-caret-left-fill::before { content: "\f22d"; }
+.bi-caret-left-square-fill::before { content: "\f22e"; }
+.bi-caret-left-square::before { content: "\f22f"; }
+.bi-caret-left::before { content: "\f230"; }
+.bi-caret-right-fill::before { content: "\f231"; }
+.bi-caret-right-square-fill::before { content: "\f232"; }
+.bi-caret-right-square::before { content: "\f233"; }
+.bi-caret-right::before { content: "\f234"; }
+.bi-caret-up-fill::before { content: "\f235"; }
+.bi-caret-up-square-fill::before { content: "\f236"; }
+.bi-caret-up-square::before { content: "\f237"; }
+.bi-caret-up::before { content: "\f238"; }
+.bi-cart-check-fill::before { content: "\f239"; }
+.bi-cart-check::before { content: "\f23a"; }
+.bi-cart-dash-fill::before { content: "\f23b"; }
+.bi-cart-dash::before { content: "\f23c"; }
+.bi-cart-fill::before { content: "\f23d"; }
+.bi-cart-plus-fill::before { content: "\f23e"; }
+.bi-cart-plus::before { content: "\f23f"; }
+.bi-cart-x-fill::before { content: "\f240"; }
+.bi-cart-x::before { content: "\f241"; }
+.bi-cart::before { content: "\f242"; }
+.bi-cart2::before { content: "\f243"; }
+.bi-cart3::before { content: "\f244"; }
+.bi-cart4::before { content: "\f245"; }
+.bi-cash-stack::before { content: "\f246"; }
+.bi-cash::before { content: "\f247"; }
+.bi-cast::before { content: "\f248"; }
+.bi-chat-dots-fill::before { content: "\f249"; }
+.bi-chat-dots::before { content: "\f24a"; }
+.bi-chat-fill::before { content: "\f24b"; }
+.bi-chat-left-dots-fill::before { content: "\f24c"; }
+.bi-chat-left-dots::before { content: "\f24d"; }
+.bi-chat-left-fill::before { content: "\f24e"; }
+.bi-chat-left-quote-fill::before { content: "\f24f"; }
+.bi-chat-left-quote::before { content: "\f250"; }
+.bi-chat-left-text-fill::before { content: "\f251"; }
+.bi-chat-left-text::before { content: "\f252"; }
+.bi-chat-left::before { content: "\f253"; }
+.bi-chat-quote-fill::before { content: "\f254"; }
+.bi-chat-quote::before { content: "\f255"; }
+.bi-chat-right-dots-fill::before { content: "\f256"; }
+.bi-chat-right-dots::before { content: "\f257"; }
+.bi-chat-right-fill::before { content: "\f258"; }
+.bi-chat-right-quote-fill::before { content: "\f259"; }
+.bi-chat-right-quote::before { content: "\f25a"; }
+.bi-chat-right-text-fill::before { content: "\f25b"; }
+.bi-chat-right-text::before { content: "\f25c"; }
+.bi-chat-right::before { content: "\f25d"; }
+.bi-chat-square-dots-fill::before { content: "\f25e"; }
+.bi-chat-square-dots::before { content: "\f25f"; }
+.bi-chat-square-fill::before { content: "\f260"; }
+.bi-chat-square-quote-fill::before { content: "\f261"; }
+.bi-chat-square-quote::before { content: "\f262"; }
+.bi-chat-square-text-fill::before { content: "\f263"; }
+.bi-chat-square-text::before { content: "\f264"; }
+.bi-chat-square::before { content: "\f265"; }
+.bi-chat-text-fill::before { content: "\f266"; }
+.bi-chat-text::before { content: "\f267"; }
+.bi-chat::before { content: "\f268"; }
+.bi-check-all::before { content: "\f269"; }
+.bi-check-circle-fill::before { content: "\f26a"; }
+.bi-check-circle::before { content: "\f26b"; }
+.bi-check-square-fill::before { content: "\f26c"; }
+.bi-check-square::before { content: "\f26d"; }
+.bi-check::before { content: "\f26e"; }
+.bi-check2-all::before { content: "\f26f"; }
+.bi-check2-circle::before { content: "\f270"; }
+.bi-check2-square::before { content: "\f271"; }
+.bi-check2::before { content: "\f272"; }
+.bi-chevron-bar-contract::before { content: "\f273"; }
+.bi-chevron-bar-down::before { content: "\f274"; }
+.bi-chevron-bar-expand::before { content: "\f275"; }
+.bi-chevron-bar-left::before { content: "\f276"; }
+.bi-chevron-bar-right::before { content: "\f277"; }
+.bi-chevron-bar-up::before { content: "\f278"; }
+.bi-chevron-compact-down::before { content: "\f279"; }
+.bi-chevron-compact-left::before { content: "\f27a"; }
+.bi-chevron-compact-right::before { content: "\f27b"; }
+.bi-chevron-compact-up::before { content: "\f27c"; }
+.bi-chevron-contract::before { content: "\f27d"; }
+.bi-chevron-double-down::before { content: "\f27e"; }
+.bi-chevron-double-left::before { content: "\f27f"; }
+.bi-chevron-double-right::before { content: "\f280"; }
+.bi-chevron-double-up::before { content: "\f281"; }
+.bi-chevron-down::before { content: "\f282"; }
+.bi-chevron-expand::before { content: "\f283"; }
+.bi-chevron-left::before { content: "\f284"; }
+.bi-chevron-right::before { content: "\f285"; }
+.bi-chevron-up::before { content: "\f286"; }
+.bi-circle-fill::before { content: "\f287"; }
+.bi-circle-half::before { content: "\f288"; }
+.bi-circle-square::before { content: "\f289"; }
+.bi-circle::before { content: "\f28a"; }
+.bi-clipboard-check::before { content: "\f28b"; }
+.bi-clipboard-data::before { content: "\f28c"; }
+.bi-clipboard-minus::before { content: "\f28d"; }
+.bi-clipboard-plus::before { content: "\f28e"; }
+.bi-clipboard-x::before { content: "\f28f"; }
+.bi-clipboard::before { content: "\f290"; }
+.bi-clock-fill::before { content: "\f291"; }
+.bi-clock-history::before { content: "\f292"; }
+.bi-clock::before { content: "\f293"; }
+.bi-cloud-arrow-down-fill::before { content: "\f294"; }
+.bi-cloud-arrow-down::before { content: "\f295"; }
+.bi-cloud-arrow-up-fill::before { content: "\f296"; }
+.bi-cloud-arrow-up::before { content: "\f297"; }
+.bi-cloud-check-fill::before { content: "\f298"; }
+.bi-cloud-check::before { content: "\f299"; }
+.bi-cloud-download-fill::before { content: "\f29a"; }
+.bi-cloud-download::before { content: "\f29b"; }
+.bi-cloud-drizzle-fill::before { content: "\f29c"; }
+.bi-cloud-drizzle::before { content: "\f29d"; }
+.bi-cloud-fill::before { content: "\f29e"; }
+.bi-cloud-fog-fill::before { content: "\f29f"; }
+.bi-cloud-fog::before { content: "\f2a0"; }
+.bi-cloud-fog2-fill::before { content: "\f2a1"; }
+.bi-cloud-fog2::before { content: "\f2a2"; }
+.bi-cloud-hail-fill::before { content: "\f2a3"; }
+.bi-cloud-hail::before { content: "\f2a4"; }
+.bi-cloud-haze-1::before { content: "\f2a5"; }
+.bi-cloud-haze-fill::before { content: "\f2a6"; }
+.bi-cloud-haze::before { content: "\f2a7"; }
+.bi-cloud-haze2-fill::before { content: "\f2a8"; }
+.bi-cloud-lightning-fill::before { content: "\f2a9"; }
+.bi-cloud-lightning-rain-fill::before { content: "\f2aa"; }
+.bi-cloud-lightning-rain::before { content: "\f2ab"; }
+.bi-cloud-lightning::before { content: "\f2ac"; }
+.bi-cloud-minus-fill::before { content: "\f2ad"; }
+.bi-cloud-minus::before { content: "\f2ae"; }
+.bi-cloud-moon-fill::before { content: "\f2af"; }
+.bi-cloud-moon::before { content: "\f2b0"; }
+.bi-cloud-plus-fill::before { content: "\f2b1"; }
+.bi-cloud-plus::before { content: "\f2b2"; }
+.bi-cloud-rain-fill::before { content: "\f2b3"; }
+.bi-cloud-rain-heavy-fill::before { content: "\f2b4"; }
+.bi-cloud-rain-heavy::before { content: "\f2b5"; }
+.bi-cloud-rain::before { content: "\f2b6"; }
+.bi-cloud-slash-fill::before { content: "\f2b7"; }
+.bi-cloud-slash::before { content: "\f2b8"; }
+.bi-cloud-sleet-fill::before { content: "\f2b9"; }
+.bi-cloud-sleet::before { content: "\f2ba"; }
+.bi-cloud-snow-fill::before { content: "\f2bb"; }
+.bi-cloud-snow::before { content: "\f2bc"; }
+.bi-cloud-sun-fill::before { content: "\f2bd"; }
+.bi-cloud-sun::before { content: "\f2be"; }
+.bi-cloud-upload-fill::before { content: "\f2bf"; }
+.bi-cloud-upload::before { content: "\f2c0"; }
+.bi-cloud::before { content: "\f2c1"; }
+.bi-clouds-fill::before { content: "\f2c2"; }
+.bi-clouds::before { content: "\f2c3"; }
+.bi-cloudy-fill::before { content: "\f2c4"; }
+.bi-cloudy::before { content: "\f2c5"; }
+.bi-code-slash::before { content: "\f2c6"; }
+.bi-code-square::before { content: "\f2c7"; }
+.bi-code::before { content: "\f2c8"; }
+.bi-collection-fill::before { content: "\f2c9"; }
+.bi-collection-play-fill::before { content: "\f2ca"; }
+.bi-collection-play::before { content: "\f2cb"; }
+.bi-collection::before { content: "\f2cc"; }
+.bi-columns-gap::before { content: "\f2cd"; }
+.bi-columns::before { content: "\f2ce"; }
+.bi-command::before { content: "\f2cf"; }
+.bi-compass-fill::before { content: "\f2d0"; }
+.bi-compass::before { content: "\f2d1"; }
+.bi-cone-striped::before { content: "\f2d2"; }
+.bi-cone::before { content: "\f2d3"; }
+.bi-controller::before { content: "\f2d4"; }
+.bi-cpu-fill::before { content: "\f2d5"; }
+.bi-cpu::before { content: "\f2d6"; }
+.bi-credit-card-2-back-fill::before { content: "\f2d7"; }
+.bi-credit-card-2-back::before { content: "\f2d8"; }
+.bi-credit-card-2-front-fill::before { content: "\f2d9"; }
+.bi-credit-card-2-front::before { content: "\f2da"; }
+.bi-credit-card-fill::before { content: "\f2db"; }
+.bi-credit-card::before { content: "\f2dc"; }
+.bi-crop::before { content: "\f2dd"; }
+.bi-cup-fill::before { content: "\f2de"; }
+.bi-cup-straw::before { content: "\f2df"; }
+.bi-cup::before { content: "\f2e0"; }
+.bi-cursor-fill::before { content: "\f2e1"; }
+.bi-cursor-text::before { content: "\f2e2"; }
+.bi-cursor::before { content: "\f2e3"; }
+.bi-dash-circle-dotted::before { content: "\f2e4"; }
+.bi-dash-circle-fill::before { content: "\f2e5"; }
+.bi-dash-circle::before { content: "\f2e6"; }
+.bi-dash-square-dotted::before { content: "\f2e7"; }
+.bi-dash-square-fill::before { content: "\f2e8"; }
+.bi-dash-square::before { content: "\f2e9"; }
+.bi-dash::before { content: "\f2ea"; }
+.bi-diagram-2-fill::before { content: "\f2eb"; }
+.bi-diagram-2::before { content: "\f2ec"; }
+.bi-diagram-3-fill::before { content: "\f2ed"; }
+.bi-diagram-3::before { content: "\f2ee"; }
+.bi-diamond-fill::before { content: "\f2ef"; }
+.bi-diamond-half::before { content: "\f2f0"; }
+.bi-diamond::before { content: "\f2f1"; }
+.bi-dice-1-fill::before { content: "\f2f2"; }
+.bi-dice-1::before { content: "\f2f3"; }
+.bi-dice-2-fill::before { content: "\f2f4"; }
+.bi-dice-2::before { content: "\f2f5"; }
+.bi-dice-3-fill::before { content: "\f2f6"; }
+.bi-dice-3::before { content: "\f2f7"; }
+.bi-dice-4-fill::before { content: "\f2f8"; }
+.bi-dice-4::before { content: "\f2f9"; }
+.bi-dice-5-fill::before { content: "\f2fa"; }
+.bi-dice-5::before { content: "\f2fb"; }
+.bi-dice-6-fill::before { content: "\f2fc"; }
+.bi-dice-6::before { content: "\f2fd"; }
+.bi-disc-fill::before { content: "\f2fe"; }
+.bi-disc::before { content: "\f2ff"; }
+.bi-discord::before { content: "\f300"; }
+.bi-display-fill::before { content: "\f301"; }
+.bi-display::before { content: "\f302"; }
+.bi-distribute-horizontal::before { content: "\f303"; }
+.bi-distribute-vertical::before { content: "\f304"; }
+.bi-door-closed-fill::before { content: "\f305"; }
+.bi-door-closed::before { content: "\f306"; }
+.bi-door-open-fill::before { content: "\f307"; }
+.bi-door-open::before { content: "\f308"; }
+.bi-dot::before { content: "\f309"; }
+.bi-download::before { content: "\f30a"; }
+.bi-droplet-fill::before { content: "\f30b"; }
+.bi-droplet-half::before { content: "\f30c"; }
+.bi-droplet::before { content: "\f30d"; }
+.bi-earbuds::before { content: "\f30e"; }
+.bi-easel-fill::before { content: "\f30f"; }
+.bi-easel::before { content: "\f310"; }
+.bi-egg-fill::before { content: "\f311"; }
+.bi-egg-fried::before { content: "\f312"; }
+.bi-egg::before { content: "\f313"; }
+.bi-eject-fill::before { content: "\f314"; }
+.bi-eject::before { content: "\f315"; }
+.bi-emoji-angry-fill::before { content: "\f316"; }
+.bi-emoji-angry::before { content: "\f317"; }
+.bi-emoji-dizzy-fill::before { content: "\f318"; }
+.bi-emoji-dizzy::before { content: "\f319"; }
+.bi-emoji-expressionless-fill::before { content: "\f31a"; }
+.bi-emoji-expressionless::before { content: "\f31b"; }
+.bi-emoji-frown-fill::before { content: "\f31c"; }
+.bi-emoji-frown::before { content: "\f31d"; }
+.bi-emoji-heart-eyes-fill::before { content: "\f31e"; }
+.bi-emoji-heart-eyes::before { content: "\f31f"; }
+.bi-emoji-laughing-fill::before { content: "\f320"; }
+.bi-emoji-laughing::before { content: "\f321"; }
+.bi-emoji-neutral-fill::before { content: "\f322"; }
+.bi-emoji-neutral::before { content: "\f323"; }
+.bi-emoji-smile-fill::before { content: "\f324"; }
+.bi-emoji-smile-upside-down-fill::before { content: "\f325"; }
+.bi-emoji-smile-upside-down::before { content: "\f326"; }
+.bi-emoji-smile::before { content: "\f327"; }
+.bi-emoji-sunglasses-fill::before { content: "\f328"; }
+.bi-emoji-sunglasses::before { content: "\f329"; }
+.bi-emoji-wink-fill::before { content: "\f32a"; }
+.bi-emoji-wink::before { content: "\f32b"; }
+.bi-envelope-fill::before { content: "\f32c"; }
+.bi-envelope-open-fill::before { content: "\f32d"; }
+.bi-envelope-open::before { content: "\f32e"; }
+.bi-envelope::before { content: "\f32f"; }
+.bi-eraser-fill::before { content: "\f330"; }
+.bi-eraser::before { content: "\f331"; }
+.bi-exclamation-circle-fill::before { content: "\f332"; }
+.bi-exclamation-circle::before { content: "\f333"; }
+.bi-exclamation-diamond-fill::before { content: "\f334"; }
+.bi-exclamation-diamond::before { content: "\f335"; }
+.bi-exclamation-octagon-fill::before { content: "\f336"; }
+.bi-exclamation-octagon::before { content: "\f337"; }
+.bi-exclamation-square-fill::before { content: "\f338"; }
+.bi-exclamation-square::before { content: "\f339"; }
+.bi-exclamation-triangle-fill::before { content: "\f33a"; }
+.bi-exclamation-triangle::before { content: "\f33b"; }
+.bi-exclamation::before { content: "\f33c"; }
+.bi-exclude::before { content: "\f33d"; }
+.bi-eye-fill::before { content: "\f33e"; }
+.bi-eye-slash-fill::before { content: "\f33f"; }
+.bi-eye-slash::before { content: "\f340"; }
+.bi-eye::before { content: "\f341"; }
+.bi-eyedropper::before { content: "\f342"; }
+.bi-eyeglasses::before { content: "\f343"; }
+.bi-facebook::before { content: "\f344"; }
+.bi-file-arrow-down-fill::before { content: "\f345"; }
+.bi-file-arrow-down::before { content: "\f346"; }
+.bi-file-arrow-up-fill::before { content: "\f347"; }
+.bi-file-arrow-up::before { content: "\f348"; }
+.bi-file-bar-graph-fill::before { content: "\f349"; }
+.bi-file-bar-graph::before { content: "\f34a"; }
+.bi-file-binary-fill::before { content: "\f34b"; }
+.bi-file-binary::before { content: "\f34c"; }
+.bi-file-break-fill::before { content: "\f34d"; }
+.bi-file-break::before { content: "\f34e"; }
+.bi-file-check-fill::before { content: "\f34f"; }
+.bi-file-check::before { content: "\f350"; }
+.bi-file-code-fill::before { content: "\f351"; }
+.bi-file-code::before { content: "\f352"; }
+.bi-file-diff-fill::before { content: "\f353"; }
+.bi-file-diff::before { content: "\f354"; }
+.bi-file-earmark-arrow-down-fill::before { content: "\f355"; }
+.bi-file-earmark-arrow-down::before { content: "\f356"; }
+.bi-file-earmark-arrow-up-fill::before { content: "\f357"; }
+.bi-file-earmark-arrow-up::before { content: "\f358"; }
+.bi-file-earmark-bar-graph-fill::before { content: "\f359"; }
+.bi-file-earmark-bar-graph::before { content: "\f35a"; }
+.bi-file-earmark-binary-fill::before { content: "\f35b"; }
+.bi-file-earmark-binary::before { content: "\f35c"; }
+.bi-file-earmark-break-fill::before { content: "\f35d"; }
+.bi-file-earmark-break::before { content: "\f35e"; }
+.bi-file-earmark-check-fill::before { content: "\f35f"; }
+.bi-file-earmark-check::before { content: "\f360"; }
+.bi-file-earmark-code-fill::before { content: "\f361"; }
+.bi-file-earmark-code::before { content: "\f362"; }
+.bi-file-earmark-diff-fill::before { content: "\f363"; }
+.bi-file-earmark-diff::before { content: "\f364"; }
+.bi-file-earmark-easel-fill::before { content: "\f365"; }
+.bi-file-earmark-easel::before { content: "\f366"; }
+.bi-file-earmark-excel-fill::before { content: "\f367"; }
+.bi-file-earmark-excel::before { content: "\f368"; }
+.bi-file-earmark-fill::before { content: "\f369"; }
+.bi-file-earmark-font-fill::before { content: "\f36a"; }
+.bi-file-earmark-font::before { content: "\f36b"; }
+.bi-file-earmark-image-fill::before { content: "\f36c"; }
+.bi-file-earmark-image::before { content: "\f36d"; }
+.bi-file-earmark-lock-fill::before { content: "\f36e"; }
+.bi-file-earmark-lock::before { content: "\f36f"; }
+.bi-file-earmark-lock2-fill::before { content: "\f370"; }
+.bi-file-earmark-lock2::before { content: "\f371"; }
+.bi-file-earmark-medical-fill::before { content: "\f372"; }
+.bi-file-earmark-medical::before { content: "\f373"; }
+.bi-file-earmark-minus-fill::before { content: "\f374"; }
+.bi-file-earmark-minus::before { content: "\f375"; }
+.bi-file-earmark-music-fill::before { content: "\f376"; }
+.bi-file-earmark-music::before { content: "\f377"; }
+.bi-file-earmark-person-fill::before { content: "\f378"; }
+.bi-file-earmark-person::before { content: "\f379"; }
+.bi-file-earmark-play-fill::before { content: "\f37a"; }
+.bi-file-earmark-play::before { content: "\f37b"; }
+.bi-file-earmark-plus-fill::before { content: "\f37c"; }
+.bi-file-earmark-plus::before { content: "\f37d"; }
+.bi-file-earmark-post-fill::before { content: "\f37e"; }
+.bi-file-earmark-post::before { content: "\f37f"; }
+.bi-file-earmark-ppt-fill::before { content: "\f380"; }
+.bi-file-earmark-ppt::before { content: "\f381"; }
+.bi-file-earmark-richtext-fill::before { content: "\f382"; }
+.bi-file-earmark-richtext::before { content: "\f383"; }
+.bi-file-earmark-ruled-fill::before { content: "\f384"; }
+.bi-file-earmark-ruled::before { content: "\f385"; }
+.bi-file-earmark-slides-fill::before { content: "\f386"; }
+.bi-file-earmark-slides::before { content: "\f387"; }
+.bi-file-earmark-spreadsheet-fill::before { content: "\f388"; }
+.bi-file-earmark-spreadsheet::before { content: "\f389"; }
+.bi-file-earmark-text-fill::before { content: "\f38a"; }
+.bi-file-earmark-text::before { content: "\f38b"; }
+.bi-file-earmark-word-fill::before { content: "\f38c"; }
+.bi-file-earmark-word::before { content: "\f38d"; }
+.bi-file-earmark-x-fill::before { content: "\f38e"; }
+.bi-file-earmark-x::before { content: "\f38f"; }
+.bi-file-earmark-zip-fill::before { content: "\f390"; }
+.bi-file-earmark-zip::before { content: "\f391"; }
+.bi-file-earmark::before { content: "\f392"; }
+.bi-file-easel-fill::before { content: "\f393"; }
+.bi-file-easel::before { content: "\f394"; }
+.bi-file-excel-fill::before { content: "\f395"; }
+.bi-file-excel::before { content: "\f396"; }
+.bi-file-fill::before { content: "\f397"; }
+.bi-file-font-fill::before { content: "\f398"; }
+.bi-file-font::before { content: "\f399"; }
+.bi-file-image-fill::before { content: "\f39a"; }
+.bi-file-image::before { content: "\f39b"; }
+.bi-file-lock-fill::before { content: "\f39c"; }
+.bi-file-lock::before { content: "\f39d"; }
+.bi-file-lock2-fill::before { content: "\f39e"; }
+.bi-file-lock2::before { content: "\f39f"; }
+.bi-file-medical-fill::before { content: "\f3a0"; }
+.bi-file-medical::before { content: "\f3a1"; }
+.bi-file-minus-fill::before { content: "\f3a2"; }
+.bi-file-minus::before { content: "\f3a3"; }
+.bi-file-music-fill::before { content: "\f3a4"; }
+.bi-file-music::before { content: "\f3a5"; }
+.bi-file-person-fill::before { content: "\f3a6"; }
+.bi-file-person::before { content: "\f3a7"; }
+.bi-file-play-fill::before { content: "\f3a8"; }
+.bi-file-play::before { content: "\f3a9"; }
+.bi-file-plus-fill::before { content: "\f3aa"; }
+.bi-file-plus::before { content: "\f3ab"; }
+.bi-file-post-fill::before { content: "\f3ac"; }
+.bi-file-post::before { content: "\f3ad"; }
+.bi-file-ppt-fill::before { content: "\f3ae"; }
+.bi-file-ppt::before { content: "\f3af"; }
+.bi-file-richtext-fill::before { content: "\f3b0"; }
+.bi-file-richtext::before { content: "\f3b1"; }
+.bi-file-ruled-fill::before { content: "\f3b2"; }
+.bi-file-ruled::before { content: "\f3b3"; }
+.bi-file-slides-fill::before { content: "\f3b4"; }
+.bi-file-slides::before { content: "\f3b5"; }
+.bi-file-spreadsheet-fill::before { content: "\f3b6"; }
+.bi-file-spreadsheet::before { content: "\f3b7"; }
+.bi-file-text-fill::before { content: "\f3b8"; }
+.bi-file-text::before { content: "\f3b9"; }
+.bi-file-word-fill::before { content: "\f3ba"; }
+.bi-file-word::before { content: "\f3bb"; }
+.bi-file-x-fill::before { content: "\f3bc"; }
+.bi-file-x::before { content: "\f3bd"; }
+.bi-file-zip-fill::before { content: "\f3be"; }
+.bi-file-zip::before { content: "\f3bf"; }
+.bi-file::before { content: "\f3c0"; }
+.bi-files-alt::before { content: "\f3c1"; }
+.bi-files::before { content: "\f3c2"; }
+.bi-film::before { content: "\f3c3"; }
+.bi-filter-circle-fill::before { content: "\f3c4"; }
+.bi-filter-circle::before { content: "\f3c5"; }
+.bi-filter-left::before { content: "\f3c6"; }
+.bi-filter-right::before { content: "\f3c7"; }
+.bi-filter-square-fill::before { content: "\f3c8"; }
+.bi-filter-square::before { content: "\f3c9"; }
+.bi-filter::before { content: "\f3ca"; }
+.bi-flag-fill::before { content: "\f3cb"; }
+.bi-flag::before { content: "\f3cc"; }
+.bi-flower1::before { content: "\f3cd"; }
+.bi-flower2::before { content: "\f3ce"; }
+.bi-flower3::before { content: "\f3cf"; }
+.bi-folder-check::before { content: "\f3d0"; }
+.bi-folder-fill::before { content: "\f3d1"; }
+.bi-folder-minus::before { content: "\f3d2"; }
+.bi-folder-plus::before { content: "\f3d3"; }
+.bi-folder-symlink-fill::before { content: "\f3d4"; }
+.bi-folder-symlink::before { content: "\f3d5"; }
+.bi-folder-x::before { content: "\f3d6"; }
+.bi-folder::before { content: "\f3d7"; }
+.bi-folder2-open::before { content: "\f3d8"; }
+.bi-folder2::before { content: "\f3d9"; }
+.bi-fonts::before { content: "\f3da"; }
+.bi-forward-fill::before { content: "\f3db"; }
+.bi-forward::before { content: "\f3dc"; }
+.bi-front::before { content: "\f3dd"; }
+.bi-fullscreen-exit::before { content: "\f3de"; }
+.bi-fullscreen::before { content: "\f3df"; }
+.bi-funnel-fill::before { content: "\f3e0"; }
+.bi-funnel::before { content: "\f3e1"; }
+.bi-gear-fill::before { content: "\f3e2"; }
+.bi-gear-wide-connected::before { content: "\f3e3"; }
+.bi-gear-wide::before { content: "\f3e4"; }
+.bi-gear::before { content: "\f3e5"; }
+.bi-gem::before { content: "\f3e6"; }
+.bi-geo-alt-fill::before { content: "\f3e7"; }
+.bi-geo-alt::before { content: "\f3e8"; }
+.bi-geo-fill::before { content: "\f3e9"; }
+.bi-geo::before { content: "\f3ea"; }
+.bi-gift-fill::before { content: "\f3eb"; }
+.bi-gift::before { content: "\f3ec"; }
+.bi-github::before { content: "\f3ed"; }
+.bi-globe::before { content: "\f3ee"; }
+.bi-globe2::before { content: "\f3ef"; }
+.bi-google::before { content: "\f3f0"; }
+.bi-graph-down::before { content: "\f3f1"; }
+.bi-graph-up::before { content: "\f3f2"; }
+.bi-grid-1x2-fill::before { content: "\f3f3"; }
+.bi-grid-1x2::before { content: "\f3f4"; }
+.bi-grid-3x2-gap-fill::before { content: "\f3f5"; }
+.bi-grid-3x2-gap::before { content: "\f3f6"; }
+.bi-grid-3x2::before { content: "\f3f7"; }
+.bi-grid-3x3-gap-fill::before { content: "\f3f8"; }
+.bi-grid-3x3-gap::before { content: "\f3f9"; }
+.bi-grid-3x3::before { content: "\f3fa"; }
+.bi-grid-fill::before { content: "\f3fb"; }
+.bi-grid::before { content: "\f3fc"; }
+.bi-grip-horizontal::before { content: "\f3fd"; }
+.bi-grip-vertical::before { content: "\f3fe"; }
+.bi-hammer::before { content: "\f3ff"; }
+.bi-hand-index-fill::before { content: "\f400"; }
+.bi-hand-index-thumb-fill::before { content: "\f401"; }
+.bi-hand-index-thumb::before { content: "\f402"; }
+.bi-hand-index::before { content: "\f403"; }
+.bi-hand-thumbs-down-fill::before { content: "\f404"; }
+.bi-hand-thumbs-down::before { content: "\f405"; }
+.bi-hand-thumbs-up-fill::before { content: "\f406"; }
+.bi-hand-thumbs-up::before { content: "\f407"; }
+.bi-handbag-fill::before { content: "\f408"; }
+.bi-handbag::before { content: "\f409"; }
+.bi-hash::before { content: "\f40a"; }
+.bi-hdd-fill::before { content: "\f40b"; }
+.bi-hdd-network-fill::before { content: "\f40c"; }
+.bi-hdd-network::before { content: "\f40d"; }
+.bi-hdd-rack-fill::before { content: "\f40e"; }
+.bi-hdd-rack::before { content: "\f40f"; }
+.bi-hdd-stack-fill::before { content: "\f410"; }
+.bi-hdd-stack::before { content: "\f411"; }
+.bi-hdd::before { content: "\f412"; }
+.bi-headphones::before { content: "\f413"; }
+.bi-headset::before { content: "\f414"; }
+.bi-heart-fill::before { content: "\f415"; }
+.bi-heart-half::before { content: "\f416"; }
+.bi-heart::before { content: "\f417"; }
+.bi-heptagon-fill::before { content: "\f418"; }
+.bi-heptagon-half::before { content: "\f419"; }
+.bi-heptagon::before { content: "\f41a"; }
+.bi-hexagon-fill::before { content: "\f41b"; }
+.bi-hexagon-half::before { content: "\f41c"; }
+.bi-hexagon::before { content: "\f41d"; }
+.bi-hourglass-bottom::before { content: "\f41e"; }
+.bi-hourglass-split::before { content: "\f41f"; }
+.bi-hourglass-top::before { content: "\f420"; }
+.bi-hourglass::before { content: "\f421"; }
+.bi-house-door-fill::before { content: "\f422"; }
+.bi-house-door::before { content: "\f423"; }
+.bi-house-fill::before { content: "\f424"; }
+.bi-house::before { content: "\f425"; }
+.bi-hr::before { content: "\f426"; }
+.bi-hurricane::before { content: "\f427"; }
+.bi-image-alt::before { content: "\f428"; }
+.bi-image-fill::before { content: "\f429"; }
+.bi-image::before { content: "\f42a"; }
+.bi-images::before { content: "\f42b"; }
+.bi-inbox-fill::before { content: "\f42c"; }
+.bi-inbox::before { content: "\f42d"; }
+.bi-inboxes-fill::before { content: "\f42e"; }
+.bi-inboxes::before { content: "\f42f"; }
+.bi-info-circle-fill::before { content: "\f430"; }
+.bi-info-circle::before { content: "\f431"; }
+.bi-info-square-fill::before { content: "\f432"; }
+.bi-info-square::before { content: "\f433"; }
+.bi-info::before { content: "\f434"; }
+.bi-input-cursor-text::before { content: "\f435"; }
+.bi-input-cursor::before { content: "\f436"; }
+.bi-instagram::before { content: "\f437"; }
+.bi-intersect::before { content: "\f438"; }
+.bi-journal-album::before { content: "\f439"; }
+.bi-journal-arrow-down::before { content: "\f43a"; }
+.bi-journal-arrow-up::before { content: "\f43b"; }
+.bi-journal-bookmark-fill::before { content: "\f43c"; }
+.bi-journal-bookmark::before { content: "\f43d"; }
+.bi-journal-check::before { content: "\f43e"; }
+.bi-journal-code::before { content: "\f43f"; }
+.bi-journal-medical::before { content: "\f440"; }
+.bi-journal-minus::before { content: "\f441"; }
+.bi-journal-plus::before { content: "\f442"; }
+.bi-journal-richtext::before { content: "\f443"; }
+.bi-journal-text::before { content: "\f444"; }
+.bi-journal-x::before { content: "\f445"; }
+.bi-journal::before { content: "\f446"; }
+.bi-journals::before { content: "\f447"; }
+.bi-joystick::before { content: "\f448"; }
+.bi-justify-left::before { content: "\f449"; }
+.bi-justify-right::before { content: "\f44a"; }
+.bi-justify::before { content: "\f44b"; }
+.bi-kanban-fill::before { content: "\f44c"; }
+.bi-kanban::before { content: "\f44d"; }
+.bi-key-fill::before { content: "\f44e"; }
+.bi-key::before { content: "\f44f"; }
+.bi-keyboard-fill::before { content: "\f450"; }
+.bi-keyboard::before { content: "\f451"; }
+.bi-ladder::before { content: "\f452"; }
+.bi-lamp-fill::before { content: "\f453"; }
+.bi-lamp::before { content: "\f454"; }
+.bi-laptop-fill::before { content: "\f455"; }
+.bi-laptop::before { content: "\f456"; }
+.bi-layer-backward::before { content: "\f457"; }
+.bi-layer-forward::before { content: "\f458"; }
+.bi-layers-fill::before { content: "\f459"; }
+.bi-layers-half::before { content: "\f45a"; }
+.bi-layers::before { content: "\f45b"; }
+.bi-layout-sidebar-inset-reverse::before { content: "\f45c"; }
+.bi-layout-sidebar-inset::before { content: "\f45d"; }
+.bi-layout-sidebar-reverse::before { content: "\f45e"; }
+.bi-layout-sidebar::before { content: "\f45f"; }
+.bi-layout-split::before { content: "\f460"; }
+.bi-layout-text-sidebar-reverse::before { content: "\f461"; }
+.bi-layout-text-sidebar::before { content: "\f462"; }
+.bi-layout-text-window-reverse::before { content: "\f463"; }
+.bi-layout-text-window::before { content: "\f464"; }
+.bi-layout-three-columns::before { content: "\f465"; }
+.bi-layout-wtf::before { content: "\f466"; }
+.bi-life-preserver::before { content: "\f467"; }
+.bi-lightbulb-fill::before { content: "\f468"; }
+.bi-lightbulb-off-fill::before { content: "\f469"; }
+.bi-lightbulb-off::before { content: "\f46a"; }
+.bi-lightbulb::before { content: "\f46b"; }
+.bi-lightning-charge-fill::before { content: "\f46c"; }
+.bi-lightning-charge::before { content: "\f46d"; }
+.bi-lightning-fill::before { content: "\f46e"; }
+.bi-lightning::before { content: "\f46f"; }
+.bi-link-45deg::before { content: "\f470"; }
+.bi-link::before { content: "\f471"; }
+.bi-linkedin::before { content: "\f472"; }
+.bi-list-check::before { content: "\f473"; }
+.bi-list-nested::before { content: "\f474"; }
+.bi-list-ol::before { content: "\f475"; }
+.bi-list-stars::before { content: "\f476"; }
+.bi-list-task::before { content: "\f477"; }
+.bi-list-ul::before { content: "\f478"; }
+.bi-list::before { content: "\f479"; }
+.bi-lock-fill::before { content: "\f47a"; }
+.bi-lock::before { content: "\f47b"; }
+.bi-mailbox::before { content: "\f47c"; }
+.bi-mailbox2::before { content: "\f47d"; }
+.bi-map-fill::before { content: "\f47e"; }
+.bi-map::before { content: "\f47f"; }
+.bi-markdown-fill::before { content: "\f480"; }
+.bi-markdown::before { content: "\f481"; }
+.bi-mask::before { content: "\f482"; }
+.bi-megaphone-fill::before { content: "\f483"; }
+.bi-megaphone::before { content: "\f484"; }
+.bi-menu-app-fill::before { content: "\f485"; }
+.bi-menu-app::before { content: "\f486"; }
+.bi-menu-button-fill::before { content: "\f487"; }
+.bi-menu-button-wide-fill::before { content: "\f488"; }
+.bi-menu-button-wide::before { content: "\f489"; }
+.bi-menu-button::before { content: "\f48a"; }
+.bi-menu-down::before { content: "\f48b"; }
+.bi-menu-up::before { content: "\f48c"; }
+.bi-mic-fill::before { content: "\f48d"; }
+.bi-mic-mute-fill::before { content: "\f48e"; }
+.bi-mic-mute::before { content: "\f48f"; }
+.bi-mic::before { content: "\f490"; }
+.bi-minecart-loaded::before { content: "\f491"; }
+.bi-minecart::before { content: "\f492"; }
+.bi-moisture::before { content: "\f493"; }
+.bi-moon-fill::before { content: "\f494"; }
+.bi-moon-stars-fill::before { content: "\f495"; }
+.bi-moon-stars::before { content: "\f496"; }
+.bi-moon::before { content: "\f497"; }
+.bi-mouse-fill::before { content: "\f498"; }
+.bi-mouse::before { content: "\f499"; }
+.bi-mouse2-fill::before { content: "\f49a"; }
+.bi-mouse2::before { content: "\f49b"; }
+.bi-mouse3-fill::before { content: "\f49c"; }
+.bi-mouse3::before { content: "\f49d"; }
+.bi-music-note-beamed::before { content: "\f49e"; }
+.bi-music-note-list::before { content: "\f49f"; }
+.bi-music-note::before { content: "\f4a0"; }
+.bi-music-player-fill::before { content: "\f4a1"; }
+.bi-music-player::before { content: "\f4a2"; }
+.bi-newspaper::before { content: "\f4a3"; }
+.bi-node-minus-fill::before { content: "\f4a4"; }
+.bi-node-minus::before { content: "\f4a5"; }
+.bi-node-plus-fill::before { content: "\f4a6"; }
+.bi-node-plus::before { content: "\f4a7"; }
+.bi-nut-fill::before { content: "\f4a8"; }
+.bi-nut::before { content: "\f4a9"; }
+.bi-octagon-fill::before { content: "\f4aa"; }
+.bi-octagon-half::before { content: "\f4ab"; }
+.bi-octagon::before { content: "\f4ac"; }
+.bi-option::before { content: "\f4ad"; }
+.bi-outlet::before { content: "\f4ae"; }
+.bi-paint-bucket::before { content: "\f4af"; }
+.bi-palette-fill::before { content: "\f4b0"; }
+.bi-palette::before { content: "\f4b1"; }
+.bi-palette2::before { content: "\f4b2"; }
+.bi-paperclip::before { content: "\f4b3"; }
+.bi-paragraph::before { content: "\f4b4"; }
+.bi-patch-check-fill::before { content: "\f4b5"; }
+.bi-patch-check::before { content: "\f4b6"; }
+.bi-patch-exclamation-fill::before { content: "\f4b7"; }
+.bi-patch-exclamation::before { content: "\f4b8"; }
+.bi-patch-minus-fill::before { content: "\f4b9"; }
+.bi-patch-minus::before { content: "\f4ba"; }
+.bi-patch-plus-fill::before { content: "\f4bb"; }
+.bi-patch-plus::before { content: "\f4bc"; }
+.bi-patch-question-fill::before { content: "\f4bd"; }
+.bi-patch-question::before { content: "\f4be"; }
+.bi-pause-btn-fill::before { content: "\f4bf"; }
+.bi-pause-btn::before { content: "\f4c0"; }
+.bi-pause-circle-fill::before { content: "\f4c1"; }
+.bi-pause-circle::before { content: "\f4c2"; }
+.bi-pause-fill::before { content: "\f4c3"; }
+.bi-pause::before { content: "\f4c4"; }
+.bi-peace-fill::before { content: "\f4c5"; }
+.bi-peace::before { content: "\f4c6"; }
+.bi-pen-fill::before { content: "\f4c7"; }
+.bi-pen::before { content: "\f4c8"; }
+.bi-pencil-fill::before { content: "\f4c9"; }
+.bi-pencil-square::before { content: "\f4ca"; }
+.bi-pencil::before { content: "\f4cb"; }
+.bi-pentagon-fill::before { content: "\f4cc"; }
+.bi-pentagon-half::before { content: "\f4cd"; }
+.bi-pentagon::before { content: "\f4ce"; }
+.bi-people-fill::before { content: "\f4cf"; }
+.bi-people::before { content: "\f4d0"; }
+.bi-percent::before { content: "\f4d1"; }
+.bi-person-badge-fill::before { content: "\f4d2"; }
+.bi-person-badge::before { content: "\f4d3"; }
+.bi-person-bounding-box::before { content: "\f4d4"; }
+.bi-person-check-fill::before { content: "\f4d5"; }
+.bi-person-check::before { content: "\f4d6"; }
+.bi-person-circle::before { content: "\f4d7"; }
+.bi-person-dash-fill::before { content: "\f4d8"; }
+.bi-person-dash::before { content: "\f4d9"; }
+.bi-person-fill::before { content: "\f4da"; }
+.bi-person-lines-fill::before { content: "\f4db"; }
+.bi-person-plus-fill::before { content: "\f4dc"; }
+.bi-person-plus::before { content: "\f4dd"; }
+.bi-person-square::before { content: "\f4de"; }
+.bi-person-x-fill::before { content: "\f4df"; }
+.bi-person-x::before { content: "\f4e0"; }
+.bi-person::before { content: "\f4e1"; }
+.bi-phone-fill::before { content: "\f4e2"; }
+.bi-phone-landscape-fill::before { content: "\f4e3"; }
+.bi-phone-landscape::before { content: "\f4e4"; }
+.bi-phone-vibrate-fill::before { content: "\f4e5"; }
+.bi-phone-vibrate::before { content: "\f4e6"; }
+.bi-phone::before { content: "\f4e7"; }
+.bi-pie-chart-fill::before { content: "\f4e8"; }
+.bi-pie-chart::before { content: "\f4e9"; }
+.bi-pin-angle-fill::before { content: "\f4ea"; }
+.bi-pin-angle::before { content: "\f4eb"; }
+.bi-pin-fill::before { content: "\f4ec"; }
+.bi-pin::before { content: "\f4ed"; }
+.bi-pip-fill::before { content: "\f4ee"; }
+.bi-pip::before { content: "\f4ef"; }
+.bi-play-btn-fill::before { content: "\f4f0"; }
+.bi-play-btn::before { content: "\f4f1"; }
+.bi-play-circle-fill::before { content: "\f4f2"; }
+.bi-play-circle::before { content: "\f4f3"; }
+.bi-play-fill::before { content: "\f4f4"; }
+.bi-play::before { content: "\f4f5"; }
+.bi-plug-fill::before { content: "\f4f6"; }
+.bi-plug::before { content: "\f4f7"; }
+.bi-plus-circle-dotted::before { content: "\f4f8"; }
+.bi-plus-circle-fill::before { content: "\f4f9"; }
+.bi-plus-circle::before { content: "\f4fa"; }
+.bi-plus-square-dotted::before { content: "\f4fb"; }
+.bi-plus-square-fill::before { content: "\f4fc"; }
+.bi-plus-square::before { content: "\f4fd"; }
+.bi-plus::before { content: "\f4fe"; }
+.bi-power::before { content: "\f4ff"; }
+.bi-printer-fill::before { content: "\f500"; }
+.bi-printer::before { content: "\f501"; }
+.bi-puzzle-fill::before { content: "\f502"; }
+.bi-puzzle::before { content: "\f503"; }
+.bi-question-circle-fill::before { content: "\f504"; }
+.bi-question-circle::before { content: "\f505"; }
+.bi-question-diamond-fill::before { content: "\f506"; }
+.bi-question-diamond::before { content: "\f507"; }
+.bi-question-octagon-fill::before { content: "\f508"; }
+.bi-question-octagon::before { content: "\f509"; }
+.bi-question-square-fill::before { content: "\f50a"; }
+.bi-question-square::before { content: "\f50b"; }
+.bi-question::before { content: "\f50c"; }
+.bi-rainbow::before { content: "\f50d"; }
+.bi-receipt-cutoff::before { content: "\f50e"; }
+.bi-receipt::before { content: "\f50f"; }
+.bi-reception-0::before { content: "\f510"; }
+.bi-reception-1::before { content: "\f511"; }
+.bi-reception-2::before { content: "\f512"; }
+.bi-reception-3::before { content: "\f513"; }
+.bi-reception-4::before { content: "\f514"; }
+.bi-record-btn-fill::before { content: "\f515"; }
+.bi-record-btn::before { content: "\f516"; }
+.bi-record-circle-fill::before { content: "\f517"; }
+.bi-record-circle::before { content: "\f518"; }
+.bi-record-fill::before { content: "\f519"; }
+.bi-record::before { content: "\f51a"; }
+.bi-record2-fill::before { content: "\f51b"; }
+.bi-record2::before { content: "\f51c"; }
+.bi-reply-all-fill::before { content: "\f51d"; }
+.bi-reply-all::before { content: "\f51e"; }
+.bi-reply-fill::before { content: "\f51f"; }
+.bi-reply::before { content: "\f520"; }
+.bi-rss-fill::before { content: "\f521"; }
+.bi-rss::before { content: "\f522"; }
+.bi-rulers::before { content: "\f523"; }
+.bi-save-fill::before { content: "\f524"; }
+.bi-save::before { content: "\f525"; }
+.bi-save2-fill::before { content: "\f526"; }
+.bi-save2::before { content: "\f527"; }
+.bi-scissors::before { content: "\f528"; }
+.bi-screwdriver::before { content: "\f529"; }
+.bi-search::before { content: "\f52a"; }
+.bi-segmented-nav::before { content: "\f52b"; }
+.bi-server::before { content: "\f52c"; }
+.bi-share-fill::before { content: "\f52d"; }
+.bi-share::before { content: "\f52e"; }
+.bi-shield-check::before { content: "\f52f"; }
+.bi-shield-exclamation::before { content: "\f530"; }
+.bi-shield-fill-check::before { content: "\f531"; }
+.bi-shield-fill-exclamation::before { content: "\f532"; }
+.bi-shield-fill-minus::before { content: "\f533"; }
+.bi-shield-fill-plus::before { content: "\f534"; }
+.bi-shield-fill-x::before { content: "\f535"; }
+.bi-shield-fill::before { content: "\f536"; }
+.bi-shield-lock-fill::before { content: "\f537"; }
+.bi-shield-lock::before { content: "\f538"; }
+.bi-shield-minus::before { content: "\f539"; }
+.bi-shield-plus::before { content: "\f53a"; }
+.bi-shield-shaded::before { content: "\f53b"; }
+.bi-shield-slash-fill::before { content: "\f53c"; }
+.bi-shield-slash::before { content: "\f53d"; }
+.bi-shield-x::before { content: "\f53e"; }
+.bi-shield::before { content: "\f53f"; }
+.bi-shift-fill::before { content: "\f540"; }
+.bi-shift::before { content: "\f541"; }
+.bi-shop-window::before { content: "\f542"; }
+.bi-shop::before { content: "\f543"; }
+.bi-shuffle::before { content: "\f544"; }
+.bi-signpost-2-fill::before { content: "\f545"; }
+.bi-signpost-2::before { content: "\f546"; }
+.bi-signpost-fill::before { content: "\f547"; }
+.bi-signpost-split-fill::before { content: "\f548"; }
+.bi-signpost-split::before { content: "\f549"; }
+.bi-signpost::before { content: "\f54a"; }
+.bi-sim-fill::before { content: "\f54b"; }
+.bi-sim::before { content: "\f54c"; }
+.bi-skip-backward-btn-fill::before { content: "\f54d"; }
+.bi-skip-backward-btn::before { content: "\f54e"; }
+.bi-skip-backward-circle-fill::before { content: "\f54f"; }
+.bi-skip-backward-circle::before { content: "\f550"; }
+.bi-skip-backward-fill::before { content: "\f551"; }
+.bi-skip-backward::before { content: "\f552"; }
+.bi-skip-end-btn-fill::before { content: "\f553"; }
+.bi-skip-end-btn::before { content: "\f554"; }
+.bi-skip-end-circle-fill::before { content: "\f555"; }
+.bi-skip-end-circle::before { content: "\f556"; }
+.bi-skip-end-fill::before { content: "\f557"; }
+.bi-skip-end::before { content: "\f558"; }
+.bi-skip-forward-btn-fill::before { content: "\f559"; }
+.bi-skip-forward-btn::before { content: "\f55a"; }
+.bi-skip-forward-circle-fill::before { content: "\f55b"; }
+.bi-skip-forward-circle::before { content: "\f55c"; }
+.bi-skip-forward-fill::before { content: "\f55d"; }
+.bi-skip-forward::before { content: "\f55e"; }
+.bi-skip-start-btn-fill::before { content: "\f55f"; }
+.bi-skip-start-btn::before { content: "\f560"; }
+.bi-skip-start-circle-fill::before { content: "\f561"; }
+.bi-skip-start-circle::before { content: "\f562"; }
+.bi-skip-start-fill::before { content: "\f563"; }
+.bi-skip-start::before { content: "\f564"; }
+.bi-slack::before { content: "\f565"; }
+.bi-slash-circle-fill::before { content: "\f566"; }
+.bi-slash-circle::before { content: "\f567"; }
+.bi-slash-square-fill::before { content: "\f568"; }
+.bi-slash-square::before { content: "\f569"; }
+.bi-slash::before { content: "\f56a"; }
+.bi-sliders::before { content: "\f56b"; }
+.bi-smartwatch::before { content: "\f56c"; }
+.bi-snow::before { content: "\f56d"; }
+.bi-snow2::before { content: "\f56e"; }
+.bi-snow3::before { content: "\f56f"; }
+.bi-sort-alpha-down-alt::before { content: "\f570"; }
+.bi-sort-alpha-down::before { content: "\f571"; }
+.bi-sort-alpha-up-alt::before { content: "\f572"; }
+.bi-sort-alpha-up::before { content: "\f573"; }
+.bi-sort-down-alt::before { content: "\f574"; }
+.bi-sort-down::before { content: "\f575"; }
+.bi-sort-numeric-down-alt::before { content: "\f576"; }
+.bi-sort-numeric-down::before { content: "\f577"; }
+.bi-sort-numeric-up-alt::before { content: "\f578"; }
+.bi-sort-numeric-up::before { content: "\f579"; }
+.bi-sort-up-alt::before { content: "\f57a"; }
+.bi-sort-up::before { content: "\f57b"; }
+.bi-soundwave::before { content: "\f57c"; }
+.bi-speaker-fill::before { content: "\f57d"; }
+.bi-speaker::before { content: "\f57e"; }
+.bi-speedometer::before { content: "\f57f"; }
+.bi-speedometer2::before { content: "\f580"; }
+.bi-spellcheck::before { content: "\f581"; }
+.bi-square-fill::before { content: "\f582"; }
+.bi-square-half::before { content: "\f583"; }
+.bi-square::before { content: "\f584"; }
+.bi-stack::before { content: "\f585"; }
+.bi-star-fill::before { content: "\f586"; }
+.bi-star-half::before { content: "\f587"; }
+.bi-star::before { content: "\f588"; }
+.bi-stars::before { content: "\f589"; }
+.bi-stickies-fill::before { content: "\f58a"; }
+.bi-stickies::before { content: "\f58b"; }
+.bi-sticky-fill::before { content: "\f58c"; }
+.bi-sticky::before { content: "\f58d"; }
+.bi-stop-btn-fill::before { content: "\f58e"; }
+.bi-stop-btn::before { content: "\f58f"; }
+.bi-stop-circle-fill::before { content: "\f590"; }
+.bi-stop-circle::before { content: "\f591"; }
+.bi-stop-fill::before { content: "\f592"; }
+.bi-stop::before { content: "\f593"; }
+.bi-stoplights-fill::before { content: "\f594"; }
+.bi-stoplights::before { content: "\f595"; }
+.bi-stopwatch-fill::before { content: "\f596"; }
+.bi-stopwatch::before { content: "\f597"; }
+.bi-subtract::before { content: "\f598"; }
+.bi-suit-club-fill::before { content: "\f599"; }
+.bi-suit-club::before { content: "\f59a"; }
+.bi-suit-diamond-fill::before { content: "\f59b"; }
+.bi-suit-diamond::before { content: "\f59c"; }
+.bi-suit-heart-fill::before { content: "\f59d"; }
+.bi-suit-heart::before { content: "\f59e"; }
+.bi-suit-spade-fill::before { content: "\f59f"; }
+.bi-suit-spade::before { content: "\f5a0"; }
+.bi-sun-fill::before { content: "\f5a1"; }
+.bi-sun::before { content: "\f5a2"; }
+.bi-sunglasses::before { content: "\f5a3"; }
+.bi-sunrise-fill::before { content: "\f5a4"; }
+.bi-sunrise::before { content: "\f5a5"; }
+.bi-sunset-fill::before { content: "\f5a6"; }
+.bi-sunset::before { content: "\f5a7"; }
+.bi-symmetry-horizontal::before { content: "\f5a8"; }
+.bi-symmetry-vertical::before { content: "\f5a9"; }
+.bi-table::before { content: "\f5aa"; }
+.bi-tablet-fill::before { content: "\f5ab"; }
+.bi-tablet-landscape-fill::before { content: "\f5ac"; }
+.bi-tablet-landscape::before { content: "\f5ad"; }
+.bi-tablet::before { content: "\f5ae"; }
+.bi-tag-fill::before { content: "\f5af"; }
+.bi-tag::before { content: "\f5b0"; }
+.bi-tags-fill::before { content: "\f5b1"; }
+.bi-tags::before { content: "\f5b2"; }
+.bi-telegram::before { content: "\f5b3"; }
+.bi-telephone-fill::before { content: "\f5b4"; }
+.bi-telephone-forward-fill::before { content: "\f5b5"; }
+.bi-telephone-forward::before { content: "\f5b6"; }
+.bi-telephone-inbound-fill::before { content: "\f5b7"; }
+.bi-telephone-inbound::before { content: "\f5b8"; }
+.bi-telephone-minus-fill::before { content: "\f5b9"; }
+.bi-telephone-minus::before { content: "\f5ba"; }
+.bi-telephone-outbound-fill::before { content: "\f5bb"; }
+.bi-telephone-outbound::before { content: "\f5bc"; }
+.bi-telephone-plus-fill::before { content: "\f5bd"; }
+.bi-telephone-plus::before { content: "\f5be"; }
+.bi-telephone-x-fill::before { content: "\f5bf"; }
+.bi-telephone-x::before { content: "\f5c0"; }
+.bi-telephone::before { content: "\f5c1"; }
+.bi-terminal-fill::before { content: "\f5c2"; }
+.bi-terminal::before { content: "\f5c3"; }
+.bi-text-center::before { content: "\f5c4"; }
+.bi-text-indent-left::before { content: "\f5c5"; }
+.bi-text-indent-right::before { content: "\f5c6"; }
+.bi-text-left::before { content: "\f5c7"; }
+.bi-text-paragraph::before { content: "\f5c8"; }
+.bi-text-right::before { content: "\f5c9"; }
+.bi-textarea-resize::before { content: "\f5ca"; }
+.bi-textarea-t::before { content: "\f5cb"; }
+.bi-textarea::before { content: "\f5cc"; }
+.bi-thermometer-half::before { content: "\f5cd"; }
+.bi-thermometer-high::before { content: "\f5ce"; }
+.bi-thermometer-low::before { content: "\f5cf"; }
+.bi-thermometer-snow::before { content: "\f5d0"; }
+.bi-thermometer-sun::before { content: "\f5d1"; }
+.bi-thermometer::before { content: "\f5d2"; }
+.bi-three-dots-vertical::before { content: "\f5d3"; }
+.bi-three-dots::before { content: "\f5d4"; }
+.bi-toggle-off::before { content: "\f5d5"; }
+.bi-toggle-on::before { content: "\f5d6"; }
+.bi-toggle2-off::before { content: "\f5d7"; }
+.bi-toggle2-on::before { content: "\f5d8"; }
+.bi-toggles::before { content: "\f5d9"; }
+.bi-toggles2::before { content: "\f5da"; }
+.bi-tools::before { content: "\f5db"; }
+.bi-tornado::before { content: "\f5dc"; }
+.bi-trash-fill::before { content: "\f5dd"; }
+.bi-trash::before { content: "\f5de"; }
+.bi-trash2-fill::before { content: "\f5df"; }
+.bi-trash2::before { content: "\f5e0"; }
+.bi-tree-fill::before { content: "\f5e1"; }
+.bi-tree::before { content: "\f5e2"; }
+.bi-triangle-fill::before { content: "\f5e3"; }
+.bi-triangle-half::before { content: "\f5e4"; }
+.bi-triangle::before { content: "\f5e5"; }
+.bi-trophy-fill::before { content: "\f5e6"; }
+.bi-trophy::before { content: "\f5e7"; }
+.bi-tropical-storm::before { content: "\f5e8"; }
+.bi-truck-flatbed::before { content: "\f5e9"; }
+.bi-truck::before { content: "\f5ea"; }
+.bi-tsunami::before { content: "\f5eb"; }
+.bi-tv-fill::before { content: "\f5ec"; }
+.bi-tv::before { content: "\f5ed"; }
+.bi-twitch::before { content: "\f5ee"; }
+.bi-twitter::before { content: "\f5ef"; }
+.bi-type-bold::before { content: "\f5f0"; }
+.bi-type-h1::before { content: "\f5f1"; }
+.bi-type-h2::before { content: "\f5f2"; }
+.bi-type-h3::before { content: "\f5f3"; }
+.bi-type-italic::before { content: "\f5f4"; }
+.bi-type-strikethrough::before { content: "\f5f5"; }
+.bi-type-underline::before { content: "\f5f6"; }
+.bi-type::before { content: "\f5f7"; }
+.bi-ui-checks-grid::before { content: "\f5f8"; }
+.bi-ui-checks::before { content: "\f5f9"; }
+.bi-ui-radios-grid::before { content: "\f5fa"; }
+.bi-ui-radios::before { content: "\f5fb"; }
+.bi-umbrella-fill::before { content: "\f5fc"; }
+.bi-umbrella::before { content: "\f5fd"; }
+.bi-union::before { content: "\f5fe"; }
+.bi-unlock-fill::before { content: "\f5ff"; }
+.bi-unlock::before { content: "\f600"; }
+.bi-upc-scan::before { content: "\f601"; }
+.bi-upc::before { content: "\f602"; }
+.bi-upload::before { content: "\f603"; }
+.bi-vector-pen::before { content: "\f604"; }
+.bi-view-list::before { content: "\f605"; }
+.bi-view-stacked::before { content: "\f606"; }
+.bi-vinyl-fill::before { content: "\f607"; }
+.bi-vinyl::before { content: "\f608"; }
+.bi-voicemail::before { content: "\f609"; }
+.bi-volume-down-fill::before { content: "\f60a"; }
+.bi-volume-down::before { content: "\f60b"; }
+.bi-volume-mute-fill::before { content: "\f60c"; }
+.bi-volume-mute::before { content: "\f60d"; }
+.bi-volume-off-fill::before { content: "\f60e"; }
+.bi-volume-off::before { content: "\f60f"; }
+.bi-volume-up-fill::before { content: "\f610"; }
+.bi-volume-up::before { content: "\f611"; }
+.bi-vr::before { content: "\f612"; }
+.bi-wallet-fill::before { content: "\f613"; }
+.bi-wallet::before { content: "\f614"; }
+.bi-wallet2::before { content: "\f615"; }
+.bi-watch::before { content: "\f616"; }
+.bi-water::before { content: "\f617"; }
+.bi-whatsapp::before { content: "\f618"; }
+.bi-wifi-1::before { content: "\f619"; }
+.bi-wifi-2::before { content: "\f61a"; }
+.bi-wifi-off::before { content: "\f61b"; }
+.bi-wifi::before { content: "\f61c"; }
+.bi-wind::before { content: "\f61d"; }
+.bi-window-dock::before { content: "\f61e"; }
+.bi-window-sidebar::before { content: "\f61f"; }
+.bi-window::before { content: "\f620"; }
+.bi-wrench::before { content: "\f621"; }
+.bi-x-circle-fill::before { content: "\f622"; }
+.bi-x-circle::before { content: "\f623"; }
+.bi-x-diamond-fill::before { content: "\f624"; }
+.bi-x-diamond::before { content: "\f625"; }
+.bi-x-octagon-fill::before { content: "\f626"; }
+.bi-x-octagon::before { content: "\f627"; }
+.bi-x-square-fill::before { content: "\f628"; }
+.bi-x-square::before { content: "\f629"; }
+.bi-x::before { content: "\f62a"; }
+.bi-youtube::before { content: "\f62b"; }
+.bi-zoom-in::before { content: "\f62c"; }
+.bi-zoom-out::before { content: "\f62d"; }
+.bi-bank::before { content: "\f62e"; }
+.bi-bank2::before { content: "\f62f"; }
+.bi-bell-slash-fill::before { content: "\f630"; }
+.bi-bell-slash::before { content: "\f631"; }
+.bi-cash-coin::before { content: "\f632"; }
+.bi-check-lg::before { content: "\f633"; }
+.bi-coin::before { content: "\f634"; }
+.bi-currency-bitcoin::before { content: "\f635"; }
+.bi-currency-dollar::before { content: "\f636"; }
+.bi-currency-euro::before { content: "\f637"; }
+.bi-currency-exchange::before { content: "\f638"; }
+.bi-currency-pound::before { content: "\f639"; }
+.bi-currency-yen::before { content: "\f63a"; }
+.bi-dash-lg::before { content: "\f63b"; }
+.bi-exclamation-lg::before { content: "\f63c"; }
+.bi-file-earmark-pdf-fill::before { content: "\f63d"; }
+.bi-file-earmark-pdf::before { content: "\f63e"; }
+.bi-file-pdf-fill::before { content: "\f63f"; }
+.bi-file-pdf::before { content: "\f640"; }
+.bi-gender-ambiguous::before { content: "\f641"; }
+.bi-gender-female::before { content: "\f642"; }
+.bi-gender-male::before { content: "\f643"; }
+.bi-gender-trans::before { content: "\f644"; }
+.bi-headset-vr::before { content: "\f645"; }
+.bi-info-lg::before { content: "\f646"; }
+.bi-mastodon::before { content: "\f647"; }
+.bi-messenger::before { content: "\f648"; }
+.bi-piggy-bank-fill::before { content: "\f649"; }
+.bi-piggy-bank::before { content: "\f64a"; }
+.bi-pin-map-fill::before { content: "\f64b"; }
+.bi-pin-map::before { content: "\f64c"; }
+.bi-plus-lg::before { content: "\f64d"; }
+.bi-question-lg::before { content: "\f64e"; }
+.bi-recycle::before { content: "\f64f"; }
+.bi-reddit::before { content: "\f650"; }
+.bi-safe-fill::before { content: "\f651"; }
+.bi-safe2-fill::before { content: "\f652"; }
+.bi-safe2::before { content: "\f653"; }
+.bi-sd-card-fill::before { content: "\f654"; }
+.bi-sd-card::before { content: "\f655"; }
+.bi-skype::before { content: "\f656"; }
+.bi-slash-lg::before { content: "\f657"; }
+.bi-translate::before { content: "\f658"; }
+.bi-x-lg::before { content: "\f659"; }
+.bi-safe::before { content: "\f65a"; }
+.bi-apple::before { content: "\f65b"; }
+.bi-microsoft::before { content: "\f65d"; }
+.bi-windows::before { content: "\f65e"; }
+.bi-behance::before { content: "\f65c"; }
+.bi-dribbble::before { content: "\f65f"; }
+.bi-line::before { content: "\f660"; }
+.bi-medium::before { content: "\f661"; }
+.bi-paypal::before { content: "\f662"; }
+.bi-pinterest::before { content: "\f663"; }
+.bi-signal::before { content: "\f664"; }
+.bi-snapchat::before { content: "\f665"; }
+.bi-spotify::before { content: "\f666"; }
+.bi-stack-overflow::before { content: "\f667"; }
+.bi-strava::before { content: "\f668"; }
+.bi-wordpress::before { content: "\f669"; }
+.bi-vimeo::before { content: "\f66a"; }
+.bi-activity::before { content: "\f66b"; }
+.bi-easel2-fill::before { content: "\f66c"; }
+.bi-easel2::before { content: "\f66d"; }
+.bi-easel3-fill::before { content: "\f66e"; }
+.bi-easel3::before { content: "\f66f"; }
+.bi-fan::before { content: "\f670"; }
+.bi-fingerprint::before { content: "\f671"; }
+.bi-graph-down-arrow::before { content: "\f672"; }
+.bi-graph-up-arrow::before { content: "\f673"; }
+.bi-hypnotize::before { content: "\f674"; }
+.bi-magic::before { content: "\f675"; }
+.bi-person-rolodex::before { content: "\f676"; }
+.bi-person-video::before { content: "\f677"; }
+.bi-person-video2::before { content: "\f678"; }
+.bi-person-video3::before { content: "\f679"; }
+.bi-person-workspace::before { content: "\f67a"; }
+.bi-radioactive::before { content: "\f67b"; }
+.bi-webcam-fill::before { content: "\f67c"; }
+.bi-webcam::before { content: "\f67d"; }
+.bi-yin-yang::before { content: "\f67e"; }
+.bi-bandaid-fill::before { content: "\f680"; }
+.bi-bandaid::before { content: "\f681"; }
+.bi-bluetooth::before { content: "\f682"; }
+.bi-body-text::before { content: "\f683"; }
+.bi-boombox::before { content: "\f684"; }
+.bi-boxes::before { content: "\f685"; }
+.bi-dpad-fill::before { content: "\f686"; }
+.bi-dpad::before { content: "\f687"; }
+.bi-ear-fill::before { content: "\f688"; }
+.bi-ear::before { content: "\f689"; }
+.bi-envelope-check-1::before { content: "\f68a"; }
+.bi-envelope-check-fill::before { content: "\f68b"; }
+.bi-envelope-check::before { content: "\f68c"; }
+.bi-envelope-dash-1::before { content: "\f68d"; }
+.bi-envelope-dash-fill::before { content: "\f68e"; }
+.bi-envelope-dash::before { content: "\f68f"; }
+.bi-envelope-exclamation-1::before { content: "\f690"; }
+.bi-envelope-exclamation-fill::before { content: "\f691"; }
+.bi-envelope-exclamation::before { content: "\f692"; }
+.bi-envelope-plus-fill::before { content: "\f693"; }
+.bi-envelope-plus::before { content: "\f694"; }
+.bi-envelope-slash-1::before { content: "\f695"; }
+.bi-envelope-slash-fill::before { content: "\f696"; }
+.bi-envelope-slash::before { content: "\f697"; }
+.bi-envelope-x-1::before { content: "\f698"; }
+.bi-envelope-x-fill::before { content: "\f699"; }
+.bi-envelope-x::before { content: "\f69a"; }
+.bi-explicit-fill::before { content: "\f69b"; }
+.bi-explicit::before { content: "\f69c"; }
+.bi-git::before { content: "\f69d"; }
+.bi-infinity::before { content: "\f69e"; }
+.bi-list-columns-reverse::before { content: "\f69f"; }
+.bi-list-columns::before { content: "\f6a0"; }
+.bi-meta::before { content: "\f6a1"; }
+.bi-mortorboard-fill::before { content: "\f6a2"; }
+.bi-mortorboard::before { content: "\f6a3"; }
+.bi-nintendo-switch::before { content: "\f6a4"; }
+.bi-pc-display-horizontal::before { content: "\f6a5"; }
+.bi-pc-display::before { content: "\f6a6"; }
+.bi-pc-horizontal::before { content: "\f6a7"; }
+.bi-pc::before { content: "\f6a8"; }
+.bi-playstation::before { content: "\f6a9"; }
+.bi-plus-slash-minus::before { content: "\f6aa"; }
+.bi-projector-fill::before { content: "\f6ab"; }
+.bi-projector::before { content: "\f6ac"; }
+.bi-qr-code-scan::before { content: "\f6ad"; }
+.bi-qr-code::before { content: "\f6ae"; }
+.bi-quora::before { content: "\f6af"; }
+.bi-quote::before { content: "\f6b0"; }
+.bi-robot::before { content: "\f6b1"; }
+.bi-send-check-fill::before { content: "\f6b2"; }
+.bi-send-check::before { content: "\f6b3"; }
+.bi-send-dash-fill::before { content: "\f6b4"; }
+.bi-send-dash::before { content: "\f6b5"; }
+.bi-send-exclamation-1::before { content: "\f6b6"; }
+.bi-send-exclamation-fill::before { content: "\f6b7"; }
+.bi-send-exclamation::before { content: "\f6b8"; }
+.bi-send-fill::before { content: "\f6b9"; }
+.bi-send-plus-fill::before { content: "\f6ba"; }
+.bi-send-plus::before { content: "\f6bb"; }
+.bi-send-slash-fill::before { content: "\f6bc"; }
+.bi-send-slash::before { content: "\f6bd"; }
+.bi-send-x-fill::before { content: "\f6be"; }
+.bi-send-x::before { content: "\f6bf"; }
+.bi-send::before { content: "\f6c0"; }
+.bi-steam::before { content: "\f6c1"; }
+.bi-terminal-dash-1::before { content: "\f6c2"; }
+.bi-terminal-dash::before { content: "\f6c3"; }
+.bi-terminal-plus::before { content: "\f6c4"; }
+.bi-terminal-split::before { content: "\f6c5"; }
+.bi-ticket-detailed-fill::before { content: "\f6c6"; }
+.bi-ticket-detailed::before { content: "\f6c7"; }
+.bi-ticket-fill::before { content: "\f6c8"; }
+.bi-ticket-perforated-fill::before { content: "\f6c9"; }
+.bi-ticket-perforated::before { content: "\f6ca"; }
+.bi-ticket::before { content: "\f6cb"; }
+.bi-tiktok::before { content: "\f6cc"; }
+.bi-window-dash::before { content: "\f6cd"; }
+.bi-window-desktop::before { content: "\f6ce"; }
+.bi-window-fullscreen::before { content: "\f6cf"; }
+.bi-window-plus::before { content: "\f6d0"; }
+.bi-window-split::before { content: "\f6d1"; }
+.bi-window-stack::before { content: "\f6d2"; }
+.bi-window-x::before { content: "\f6d3"; }
+.bi-xbox::before { content: "\f6d4"; }
+.bi-ethernet::before { content: "\f6d5"; }
+.bi-hdmi-fill::before { content: "\f6d6"; }
+.bi-hdmi::before { content: "\f6d7"; }
+.bi-usb-c-fill::before { content: "\f6d8"; }
+.bi-usb-c::before { content: "\f6d9"; }
+.bi-usb-fill::before { content: "\f6da"; }
+.bi-usb-plug-fill::before { content: "\f6db"; }
+.bi-usb-plug::before { content: "\f6dc"; }
+.bi-usb-symbol::before { content: "\f6dd"; }
+.bi-usb::before { content: "\f6de"; }
+.bi-boombox-fill::before { content: "\f6df"; }
+.bi-displayport-1::before { content: "\f6e0"; }
+.bi-displayport::before { content: "\f6e1"; }
+.bi-gpu-card::before { content: "\f6e2"; }
+.bi-memory::before { content: "\f6e3"; }
+.bi-modem-fill::before { content: "\f6e4"; }
+.bi-modem::before { content: "\f6e5"; }
+.bi-motherboard-fill::before { content: "\f6e6"; }
+.bi-motherboard::before { content: "\f6e7"; }
+.bi-optical-audio-fill::before { content: "\f6e8"; }
+.bi-optical-audio::before { content: "\f6e9"; }
+.bi-pci-card::before { content: "\f6ea"; }
+.bi-router-fill::before { content: "\f6eb"; }
+.bi-router::before { content: "\f6ec"; }
+.bi-ssd-fill::before { content: "\f6ed"; }
+.bi-ssd::before { content: "\f6ee"; }
+.bi-thunderbolt-fill::before { content: "\f6ef"; }
+.bi-thunderbolt::before { content: "\f6f0"; }
+.bi-usb-drive-fill::before { content: "\f6f1"; }
+.bi-usb-drive::before { content: "\f6f2"; }
+.bi-usb-micro-fill::before { content: "\f6f3"; }
+.bi-usb-micro::before { content: "\f6f4"; }
+.bi-usb-mini-fill::before { content: "\f6f5"; }
+.bi-usb-mini::before { content: "\f6f6"; }
+.bi-cloud-haze2::before { content: "\f6f7"; }
+.bi-device-hdd-fill::before { content: "\f6f8"; }
+.bi-device-hdd::before { content: "\f6f9"; }
+.bi-device-ssd-fill::before { content: "\f6fa"; }
+.bi-device-ssd::before { content: "\f6fb"; }
+.bi-displayport-fill::before { content: "\f6fc"; }
+.bi-mortarboard-fill::before { content: "\f6fd"; }
+.bi-mortarboard::before { content: "\f6fe"; }
+.bi-terminal-x::before { content: "\f6ff"; }
+.bi-arrow-through-heart-fill::before { content: "\f700"; }
+.bi-arrow-through-heart::before { content: "\f701"; }
+.bi-badge-sd-fill::before { content: "\f702"; }
+.bi-badge-sd::before { content: "\f703"; }
+.bi-bag-heart-fill::before { content: "\f704"; }
+.bi-bag-heart::before { content: "\f705"; }
+.bi-balloon-fill::before { content: "\f706"; }
+.bi-balloon-heart-fill::before { content: "\f707"; }
+.bi-balloon-heart::before { content: "\f708"; }
+.bi-balloon::before { content: "\f709"; }
+.bi-box2-fill::before { content: "\f70a"; }
+.bi-box2-heart-fill::before { content: "\f70b"; }
+.bi-box2-heart::before { content: "\f70c"; }
+.bi-box2::before { content: "\f70d"; }
+.bi-braces-asterisk::before { content: "\f70e"; }
+.bi-calendar-heart-fill::before { content: "\f70f"; }
+.bi-calendar-heart::before { content: "\f710"; }
+.bi-calendar2-heart-fill::before { content: "\f711"; }
+.bi-calendar2-heart::before { content: "\f712"; }
+.bi-chat-heart-fill::before { content: "\f713"; }
+.bi-chat-heart::before { content: "\f714"; }
+.bi-chat-left-heart-fill::before { content: "\f715"; }
+.bi-chat-left-heart::before { content: "\f716"; }
+.bi-chat-right-heart-fill::before { content: "\f717"; }
+.bi-chat-right-heart::before { content: "\f718"; }
+.bi-chat-square-heart-fill::before { content: "\f719"; }
+.bi-chat-square-heart::before { content: "\f71a"; }
+.bi-clipboard-check-fill::before { content: "\f71b"; }
+.bi-clipboard-data-fill::before { content: "\f71c"; }
+.bi-clipboard-fill::before { content: "\f71d"; }
+.bi-clipboard-heart-fill::before { content: "\f71e"; }
+.bi-clipboard-heart::before { content: "\f71f"; }
+.bi-clipboard-minus-fill::before { content: "\f720"; }
+.bi-clipboard-plus-fill::before { content: "\f721"; }
+.bi-clipboard-pulse::before { content: "\f722"; }
+.bi-clipboard-x-fill::before { content: "\f723"; }
+.bi-clipboard2-check-fill::before { content: "\f724"; }
+.bi-clipboard2-check::before { content: "\f725"; }
+.bi-clipboard2-data-fill::before { content: "\f726"; }
+.bi-clipboard2-data::before { content: "\f727"; }
+.bi-clipboard2-fill::before { content: "\f728"; }
+.bi-clipboard2-heart-fill::before { content: "\f729"; }
+.bi-clipboard2-heart::before { content: "\f72a"; }
+.bi-clipboard2-minus-fill::before { content: "\f72b"; }
+.bi-clipboard2-minus::before { content: "\f72c"; }
+.bi-clipboard2-plus-fill::before { content: "\f72d"; }
+.bi-clipboard2-plus::before { content: "\f72e"; }
+.bi-clipboard2-pulse-fill::before { content: "\f72f"; }
+.bi-clipboard2-pulse::before { content: "\f730"; }
+.bi-clipboard2-x-fill::before { content: "\f731"; }
+.bi-clipboard2-x::before { content: "\f732"; }
+.bi-clipboard2::before { content: "\f733"; }
+.bi-emoji-kiss-fill::before { content: "\f734"; }
+.bi-emoji-kiss::before { content: "\f735"; }
+.bi-envelope-heart-fill::before { content: "\f736"; }
+.bi-envelope-heart::before { content: "\f737"; }
+.bi-envelope-open-heart-fill::before { content: "\f738"; }
+.bi-envelope-open-heart::before { content: "\f739"; }
+.bi-envelope-paper-fill::before { content: "\f73a"; }
+.bi-envelope-paper-heart-fill::before { content: "\f73b"; }
+.bi-envelope-paper-heart::before { content: "\f73c"; }
+.bi-envelope-paper::before { content: "\f73d"; }
+.bi-filetype-aac::before { content: "\f73e"; }
+.bi-filetype-ai::before { content: "\f73f"; }
+.bi-filetype-bmp::before { content: "\f740"; }
+.bi-filetype-cs::before { content: "\f741"; }
+.bi-filetype-css::before { content: "\f742"; }
+.bi-filetype-csv::before { content: "\f743"; }
+.bi-filetype-doc::before { content: "\f744"; }
+.bi-filetype-docx::before { content: "\f745"; }
+.bi-filetype-exe::before { content: "\f746"; }
+.bi-filetype-gif::before { content: "\f747"; }
+.bi-filetype-heic::before { content: "\f748"; }
+.bi-filetype-html::before { content: "\f749"; }
+.bi-filetype-java::before { content: "\f74a"; }
+.bi-filetype-jpg::before { content: "\f74b"; }
+.bi-filetype-js::before { content: "\f74c"; }
+.bi-filetype-jsx::before { content: "\f74d"; }
+.bi-filetype-key::before { content: "\f74e"; }
+.bi-filetype-m4p::before { content: "\f74f"; }
+.bi-filetype-md::before { content: "\f750"; }
+.bi-filetype-mdx::before { content: "\f751"; }
+.bi-filetype-mov::before { content: "\f752"; }
+.bi-filetype-mp3::before { content: "\f753"; }
+.bi-filetype-mp4::before { content: "\f754"; }
+.bi-filetype-otf::before { content: "\f755"; }
+.bi-filetype-pdf::before { content: "\f756"; }
+.bi-filetype-php::before { content: "\f757"; }
+.bi-filetype-png::before { content: "\f758"; }
+.bi-filetype-ppt-1::before { content: "\f759"; }
+.bi-filetype-ppt::before { content: "\f75a"; }
+.bi-filetype-psd::before { content: "\f75b"; }
+.bi-filetype-py::before { content: "\f75c"; }
+.bi-filetype-raw::before { content: "\f75d"; }
+.bi-filetype-rb::before { content: "\f75e"; }
+.bi-filetype-sass::before { content: "\f75f"; }
+.bi-filetype-scss::before { content: "\f760"; }
+.bi-filetype-sh::before { content: "\f761"; }
+.bi-filetype-svg::before { content: "\f762"; }
+.bi-filetype-tiff::before { content: "\f763"; }
+.bi-filetype-tsx::before { content: "\f764"; }
+.bi-filetype-ttf::before { content: "\f765"; }
+.bi-filetype-txt::before { content: "\f766"; }
+.bi-filetype-wav::before { content: "\f767"; }
+.bi-filetype-woff::before { content: "\f768"; }
+.bi-filetype-xls-1::before { content: "\f769"; }
+.bi-filetype-xls::before { content: "\f76a"; }
+.bi-filetype-xml::before { content: "\f76b"; }
+.bi-filetype-yml::before { content: "\f76c"; }
+.bi-heart-arrow::before { content: "\f76d"; }
+.bi-heart-pulse-fill::before { content: "\f76e"; }
+.bi-heart-pulse::before { content: "\f76f"; }
+.bi-heartbreak-fill::before { content: "\f770"; }
+.bi-heartbreak::before { content: "\f771"; }
+.bi-hearts::before { content: "\f772"; }
+.bi-hospital-fill::before { content: "\f773"; }
+.bi-hospital::before { content: "\f774"; }
+.bi-house-heart-fill::before { content: "\f775"; }
+.bi-house-heart::before { content: "\f776"; }
+.bi-incognito::before { content: "\f777"; }
+.bi-magnet-fill::before { content: "\f778"; }
+.bi-magnet::before { content: "\f779"; }
+.bi-person-heart::before { content: "\f77a"; }
+.bi-person-hearts::before { content: "\f77b"; }
+.bi-phone-flip::before { content: "\f77c"; }
+.bi-plugin::before { content: "\f77d"; }
+.bi-postage-fill::before { content: "\f77e"; }
+.bi-postage-heart-fill::before { content: "\f77f"; }
+.bi-postage-heart::before { content: "\f780"; }
+.bi-postage::before { content: "\f781"; }
+.bi-postcard-fill::before { content: "\f782"; }
+.bi-postcard-heart-fill::before { content: "\f783"; }
+.bi-postcard-heart::before { content: "\f784"; }
+.bi-postcard::before { content: "\f785"; }
+.bi-search-heart-fill::before { content: "\f786"; }
+.bi-search-heart::before { content: "\f787"; }
+.bi-sliders2-vertical::before { content: "\f788"; }
+.bi-sliders2::before { content: "\f789"; }
+.bi-trash3-fill::before { content: "\f78a"; }
+.bi-trash3::before { content: "\f78b"; }
+.bi-valentine::before { content: "\f78c"; }
+.bi-valentine2::before { content: "\f78d"; }
+.bi-wrench-adjustable-circle-fill::before { content: "\f78e"; }
+.bi-wrench-adjustable-circle::before { content: "\f78f"; }
+.bi-wrench-adjustable::before { content: "\f790"; }
+.bi-filetype-json::before { content: "\f791"; }
+.bi-filetype-pptx::before { content: "\f792"; }
+.bi-filetype-xlsx::before { content: "\f793"; }
+.bi-1-circle-1::before { content: "\f794"; }
+.bi-1-circle-fill-1::before { content: "\f795"; }
+.bi-1-circle-fill::before { content: "\f796"; }
+.bi-1-circle::before { content: "\f797"; }
+.bi-1-square-fill::before { content: "\f798"; }
+.bi-1-square::before { content: "\f799"; }
+.bi-2-circle-1::before { content: "\f79a"; }
+.bi-2-circle-fill-1::before { content: "\f79b"; }
+.bi-2-circle-fill::before { content: "\f79c"; }
+.bi-2-circle::before { content: "\f79d"; }
+.bi-2-square-fill::before { content: "\f79e"; }
+.bi-2-square::before { content: "\f79f"; }
+.bi-3-circle-1::before { content: "\f7a0"; }
+.bi-3-circle-fill-1::before { content: "\f7a1"; }
+.bi-3-circle-fill::before { content: "\f7a2"; }
+.bi-3-circle::before { content: "\f7a3"; }
+.bi-3-square-fill::before { content: "\f7a4"; }
+.bi-3-square::before { content: "\f7a5"; }
+.bi-4-circle-1::before { content: "\f7a6"; }
+.bi-4-circle-fill-1::before { content: "\f7a7"; }
+.bi-4-circle-fill::before { content: "\f7a8"; }
+.bi-4-circle::before { content: "\f7a9"; }
+.bi-4-square-fill::before { content: "\f7aa"; }
+.bi-4-square::before { content: "\f7ab"; }
+.bi-5-circle-1::before { content: "\f7ac"; }
+.bi-5-circle-fill-1::before { content: "\f7ad"; }
+.bi-5-circle-fill::before { content: "\f7ae"; }
+.bi-5-circle::before { content: "\f7af"; }
+.bi-5-square-fill::before { content: "\f7b0"; }
+.bi-5-square::before { content: "\f7b1"; }
+.bi-6-circle-1::before { content: "\f7b2"; }
+.bi-6-circle-fill-1::before { content: "\f7b3"; }
+.bi-6-circle-fill::before { content: "\f7b4"; }
+.bi-6-circle::before { content: "\f7b5"; }
+.bi-6-square-fill::before { content: "\f7b6"; }
+.bi-6-square::before { content: "\f7b7"; }
+.bi-7-circle-1::before { content: "\f7b8"; }
+.bi-7-circle-fill-1::before { content: "\f7b9"; }
+.bi-7-circle-fill::before { content: "\f7ba"; }
+.bi-7-circle::before { content: "\f7bb"; }
+.bi-7-square-fill::before { content: "\f7bc"; }
+.bi-7-square::before { content: "\f7bd"; }
+.bi-8-circle-1::before { content: "\f7be"; }
+.bi-8-circle-fill-1::before { content: "\f7bf"; }
+.bi-8-circle-fill::before { content: "\f7c0"; }
+.bi-8-circle::before { content: "\f7c1"; }
+.bi-8-square-fill::before { content: "\f7c2"; }
+.bi-8-square::before { content: "\f7c3"; }
+.bi-9-circle-1::before { content: "\f7c4"; }
+.bi-9-circle-fill-1::before { content: "\f7c5"; }
+.bi-9-circle-fill::before { content: "\f7c6"; }
+.bi-9-circle::before { content: "\f7c7"; }
+.bi-9-square-fill::before { content: "\f7c8"; }
+.bi-9-square::before { content: "\f7c9"; }
+.bi-airplane-engines-fill::before { content: "\f7ca"; }
+.bi-airplane-engines::before { content: "\f7cb"; }
+.bi-airplane-fill::before { content: "\f7cc"; }
+.bi-airplane::before { content: "\f7cd"; }
+.bi-alexa::before { content: "\f7ce"; }
+.bi-alipay::before { content: "\f7cf"; }
+.bi-android::before { content: "\f7d0"; }
+.bi-android2::before { content: "\f7d1"; }
+.bi-box-fill::before { content: "\f7d2"; }
+.bi-box-seam-fill::before { content: "\f7d3"; }
+.bi-browser-chrome::before { content: "\f7d4"; }
+.bi-browser-edge::before { content: "\f7d5"; }
+.bi-browser-firefox::before { content: "\f7d6"; }
+.bi-browser-safari::before { content: "\f7d7"; }
+.bi-c-circle-1::before { content: "\f7d8"; }
+.bi-c-circle-fill-1::before { content: "\f7d9"; }
+.bi-c-circle-fill::before { content: "\f7da"; }
+.bi-c-circle::before { content: "\f7db"; }
+.bi-c-square-fill::before { content: "\f7dc"; }
+.bi-c-square::before { content: "\f7dd"; }
+.bi-capsule-pill::before { content: "\f7de"; }
+.bi-capsule::before { content: "\f7df"; }
+.bi-car-front-fill::before { content: "\f7e0"; }
+.bi-car-front::before { content: "\f7e1"; }
+.bi-cassette-fill::before { content: "\f7e2"; }
+.bi-cassette::before { content: "\f7e3"; }
+.bi-cc-circle-1::before { content: "\f7e4"; }
+.bi-cc-circle-fill-1::before { content: "\f7e5"; }
+.bi-cc-circle-fill::before { content: "\f7e6"; }
+.bi-cc-circle::before { content: "\f7e7"; }
+.bi-cc-square-fill::before { content: "\f7e8"; }
+.bi-cc-square::before { content: "\f7e9"; }
+.bi-cup-hot-fill::before { content: "\f7ea"; }
+.bi-cup-hot::before { content: "\f7eb"; }
+.bi-currency-rupee::before { content: "\f7ec"; }
+.bi-dropbox::before { content: "\f7ed"; }
+.bi-escape::before { content: "\f7ee"; }
+.bi-fast-forward-btn-fill::before { content: "\f7ef"; }
+.bi-fast-forward-btn::before { content: "\f7f0"; }
+.bi-fast-forward-circle-fill::before { content: "\f7f1"; }
+.bi-fast-forward-circle::before { content: "\f7f2"; }
+.bi-fast-forward-fill::before { content: "\f7f3"; }
+.bi-fast-forward::before { content: "\f7f4"; }
+.bi-filetype-sql::before { content: "\f7f5"; }
+.bi-fire::before { content: "\f7f6"; }
+.bi-google-play::before { content: "\f7f7"; }
+.bi-h-circle-1::before { content: "\f7f8"; }
+.bi-h-circle-fill-1::before { content: "\f7f9"; }
+.bi-h-circle-fill::before { content: "\f7fa"; }
+.bi-h-circle::before { content: "\f7fb"; }
+.bi-h-square-fill::before { content: "\f7fc"; }
+.bi-h-square::before { content: "\f7fd"; }
+.bi-indent::before { content: "\f7fe"; }
+.bi-lungs-fill::before { content: "\f7ff"; }
+.bi-lungs::before { content: "\f800"; }
+.bi-microsoft-teams::before { content: "\f801"; }
+.bi-p-circle-1::before { content: "\f802"; }
+.bi-p-circle-fill-1::before { content: "\f803"; }
+.bi-p-circle-fill::before { content: "\f804"; }
+.bi-p-circle::before { content: "\f805"; }
+.bi-p-square-fill::before { content: "\f806"; }
+.bi-p-square::before { content: "\f807"; }
+.bi-pass-fill::before { content: "\f808"; }
+.bi-pass::before { content: "\f809"; }
+.bi-prescription::before { content: "\f80a"; }
+.bi-prescription2::before { content: "\f80b"; }
+.bi-r-circle-1::before { content: "\f80c"; }
+.bi-r-circle-fill-1::before { content: "\f80d"; }
+.bi-r-circle-fill::before { content: "\f80e"; }
+.bi-r-circle::before { content: "\f80f"; }
+.bi-r-square-fill::before { content: "\f810"; }
+.bi-r-square::before { content: "\f811"; }
+.bi-repeat-1::before { content: "\f812"; }
+.bi-repeat::before { content: "\f813"; }
+.bi-rewind-btn-fill::before { content: "\f814"; }
+.bi-rewind-btn::before { content: "\f815"; }
+.bi-rewind-circle-fill::before { content: "\f816"; }
+.bi-rewind-circle::before { content: "\f817"; }
+.bi-rewind-fill::before { content: "\f818"; }
+.bi-rewind::before { content: "\f819"; }
+.bi-train-freight-front-fill::before { content: "\f81a"; }
+.bi-train-freight-front::before { content: "\f81b"; }
+.bi-train-front-fill::before { content: "\f81c"; }
+.bi-train-front::before { content: "\f81d"; }
+.bi-train-lightrail-front-fill::before { content: "\f81e"; }
+.bi-train-lightrail-front::before { content: "\f81f"; }
+.bi-truck-front-fill::before { content: "\f820"; }
+.bi-truck-front::before { content: "\f821"; }
+.bi-ubuntu::before { content: "\f822"; }
+.bi-unindent::before { content: "\f823"; }
+.bi-unity::before { content: "\f824"; }
+.bi-universal-access-circle::before { content: "\f825"; }
+.bi-universal-access::before { content: "\f826"; }
+.bi-virus::before { content: "\f827"; }
+.bi-virus2::before { content: "\f828"; }
+.bi-wechat::before { content: "\f829"; }
+.bi-yelp::before { content: "\f82a"; }
+.bi-sign-stop-fill::before { content: "\f82b"; }
+.bi-sign-stop-lights-fill::before { content: "\f82c"; }
+.bi-sign-stop-lights::before { content: "\f82d"; }
+.bi-sign-stop::before { content: "\f82e"; }
+.bi-sign-turn-left-fill::before { content: "\f82f"; }
+.bi-sign-turn-left::before { content: "\f830"; }
+.bi-sign-turn-right-fill::before { content: "\f831"; }
+.bi-sign-turn-right::before { content: "\f832"; }
+.bi-sign-turn-slight-left-fill::before { content: "\f833"; }
+.bi-sign-turn-slight-left::before { content: "\f834"; }
+.bi-sign-turn-slight-right-fill::before { content: "\f835"; }
+.bi-sign-turn-slight-right::before { content: "\f836"; }
+.bi-sign-yield-fill::before { content: "\f837"; }
+.bi-sign-yield::before { content: "\f838"; }
+.bi-ev-station-fill::before { content: "\f839"; }
+.bi-ev-station::before { content: "\f83a"; }
+.bi-fuel-pump-diesel-fill::before { content: "\f83b"; }
+.bi-fuel-pump-diesel::before { content: "\f83c"; }
+.bi-fuel-pump-fill::before { content: "\f83d"; }
+.bi-fuel-pump::before { content: "\f83e"; }
+.bi-0-circle-fill::before { content: "\f83f"; }
+.bi-0-circle::before { content: "\f840"; }
+.bi-0-square-fill::before { content: "\f841"; }
+.bi-0-square::before { content: "\f842"; }
+.bi-rocket-fill::before { content: "\f843"; }
+.bi-rocket-takeoff-fill::before { content: "\f844"; }
+.bi-rocket-takeoff::before { content: "\f845"; }
+.bi-rocket::before { content: "\f846"; }
+.bi-stripe::before { content: "\f847"; }
+.bi-subscript::before { content: "\f848"; }
+.bi-superscript::before { content: "\f849"; }
+.bi-trello::before { content: "\f84a"; }
+.bi-envelope-at-fill::before { content: "\f84b"; }
+.bi-envelope-at::before { content: "\f84c"; }
+.bi-regex::before { content: "\f84d"; }
+.bi-text-wrap::before { content: "\f84e"; }
+.bi-sign-dead-end-fill::before { content: "\f84f"; }
+.bi-sign-dead-end::before { content: "\f850"; }
+.bi-sign-do-not-enter-fill::before { content: "\f851"; }
+.bi-sign-do-not-enter::before { content: "\f852"; }
+.bi-sign-intersection-fill::before { content: "\f853"; }
+.bi-sign-intersection-side-fill::before { content: "\f854"; }
+.bi-sign-intersection-side::before { content: "\f855"; }
+.bi-sign-intersection-t-fill::before { content: "\f856"; }
+.bi-sign-intersection-t::before { content: "\f857"; }
+.bi-sign-intersection-y-fill::before { content: "\f858"; }
+.bi-sign-intersection-y::before { content: "\f859"; }
+.bi-sign-intersection::before { content: "\f85a"; }
+.bi-sign-merge-left-fill::before { content: "\f85b"; }
+.bi-sign-merge-left::before { content: "\f85c"; }
+.bi-sign-merge-right-fill::before { content: "\f85d"; }
+.bi-sign-merge-right::before { content: "\f85e"; }
+.bi-sign-no-left-turn-fill::before { content: "\f85f"; }
+.bi-sign-no-left-turn::before { content: "\f860"; }
+.bi-sign-no-parking-fill::before { content: "\f861"; }
+.bi-sign-no-parking::before { content: "\f862"; }
+.bi-sign-no-right-turn-fill::before { content: "\f863"; }
+.bi-sign-no-right-turn::before { content: "\f864"; }
+.bi-sign-railroad-fill::before { content: "\f865"; }
+.bi-sign-railroad::before { content: "\f866"; }
+.bi-building-add::before { content: "\f867"; }
+.bi-building-check::before { content: "\f868"; }
+.bi-building-dash::before { content: "\f869"; }
+.bi-building-down::before { content: "\f86a"; }
+.bi-building-exclamation::before { content: "\f86b"; }
+.bi-building-fill-add::before { content: "\f86c"; }
+.bi-building-fill-check::before { content: "\f86d"; }
+.bi-building-fill-dash::before { content: "\f86e"; }
+.bi-building-fill-down::before { content: "\f86f"; }
+.bi-building-fill-exclamation::before { content: "\f870"; }
+.bi-building-fill-gear::before { content: "\f871"; }
+.bi-building-fill-lock::before { content: "\f872"; }
+.bi-building-fill-slash::before { content: "\f873"; }
+.bi-building-fill-up::before { content: "\f874"; }
+.bi-building-fill-x::before { content: "\f875"; }
+.bi-building-fill::before { content: "\f876"; }
+.bi-building-gear::before { content: "\f877"; }
+.bi-building-lock::before { content: "\f878"; }
+.bi-building-slash::before { content: "\f879"; }
+.bi-building-up::before { content: "\f87a"; }
+.bi-building-x::before { content: "\f87b"; }
+.bi-buildings-fill::before { content: "\f87c"; }
+.bi-buildings::before { content: "\f87d"; }
+.bi-bus-front-fill::before { content: "\f87e"; }
+.bi-bus-front::before { content: "\f87f"; }
+.bi-ev-front-fill::before { content: "\f880"; }
+.bi-ev-front::before { content: "\f881"; }
+.bi-globe-americas::before { content: "\f882"; }
+.bi-globe-asia-australia::before { content: "\f883"; }
+.bi-globe-central-south-asia::before { content: "\f884"; }
+.bi-globe-europe-africa::before { content: "\f885"; }
+.bi-house-add-fill::before { content: "\f886"; }
+.bi-house-add::before { content: "\f887"; }
+.bi-house-check-fill::before { content: "\f888"; }
+.bi-house-check::before { content: "\f889"; }
+.bi-house-dash-fill::before { content: "\f88a"; }
+.bi-house-dash::before { content: "\f88b"; }
+.bi-house-down-fill::before { content: "\f88c"; }
+.bi-house-down::before { content: "\f88d"; }
+.bi-house-exclamation-fill::before { content: "\f88e"; }
+.bi-house-exclamation::before { content: "\f88f"; }
+.bi-house-gear-fill::before { content: "\f890"; }
+.bi-house-gear::before { content: "\f891"; }
+.bi-house-lock-fill::before { content: "\f892"; }
+.bi-house-lock::before { content: "\f893"; }
+.bi-house-slash-fill::before { content: "\f894"; }
+.bi-house-slash::before { content: "\f895"; }
+.bi-house-up-fill::before { content: "\f896"; }
+.bi-house-up::before { content: "\f897"; }
+.bi-house-x-fill::before { content: "\f898"; }
+.bi-house-x::before { content: "\f899"; }
+.bi-person-add::before { content: "\f89a"; }
+.bi-person-down::before { content: "\f89b"; }
+.bi-person-exclamation::before { content: "\f89c"; }
+.bi-person-fill-add::before { content: "\f89d"; }
+.bi-person-fill-check::before { content: "\f89e"; }
+.bi-person-fill-dash::before { content: "\f89f"; }
+.bi-person-fill-down::before { content: "\f8a0"; }
+.bi-person-fill-exclamation::before { content: "\f8a1"; }
+.bi-person-fill-gear::before { content: "\f8a2"; }
+.bi-person-fill-lock::before { content: "\f8a3"; }
+.bi-person-fill-slash::before { content: "\f8a4"; }
+.bi-person-fill-up::before { content: "\f8a5"; }
+.bi-person-fill-x::before { content: "\f8a6"; }
+.bi-person-gear::before { content: "\f8a7"; }
+.bi-person-lock::before { content: "\f8a8"; }
+.bi-person-slash::before { content: "\f8a9"; }
+.bi-person-up::before { content: "\f8aa"; }
+.bi-scooter::before { content: "\f8ab"; }
+.bi-taxi-front-fill::before { content: "\f8ac"; }
+.bi-taxi-front::before { content: "\f8ad"; }
+.bi-amd::before { content: "\f8ae"; }
+.bi-database-add::before { content: "\f8af"; }
+.bi-database-check::before { content: "\f8b0"; }
+.bi-database-dash::before { content: "\f8b1"; }
+.bi-database-down::before { content: "\f8b2"; }
+.bi-database-exclamation::before { content: "\f8b3"; }
+.bi-database-fill-add::before { content: "\f8b4"; }
+.bi-database-fill-check::before { content: "\f8b5"; }
+.bi-database-fill-dash::before { content: "\f8b6"; }
+.bi-database-fill-down::before { content: "\f8b7"; }
+.bi-database-fill-exclamation::before { content: "\f8b8"; }
+.bi-database-fill-gear::before { content: "\f8b9"; }
+.bi-database-fill-lock::before { content: "\f8ba"; }
+.bi-database-fill-slash::before { content: "\f8bb"; }
+.bi-database-fill-up::before { content: "\f8bc"; }
+.bi-database-fill-x::before { content: "\f8bd"; }
+.bi-database-fill::before { content: "\f8be"; }
+.bi-database-gear::before { content: "\f8bf"; }
+.bi-database-lock::before { content: "\f8c0"; }
+.bi-database-slash::before { content: "\f8c1"; }
+.bi-database-up::before { content: "\f8c2"; }
+.bi-database-x::before { content: "\f8c3"; }
+.bi-database::before { content: "\f8c4"; }
+.bi-houses-fill::before { content: "\f8c5"; }
+.bi-houses::before { content: "\f8c6"; }
+.bi-nvidia::before { content: "\f8c7"; }
+.bi-person-vcard-fill::before { content: "\f8c8"; }
+.bi-person-vcard::before { content: "\f8c9"; }
+.bi-sina-weibo::before { content: "\f8ca"; }
+.bi-tencent-qq::before { content: "\f8cb"; }
+.bi-wikipedia::before { content: "\f8cc"; }
diff --git a/site_libs/bootstrap/bootstrap-icons.woff b/site_libs/bootstrap/bootstrap-icons.woff
new file mode 100644
index 00000000..18d21d45
Binary files /dev/null and b/site_libs/bootstrap/bootstrap-icons.woff differ
diff --git a/site_libs/bootstrap/bootstrap.min.css b/site_libs/bootstrap/bootstrap.min.css
new file mode 100644
index 00000000..7a2c054c
--- /dev/null
+++ b/site_libs/bootstrap/bootstrap.min.css
@@ -0,0 +1,10 @@
+/*!
+ * Bootstrap v5.1.3 (https://getbootstrap.com/)
+ * Copyright 2011-2021 The Bootstrap Authors
+ * Copyright 2011-2021 Twitter, Inc.
+ * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)
+ */@import"https://fonts.googleapis.com/css2?family=Source+Sans+Pro:wght@300;400;700&display=swap";:root{--bs-blue: #2780e3;--bs-indigo: #6610f2;--bs-purple: #613d7c;--bs-pink: #e83e8c;--bs-red: #ff0039;--bs-orange: #f0ad4e;--bs-yellow: #ff7518;--bs-green: #3fb618;--bs-teal: #20c997;--bs-cyan: #9954bb;--bs-white: #fff;--bs-gray: #6c757d;--bs-gray-dark: #373a3c;--bs-gray-100: #f8f9fa;--bs-gray-200: #e9ecef;--bs-gray-300: #dee2e6;--bs-gray-400: #ced4da;--bs-gray-500: #adb5bd;--bs-gray-600: #6c757d;--bs-gray-700: #495057;--bs-gray-800: #373a3c;--bs-gray-900: #212529;--bs-default: #373a3c;--bs-primary: #2780e3;--bs-secondary: #373a3c;--bs-success: #3fb618;--bs-info: #9954bb;--bs-warning: #ff7518;--bs-danger: #ff0039;--bs-light: #f8f9fa;--bs-dark: #373a3c;--bs-default-rgb: 55, 58, 60;--bs-primary-rgb: 39, 128, 227;--bs-secondary-rgb: 55, 58, 60;--bs-success-rgb: 63, 182, 24;--bs-info-rgb: 153, 84, 187;--bs-warning-rgb: 255, 117, 24;--bs-danger-rgb: 255, 0, 57;--bs-light-rgb: 248, 249, 250;--bs-dark-rgb: 55, 58, 60;--bs-white-rgb: 255, 255, 255;--bs-black-rgb: 0, 0, 0;--bs-body-color-rgb: 55, 58, 60;--bs-body-bg-rgb: 255, 255, 255;--bs-font-sans-serif: "Source Sans Pro", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol";--bs-font-monospace: SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;--bs-gradient: linear-gradient(180deg, rgba(255, 255, 255, 0.15), rgba(255, 255, 255, 0));--bs-root-font-size: 1em;--bs-body-font-family: var(--bs-font-sans-serif);--bs-body-font-size: 1rem;--bs-body-font-weight: 400;--bs-body-line-height: 1.7;--bs-body-color: #373a3c;--bs-body-bg: #fff}*,*::before,*::after{box-sizing:border-box}:root{font-size:var(--bs-root-font-size)}body{margin:0;font-family:var(--bs-body-font-family);font-size:var(--bs-body-font-size);font-weight:var(--bs-body-font-weight);line-height:var(--bs-body-line-height);color:var(--bs-body-color);text-align:var(--bs-body-text-align);background-color:var(--bs-body-bg);-webkit-text-size-adjust:100%;-webkit-tap-highlight-color:rgba(0,0,0,0)}hr{margin:1rem 0;color:inherit;background-color:currentColor;border:0;opacity:.25}hr:not([size]){height:1px}h6,.h6,h5,.h5,h4,.h4,h3,.h3,h2,.h2,h1,.h1{margin-top:0;margin-bottom:.5rem;font-weight:400;line-height:1.2}h1,.h1{font-size:calc(1.325rem + 0.9vw)}@media(min-width: 1200px){h1,.h1{font-size:2rem}}h2,.h2{font-size:calc(1.29rem + 0.48vw)}@media(min-width: 1200px){h2,.h2{font-size:1.65rem}}h3,.h3{font-size:calc(1.27rem + 0.24vw)}@media(min-width: 1200px){h3,.h3{font-size:1.45rem}}h4,.h4{font-size:1.25rem}h5,.h5{font-size:1.1rem}h6,.h6{font-size:1rem}p{margin-top:0;margin-bottom:1rem}abbr[title],abbr[data-bs-original-title]{text-decoration:underline dotted;-webkit-text-decoration:underline dotted;-moz-text-decoration:underline dotted;-ms-text-decoration:underline dotted;-o-text-decoration:underline dotted;cursor:help;text-decoration-skip-ink:none}address{margin-bottom:1rem;font-style:normal;line-height:inherit}ol,ul{padding-left:2rem}ol,ul,dl{margin-top:0;margin-bottom:1rem}ol ol,ul ul,ol ul,ul ol{margin-bottom:0}dt{font-weight:700}dd{margin-bottom:.5rem;margin-left:0}blockquote{margin:0 0 1rem;padding:.625rem 1.25rem;border-left:.25rem solid #e9ecef}blockquote p:last-child,blockquote ul:last-child,blockquote ol:last-child{margin-bottom:0}b,strong{font-weight:bolder}small,.small{font-size:0.875em}mark,.mark{padding:.2em;background-color:#fcf8e3}sub,sup{position:relative;font-size:0.75em;line-height:0;vertical-align:baseline}sub{bottom:-0.25em}sup{top:-0.5em}a{color:#2780e3;text-decoration:underline;-webkit-text-decoration:underline;-moz-text-decoration:underline;-ms-text-decoration:underline;-o-text-decoration:underline}a:hover{color:#1f66b6}a:not([href]):not([class]),a:not([href]):not([class]):hover{color:inherit;text-decoration:none}pre,code,kbd,samp{font-family:var(--bs-font-monospace);font-size:1em;direction:ltr /* rtl:ignore */;unicode-bidi:bidi-override}pre{display:block;margin-top:0;margin-bottom:1rem;overflow:auto;font-size:0.875em;color:#000;background-color:#f7f7f7;padding:.5rem;border:1px solid #dee2e6}pre code{background-color:rgba(0,0,0,0);font-size:inherit;color:inherit;word-break:normal}code{font-size:0.875em;color:#9753b8;background-color:#f7f7f7;padding:.125rem .25rem;word-wrap:break-word}a>code{color:inherit}kbd{padding:.4rem .4rem;font-size:0.875em;color:#fff;background-color:#212529}kbd kbd{padding:0;font-size:1em;font-weight:700}figure{margin:0 0 1rem}img,svg{vertical-align:middle}table{caption-side:bottom;border-collapse:collapse}caption{padding-top:.5rem;padding-bottom:.5rem;color:#6c757d;text-align:left}th{text-align:inherit;text-align:-webkit-match-parent}thead,tbody,tfoot,tr,td,th{border-color:inherit;border-style:solid;border-width:0}label{display:inline-block}button{border-radius:0}button:focus:not(:focus-visible){outline:0}input,button,select,optgroup,textarea{margin:0;font-family:inherit;font-size:inherit;line-height:inherit}button,select{text-transform:none}[role=button]{cursor:pointer}select{word-wrap:normal}select:disabled{opacity:1}[list]::-webkit-calendar-picker-indicator{display:none}button,[type=button],[type=reset],[type=submit]{-webkit-appearance:button}button:not(:disabled),[type=button]:not(:disabled),[type=reset]:not(:disabled),[type=submit]:not(:disabled){cursor:pointer}::-moz-focus-inner{padding:0;border-style:none}textarea{resize:vertical}fieldset{min-width:0;padding:0;margin:0;border:0}legend{float:left;width:100%;padding:0;margin-bottom:.5rem;font-size:calc(1.275rem + 0.3vw);line-height:inherit}@media(min-width: 1200px){legend{font-size:1.5rem}}legend+*{clear:left}::-webkit-datetime-edit-fields-wrapper,::-webkit-datetime-edit-text,::-webkit-datetime-edit-minute,::-webkit-datetime-edit-hour-field,::-webkit-datetime-edit-day-field,::-webkit-datetime-edit-month-field,::-webkit-datetime-edit-year-field{padding:0}::-webkit-inner-spin-button{height:auto}[type=search]{outline-offset:-2px;-webkit-appearance:textfield}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-color-swatch-wrapper{padding:0}::file-selector-button{font:inherit}::-webkit-file-upload-button{font:inherit;-webkit-appearance:button}output{display:inline-block}iframe{border:0}summary{display:list-item;cursor:pointer}progress{vertical-align:baseline}[hidden]{display:none !important}.lead{font-size:1.25rem;font-weight:300}.display-1{font-size:calc(1.625rem + 4.5vw);font-weight:300;line-height:1.2}@media(min-width: 1200px){.display-1{font-size:5rem}}.display-2{font-size:calc(1.575rem + 3.9vw);font-weight:300;line-height:1.2}@media(min-width: 1200px){.display-2{font-size:4.5rem}}.display-3{font-size:calc(1.525rem + 3.3vw);font-weight:300;line-height:1.2}@media(min-width: 1200px){.display-3{font-size:4rem}}.display-4{font-size:calc(1.475rem + 2.7vw);font-weight:300;line-height:1.2}@media(min-width: 1200px){.display-4{font-size:3.5rem}}.display-5{font-size:calc(1.425rem + 2.1vw);font-weight:300;line-height:1.2}@media(min-width: 1200px){.display-5{font-size:3rem}}.display-6{font-size:calc(1.375rem + 1.5vw);font-weight:300;line-height:1.2}@media(min-width: 1200px){.display-6{font-size:2.5rem}}.list-unstyled{padding-left:0;list-style:none}.list-inline{padding-left:0;list-style:none}.list-inline-item{display:inline-block}.list-inline-item:not(:last-child){margin-right:.5rem}.initialism{font-size:0.875em;text-transform:uppercase}.blockquote{margin-bottom:1rem;font-size:1.25rem}.blockquote>:last-child{margin-bottom:0}.blockquote-footer{margin-top:-1rem;margin-bottom:1rem;font-size:0.875em;color:#6c757d}.blockquote-footer::before{content:"— "}.img-fluid{max-width:100%;height:auto}.img-thumbnail{padding:.25rem;background-color:#fff;border:1px solid #dee2e6;max-width:100%;height:auto}.figure{display:inline-block}.figure-img{margin-bottom:.5rem;line-height:1}.figure-caption{font-size:0.875em;color:#6c757d}.grid{display:grid;grid-template-rows:repeat(var(--bs-rows, 1), 1fr);grid-template-columns:repeat(var(--bs-columns, 12), 1fr);gap:var(--bs-gap, 1.5rem)}.grid .g-col-1{grid-column:auto/span 1}.grid .g-col-2{grid-column:auto/span 2}.grid .g-col-3{grid-column:auto/span 3}.grid .g-col-4{grid-column:auto/span 4}.grid .g-col-5{grid-column:auto/span 5}.grid .g-col-6{grid-column:auto/span 6}.grid .g-col-7{grid-column:auto/span 7}.grid .g-col-8{grid-column:auto/span 8}.grid .g-col-9{grid-column:auto/span 9}.grid .g-col-10{grid-column:auto/span 10}.grid .g-col-11{grid-column:auto/span 11}.grid .g-col-12{grid-column:auto/span 12}.grid .g-start-1{grid-column-start:1}.grid .g-start-2{grid-column-start:2}.grid .g-start-3{grid-column-start:3}.grid .g-start-4{grid-column-start:4}.grid .g-start-5{grid-column-start:5}.grid .g-start-6{grid-column-start:6}.grid .g-start-7{grid-column-start:7}.grid .g-start-8{grid-column-start:8}.grid .g-start-9{grid-column-start:9}.grid .g-start-10{grid-column-start:10}.grid .g-start-11{grid-column-start:11}@media(min-width: 576px){.grid .g-col-sm-1{grid-column:auto/span 1}.grid .g-col-sm-2{grid-column:auto/span 2}.grid .g-col-sm-3{grid-column:auto/span 3}.grid .g-col-sm-4{grid-column:auto/span 4}.grid .g-col-sm-5{grid-column:auto/span 5}.grid .g-col-sm-6{grid-column:auto/span 6}.grid .g-col-sm-7{grid-column:auto/span 7}.grid .g-col-sm-8{grid-column:auto/span 8}.grid .g-col-sm-9{grid-column:auto/span 9}.grid .g-col-sm-10{grid-column:auto/span 10}.grid .g-col-sm-11{grid-column:auto/span 11}.grid .g-col-sm-12{grid-column:auto/span 12}.grid .g-start-sm-1{grid-column-start:1}.grid .g-start-sm-2{grid-column-start:2}.grid .g-start-sm-3{grid-column-start:3}.grid .g-start-sm-4{grid-column-start:4}.grid .g-start-sm-5{grid-column-start:5}.grid .g-start-sm-6{grid-column-start:6}.grid .g-start-sm-7{grid-column-start:7}.grid .g-start-sm-8{grid-column-start:8}.grid .g-start-sm-9{grid-column-start:9}.grid .g-start-sm-10{grid-column-start:10}.grid .g-start-sm-11{grid-column-start:11}}@media(min-width: 768px){.grid .g-col-md-1{grid-column:auto/span 1}.grid .g-col-md-2{grid-column:auto/span 2}.grid .g-col-md-3{grid-column:auto/span 3}.grid .g-col-md-4{grid-column:auto/span 4}.grid .g-col-md-5{grid-column:auto/span 5}.grid .g-col-md-6{grid-column:auto/span 6}.grid .g-col-md-7{grid-column:auto/span 7}.grid .g-col-md-8{grid-column:auto/span 8}.grid .g-col-md-9{grid-column:auto/span 9}.grid .g-col-md-10{grid-column:auto/span 10}.grid .g-col-md-11{grid-column:auto/span 11}.grid .g-col-md-12{grid-column:auto/span 12}.grid .g-start-md-1{grid-column-start:1}.grid .g-start-md-2{grid-column-start:2}.grid .g-start-md-3{grid-column-start:3}.grid .g-start-md-4{grid-column-start:4}.grid .g-start-md-5{grid-column-start:5}.grid .g-start-md-6{grid-column-start:6}.grid .g-start-md-7{grid-column-start:7}.grid .g-start-md-8{grid-column-start:8}.grid .g-start-md-9{grid-column-start:9}.grid .g-start-md-10{grid-column-start:10}.grid .g-start-md-11{grid-column-start:11}}@media(min-width: 992px){.grid .g-col-lg-1{grid-column:auto/span 1}.grid .g-col-lg-2{grid-column:auto/span 2}.grid .g-col-lg-3{grid-column:auto/span 3}.grid .g-col-lg-4{grid-column:auto/span 4}.grid .g-col-lg-5{grid-column:auto/span 5}.grid .g-col-lg-6{grid-column:auto/span 6}.grid .g-col-lg-7{grid-column:auto/span 7}.grid .g-col-lg-8{grid-column:auto/span 8}.grid .g-col-lg-9{grid-column:auto/span 9}.grid .g-col-lg-10{grid-column:auto/span 10}.grid .g-col-lg-11{grid-column:auto/span 11}.grid .g-col-lg-12{grid-column:auto/span 12}.grid .g-start-lg-1{grid-column-start:1}.grid .g-start-lg-2{grid-column-start:2}.grid .g-start-lg-3{grid-column-start:3}.grid .g-start-lg-4{grid-column-start:4}.grid .g-start-lg-5{grid-column-start:5}.grid .g-start-lg-6{grid-column-start:6}.grid .g-start-lg-7{grid-column-start:7}.grid .g-start-lg-8{grid-column-start:8}.grid .g-start-lg-9{grid-column-start:9}.grid .g-start-lg-10{grid-column-start:10}.grid .g-start-lg-11{grid-column-start:11}}@media(min-width: 1200px){.grid .g-col-xl-1{grid-column:auto/span 1}.grid .g-col-xl-2{grid-column:auto/span 2}.grid .g-col-xl-3{grid-column:auto/span 3}.grid .g-col-xl-4{grid-column:auto/span 4}.grid .g-col-xl-5{grid-column:auto/span 5}.grid .g-col-xl-6{grid-column:auto/span 6}.grid .g-col-xl-7{grid-column:auto/span 7}.grid .g-col-xl-8{grid-column:auto/span 8}.grid .g-col-xl-9{grid-column:auto/span 9}.grid .g-col-xl-10{grid-column:auto/span 10}.grid .g-col-xl-11{grid-column:auto/span 11}.grid .g-col-xl-12{grid-column:auto/span 12}.grid .g-start-xl-1{grid-column-start:1}.grid .g-start-xl-2{grid-column-start:2}.grid .g-start-xl-3{grid-column-start:3}.grid .g-start-xl-4{grid-column-start:4}.grid .g-start-xl-5{grid-column-start:5}.grid .g-start-xl-6{grid-column-start:6}.grid .g-start-xl-7{grid-column-start:7}.grid .g-start-xl-8{grid-column-start:8}.grid .g-start-xl-9{grid-column-start:9}.grid .g-start-xl-10{grid-column-start:10}.grid .g-start-xl-11{grid-column-start:11}}@media(min-width: 1400px){.grid .g-col-xxl-1{grid-column:auto/span 1}.grid .g-col-xxl-2{grid-column:auto/span 2}.grid .g-col-xxl-3{grid-column:auto/span 3}.grid .g-col-xxl-4{grid-column:auto/span 4}.grid .g-col-xxl-5{grid-column:auto/span 5}.grid .g-col-xxl-6{grid-column:auto/span 6}.grid .g-col-xxl-7{grid-column:auto/span 7}.grid .g-col-xxl-8{grid-column:auto/span 8}.grid .g-col-xxl-9{grid-column:auto/span 9}.grid .g-col-xxl-10{grid-column:auto/span 10}.grid .g-col-xxl-11{grid-column:auto/span 11}.grid .g-col-xxl-12{grid-column:auto/span 12}.grid .g-start-xxl-1{grid-column-start:1}.grid .g-start-xxl-2{grid-column-start:2}.grid .g-start-xxl-3{grid-column-start:3}.grid .g-start-xxl-4{grid-column-start:4}.grid .g-start-xxl-5{grid-column-start:5}.grid .g-start-xxl-6{grid-column-start:6}.grid .g-start-xxl-7{grid-column-start:7}.grid .g-start-xxl-8{grid-column-start:8}.grid .g-start-xxl-9{grid-column-start:9}.grid .g-start-xxl-10{grid-column-start:10}.grid .g-start-xxl-11{grid-column-start:11}}.table{--bs-table-bg: transparent;--bs-table-accent-bg: transparent;--bs-table-striped-color: #373a3c;--bs-table-striped-bg: rgba(0, 0, 0, 0.05);--bs-table-active-color: #373a3c;--bs-table-active-bg: rgba(0, 0, 0, 0.1);--bs-table-hover-color: #373a3c;--bs-table-hover-bg: rgba(0, 0, 0, 0.075);width:100%;margin-bottom:1rem;color:#373a3c;vertical-align:top;border-color:#dee2e6}.table>:not(caption)>*>*{padding:.5rem .5rem;background-color:var(--bs-table-bg);border-bottom-width:1px;box-shadow:inset 0 0 0 9999px var(--bs-table-accent-bg)}.table>tbody{vertical-align:inherit}.table>thead{vertical-align:bottom}.table>:not(:first-child){border-top:2px solid #b6babc}.caption-top{caption-side:top}.table-sm>:not(caption)>*>*{padding:.25rem .25rem}.table-bordered>:not(caption)>*{border-width:1px 0}.table-bordered>:not(caption)>*>*{border-width:0 1px}.table-borderless>:not(caption)>*>*{border-bottom-width:0}.table-borderless>:not(:first-child){border-top-width:0}.table-striped>tbody>tr:nth-of-type(odd)>*{--bs-table-accent-bg: var(--bs-table-striped-bg);color:var(--bs-table-striped-color)}.table-active{--bs-table-accent-bg: var(--bs-table-active-bg);color:var(--bs-table-active-color)}.table-hover>tbody>tr:hover>*{--bs-table-accent-bg: var(--bs-table-hover-bg);color:var(--bs-table-hover-color)}.table-primary{--bs-table-bg: #d4e6f9;--bs-table-striped-bg: #c9dbed;--bs-table-striped-color: #000;--bs-table-active-bg: #bfcfe0;--bs-table-active-color: #000;--bs-table-hover-bg: #c4d5e6;--bs-table-hover-color: #000;color:#000;border-color:#bfcfe0}.table-secondary{--bs-table-bg: #d7d8d8;--bs-table-striped-bg: #cccdcd;--bs-table-striped-color: #000;--bs-table-active-bg: #c2c2c2;--bs-table-active-color: #000;--bs-table-hover-bg: #c7c8c8;--bs-table-hover-color: #000;color:#000;border-color:#c2c2c2}.table-success{--bs-table-bg: #d9f0d1;--bs-table-striped-bg: #cee4c7;--bs-table-striped-color: #000;--bs-table-active-bg: #c3d8bc;--bs-table-active-color: #000;--bs-table-hover-bg: #c9dec1;--bs-table-hover-color: #000;color:#000;border-color:#c3d8bc}.table-info{--bs-table-bg: #ebddf1;--bs-table-striped-bg: #dfd2e5;--bs-table-striped-color: #000;--bs-table-active-bg: #d4c7d9;--bs-table-active-color: #000;--bs-table-hover-bg: #d9ccdf;--bs-table-hover-color: #000;color:#000;border-color:#d4c7d9}.table-warning{--bs-table-bg: #ffe3d1;--bs-table-striped-bg: #f2d8c7;--bs-table-striped-color: #000;--bs-table-active-bg: #e6ccbc;--bs-table-active-color: #000;--bs-table-hover-bg: #ecd2c1;--bs-table-hover-color: #000;color:#000;border-color:#e6ccbc}.table-danger{--bs-table-bg: #ffccd7;--bs-table-striped-bg: #f2c2cc;--bs-table-striped-color: #000;--bs-table-active-bg: #e6b8c2;--bs-table-active-color: #000;--bs-table-hover-bg: #ecbdc7;--bs-table-hover-color: #000;color:#000;border-color:#e6b8c2}.table-light{--bs-table-bg: #f8f9fa;--bs-table-striped-bg: #ecedee;--bs-table-striped-color: #000;--bs-table-active-bg: #dfe0e1;--bs-table-active-color: #000;--bs-table-hover-bg: #e5e6e7;--bs-table-hover-color: #000;color:#000;border-color:#dfe0e1}.table-dark{--bs-table-bg: #373a3c;--bs-table-striped-bg: #414446;--bs-table-striped-color: #fff;--bs-table-active-bg: #4b4e50;--bs-table-active-color: #fff;--bs-table-hover-bg: #46494b;--bs-table-hover-color: #fff;color:#fff;border-color:#4b4e50}.table-responsive{overflow-x:auto;-webkit-overflow-scrolling:touch}@media(max-width: 575.98px){.table-responsive-sm{overflow-x:auto;-webkit-overflow-scrolling:touch}}@media(max-width: 767.98px){.table-responsive-md{overflow-x:auto;-webkit-overflow-scrolling:touch}}@media(max-width: 991.98px){.table-responsive-lg{overflow-x:auto;-webkit-overflow-scrolling:touch}}@media(max-width: 1199.98px){.table-responsive-xl{overflow-x:auto;-webkit-overflow-scrolling:touch}}@media(max-width: 1399.98px){.table-responsive-xxl{overflow-x:auto;-webkit-overflow-scrolling:touch}}.form-label,.shiny-input-container .control-label{margin-bottom:.5rem}.col-form-label{padding-top:calc(0.375rem + 1px);padding-bottom:calc(0.375rem + 1px);margin-bottom:0;font-size:inherit;line-height:1.5}.col-form-label-lg{padding-top:calc(0.5rem + 1px);padding-bottom:calc(0.5rem + 1px);font-size:1.25rem}.col-form-label-sm{padding-top:calc(0.25rem + 1px);padding-bottom:calc(0.25rem + 1px);font-size:0.875rem}.form-text{margin-top:.25rem;font-size:0.875em;color:#6c757d}.form-control{display:block;width:100%;padding:.375rem .75rem;font-size:1rem;font-weight:400;line-height:1.5;color:#373a3c;background-color:#fff;background-clip:padding-box;border:1px solid #ced4da;appearance:none;-webkit-appearance:none;-moz-appearance:none;-ms-appearance:none;-o-appearance:none;border-radius:0;transition:border-color .15s ease-in-out,box-shadow .15s ease-in-out}@media(prefers-reduced-motion: reduce){.form-control{transition:none}}.form-control[type=file]{overflow:hidden}.form-control[type=file]:not(:disabled):not([readonly]){cursor:pointer}.form-control:focus{color:#373a3c;background-color:#fff;border-color:#93c0f1;outline:0;box-shadow:0 0 0 .25rem rgba(39,128,227,.25)}.form-control::-webkit-date-and-time-value{height:1.5em}.form-control::placeholder{color:#6c757d;opacity:1}.form-control:disabled,.form-control[readonly]{background-color:#e9ecef;opacity:1}.form-control::file-selector-button{padding:.375rem .75rem;margin:-0.375rem -0.75rem;margin-inline-end:.75rem;color:#373a3c;background-color:#e9ecef;pointer-events:none;border-color:inherit;border-style:solid;border-width:0;border-inline-end-width:1px;border-radius:0;transition:color .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out}@media(prefers-reduced-motion: reduce){.form-control::file-selector-button{transition:none}}.form-control:hover:not(:disabled):not([readonly])::file-selector-button{background-color:#dde0e3}.form-control::-webkit-file-upload-button{padding:.375rem .75rem;margin:-0.375rem -0.75rem;margin-inline-end:.75rem;color:#373a3c;background-color:#e9ecef;pointer-events:none;border-color:inherit;border-style:solid;border-width:0;border-inline-end-width:1px;border-radius:0;transition:color .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out}@media(prefers-reduced-motion: reduce){.form-control::-webkit-file-upload-button{transition:none}}.form-control:hover:not(:disabled):not([readonly])::-webkit-file-upload-button{background-color:#dde0e3}.form-control-plaintext{display:block;width:100%;padding:.375rem 0;margin-bottom:0;line-height:1.5;color:#373a3c;background-color:rgba(0,0,0,0);border:solid rgba(0,0,0,0);border-width:1px 0}.form-control-plaintext.form-control-sm,.form-control-plaintext.form-control-lg{padding-right:0;padding-left:0}.form-control-sm{min-height:calc(1.5em + 0.5rem + 2px);padding:.25rem .5rem;font-size:0.875rem}.form-control-sm::file-selector-button{padding:.25rem .5rem;margin:-0.25rem -0.5rem;margin-inline-end:.5rem}.form-control-sm::-webkit-file-upload-button{padding:.25rem .5rem;margin:-0.25rem -0.5rem;margin-inline-end:.5rem}.form-control-lg{min-height:calc(1.5em + 1rem + 2px);padding:.5rem 1rem;font-size:1.25rem}.form-control-lg::file-selector-button{padding:.5rem 1rem;margin:-0.5rem -1rem;margin-inline-end:1rem}.form-control-lg::-webkit-file-upload-button{padding:.5rem 1rem;margin:-0.5rem -1rem;margin-inline-end:1rem}textarea.form-control{min-height:calc(1.5em + 0.75rem + 2px)}textarea.form-control-sm{min-height:calc(1.5em + 0.5rem + 2px)}textarea.form-control-lg{min-height:calc(1.5em + 1rem + 2px)}.form-control-color{width:3rem;height:auto;padding:.375rem}.form-control-color:not(:disabled):not([readonly]){cursor:pointer}.form-control-color::-moz-color-swatch{height:1.5em}.form-control-color::-webkit-color-swatch{height:1.5em}.form-select{display:block;width:100%;padding:.375rem 2.25rem .375rem .75rem;-moz-padding-start:calc(0.75rem - 3px);font-size:1rem;font-weight:400;line-height:1.5;color:#373a3c;background-color:#fff;background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3e%3cpath fill='none' stroke='%23373a3c' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='M2 5l6 6 6-6'/%3e%3c/svg%3e");background-repeat:no-repeat;background-position:right .75rem center;background-size:16px 12px;border:1px solid #ced4da;border-radius:0;transition:border-color .15s ease-in-out,box-shadow .15s ease-in-out;appearance:none;-webkit-appearance:none;-moz-appearance:none;-ms-appearance:none;-o-appearance:none}@media(prefers-reduced-motion: reduce){.form-select{transition:none}}.form-select:focus{border-color:#93c0f1;outline:0;box-shadow:0 0 0 .25rem rgba(39,128,227,.25)}.form-select[multiple],.form-select[size]:not([size="1"]){padding-right:.75rem;background-image:none}.form-select:disabled{background-color:#e9ecef}.form-select:-moz-focusring{color:rgba(0,0,0,0);text-shadow:0 0 0 #373a3c}.form-select-sm{padding-top:.25rem;padding-bottom:.25rem;padding-left:.5rem;font-size:0.875rem}.form-select-lg{padding-top:.5rem;padding-bottom:.5rem;padding-left:1rem;font-size:1.25rem}.form-check,.shiny-input-container .checkbox,.shiny-input-container .radio{display:block;min-height:1.5rem;padding-left:0;margin-bottom:.125rem}.form-check .form-check-input,.form-check .shiny-input-container .checkbox input,.form-check .shiny-input-container .radio input,.shiny-input-container .checkbox .form-check-input,.shiny-input-container .checkbox .shiny-input-container .checkbox input,.shiny-input-container .checkbox .shiny-input-container .radio input,.shiny-input-container .radio .form-check-input,.shiny-input-container .radio .shiny-input-container .checkbox input,.shiny-input-container .radio .shiny-input-container .radio input{float:left;margin-left:0}.form-check-input,.shiny-input-container .checkbox input,.shiny-input-container .checkbox-inline input,.shiny-input-container .radio input,.shiny-input-container .radio-inline input{width:1em;height:1em;margin-top:.35em;vertical-align:top;background-color:#fff;background-repeat:no-repeat;background-position:center;background-size:contain;border:1px solid rgba(0,0,0,.25);appearance:none;-webkit-appearance:none;-moz-appearance:none;-ms-appearance:none;-o-appearance:none;color-adjust:exact;-webkit-print-color-adjust:exact}.form-check-input[type=radio],.shiny-input-container .checkbox input[type=radio],.shiny-input-container .checkbox-inline input[type=radio],.shiny-input-container .radio input[type=radio],.shiny-input-container .radio-inline input[type=radio]{border-radius:50%}.form-check-input:active,.shiny-input-container .checkbox input:active,.shiny-input-container .checkbox-inline input:active,.shiny-input-container .radio input:active,.shiny-input-container .radio-inline input:active{filter:brightness(90%)}.form-check-input:focus,.shiny-input-container .checkbox input:focus,.shiny-input-container .checkbox-inline input:focus,.shiny-input-container .radio input:focus,.shiny-input-container .radio-inline input:focus{border-color:#93c0f1;outline:0;box-shadow:0 0 0 .25rem rgba(39,128,227,.25)}.form-check-input:checked,.shiny-input-container .checkbox input:checked,.shiny-input-container .checkbox-inline input:checked,.shiny-input-container .radio input:checked,.shiny-input-container .radio-inline input:checked{background-color:#2780e3;border-color:#2780e3}.form-check-input:checked[type=checkbox],.shiny-input-container .checkbox input:checked[type=checkbox],.shiny-input-container .checkbox-inline input:checked[type=checkbox],.shiny-input-container .radio input:checked[type=checkbox],.shiny-input-container .radio-inline input:checked[type=checkbox]{background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20'%3e%3cpath fill='none' stroke='%23fff' stroke-linecap='round' stroke-linejoin='round' stroke-width='3' d='M6 10l3 3l6-6'/%3e%3c/svg%3e")}.form-check-input:checked[type=radio],.shiny-input-container .checkbox input:checked[type=radio],.shiny-input-container .checkbox-inline input:checked[type=radio],.shiny-input-container .radio input:checked[type=radio],.shiny-input-container .radio-inline input:checked[type=radio]{background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'%3e%3ccircle r='2' fill='%23fff'/%3e%3c/svg%3e")}.form-check-input[type=checkbox]:indeterminate,.shiny-input-container .checkbox input[type=checkbox]:indeterminate,.shiny-input-container .checkbox-inline input[type=checkbox]:indeterminate,.shiny-input-container .radio input[type=checkbox]:indeterminate,.shiny-input-container .radio-inline input[type=checkbox]:indeterminate{background-color:#2780e3;border-color:#2780e3;background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20'%3e%3cpath fill='none' stroke='%23fff' stroke-linecap='round' stroke-linejoin='round' stroke-width='3' d='M6 10h8'/%3e%3c/svg%3e")}.form-check-input:disabled,.shiny-input-container .checkbox input:disabled,.shiny-input-container .checkbox-inline input:disabled,.shiny-input-container .radio input:disabled,.shiny-input-container .radio-inline input:disabled{pointer-events:none;filter:none;opacity:.5}.form-check-input[disabled]~.form-check-label,.form-check-input[disabled]~span,.form-check-input:disabled~.form-check-label,.form-check-input:disabled~span,.shiny-input-container .checkbox input[disabled]~.form-check-label,.shiny-input-container .checkbox input[disabled]~span,.shiny-input-container .checkbox input:disabled~.form-check-label,.shiny-input-container .checkbox input:disabled~span,.shiny-input-container .checkbox-inline input[disabled]~.form-check-label,.shiny-input-container .checkbox-inline input[disabled]~span,.shiny-input-container .checkbox-inline input:disabled~.form-check-label,.shiny-input-container .checkbox-inline input:disabled~span,.shiny-input-container .radio input[disabled]~.form-check-label,.shiny-input-container .radio input[disabled]~span,.shiny-input-container .radio input:disabled~.form-check-label,.shiny-input-container .radio input:disabled~span,.shiny-input-container .radio-inline input[disabled]~.form-check-label,.shiny-input-container .radio-inline input[disabled]~span,.shiny-input-container .radio-inline input:disabled~.form-check-label,.shiny-input-container .radio-inline input:disabled~span{opacity:.5}.form-check-label,.shiny-input-container .checkbox label,.shiny-input-container .checkbox-inline label,.shiny-input-container .radio label,.shiny-input-container .radio-inline label{cursor:pointer}.form-switch{padding-left:2.5em}.form-switch .form-check-input{width:2em;margin-left:-2.5em;background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'%3e%3ccircle r='3' fill='rgba%280, 0, 0, 0.25%29'/%3e%3c/svg%3e");background-position:left center;transition:background-position .15s ease-in-out}@media(prefers-reduced-motion: reduce){.form-switch .form-check-input{transition:none}}.form-switch .form-check-input:focus{background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'%3e%3ccircle r='3' fill='%2393c0f1'/%3e%3c/svg%3e")}.form-switch .form-check-input:checked{background-position:right center;background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'%3e%3ccircle r='3' fill='%23fff'/%3e%3c/svg%3e")}.form-check-inline,.shiny-input-container .checkbox-inline,.shiny-input-container .radio-inline{display:inline-block;margin-right:1rem}.btn-check{position:absolute;clip:rect(0, 0, 0, 0);pointer-events:none}.btn-check[disabled]+.btn,.btn-check:disabled+.btn{pointer-events:none;filter:none;opacity:.65}.form-range{width:100%;height:1.5rem;padding:0;background-color:rgba(0,0,0,0);appearance:none;-webkit-appearance:none;-moz-appearance:none;-ms-appearance:none;-o-appearance:none}.form-range:focus{outline:0}.form-range:focus::-webkit-slider-thumb{box-shadow:0 0 0 1px #fff,0 0 0 .25rem rgba(39,128,227,.25)}.form-range:focus::-moz-range-thumb{box-shadow:0 0 0 1px #fff,0 0 0 .25rem rgba(39,128,227,.25)}.form-range::-moz-focus-outer{border:0}.form-range::-webkit-slider-thumb{width:1rem;height:1rem;margin-top:-0.25rem;background-color:#2780e3;border:0;transition:background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out;appearance:none;-webkit-appearance:none;-moz-appearance:none;-ms-appearance:none;-o-appearance:none}@media(prefers-reduced-motion: reduce){.form-range::-webkit-slider-thumb{transition:none}}.form-range::-webkit-slider-thumb:active{background-color:#bed9f7}.form-range::-webkit-slider-runnable-track{width:100%;height:.5rem;color:rgba(0,0,0,0);cursor:pointer;background-color:#dee2e6;border-color:rgba(0,0,0,0)}.form-range::-moz-range-thumb{width:1rem;height:1rem;background-color:#2780e3;border:0;transition:background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out;appearance:none;-webkit-appearance:none;-moz-appearance:none;-ms-appearance:none;-o-appearance:none}@media(prefers-reduced-motion: reduce){.form-range::-moz-range-thumb{transition:none}}.form-range::-moz-range-thumb:active{background-color:#bed9f7}.form-range::-moz-range-track{width:100%;height:.5rem;color:rgba(0,0,0,0);cursor:pointer;background-color:#dee2e6;border-color:rgba(0,0,0,0)}.form-range:disabled{pointer-events:none}.form-range:disabled::-webkit-slider-thumb{background-color:#adb5bd}.form-range:disabled::-moz-range-thumb{background-color:#adb5bd}.form-floating{position:relative}.form-floating>.form-control,.form-floating>.form-select{height:calc(3.5rem + 2px);line-height:1.25}.form-floating>label{position:absolute;top:0;left:0;height:100%;padding:1rem .75rem;pointer-events:none;border:1px solid rgba(0,0,0,0);transform-origin:0 0;transition:opacity .1s ease-in-out,transform .1s ease-in-out}@media(prefers-reduced-motion: reduce){.form-floating>label{transition:none}}.form-floating>.form-control{padding:1rem .75rem}.form-floating>.form-control::placeholder{color:rgba(0,0,0,0)}.form-floating>.form-control:focus,.form-floating>.form-control:not(:placeholder-shown){padding-top:1.625rem;padding-bottom:.625rem}.form-floating>.form-control:-webkit-autofill{padding-top:1.625rem;padding-bottom:.625rem}.form-floating>.form-select{padding-top:1.625rem;padding-bottom:.625rem}.form-floating>.form-control:focus~label,.form-floating>.form-control:not(:placeholder-shown)~label,.form-floating>.form-select~label{opacity:.65;transform:scale(0.85) translateY(-0.5rem) translateX(0.15rem)}.form-floating>.form-control:-webkit-autofill~label{opacity:.65;transform:scale(0.85) translateY(-0.5rem) translateX(0.15rem)}.input-group{position:relative;display:flex;display:-webkit-flex;flex-wrap:wrap;-webkit-flex-wrap:wrap;align-items:stretch;-webkit-align-items:stretch;width:100%}.input-group>.form-control,.input-group>.form-select{position:relative;flex:1 1 auto;-webkit-flex:1 1 auto;width:1%;min-width:0}.input-group>.form-control:focus,.input-group>.form-select:focus{z-index:3}.input-group .btn{position:relative;z-index:2}.input-group .btn:focus{z-index:3}.input-group-text{display:flex;display:-webkit-flex;align-items:center;-webkit-align-items:center;padding:.375rem .75rem;font-size:1rem;font-weight:400;line-height:1.5;color:#373a3c;text-align:center;white-space:nowrap;background-color:#e9ecef;border:1px solid #ced4da}.input-group-lg>.form-control,.input-group-lg>.form-select,.input-group-lg>.input-group-text,.input-group-lg>.btn{padding:.5rem 1rem;font-size:1.25rem}.input-group-sm>.form-control,.input-group-sm>.form-select,.input-group-sm>.input-group-text,.input-group-sm>.btn{padding:.25rem .5rem;font-size:0.875rem}.input-group-lg>.form-select,.input-group-sm>.form-select{padding-right:3rem}.input-group>:not(:first-child):not(.dropdown-menu):not(.valid-tooltip):not(.valid-feedback):not(.invalid-tooltip):not(.invalid-feedback){margin-left:-1px}.valid-feedback{display:none;width:100%;margin-top:.25rem;font-size:0.875em;color:#3fb618}.valid-tooltip{position:absolute;top:100%;z-index:5;display:none;max-width:100%;padding:.25rem .5rem;margin-top:.1rem;font-size:0.875rem;color:#fff;background-color:rgba(63,182,24,.9)}.was-validated :valid~.valid-feedback,.was-validated :valid~.valid-tooltip,.is-valid~.valid-feedback,.is-valid~.valid-tooltip{display:block}.was-validated .form-control:valid,.form-control.is-valid{border-color:#3fb618;padding-right:calc(1.5em + 0.75rem);background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 8 8'%3e%3cpath fill='%233fb618' d='M2.3 6.73L.6 4.53c-.4-1.04.46-1.4 1.1-.8l1.1 1.4 3.4-3.8c.6-.63 1.6-.27 1.2.7l-4 4.6c-.43.5-.8.4-1.1.1z'/%3e%3c/svg%3e");background-repeat:no-repeat;background-position:right calc(0.375em + 0.1875rem) center;background-size:calc(0.75em + 0.375rem) calc(0.75em + 0.375rem)}.was-validated .form-control:valid:focus,.form-control.is-valid:focus{border-color:#3fb618;box-shadow:0 0 0 .25rem rgba(63,182,24,.25)}.was-validated textarea.form-control:valid,textarea.form-control.is-valid{padding-right:calc(1.5em + 0.75rem);background-position:top calc(0.375em + 0.1875rem) right calc(0.375em + 0.1875rem)}.was-validated .form-select:valid,.form-select.is-valid{border-color:#3fb618}.was-validated .form-select:valid:not([multiple]):not([size]),.was-validated .form-select:valid:not([multiple])[size="1"],.form-select.is-valid:not([multiple]):not([size]),.form-select.is-valid:not([multiple])[size="1"]{padding-right:4.125rem;background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3e%3cpath fill='none' stroke='%23373a3c' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='M2 5l6 6 6-6'/%3e%3c/svg%3e"),url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 8 8'%3e%3cpath fill='%233fb618' d='M2.3 6.73L.6 4.53c-.4-1.04.46-1.4 1.1-.8l1.1 1.4 3.4-3.8c.6-.63 1.6-.27 1.2.7l-4 4.6c-.43.5-.8.4-1.1.1z'/%3e%3c/svg%3e");background-position:right .75rem center,center right 2.25rem;background-size:16px 12px,calc(0.75em + 0.375rem) calc(0.75em + 0.375rem)}.was-validated .form-select:valid:focus,.form-select.is-valid:focus{border-color:#3fb618;box-shadow:0 0 0 .25rem rgba(63,182,24,.25)}.was-validated .form-check-input:valid,.form-check-input.is-valid{border-color:#3fb618}.was-validated .form-check-input:valid:checked,.form-check-input.is-valid:checked{background-color:#3fb618}.was-validated .form-check-input:valid:focus,.form-check-input.is-valid:focus{box-shadow:0 0 0 .25rem rgba(63,182,24,.25)}.was-validated .form-check-input:valid~.form-check-label,.form-check-input.is-valid~.form-check-label{color:#3fb618}.form-check-inline .form-check-input~.valid-feedback{margin-left:.5em}.was-validated .input-group .form-control:valid,.input-group .form-control.is-valid,.was-validated .input-group .form-select:valid,.input-group .form-select.is-valid{z-index:1}.was-validated .input-group .form-control:valid:focus,.input-group .form-control.is-valid:focus,.was-validated .input-group .form-select:valid:focus,.input-group .form-select.is-valid:focus{z-index:3}.invalid-feedback{display:none;width:100%;margin-top:.25rem;font-size:0.875em;color:#ff0039}.invalid-tooltip{position:absolute;top:100%;z-index:5;display:none;max-width:100%;padding:.25rem .5rem;margin-top:.1rem;font-size:0.875rem;color:#fff;background-color:rgba(255,0,57,.9)}.was-validated :invalid~.invalid-feedback,.was-validated :invalid~.invalid-tooltip,.is-invalid~.invalid-feedback,.is-invalid~.invalid-tooltip{display:block}.was-validated .form-control:invalid,.form-control.is-invalid{border-color:#ff0039;padding-right:calc(1.5em + 0.75rem);background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 12 12' width='12' height='12' fill='none' stroke='%23ff0039'%3e%3ccircle cx='6' cy='6' r='4.5'/%3e%3cpath stroke-linejoin='round' d='M5.8 3.6h.4L6 6.5z'/%3e%3ccircle cx='6' cy='8.2' r='.6' fill='%23ff0039' stroke='none'/%3e%3c/svg%3e");background-repeat:no-repeat;background-position:right calc(0.375em + 0.1875rem) center;background-size:calc(0.75em + 0.375rem) calc(0.75em + 0.375rem)}.was-validated .form-control:invalid:focus,.form-control.is-invalid:focus{border-color:#ff0039;box-shadow:0 0 0 .25rem rgba(255,0,57,.25)}.was-validated textarea.form-control:invalid,textarea.form-control.is-invalid{padding-right:calc(1.5em + 0.75rem);background-position:top calc(0.375em + 0.1875rem) right calc(0.375em + 0.1875rem)}.was-validated .form-select:invalid,.form-select.is-invalid{border-color:#ff0039}.was-validated .form-select:invalid:not([multiple]):not([size]),.was-validated .form-select:invalid:not([multiple])[size="1"],.form-select.is-invalid:not([multiple]):not([size]),.form-select.is-invalid:not([multiple])[size="1"]{padding-right:4.125rem;background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3e%3cpath fill='none' stroke='%23373a3c' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='M2 5l6 6 6-6'/%3e%3c/svg%3e"),url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 12 12' width='12' height='12' fill='none' stroke='%23ff0039'%3e%3ccircle cx='6' cy='6' r='4.5'/%3e%3cpath stroke-linejoin='round' d='M5.8 3.6h.4L6 6.5z'/%3e%3ccircle cx='6' cy='8.2' r='.6' fill='%23ff0039' stroke='none'/%3e%3c/svg%3e");background-position:right .75rem center,center right 2.25rem;background-size:16px 12px,calc(0.75em + 0.375rem) calc(0.75em + 0.375rem)}.was-validated .form-select:invalid:focus,.form-select.is-invalid:focus{border-color:#ff0039;box-shadow:0 0 0 .25rem rgba(255,0,57,.25)}.was-validated .form-check-input:invalid,.form-check-input.is-invalid{border-color:#ff0039}.was-validated .form-check-input:invalid:checked,.form-check-input.is-invalid:checked{background-color:#ff0039}.was-validated .form-check-input:invalid:focus,.form-check-input.is-invalid:focus{box-shadow:0 0 0 .25rem rgba(255,0,57,.25)}.was-validated .form-check-input:invalid~.form-check-label,.form-check-input.is-invalid~.form-check-label{color:#ff0039}.form-check-inline .form-check-input~.invalid-feedback{margin-left:.5em}.was-validated .input-group .form-control:invalid,.input-group .form-control.is-invalid,.was-validated .input-group .form-select:invalid,.input-group .form-select.is-invalid{z-index:2}.was-validated .input-group .form-control:invalid:focus,.input-group .form-control.is-invalid:focus,.was-validated .input-group .form-select:invalid:focus,.input-group .form-select.is-invalid:focus{z-index:3}.btn{display:inline-block;font-weight:400;line-height:1.5;color:#373a3c;text-align:center;text-decoration:none;-webkit-text-decoration:none;-moz-text-decoration:none;-ms-text-decoration:none;-o-text-decoration:none;vertical-align:middle;cursor:pointer;user-select:none;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;-o-user-select:none;background-color:rgba(0,0,0,0);border:1px solid rgba(0,0,0,0);padding:.375rem .75rem;font-size:1rem;border-radius:0;transition:color .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out}@media(prefers-reduced-motion: reduce){.btn{transition:none}}.btn:hover{color:#373a3c}.btn-check:focus+.btn,.btn:focus{outline:0;box-shadow:0 0 0 .25rem rgba(39,128,227,.25)}.btn:disabled,.btn.disabled,fieldset:disabled .btn{pointer-events:none;opacity:.65}.btn-default{color:#fff;background-color:#373a3c;border-color:#373a3c}.btn-default:hover{color:#fff;background-color:#2f3133;border-color:#2c2e30}.btn-check:focus+.btn-default,.btn-default:focus{color:#fff;background-color:#2f3133;border-color:#2c2e30;box-shadow:0 0 0 .25rem rgba(85,88,89,.5)}.btn-check:checked+.btn-default,.btn-check:active+.btn-default,.btn-default:active,.btn-default.active,.show>.btn-default.dropdown-toggle{color:#fff;background-color:#2c2e30;border-color:#292c2d}.btn-check:checked+.btn-default:focus,.btn-check:active+.btn-default:focus,.btn-default:active:focus,.btn-default.active:focus,.show>.btn-default.dropdown-toggle:focus{box-shadow:0 0 0 .25rem rgba(85,88,89,.5)}.btn-default:disabled,.btn-default.disabled{color:#fff;background-color:#373a3c;border-color:#373a3c}.btn-primary{color:#fff;background-color:#2780e3;border-color:#2780e3}.btn-primary:hover{color:#fff;background-color:#216dc1;border-color:#1f66b6}.btn-check:focus+.btn-primary,.btn-primary:focus{color:#fff;background-color:#216dc1;border-color:#1f66b6;box-shadow:0 0 0 .25rem rgba(71,147,231,.5)}.btn-check:checked+.btn-primary,.btn-check:active+.btn-primary,.btn-primary:active,.btn-primary.active,.show>.btn-primary.dropdown-toggle{color:#fff;background-color:#1f66b6;border-color:#1d60aa}.btn-check:checked+.btn-primary:focus,.btn-check:active+.btn-primary:focus,.btn-primary:active:focus,.btn-primary.active:focus,.show>.btn-primary.dropdown-toggle:focus{box-shadow:0 0 0 .25rem rgba(71,147,231,.5)}.btn-primary:disabled,.btn-primary.disabled{color:#fff;background-color:#2780e3;border-color:#2780e3}.btn-secondary{color:#fff;background-color:#373a3c;border-color:#373a3c}.btn-secondary:hover{color:#fff;background-color:#2f3133;border-color:#2c2e30}.btn-check:focus+.btn-secondary,.btn-secondary:focus{color:#fff;background-color:#2f3133;border-color:#2c2e30;box-shadow:0 0 0 .25rem rgba(85,88,89,.5)}.btn-check:checked+.btn-secondary,.btn-check:active+.btn-secondary,.btn-secondary:active,.btn-secondary.active,.show>.btn-secondary.dropdown-toggle{color:#fff;background-color:#2c2e30;border-color:#292c2d}.btn-check:checked+.btn-secondary:focus,.btn-check:active+.btn-secondary:focus,.btn-secondary:active:focus,.btn-secondary.active:focus,.show>.btn-secondary.dropdown-toggle:focus{box-shadow:0 0 0 .25rem rgba(85,88,89,.5)}.btn-secondary:disabled,.btn-secondary.disabled{color:#fff;background-color:#373a3c;border-color:#373a3c}.btn-success{color:#fff;background-color:#3fb618;border-color:#3fb618}.btn-success:hover{color:#fff;background-color:#369b14;border-color:#329213}.btn-check:focus+.btn-success,.btn-success:focus{color:#fff;background-color:#369b14;border-color:#329213;box-shadow:0 0 0 .25rem rgba(92,193,59,.5)}.btn-check:checked+.btn-success,.btn-check:active+.btn-success,.btn-success:active,.btn-success.active,.show>.btn-success.dropdown-toggle{color:#fff;background-color:#329213;border-color:#2f8912}.btn-check:checked+.btn-success:focus,.btn-check:active+.btn-success:focus,.btn-success:active:focus,.btn-success.active:focus,.show>.btn-success.dropdown-toggle:focus{box-shadow:0 0 0 .25rem rgba(92,193,59,.5)}.btn-success:disabled,.btn-success.disabled{color:#fff;background-color:#3fb618;border-color:#3fb618}.btn-info{color:#fff;background-color:#9954bb;border-color:#9954bb}.btn-info:hover{color:#fff;background-color:#82479f;border-color:#7a4396}.btn-check:focus+.btn-info,.btn-info:focus{color:#fff;background-color:#82479f;border-color:#7a4396;box-shadow:0 0 0 .25rem rgba(168,110,197,.5)}.btn-check:checked+.btn-info,.btn-check:active+.btn-info,.btn-info:active,.btn-info.active,.show>.btn-info.dropdown-toggle{color:#fff;background-color:#7a4396;border-color:#733f8c}.btn-check:checked+.btn-info:focus,.btn-check:active+.btn-info:focus,.btn-info:active:focus,.btn-info.active:focus,.show>.btn-info.dropdown-toggle:focus{box-shadow:0 0 0 .25rem rgba(168,110,197,.5)}.btn-info:disabled,.btn-info.disabled{color:#fff;background-color:#9954bb;border-color:#9954bb}.btn-warning{color:#fff;background-color:#ff7518;border-color:#ff7518}.btn-warning:hover{color:#fff;background-color:#d96314;border-color:#cc5e13}.btn-check:focus+.btn-warning,.btn-warning:focus{color:#fff;background-color:#d96314;border-color:#cc5e13;box-shadow:0 0 0 .25rem rgba(255,138,59,.5)}.btn-check:checked+.btn-warning,.btn-check:active+.btn-warning,.btn-warning:active,.btn-warning.active,.show>.btn-warning.dropdown-toggle{color:#fff;background-color:#cc5e13;border-color:#bf5812}.btn-check:checked+.btn-warning:focus,.btn-check:active+.btn-warning:focus,.btn-warning:active:focus,.btn-warning.active:focus,.show>.btn-warning.dropdown-toggle:focus{box-shadow:0 0 0 .25rem rgba(255,138,59,.5)}.btn-warning:disabled,.btn-warning.disabled{color:#fff;background-color:#ff7518;border-color:#ff7518}.btn-danger{color:#fff;background-color:#ff0039;border-color:#ff0039}.btn-danger:hover{color:#fff;background-color:#d90030;border-color:#cc002e}.btn-check:focus+.btn-danger,.btn-danger:focus{color:#fff;background-color:#d90030;border-color:#cc002e;box-shadow:0 0 0 .25rem rgba(255,38,87,.5)}.btn-check:checked+.btn-danger,.btn-check:active+.btn-danger,.btn-danger:active,.btn-danger.active,.show>.btn-danger.dropdown-toggle{color:#fff;background-color:#cc002e;border-color:#bf002b}.btn-check:checked+.btn-danger:focus,.btn-check:active+.btn-danger:focus,.btn-danger:active:focus,.btn-danger.active:focus,.show>.btn-danger.dropdown-toggle:focus{box-shadow:0 0 0 .25rem rgba(255,38,87,.5)}.btn-danger:disabled,.btn-danger.disabled{color:#fff;background-color:#ff0039;border-color:#ff0039}.btn-light{color:#000;background-color:#f8f9fa;border-color:#f8f9fa}.btn-light:hover{color:#000;background-color:#f9fafb;border-color:#f9fafb}.btn-check:focus+.btn-light,.btn-light:focus{color:#000;background-color:#f9fafb;border-color:#f9fafb;box-shadow:0 0 0 .25rem rgba(211,212,213,.5)}.btn-check:checked+.btn-light,.btn-check:active+.btn-light,.btn-light:active,.btn-light.active,.show>.btn-light.dropdown-toggle{color:#000;background-color:#f9fafb;border-color:#f9fafb}.btn-check:checked+.btn-light:focus,.btn-check:active+.btn-light:focus,.btn-light:active:focus,.btn-light.active:focus,.show>.btn-light.dropdown-toggle:focus{box-shadow:0 0 0 .25rem rgba(211,212,213,.5)}.btn-light:disabled,.btn-light.disabled{color:#000;background-color:#f8f9fa;border-color:#f8f9fa}.btn-dark{color:#fff;background-color:#373a3c;border-color:#373a3c}.btn-dark:hover{color:#fff;background-color:#2f3133;border-color:#2c2e30}.btn-check:focus+.btn-dark,.btn-dark:focus{color:#fff;background-color:#2f3133;border-color:#2c2e30;box-shadow:0 0 0 .25rem rgba(85,88,89,.5)}.btn-check:checked+.btn-dark,.btn-check:active+.btn-dark,.btn-dark:active,.btn-dark.active,.show>.btn-dark.dropdown-toggle{color:#fff;background-color:#2c2e30;border-color:#292c2d}.btn-check:checked+.btn-dark:focus,.btn-check:active+.btn-dark:focus,.btn-dark:active:focus,.btn-dark.active:focus,.show>.btn-dark.dropdown-toggle:focus{box-shadow:0 0 0 .25rem rgba(85,88,89,.5)}.btn-dark:disabled,.btn-dark.disabled{color:#fff;background-color:#373a3c;border-color:#373a3c}.btn-outline-default{color:#373a3c;border-color:#373a3c;background-color:rgba(0,0,0,0)}.btn-outline-default:hover{color:#fff;background-color:#373a3c;border-color:#373a3c}.btn-check:focus+.btn-outline-default,.btn-outline-default:focus{box-shadow:0 0 0 .25rem rgba(55,58,60,.5)}.btn-check:checked+.btn-outline-default,.btn-check:active+.btn-outline-default,.btn-outline-default:active,.btn-outline-default.active,.btn-outline-default.dropdown-toggle.show{color:#fff;background-color:#373a3c;border-color:#373a3c}.btn-check:checked+.btn-outline-default:focus,.btn-check:active+.btn-outline-default:focus,.btn-outline-default:active:focus,.btn-outline-default.active:focus,.btn-outline-default.dropdown-toggle.show:focus{box-shadow:0 0 0 .25rem rgba(55,58,60,.5)}.btn-outline-default:disabled,.btn-outline-default.disabled{color:#373a3c;background-color:rgba(0,0,0,0)}.btn-outline-primary{color:#2780e3;border-color:#2780e3;background-color:rgba(0,0,0,0)}.btn-outline-primary:hover{color:#fff;background-color:#2780e3;border-color:#2780e3}.btn-check:focus+.btn-outline-primary,.btn-outline-primary:focus{box-shadow:0 0 0 .25rem rgba(39,128,227,.5)}.btn-check:checked+.btn-outline-primary,.btn-check:active+.btn-outline-primary,.btn-outline-primary:active,.btn-outline-primary.active,.btn-outline-primary.dropdown-toggle.show{color:#fff;background-color:#2780e3;border-color:#2780e3}.btn-check:checked+.btn-outline-primary:focus,.btn-check:active+.btn-outline-primary:focus,.btn-outline-primary:active:focus,.btn-outline-primary.active:focus,.btn-outline-primary.dropdown-toggle.show:focus{box-shadow:0 0 0 .25rem rgba(39,128,227,.5)}.btn-outline-primary:disabled,.btn-outline-primary.disabled{color:#2780e3;background-color:rgba(0,0,0,0)}.btn-outline-secondary{color:#373a3c;border-color:#373a3c;background-color:rgba(0,0,0,0)}.btn-outline-secondary:hover{color:#fff;background-color:#373a3c;border-color:#373a3c}.btn-check:focus+.btn-outline-secondary,.btn-outline-secondary:focus{box-shadow:0 0 0 .25rem rgba(55,58,60,.5)}.btn-check:checked+.btn-outline-secondary,.btn-check:active+.btn-outline-secondary,.btn-outline-secondary:active,.btn-outline-secondary.active,.btn-outline-secondary.dropdown-toggle.show{color:#fff;background-color:#373a3c;border-color:#373a3c}.btn-check:checked+.btn-outline-secondary:focus,.btn-check:active+.btn-outline-secondary:focus,.btn-outline-secondary:active:focus,.btn-outline-secondary.active:focus,.btn-outline-secondary.dropdown-toggle.show:focus{box-shadow:0 0 0 .25rem rgba(55,58,60,.5)}.btn-outline-secondary:disabled,.btn-outline-secondary.disabled{color:#373a3c;background-color:rgba(0,0,0,0)}.btn-outline-success{color:#3fb618;border-color:#3fb618;background-color:rgba(0,0,0,0)}.btn-outline-success:hover{color:#fff;background-color:#3fb618;border-color:#3fb618}.btn-check:focus+.btn-outline-success,.btn-outline-success:focus{box-shadow:0 0 0 .25rem rgba(63,182,24,.5)}.btn-check:checked+.btn-outline-success,.btn-check:active+.btn-outline-success,.btn-outline-success:active,.btn-outline-success.active,.btn-outline-success.dropdown-toggle.show{color:#fff;background-color:#3fb618;border-color:#3fb618}.btn-check:checked+.btn-outline-success:focus,.btn-check:active+.btn-outline-success:focus,.btn-outline-success:active:focus,.btn-outline-success.active:focus,.btn-outline-success.dropdown-toggle.show:focus{box-shadow:0 0 0 .25rem rgba(63,182,24,.5)}.btn-outline-success:disabled,.btn-outline-success.disabled{color:#3fb618;background-color:rgba(0,0,0,0)}.btn-outline-info{color:#9954bb;border-color:#9954bb;background-color:rgba(0,0,0,0)}.btn-outline-info:hover{color:#fff;background-color:#9954bb;border-color:#9954bb}.btn-check:focus+.btn-outline-info,.btn-outline-info:focus{box-shadow:0 0 0 .25rem rgba(153,84,187,.5)}.btn-check:checked+.btn-outline-info,.btn-check:active+.btn-outline-info,.btn-outline-info:active,.btn-outline-info.active,.btn-outline-info.dropdown-toggle.show{color:#fff;background-color:#9954bb;border-color:#9954bb}.btn-check:checked+.btn-outline-info:focus,.btn-check:active+.btn-outline-info:focus,.btn-outline-info:active:focus,.btn-outline-info.active:focus,.btn-outline-info.dropdown-toggle.show:focus{box-shadow:0 0 0 .25rem rgba(153,84,187,.5)}.btn-outline-info:disabled,.btn-outline-info.disabled{color:#9954bb;background-color:rgba(0,0,0,0)}.btn-outline-warning{color:#ff7518;border-color:#ff7518;background-color:rgba(0,0,0,0)}.btn-outline-warning:hover{color:#fff;background-color:#ff7518;border-color:#ff7518}.btn-check:focus+.btn-outline-warning,.btn-outline-warning:focus{box-shadow:0 0 0 .25rem rgba(255,117,24,.5)}.btn-check:checked+.btn-outline-warning,.btn-check:active+.btn-outline-warning,.btn-outline-warning:active,.btn-outline-warning.active,.btn-outline-warning.dropdown-toggle.show{color:#fff;background-color:#ff7518;border-color:#ff7518}.btn-check:checked+.btn-outline-warning:focus,.btn-check:active+.btn-outline-warning:focus,.btn-outline-warning:active:focus,.btn-outline-warning.active:focus,.btn-outline-warning.dropdown-toggle.show:focus{box-shadow:0 0 0 .25rem rgba(255,117,24,.5)}.btn-outline-warning:disabled,.btn-outline-warning.disabled{color:#ff7518;background-color:rgba(0,0,0,0)}.btn-outline-danger{color:#ff0039;border-color:#ff0039;background-color:rgba(0,0,0,0)}.btn-outline-danger:hover{color:#fff;background-color:#ff0039;border-color:#ff0039}.btn-check:focus+.btn-outline-danger,.btn-outline-danger:focus{box-shadow:0 0 0 .25rem rgba(255,0,57,.5)}.btn-check:checked+.btn-outline-danger,.btn-check:active+.btn-outline-danger,.btn-outline-danger:active,.btn-outline-danger.active,.btn-outline-danger.dropdown-toggle.show{color:#fff;background-color:#ff0039;border-color:#ff0039}.btn-check:checked+.btn-outline-danger:focus,.btn-check:active+.btn-outline-danger:focus,.btn-outline-danger:active:focus,.btn-outline-danger.active:focus,.btn-outline-danger.dropdown-toggle.show:focus{box-shadow:0 0 0 .25rem rgba(255,0,57,.5)}.btn-outline-danger:disabled,.btn-outline-danger.disabled{color:#ff0039;background-color:rgba(0,0,0,0)}.btn-outline-light{color:#f8f9fa;border-color:#f8f9fa;background-color:rgba(0,0,0,0)}.btn-outline-light:hover{color:#000;background-color:#f8f9fa;border-color:#f8f9fa}.btn-check:focus+.btn-outline-light,.btn-outline-light:focus{box-shadow:0 0 0 .25rem rgba(248,249,250,.5)}.btn-check:checked+.btn-outline-light,.btn-check:active+.btn-outline-light,.btn-outline-light:active,.btn-outline-light.active,.btn-outline-light.dropdown-toggle.show{color:#000;background-color:#f8f9fa;border-color:#f8f9fa}.btn-check:checked+.btn-outline-light:focus,.btn-check:active+.btn-outline-light:focus,.btn-outline-light:active:focus,.btn-outline-light.active:focus,.btn-outline-light.dropdown-toggle.show:focus{box-shadow:0 0 0 .25rem rgba(248,249,250,.5)}.btn-outline-light:disabled,.btn-outline-light.disabled{color:#f8f9fa;background-color:rgba(0,0,0,0)}.btn-outline-dark{color:#373a3c;border-color:#373a3c;background-color:rgba(0,0,0,0)}.btn-outline-dark:hover{color:#fff;background-color:#373a3c;border-color:#373a3c}.btn-check:focus+.btn-outline-dark,.btn-outline-dark:focus{box-shadow:0 0 0 .25rem rgba(55,58,60,.5)}.btn-check:checked+.btn-outline-dark,.btn-check:active+.btn-outline-dark,.btn-outline-dark:active,.btn-outline-dark.active,.btn-outline-dark.dropdown-toggle.show{color:#fff;background-color:#373a3c;border-color:#373a3c}.btn-check:checked+.btn-outline-dark:focus,.btn-check:active+.btn-outline-dark:focus,.btn-outline-dark:active:focus,.btn-outline-dark.active:focus,.btn-outline-dark.dropdown-toggle.show:focus{box-shadow:0 0 0 .25rem rgba(55,58,60,.5)}.btn-outline-dark:disabled,.btn-outline-dark.disabled{color:#373a3c;background-color:rgba(0,0,0,0)}.btn-link{font-weight:400;color:#2780e3;text-decoration:underline;-webkit-text-decoration:underline;-moz-text-decoration:underline;-ms-text-decoration:underline;-o-text-decoration:underline}.btn-link:hover{color:#1f66b6}.btn-link:disabled,.btn-link.disabled{color:#6c757d}.btn-lg,.btn-group-lg>.btn{padding:.5rem 1rem;font-size:1.25rem;border-radius:0}.btn-sm,.btn-group-sm>.btn{padding:.25rem .5rem;font-size:0.875rem;border-radius:0}.fade{transition:opacity .15s linear}@media(prefers-reduced-motion: reduce){.fade{transition:none}}.fade:not(.show){opacity:0}.collapse:not(.show){display:none}.collapsing{height:0;overflow:hidden;transition:height .2s ease}@media(prefers-reduced-motion: reduce){.collapsing{transition:none}}.collapsing.collapse-horizontal{width:0;height:auto;transition:width .35s ease}@media(prefers-reduced-motion: reduce){.collapsing.collapse-horizontal{transition:none}}.dropup,.dropend,.dropdown,.dropstart{position:relative}.dropdown-toggle{white-space:nowrap}.dropdown-toggle::after{display:inline-block;margin-left:.255em;vertical-align:.255em;content:"";border-top:.3em solid;border-right:.3em solid rgba(0,0,0,0);border-bottom:0;border-left:.3em solid rgba(0,0,0,0)}.dropdown-toggle:empty::after{margin-left:0}.dropdown-menu{position:absolute;z-index:1000;display:none;min-width:10rem;padding:.5rem 0;margin:0;font-size:1rem;color:#373a3c;text-align:left;list-style:none;background-color:#fff;background-clip:padding-box;border:1px solid rgba(0,0,0,.15)}.dropdown-menu[data-bs-popper]{top:100%;left:0;margin-top:.125rem}.dropdown-menu-start{--bs-position: start}.dropdown-menu-start[data-bs-popper]{right:auto;left:0}.dropdown-menu-end{--bs-position: end}.dropdown-menu-end[data-bs-popper]{right:0;left:auto}@media(min-width: 576px){.dropdown-menu-sm-start{--bs-position: start}.dropdown-menu-sm-start[data-bs-popper]{right:auto;left:0}.dropdown-menu-sm-end{--bs-position: end}.dropdown-menu-sm-end[data-bs-popper]{right:0;left:auto}}@media(min-width: 768px){.dropdown-menu-md-start{--bs-position: start}.dropdown-menu-md-start[data-bs-popper]{right:auto;left:0}.dropdown-menu-md-end{--bs-position: end}.dropdown-menu-md-end[data-bs-popper]{right:0;left:auto}}@media(min-width: 992px){.dropdown-menu-lg-start{--bs-position: start}.dropdown-menu-lg-start[data-bs-popper]{right:auto;left:0}.dropdown-menu-lg-end{--bs-position: end}.dropdown-menu-lg-end[data-bs-popper]{right:0;left:auto}}@media(min-width: 1200px){.dropdown-menu-xl-start{--bs-position: start}.dropdown-menu-xl-start[data-bs-popper]{right:auto;left:0}.dropdown-menu-xl-end{--bs-position: end}.dropdown-menu-xl-end[data-bs-popper]{right:0;left:auto}}@media(min-width: 1400px){.dropdown-menu-xxl-start{--bs-position: start}.dropdown-menu-xxl-start[data-bs-popper]{right:auto;left:0}.dropdown-menu-xxl-end{--bs-position: end}.dropdown-menu-xxl-end[data-bs-popper]{right:0;left:auto}}.dropup .dropdown-menu[data-bs-popper]{top:auto;bottom:100%;margin-top:0;margin-bottom:.125rem}.dropup .dropdown-toggle::after{display:inline-block;margin-left:.255em;vertical-align:.255em;content:"";border-top:0;border-right:.3em solid rgba(0,0,0,0);border-bottom:.3em solid;border-left:.3em solid rgba(0,0,0,0)}.dropup .dropdown-toggle:empty::after{margin-left:0}.dropend .dropdown-menu[data-bs-popper]{top:0;right:auto;left:100%;margin-top:0;margin-left:.125rem}.dropend .dropdown-toggle::after{display:inline-block;margin-left:.255em;vertical-align:.255em;content:"";border-top:.3em solid rgba(0,0,0,0);border-right:0;border-bottom:.3em solid rgba(0,0,0,0);border-left:.3em solid}.dropend .dropdown-toggle:empty::after{margin-left:0}.dropend .dropdown-toggle::after{vertical-align:0}.dropstart .dropdown-menu[data-bs-popper]{top:0;right:100%;left:auto;margin-top:0;margin-right:.125rem}.dropstart .dropdown-toggle::after{display:inline-block;margin-left:.255em;vertical-align:.255em;content:""}.dropstart .dropdown-toggle::after{display:none}.dropstart .dropdown-toggle::before{display:inline-block;margin-right:.255em;vertical-align:.255em;content:"";border-top:.3em solid rgba(0,0,0,0);border-right:.3em solid;border-bottom:.3em solid rgba(0,0,0,0)}.dropstart .dropdown-toggle:empty::after{margin-left:0}.dropstart .dropdown-toggle::before{vertical-align:0}.dropdown-divider{height:0;margin:.5rem 0;overflow:hidden;border-top:1px solid rgba(0,0,0,.15)}.dropdown-item{display:block;width:100%;padding:.25rem 1rem;clear:both;font-weight:400;color:#212529;text-align:inherit;text-decoration:none;-webkit-text-decoration:none;-moz-text-decoration:none;-ms-text-decoration:none;-o-text-decoration:none;white-space:nowrap;background-color:rgba(0,0,0,0);border:0}.dropdown-item:hover,.dropdown-item:focus{color:#1e2125;background-color:#e9ecef}.dropdown-item.active,.dropdown-item:active{color:#fff;text-decoration:none;background-color:#2780e3}.dropdown-item.disabled,.dropdown-item:disabled{color:#adb5bd;pointer-events:none;background-color:rgba(0,0,0,0)}.dropdown-menu.show{display:block}.dropdown-header{display:block;padding:.5rem 1rem;margin-bottom:0;font-size:0.875rem;color:#6c757d;white-space:nowrap}.dropdown-item-text{display:block;padding:.25rem 1rem;color:#212529}.dropdown-menu-dark{color:#dee2e6;background-color:#373a3c;border-color:rgba(0,0,0,.15)}.dropdown-menu-dark .dropdown-item{color:#dee2e6}.dropdown-menu-dark .dropdown-item:hover,.dropdown-menu-dark .dropdown-item:focus{color:#fff;background-color:rgba(255,255,255,.15)}.dropdown-menu-dark .dropdown-item.active,.dropdown-menu-dark .dropdown-item:active{color:#fff;background-color:#2780e3}.dropdown-menu-dark .dropdown-item.disabled,.dropdown-menu-dark .dropdown-item:disabled{color:#adb5bd}.dropdown-menu-dark .dropdown-divider{border-color:rgba(0,0,0,.15)}.dropdown-menu-dark .dropdown-item-text{color:#dee2e6}.dropdown-menu-dark .dropdown-header{color:#adb5bd}.btn-group,.btn-group-vertical{position:relative;display:inline-flex;vertical-align:middle}.btn-group>.btn,.btn-group-vertical>.btn{position:relative;flex:1 1 auto;-webkit-flex:1 1 auto}.btn-group>.btn-check:checked+.btn,.btn-group>.btn-check:focus+.btn,.btn-group>.btn:hover,.btn-group>.btn:focus,.btn-group>.btn:active,.btn-group>.btn.active,.btn-group-vertical>.btn-check:checked+.btn,.btn-group-vertical>.btn-check:focus+.btn,.btn-group-vertical>.btn:hover,.btn-group-vertical>.btn:focus,.btn-group-vertical>.btn:active,.btn-group-vertical>.btn.active{z-index:1}.btn-toolbar{display:flex;display:-webkit-flex;flex-wrap:wrap;-webkit-flex-wrap:wrap;justify-content:flex-start;-webkit-justify-content:flex-start}.btn-toolbar .input-group{width:auto}.btn-group>.btn:not(:first-child),.btn-group>.btn-group:not(:first-child){margin-left:-1px}.dropdown-toggle-split{padding-right:.5625rem;padding-left:.5625rem}.dropdown-toggle-split::after,.dropup .dropdown-toggle-split::after,.dropend .dropdown-toggle-split::after{margin-left:0}.dropstart .dropdown-toggle-split::before{margin-right:0}.btn-sm+.dropdown-toggle-split,.btn-group-sm>.btn+.dropdown-toggle-split{padding-right:.375rem;padding-left:.375rem}.btn-lg+.dropdown-toggle-split,.btn-group-lg>.btn+.dropdown-toggle-split{padding-right:.75rem;padding-left:.75rem}.btn-group-vertical{flex-direction:column;-webkit-flex-direction:column;align-items:flex-start;-webkit-align-items:flex-start;justify-content:center;-webkit-justify-content:center}.btn-group-vertical>.btn,.btn-group-vertical>.btn-group{width:100%}.btn-group-vertical>.btn:not(:first-child),.btn-group-vertical>.btn-group:not(:first-child){margin-top:-1px}.nav{display:flex;display:-webkit-flex;flex-wrap:wrap;-webkit-flex-wrap:wrap;padding-left:0;margin-bottom:0;list-style:none}.nav-link{display:block;padding:.5rem 1rem;color:#2780e3;text-decoration:none;-webkit-text-decoration:none;-moz-text-decoration:none;-ms-text-decoration:none;-o-text-decoration:none;transition:color .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out}@media(prefers-reduced-motion: reduce){.nav-link{transition:none}}.nav-link:hover,.nav-link:focus{color:#1f66b6}.nav-link.disabled{color:#6c757d;pointer-events:none;cursor:default}.nav-tabs{border-bottom:1px solid #dee2e6}.nav-tabs .nav-link{margin-bottom:-1px;background:none;border:1px solid rgba(0,0,0,0)}.nav-tabs .nav-link:hover,.nav-tabs .nav-link:focus{border-color:#e9ecef #e9ecef #dee2e6;isolation:isolate}.nav-tabs .nav-link.disabled{color:#6c757d;background-color:rgba(0,0,0,0);border-color:rgba(0,0,0,0)}.nav-tabs .nav-link.active,.nav-tabs .nav-item.show .nav-link{color:#495057;background-color:#fff;border-color:#dee2e6 #dee2e6 #fff}.nav-tabs .dropdown-menu{margin-top:-1px}.nav-pills .nav-link{background:none;border:0}.nav-pills .nav-link.active,.nav-pills .show>.nav-link{color:#fff;background-color:#2780e3}.nav-fill>.nav-link,.nav-fill .nav-item{flex:1 1 auto;-webkit-flex:1 1 auto;text-align:center}.nav-justified>.nav-link,.nav-justified .nav-item{flex-basis:0;-webkit-flex-basis:0;flex-grow:1;-webkit-flex-grow:1;text-align:center}.nav-fill .nav-item .nav-link,.nav-justified .nav-item .nav-link{width:100%}.tab-content>.tab-pane{display:none}.tab-content>.active{display:block}.navbar{position:relative;display:flex;display:-webkit-flex;flex-wrap:wrap;-webkit-flex-wrap:wrap;align-items:center;-webkit-align-items:center;justify-content:space-between;-webkit-justify-content:space-between;padding-top:.5rem;padding-bottom:.5rem}.navbar>.container-xxl,.navbar>.container-xl,.navbar>.container-lg,.navbar>.container-md,.navbar>.container-sm,.navbar>.container,.navbar>.container-fluid{display:flex;display:-webkit-flex;flex-wrap:inherit;-webkit-flex-wrap:inherit;align-items:center;-webkit-align-items:center;justify-content:space-between;-webkit-justify-content:space-between}.navbar-brand{padding-top:.3125rem;padding-bottom:.3125rem;margin-right:1rem;font-size:1.25rem;text-decoration:none;-webkit-text-decoration:none;-moz-text-decoration:none;-ms-text-decoration:none;-o-text-decoration:none;white-space:nowrap}.navbar-nav{display:flex;display:-webkit-flex;flex-direction:column;-webkit-flex-direction:column;padding-left:0;margin-bottom:0;list-style:none}.navbar-nav .nav-link{padding-right:0;padding-left:0}.navbar-nav .dropdown-menu{position:static}.navbar-text{padding-top:.5rem;padding-bottom:.5rem}.navbar-collapse{flex-basis:100%;-webkit-flex-basis:100%;flex-grow:1;-webkit-flex-grow:1;align-items:center;-webkit-align-items:center}.navbar-toggler{padding:.25 0;font-size:1.25rem;line-height:1;background-color:rgba(0,0,0,0);border:1px solid rgba(0,0,0,0);transition:box-shadow .15s ease-in-out}@media(prefers-reduced-motion: reduce){.navbar-toggler{transition:none}}.navbar-toggler:hover{text-decoration:none}.navbar-toggler:focus{text-decoration:none;outline:0;box-shadow:0 0 0 .25rem}.navbar-toggler-icon{display:inline-block;width:1.5em;height:1.5em;vertical-align:middle;background-repeat:no-repeat;background-position:center;background-size:100%}.navbar-nav-scroll{max-height:var(--bs-scroll-height, 75vh);overflow-y:auto}@media(min-width: 576px){.navbar-expand-sm{flex-wrap:nowrap;-webkit-flex-wrap:nowrap;justify-content:flex-start;-webkit-justify-content:flex-start}.navbar-expand-sm .navbar-nav{flex-direction:row;-webkit-flex-direction:row}.navbar-expand-sm .navbar-nav .dropdown-menu{position:absolute}.navbar-expand-sm .navbar-nav .nav-link{padding-right:.5rem;padding-left:.5rem}.navbar-expand-sm .navbar-nav-scroll{overflow:visible}.navbar-expand-sm .navbar-collapse{display:flex !important;display:-webkit-flex !important;flex-basis:auto;-webkit-flex-basis:auto}.navbar-expand-sm .navbar-toggler{display:none}.navbar-expand-sm .offcanvas-header{display:none}.navbar-expand-sm .offcanvas{position:inherit;bottom:0;z-index:1000;flex-grow:1;-webkit-flex-grow:1;visibility:visible !important;background-color:rgba(0,0,0,0);border-right:0;border-left:0;transition:none;transform:none}.navbar-expand-sm .offcanvas-top,.navbar-expand-sm .offcanvas-bottom{height:auto;border-top:0;border-bottom:0}.navbar-expand-sm .offcanvas-body{display:flex;display:-webkit-flex;flex-grow:0;-webkit-flex-grow:0;padding:0;overflow-y:visible}}@media(min-width: 768px){.navbar-expand-md{flex-wrap:nowrap;-webkit-flex-wrap:nowrap;justify-content:flex-start;-webkit-justify-content:flex-start}.navbar-expand-md .navbar-nav{flex-direction:row;-webkit-flex-direction:row}.navbar-expand-md .navbar-nav .dropdown-menu{position:absolute}.navbar-expand-md .navbar-nav .nav-link{padding-right:.5rem;padding-left:.5rem}.navbar-expand-md .navbar-nav-scroll{overflow:visible}.navbar-expand-md .navbar-collapse{display:flex !important;display:-webkit-flex !important;flex-basis:auto;-webkit-flex-basis:auto}.navbar-expand-md .navbar-toggler{display:none}.navbar-expand-md .offcanvas-header{display:none}.navbar-expand-md .offcanvas{position:inherit;bottom:0;z-index:1000;flex-grow:1;-webkit-flex-grow:1;visibility:visible !important;background-color:rgba(0,0,0,0);border-right:0;border-left:0;transition:none;transform:none}.navbar-expand-md .offcanvas-top,.navbar-expand-md .offcanvas-bottom{height:auto;border-top:0;border-bottom:0}.navbar-expand-md .offcanvas-body{display:flex;display:-webkit-flex;flex-grow:0;-webkit-flex-grow:0;padding:0;overflow-y:visible}}@media(min-width: 992px){.navbar-expand-lg{flex-wrap:nowrap;-webkit-flex-wrap:nowrap;justify-content:flex-start;-webkit-justify-content:flex-start}.navbar-expand-lg .navbar-nav{flex-direction:row;-webkit-flex-direction:row}.navbar-expand-lg .navbar-nav .dropdown-menu{position:absolute}.navbar-expand-lg .navbar-nav .nav-link{padding-right:.5rem;padding-left:.5rem}.navbar-expand-lg .navbar-nav-scroll{overflow:visible}.navbar-expand-lg .navbar-collapse{display:flex !important;display:-webkit-flex !important;flex-basis:auto;-webkit-flex-basis:auto}.navbar-expand-lg .navbar-toggler{display:none}.navbar-expand-lg .offcanvas-header{display:none}.navbar-expand-lg .offcanvas{position:inherit;bottom:0;z-index:1000;flex-grow:1;-webkit-flex-grow:1;visibility:visible !important;background-color:rgba(0,0,0,0);border-right:0;border-left:0;transition:none;transform:none}.navbar-expand-lg .offcanvas-top,.navbar-expand-lg .offcanvas-bottom{height:auto;border-top:0;border-bottom:0}.navbar-expand-lg .offcanvas-body{display:flex;display:-webkit-flex;flex-grow:0;-webkit-flex-grow:0;padding:0;overflow-y:visible}}@media(min-width: 1200px){.navbar-expand-xl{flex-wrap:nowrap;-webkit-flex-wrap:nowrap;justify-content:flex-start;-webkit-justify-content:flex-start}.navbar-expand-xl .navbar-nav{flex-direction:row;-webkit-flex-direction:row}.navbar-expand-xl .navbar-nav .dropdown-menu{position:absolute}.navbar-expand-xl .navbar-nav .nav-link{padding-right:.5rem;padding-left:.5rem}.navbar-expand-xl .navbar-nav-scroll{overflow:visible}.navbar-expand-xl .navbar-collapse{display:flex !important;display:-webkit-flex !important;flex-basis:auto;-webkit-flex-basis:auto}.navbar-expand-xl .navbar-toggler{display:none}.navbar-expand-xl .offcanvas-header{display:none}.navbar-expand-xl .offcanvas{position:inherit;bottom:0;z-index:1000;flex-grow:1;-webkit-flex-grow:1;visibility:visible !important;background-color:rgba(0,0,0,0);border-right:0;border-left:0;transition:none;transform:none}.navbar-expand-xl .offcanvas-top,.navbar-expand-xl .offcanvas-bottom{height:auto;border-top:0;border-bottom:0}.navbar-expand-xl .offcanvas-body{display:flex;display:-webkit-flex;flex-grow:0;-webkit-flex-grow:0;padding:0;overflow-y:visible}}@media(min-width: 1400px){.navbar-expand-xxl{flex-wrap:nowrap;-webkit-flex-wrap:nowrap;justify-content:flex-start;-webkit-justify-content:flex-start}.navbar-expand-xxl .navbar-nav{flex-direction:row;-webkit-flex-direction:row}.navbar-expand-xxl .navbar-nav .dropdown-menu{position:absolute}.navbar-expand-xxl .navbar-nav .nav-link{padding-right:.5rem;padding-left:.5rem}.navbar-expand-xxl .navbar-nav-scroll{overflow:visible}.navbar-expand-xxl .navbar-collapse{display:flex !important;display:-webkit-flex !important;flex-basis:auto;-webkit-flex-basis:auto}.navbar-expand-xxl .navbar-toggler{display:none}.navbar-expand-xxl .offcanvas-header{display:none}.navbar-expand-xxl .offcanvas{position:inherit;bottom:0;z-index:1000;flex-grow:1;-webkit-flex-grow:1;visibility:visible !important;background-color:rgba(0,0,0,0);border-right:0;border-left:0;transition:none;transform:none}.navbar-expand-xxl .offcanvas-top,.navbar-expand-xxl .offcanvas-bottom{height:auto;border-top:0;border-bottom:0}.navbar-expand-xxl .offcanvas-body{display:flex;display:-webkit-flex;flex-grow:0;-webkit-flex-grow:0;padding:0;overflow-y:visible}}.navbar-expand{flex-wrap:nowrap;-webkit-flex-wrap:nowrap;justify-content:flex-start;-webkit-justify-content:flex-start}.navbar-expand .navbar-nav{flex-direction:row;-webkit-flex-direction:row}.navbar-expand .navbar-nav .dropdown-menu{position:absolute}.navbar-expand .navbar-nav .nav-link{padding-right:.5rem;padding-left:.5rem}.navbar-expand .navbar-nav-scroll{overflow:visible}.navbar-expand .navbar-collapse{display:flex !important;display:-webkit-flex !important;flex-basis:auto;-webkit-flex-basis:auto}.navbar-expand .navbar-toggler{display:none}.navbar-expand .offcanvas-header{display:none}.navbar-expand .offcanvas{position:inherit;bottom:0;z-index:1000;flex-grow:1;-webkit-flex-grow:1;visibility:visible !important;background-color:rgba(0,0,0,0);border-right:0;border-left:0;transition:none;transform:none}.navbar-expand .offcanvas-top,.navbar-expand .offcanvas-bottom{height:auto;border-top:0;border-bottom:0}.navbar-expand .offcanvas-body{display:flex;display:-webkit-flex;flex-grow:0;-webkit-flex-grow:0;padding:0;overflow-y:visible}.navbar-light{background-color:#2780e3}.navbar-light .navbar-brand{color:#fdfeff}.navbar-light .navbar-brand:hover,.navbar-light .navbar-brand:focus{color:#fdfeff}.navbar-light .navbar-nav .nav-link{color:#fdfeff}.navbar-light .navbar-nav .nav-link:hover,.navbar-light .navbar-nav .nav-link:focus{color:rgba(253,254,255,.8)}.navbar-light .navbar-nav .nav-link.disabled{color:rgba(253,254,255,.75)}.navbar-light .navbar-nav .show>.nav-link,.navbar-light .navbar-nav .nav-link.active{color:#fdfeff}.navbar-light .navbar-toggler{color:#fdfeff;border-color:rgba(253,254,255,0)}.navbar-light .navbar-toggler-icon{background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 30 30'%3e%3cpath stroke='%23fdfeff' stroke-linecap='round' stroke-miterlimit='10' stroke-width='2' d='M4 7h22M4 15h22M4 23h22'/%3e%3c/svg%3e")}.navbar-light .navbar-text{color:#fdfeff}.navbar-light .navbar-text a,.navbar-light .navbar-text a:hover,.navbar-light .navbar-text a:focus{color:#fdfeff}.navbar-dark{background-color:#2780e3}.navbar-dark .navbar-brand{color:#fdfeff}.navbar-dark .navbar-brand:hover,.navbar-dark .navbar-brand:focus{color:#fdfeff}.navbar-dark .navbar-nav .nav-link{color:#fdfeff}.navbar-dark .navbar-nav .nav-link:hover,.navbar-dark .navbar-nav .nav-link:focus{color:rgba(253,254,255,.8)}.navbar-dark .navbar-nav .nav-link.disabled{color:rgba(253,254,255,.75)}.navbar-dark .navbar-nav .show>.nav-link,.navbar-dark .navbar-nav .active>.nav-link,.navbar-dark .navbar-nav .nav-link.active{color:#fdfeff}.navbar-dark .navbar-toggler{color:#fdfeff;border-color:rgba(253,254,255,0)}.navbar-dark .navbar-toggler-icon{background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 30 30'%3e%3cpath stroke='%23fdfeff' stroke-linecap='round' stroke-miterlimit='10' stroke-width='2' d='M4 7h22M4 15h22M4 23h22'/%3e%3c/svg%3e")}.navbar-dark .navbar-text{color:#fdfeff}.navbar-dark .navbar-text a,.navbar-dark .navbar-text a:hover,.navbar-dark .navbar-text a:focus{color:#fdfeff}.card{position:relative;display:flex;display:-webkit-flex;flex-direction:column;-webkit-flex-direction:column;min-width:0;word-wrap:break-word;background-color:#fff;background-clip:border-box;border:1px solid rgba(0,0,0,.125)}.card>hr{margin-right:0;margin-left:0}.card>.list-group{border-top:inherit;border-bottom:inherit}.card>.list-group:first-child{border-top-width:0}.card>.list-group:last-child{border-bottom-width:0}.card>.card-header+.list-group,.card>.list-group+.card-footer{border-top:0}.card-body{flex:1 1 auto;-webkit-flex:1 1 auto;padding:1rem 1rem}.card-title{margin-bottom:.5rem}.card-subtitle{margin-top:-0.25rem;margin-bottom:0}.card-text:last-child{margin-bottom:0}.card-link+.card-link{margin-left:1rem}.card-header{padding:.5rem 1rem;margin-bottom:0;background-color:#adb5bd;border-bottom:1px solid rgba(0,0,0,.125)}.card-footer{padding:.5rem 1rem;background-color:#adb5bd;border-top:1px solid rgba(0,0,0,.125)}.card-header-tabs{margin-right:-0.5rem;margin-bottom:-0.5rem;margin-left:-0.5rem;border-bottom:0}.card-header-pills{margin-right:-0.5rem;margin-left:-0.5rem}.card-img-overlay{position:absolute;top:0;right:0;bottom:0;left:0;padding:1rem}.card-img,.card-img-top,.card-img-bottom{width:100%}.card-group>.card{margin-bottom:.75rem}@media(min-width: 576px){.card-group{display:flex;display:-webkit-flex;flex-flow:row wrap;-webkit-flex-flow:row wrap}.card-group>.card{flex:1 0 0%;-webkit-flex:1 0 0%;margin-bottom:0}.card-group>.card+.card{margin-left:0;border-left:0}}.accordion-button{position:relative;display:flex;display:-webkit-flex;align-items:center;-webkit-align-items:center;width:100%;padding:1rem 1.25rem;font-size:1rem;color:#373a3c;text-align:left;background-color:#fff;border:0;overflow-anchor:none;transition:color .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out,border-radius .15s ease}@media(prefers-reduced-motion: reduce){.accordion-button{transition:none}}.accordion-button:not(.collapsed){color:#2373cc;background-color:#e9f2fc;box-shadow:inset 0 -1px 0 rgba(0,0,0,.125)}.accordion-button:not(.collapsed)::after{background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%232373cc'%3e%3cpath fill-rule='evenodd' d='M1.646 4.646a.5.5 0 0 1 .708 0L8 10.293l5.646-5.647a.5.5 0 0 1 .708.708l-6 6a.5.5 0 0 1-.708 0l-6-6a.5.5 0 0 1 0-.708z'/%3e%3c/svg%3e");transform:rotate(-180deg)}.accordion-button::after{flex-shrink:0;-webkit-flex-shrink:0;width:1.25rem;height:1.25rem;margin-left:auto;content:"";background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%23373a3c'%3e%3cpath fill-rule='evenodd' d='M1.646 4.646a.5.5 0 0 1 .708 0L8 10.293l5.646-5.647a.5.5 0 0 1 .708.708l-6 6a.5.5 0 0 1-.708 0l-6-6a.5.5 0 0 1 0-.708z'/%3e%3c/svg%3e");background-repeat:no-repeat;background-size:1.25rem;transition:transform .2s ease-in-out}@media(prefers-reduced-motion: reduce){.accordion-button::after{transition:none}}.accordion-button:hover{z-index:2}.accordion-button:focus{z-index:3;border-color:#93c0f1;outline:0;box-shadow:0 0 0 .25rem rgba(39,128,227,.25)}.accordion-header{margin-bottom:0}.accordion-item{background-color:#fff;border:1px solid rgba(0,0,0,.125)}.accordion-item:not(:first-of-type){border-top:0}.accordion-body{padding:1rem 1.25rem}.accordion-flush .accordion-collapse{border-width:0}.accordion-flush .accordion-item{border-right:0;border-left:0}.accordion-flush .accordion-item:first-child{border-top:0}.accordion-flush .accordion-item:last-child{border-bottom:0}.breadcrumb{display:flex;display:-webkit-flex;flex-wrap:wrap;-webkit-flex-wrap:wrap;padding:0 0;margin-bottom:1rem;list-style:none}.breadcrumb-item+.breadcrumb-item{padding-left:.5rem}.breadcrumb-item+.breadcrumb-item::before{float:left;padding-right:.5rem;color:#6c757d;content:var(--bs-breadcrumb-divider, ">") /* rtl: var(--bs-breadcrumb-divider, ">") */}.breadcrumb-item.active{color:#6c757d}.pagination{display:flex;display:-webkit-flex;padding-left:0;list-style:none}.page-link{position:relative;display:block;color:#2780e3;text-decoration:none;-webkit-text-decoration:none;-moz-text-decoration:none;-ms-text-decoration:none;-o-text-decoration:none;background-color:#fff;border:1px solid #dee2e6;transition:color .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out}@media(prefers-reduced-motion: reduce){.page-link{transition:none}}.page-link:hover{z-index:2;color:#1f66b6;background-color:#e9ecef;border-color:#dee2e6}.page-link:focus{z-index:3;color:#1f66b6;background-color:#e9ecef;outline:0;box-shadow:0 0 0 .25rem rgba(39,128,227,.25)}.page-item:not(:first-child) .page-link{margin-left:-1px}.page-item.active .page-link{z-index:3;color:#fff;background-color:#2780e3;border-color:#2780e3}.page-item.disabled .page-link{color:#6c757d;pointer-events:none;background-color:#fff;border-color:#dee2e6}.page-link{padding:.375rem .75rem}.pagination-lg .page-link{padding:.75rem 1.5rem;font-size:1.25rem}.pagination-sm .page-link{padding:.25rem .5rem;font-size:0.875rem}.badge{display:inline-block;padding:.35em .65em;font-size:0.75em;font-weight:700;line-height:1;color:#fff;text-align:center;white-space:nowrap;vertical-align:baseline}.badge:empty{display:none}.btn .badge{position:relative;top:-1px}.alert{position:relative;padding:1rem 1rem;margin-bottom:1rem;border:0 solid rgba(0,0,0,0)}.alert-heading{color:inherit}.alert-link{font-weight:700}.alert-dismissible{padding-right:3rem}.alert-dismissible .btn-close{position:absolute;top:0;right:0;z-index:2;padding:1.25rem 1rem}.alert-default{color:#212324;background-color:#d7d8d8;border-color:#c3c4c5}.alert-default .alert-link{color:#1a1c1d}.alert-primary{color:#174d88;background-color:#d4e6f9;border-color:#bed9f7}.alert-primary .alert-link{color:#123e6d}.alert-secondary{color:#212324;background-color:#d7d8d8;border-color:#c3c4c5}.alert-secondary .alert-link{color:#1a1c1d}.alert-success{color:#266d0e;background-color:#d9f0d1;border-color:#c5e9ba}.alert-success .alert-link{color:#1e570b}.alert-info{color:#5c3270;background-color:#ebddf1;border-color:#e0cceb}.alert-info .alert-link{color:#4a285a}.alert-warning{color:#99460e;background-color:#ffe3d1;border-color:#ffd6ba}.alert-warning .alert-link{color:#7a380b}.alert-danger{color:#902;background-color:#ffccd7;border-color:#ffb3c4}.alert-danger .alert-link{color:#7a001b}.alert-light{color:#959596;background-color:#fefefe;border-color:#fdfdfe}.alert-light .alert-link{color:#777778}.alert-dark{color:#212324;background-color:#d7d8d8;border-color:#c3c4c5}.alert-dark .alert-link{color:#1a1c1d}@keyframes progress-bar-stripes{0%{background-position-x:.5rem}}.progress{display:flex;display:-webkit-flex;height:.5rem;overflow:hidden;font-size:0.75rem;background-color:#e9ecef}.progress-bar{display:flex;display:-webkit-flex;flex-direction:column;-webkit-flex-direction:column;justify-content:center;-webkit-justify-content:center;overflow:hidden;color:#fff;text-align:center;white-space:nowrap;background-color:#2780e3;transition:width .6s ease}@media(prefers-reduced-motion: reduce){.progress-bar{transition:none}}.progress-bar-striped{background-image:linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);background-size:.5rem .5rem}.progress-bar-animated{animation:1s linear infinite progress-bar-stripes}@media(prefers-reduced-motion: reduce){.progress-bar-animated{animation:none}}.list-group{display:flex;display:-webkit-flex;flex-direction:column;-webkit-flex-direction:column;padding-left:0;margin-bottom:0}.list-group-numbered{list-style-type:none;counter-reset:section}.list-group-numbered>li::before{content:counters(section, ".") ". ";counter-increment:section}.list-group-item-action{width:100%;color:#495057;text-align:inherit}.list-group-item-action:hover,.list-group-item-action:focus{z-index:1;color:#495057;text-decoration:none;background-color:#f8f9fa}.list-group-item-action:active{color:#373a3c;background-color:#e9ecef}.list-group-item{position:relative;display:block;padding:.5rem 1rem;color:#212529;text-decoration:none;-webkit-text-decoration:none;-moz-text-decoration:none;-ms-text-decoration:none;-o-text-decoration:none;background-color:#fff;border:1px solid rgba(0,0,0,.125)}.list-group-item.disabled,.list-group-item:disabled{color:#6c757d;pointer-events:none;background-color:#fff}.list-group-item.active{z-index:2;color:#fff;background-color:#2780e3;border-color:#2780e3}.list-group-item+.list-group-item{border-top-width:0}.list-group-item+.list-group-item.active{margin-top:-1px;border-top-width:1px}.list-group-horizontal{flex-direction:row;-webkit-flex-direction:row}.list-group-horizontal>.list-group-item.active{margin-top:0}.list-group-horizontal>.list-group-item+.list-group-item{border-top-width:1px;border-left-width:0}.list-group-horizontal>.list-group-item+.list-group-item.active{margin-left:-1px;border-left-width:1px}@media(min-width: 576px){.list-group-horizontal-sm{flex-direction:row;-webkit-flex-direction:row}.list-group-horizontal-sm>.list-group-item.active{margin-top:0}.list-group-horizontal-sm>.list-group-item+.list-group-item{border-top-width:1px;border-left-width:0}.list-group-horizontal-sm>.list-group-item+.list-group-item.active{margin-left:-1px;border-left-width:1px}}@media(min-width: 768px){.list-group-horizontal-md{flex-direction:row;-webkit-flex-direction:row}.list-group-horizontal-md>.list-group-item.active{margin-top:0}.list-group-horizontal-md>.list-group-item+.list-group-item{border-top-width:1px;border-left-width:0}.list-group-horizontal-md>.list-group-item+.list-group-item.active{margin-left:-1px;border-left-width:1px}}@media(min-width: 992px){.list-group-horizontal-lg{flex-direction:row;-webkit-flex-direction:row}.list-group-horizontal-lg>.list-group-item.active{margin-top:0}.list-group-horizontal-lg>.list-group-item+.list-group-item{border-top-width:1px;border-left-width:0}.list-group-horizontal-lg>.list-group-item+.list-group-item.active{margin-left:-1px;border-left-width:1px}}@media(min-width: 1200px){.list-group-horizontal-xl{flex-direction:row;-webkit-flex-direction:row}.list-group-horizontal-xl>.list-group-item.active{margin-top:0}.list-group-horizontal-xl>.list-group-item+.list-group-item{border-top-width:1px;border-left-width:0}.list-group-horizontal-xl>.list-group-item+.list-group-item.active{margin-left:-1px;border-left-width:1px}}@media(min-width: 1400px){.list-group-horizontal-xxl{flex-direction:row;-webkit-flex-direction:row}.list-group-horizontal-xxl>.list-group-item.active{margin-top:0}.list-group-horizontal-xxl>.list-group-item+.list-group-item{border-top-width:1px;border-left-width:0}.list-group-horizontal-xxl>.list-group-item+.list-group-item.active{margin-left:-1px;border-left-width:1px}}.list-group-flush>.list-group-item{border-width:0 0 1px}.list-group-flush>.list-group-item:last-child{border-bottom-width:0}.list-group-item-default{color:#212324;background-color:#d7d8d8}.list-group-item-default.list-group-item-action:hover,.list-group-item-default.list-group-item-action:focus{color:#212324;background-color:#c2c2c2}.list-group-item-default.list-group-item-action.active{color:#fff;background-color:#212324;border-color:#212324}.list-group-item-primary{color:#174d88;background-color:#d4e6f9}.list-group-item-primary.list-group-item-action:hover,.list-group-item-primary.list-group-item-action:focus{color:#174d88;background-color:#bfcfe0}.list-group-item-primary.list-group-item-action.active{color:#fff;background-color:#174d88;border-color:#174d88}.list-group-item-secondary{color:#212324;background-color:#d7d8d8}.list-group-item-secondary.list-group-item-action:hover,.list-group-item-secondary.list-group-item-action:focus{color:#212324;background-color:#c2c2c2}.list-group-item-secondary.list-group-item-action.active{color:#fff;background-color:#212324;border-color:#212324}.list-group-item-success{color:#266d0e;background-color:#d9f0d1}.list-group-item-success.list-group-item-action:hover,.list-group-item-success.list-group-item-action:focus{color:#266d0e;background-color:#c3d8bc}.list-group-item-success.list-group-item-action.active{color:#fff;background-color:#266d0e;border-color:#266d0e}.list-group-item-info{color:#5c3270;background-color:#ebddf1}.list-group-item-info.list-group-item-action:hover,.list-group-item-info.list-group-item-action:focus{color:#5c3270;background-color:#d4c7d9}.list-group-item-info.list-group-item-action.active{color:#fff;background-color:#5c3270;border-color:#5c3270}.list-group-item-warning{color:#99460e;background-color:#ffe3d1}.list-group-item-warning.list-group-item-action:hover,.list-group-item-warning.list-group-item-action:focus{color:#99460e;background-color:#e6ccbc}.list-group-item-warning.list-group-item-action.active{color:#fff;background-color:#99460e;border-color:#99460e}.list-group-item-danger{color:#902;background-color:#ffccd7}.list-group-item-danger.list-group-item-action:hover,.list-group-item-danger.list-group-item-action:focus{color:#902;background-color:#e6b8c2}.list-group-item-danger.list-group-item-action.active{color:#fff;background-color:#902;border-color:#902}.list-group-item-light{color:#959596;background-color:#fefefe}.list-group-item-light.list-group-item-action:hover,.list-group-item-light.list-group-item-action:focus{color:#959596;background-color:#e5e5e5}.list-group-item-light.list-group-item-action.active{color:#fff;background-color:#959596;border-color:#959596}.list-group-item-dark{color:#212324;background-color:#d7d8d8}.list-group-item-dark.list-group-item-action:hover,.list-group-item-dark.list-group-item-action:focus{color:#212324;background-color:#c2c2c2}.list-group-item-dark.list-group-item-action.active{color:#fff;background-color:#212324;border-color:#212324}.btn-close{box-sizing:content-box;width:1em;height:1em;padding:.25em .25em;color:#000;background:rgba(0,0,0,0) url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%23000'%3e%3cpath d='M.293.293a1 1 0 011.414 0L8 6.586 14.293.293a1 1 0 111.414 1.414L9.414 8l6.293 6.293a1 1 0 01-1.414 1.414L8 9.414l-6.293 6.293a1 1 0 01-1.414-1.414L6.586 8 .293 1.707a1 1 0 010-1.414z'/%3e%3c/svg%3e") center/1em auto no-repeat;border:0;opacity:.5}.btn-close:hover{color:#000;text-decoration:none;opacity:.75}.btn-close:focus{outline:0;box-shadow:0 0 0 .25rem rgba(39,128,227,.25);opacity:1}.btn-close:disabled,.btn-close.disabled{pointer-events:none;user-select:none;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;-o-user-select:none;opacity:.25}.btn-close-white{filter:invert(1) grayscale(100%) brightness(200%)}.toast{width:350px;max-width:100%;font-size:0.875rem;pointer-events:auto;background-color:rgba(255,255,255,.85);background-clip:padding-box;border:1px solid rgba(0,0,0,.1);box-shadow:0 .5rem 1rem rgba(0,0,0,.15)}.toast.showing{opacity:0}.toast:not(.show){display:none}.toast-container{width:max-content;width:-webkit-max-content;width:-moz-max-content;width:-ms-max-content;width:-o-max-content;max-width:100%;pointer-events:none}.toast-container>:not(:last-child){margin-bottom:.75rem}.toast-header{display:flex;display:-webkit-flex;align-items:center;-webkit-align-items:center;padding:.5rem .75rem;color:#6c757d;background-color:rgba(255,255,255,.85);background-clip:padding-box;border-bottom:1px solid rgba(0,0,0,.05)}.toast-header .btn-close{margin-right:-0.375rem;margin-left:.75rem}.toast-body{padding:.75rem;word-wrap:break-word}.modal{position:fixed;top:0;left:0;z-index:1055;display:none;width:100%;height:100%;overflow-x:hidden;overflow-y:auto;outline:0}.modal-dialog{position:relative;width:auto;margin:.5rem;pointer-events:none}.modal.fade .modal-dialog{transition:transform .3s ease-out;transform:translate(0, -50px)}@media(prefers-reduced-motion: reduce){.modal.fade .modal-dialog{transition:none}}.modal.show .modal-dialog{transform:none}.modal.modal-static .modal-dialog{transform:scale(1.02)}.modal-dialog-scrollable{height:calc(100% - 1rem)}.modal-dialog-scrollable .modal-content{max-height:100%;overflow:hidden}.modal-dialog-scrollable .modal-body{overflow-y:auto}.modal-dialog-centered{display:flex;display:-webkit-flex;align-items:center;-webkit-align-items:center;min-height:calc(100% - 1rem)}.modal-content{position:relative;display:flex;display:-webkit-flex;flex-direction:column;-webkit-flex-direction:column;width:100%;pointer-events:auto;background-color:#fff;background-clip:padding-box;border:1px solid rgba(0,0,0,.2);outline:0}.modal-backdrop{position:fixed;top:0;left:0;z-index:1050;width:100vw;height:100vh;background-color:#000}.modal-backdrop.fade{opacity:0}.modal-backdrop.show{opacity:.5}.modal-header{display:flex;display:-webkit-flex;flex-shrink:0;-webkit-flex-shrink:0;align-items:center;-webkit-align-items:center;justify-content:space-between;-webkit-justify-content:space-between;padding:1rem 1rem;border-bottom:1px solid #dee2e6}.modal-header .btn-close{padding:.5rem .5rem;margin:-0.5rem -0.5rem -0.5rem auto}.modal-title{margin-bottom:0;line-height:1.5}.modal-body{position:relative;flex:1 1 auto;-webkit-flex:1 1 auto;padding:1rem}.modal-footer{display:flex;display:-webkit-flex;flex-wrap:wrap;-webkit-flex-wrap:wrap;flex-shrink:0;-webkit-flex-shrink:0;align-items:center;-webkit-align-items:center;justify-content:flex-end;-webkit-justify-content:flex-end;padding:.75rem;border-top:1px solid #dee2e6}.modal-footer>*{margin:.25rem}@media(min-width: 576px){.modal-dialog{max-width:500px;margin:1.75rem auto}.modal-dialog-scrollable{height:calc(100% - 3.5rem)}.modal-dialog-centered{min-height:calc(100% - 3.5rem)}.modal-sm{max-width:300px}}@media(min-width: 992px){.modal-lg,.modal-xl{max-width:800px}}@media(min-width: 1200px){.modal-xl{max-width:1140px}}.modal-fullscreen{width:100vw;max-width:none;height:100%;margin:0}.modal-fullscreen .modal-content{height:100%;border:0}.modal-fullscreen .modal-body{overflow-y:auto}@media(max-width: 575.98px){.modal-fullscreen-sm-down{width:100vw;max-width:none;height:100%;margin:0}.modal-fullscreen-sm-down .modal-content{height:100%;border:0}.modal-fullscreen-sm-down .modal-body{overflow-y:auto}}@media(max-width: 767.98px){.modal-fullscreen-md-down{width:100vw;max-width:none;height:100%;margin:0}.modal-fullscreen-md-down .modal-content{height:100%;border:0}.modal-fullscreen-md-down .modal-body{overflow-y:auto}}@media(max-width: 991.98px){.modal-fullscreen-lg-down{width:100vw;max-width:none;height:100%;margin:0}.modal-fullscreen-lg-down .modal-content{height:100%;border:0}.modal-fullscreen-lg-down .modal-body{overflow-y:auto}}@media(max-width: 1199.98px){.modal-fullscreen-xl-down{width:100vw;max-width:none;height:100%;margin:0}.modal-fullscreen-xl-down .modal-content{height:100%;border:0}.modal-fullscreen-xl-down .modal-body{overflow-y:auto}}@media(max-width: 1399.98px){.modal-fullscreen-xxl-down{width:100vw;max-width:none;height:100%;margin:0}.modal-fullscreen-xxl-down .modal-content{height:100%;border:0}.modal-fullscreen-xxl-down .modal-body{overflow-y:auto}}.tooltip{position:absolute;z-index:1080;display:block;margin:0;font-family:var(--bs-font-sans-serif);font-style:normal;font-weight:400;line-height:1.7;text-align:left;text-align:start;text-decoration:none;text-shadow:none;text-transform:none;letter-spacing:normal;word-break:normal;word-spacing:normal;white-space:normal;line-break:auto;font-size:0.875rem;word-wrap:break-word;opacity:0}.tooltip.show{opacity:.9}.tooltip .tooltip-arrow{position:absolute;display:block;width:.8rem;height:.4rem}.tooltip .tooltip-arrow::before{position:absolute;content:"";border-color:rgba(0,0,0,0);border-style:solid}.bs-tooltip-top,.bs-tooltip-auto[data-popper-placement^=top]{padding:.4rem 0}.bs-tooltip-top .tooltip-arrow,.bs-tooltip-auto[data-popper-placement^=top] .tooltip-arrow{bottom:0}.bs-tooltip-top .tooltip-arrow::before,.bs-tooltip-auto[data-popper-placement^=top] .tooltip-arrow::before{top:-1px;border-width:.4rem .4rem 0;border-top-color:#000}.bs-tooltip-end,.bs-tooltip-auto[data-popper-placement^=right]{padding:0 .4rem}.bs-tooltip-end .tooltip-arrow,.bs-tooltip-auto[data-popper-placement^=right] .tooltip-arrow{left:0;width:.4rem;height:.8rem}.bs-tooltip-end .tooltip-arrow::before,.bs-tooltip-auto[data-popper-placement^=right] .tooltip-arrow::before{right:-1px;border-width:.4rem .4rem .4rem 0;border-right-color:#000}.bs-tooltip-bottom,.bs-tooltip-auto[data-popper-placement^=bottom]{padding:.4rem 0}.bs-tooltip-bottom .tooltip-arrow,.bs-tooltip-auto[data-popper-placement^=bottom] .tooltip-arrow{top:0}.bs-tooltip-bottom .tooltip-arrow::before,.bs-tooltip-auto[data-popper-placement^=bottom] .tooltip-arrow::before{bottom:-1px;border-width:0 .4rem .4rem;border-bottom-color:#000}.bs-tooltip-start,.bs-tooltip-auto[data-popper-placement^=left]{padding:0 .4rem}.bs-tooltip-start .tooltip-arrow,.bs-tooltip-auto[data-popper-placement^=left] .tooltip-arrow{right:0;width:.4rem;height:.8rem}.bs-tooltip-start .tooltip-arrow::before,.bs-tooltip-auto[data-popper-placement^=left] .tooltip-arrow::before{left:-1px;border-width:.4rem 0 .4rem .4rem;border-left-color:#000}.tooltip-inner{max-width:200px;padding:.25rem .5rem;color:#fff;text-align:center;background-color:#000}.popover{position:absolute;top:0;left:0 /* rtl:ignore */;z-index:1070;display:block;max-width:276px;font-family:var(--bs-font-sans-serif);font-style:normal;font-weight:400;line-height:1.7;text-align:left;text-align:start;text-decoration:none;text-shadow:none;text-transform:none;letter-spacing:normal;word-break:normal;word-spacing:normal;white-space:normal;line-break:auto;font-size:0.875rem;word-wrap:break-word;background-color:#fff;background-clip:padding-box;border:1px solid rgba(0,0,0,.2)}.popover .popover-arrow{position:absolute;display:block;width:1rem;height:.5rem}.popover .popover-arrow::before,.popover .popover-arrow::after{position:absolute;display:block;content:"";border-color:rgba(0,0,0,0);border-style:solid}.bs-popover-top>.popover-arrow,.bs-popover-auto[data-popper-placement^=top]>.popover-arrow{bottom:calc(-0.5rem - 1px)}.bs-popover-top>.popover-arrow::before,.bs-popover-auto[data-popper-placement^=top]>.popover-arrow::before{bottom:0;border-width:.5rem .5rem 0;border-top-color:rgba(0,0,0,.25)}.bs-popover-top>.popover-arrow::after,.bs-popover-auto[data-popper-placement^=top]>.popover-arrow::after{bottom:1px;border-width:.5rem .5rem 0;border-top-color:#fff}.bs-popover-end>.popover-arrow,.bs-popover-auto[data-popper-placement^=right]>.popover-arrow{left:calc(-0.5rem - 1px);width:.5rem;height:1rem}.bs-popover-end>.popover-arrow::before,.bs-popover-auto[data-popper-placement^=right]>.popover-arrow::before{left:0;border-width:.5rem .5rem .5rem 0;border-right-color:rgba(0,0,0,.25)}.bs-popover-end>.popover-arrow::after,.bs-popover-auto[data-popper-placement^=right]>.popover-arrow::after{left:1px;border-width:.5rem .5rem .5rem 0;border-right-color:#fff}.bs-popover-bottom>.popover-arrow,.bs-popover-auto[data-popper-placement^=bottom]>.popover-arrow{top:calc(-0.5rem - 1px)}.bs-popover-bottom>.popover-arrow::before,.bs-popover-auto[data-popper-placement^=bottom]>.popover-arrow::before{top:0;border-width:0 .5rem .5rem .5rem;border-bottom-color:rgba(0,0,0,.25)}.bs-popover-bottom>.popover-arrow::after,.bs-popover-auto[data-popper-placement^=bottom]>.popover-arrow::after{top:1px;border-width:0 .5rem .5rem .5rem;border-bottom-color:#fff}.bs-popover-bottom .popover-header::before,.bs-popover-auto[data-popper-placement^=bottom] .popover-header::before{position:absolute;top:0;left:50%;display:block;width:1rem;margin-left:-0.5rem;content:"";border-bottom:1px solid #f0f0f0}.bs-popover-start>.popover-arrow,.bs-popover-auto[data-popper-placement^=left]>.popover-arrow{right:calc(-0.5rem - 1px);width:.5rem;height:1rem}.bs-popover-start>.popover-arrow::before,.bs-popover-auto[data-popper-placement^=left]>.popover-arrow::before{right:0;border-width:.5rem 0 .5rem .5rem;border-left-color:rgba(0,0,0,.25)}.bs-popover-start>.popover-arrow::after,.bs-popover-auto[data-popper-placement^=left]>.popover-arrow::after{right:1px;border-width:.5rem 0 .5rem .5rem;border-left-color:#fff}.popover-header{padding:.5rem 1rem;margin-bottom:0;font-size:1rem;background-color:#f0f0f0;border-bottom:1px solid rgba(0,0,0,.2)}.popover-header:empty{display:none}.popover-body{padding:1rem 1rem;color:#373a3c}.carousel{position:relative}.carousel.pointer-event{touch-action:pan-y;-webkit-touch-action:pan-y;-moz-touch-action:pan-y;-ms-touch-action:pan-y;-o-touch-action:pan-y}.carousel-inner{position:relative;width:100%;overflow:hidden}.carousel-inner::after{display:block;clear:both;content:""}.carousel-item{position:relative;display:none;float:left;width:100%;margin-right:-100%;backface-visibility:hidden;-webkit-backface-visibility:hidden;-moz-backface-visibility:hidden;-ms-backface-visibility:hidden;-o-backface-visibility:hidden;transition:transform .6s ease-in-out}@media(prefers-reduced-motion: reduce){.carousel-item{transition:none}}.carousel-item.active,.carousel-item-next,.carousel-item-prev{display:block}.carousel-item-next:not(.carousel-item-start),.active.carousel-item-end{transform:translateX(100%)}.carousel-item-prev:not(.carousel-item-end),.active.carousel-item-start{transform:translateX(-100%)}.carousel-fade .carousel-item{opacity:0;transition-property:opacity;transform:none}.carousel-fade .carousel-item.active,.carousel-fade .carousel-item-next.carousel-item-start,.carousel-fade .carousel-item-prev.carousel-item-end{z-index:1;opacity:1}.carousel-fade .active.carousel-item-start,.carousel-fade .active.carousel-item-end{z-index:0;opacity:0;transition:opacity 0s .6s}@media(prefers-reduced-motion: reduce){.carousel-fade .active.carousel-item-start,.carousel-fade .active.carousel-item-end{transition:none}}.carousel-control-prev,.carousel-control-next{position:absolute;top:0;bottom:0;z-index:1;display:flex;display:-webkit-flex;align-items:center;-webkit-align-items:center;justify-content:center;-webkit-justify-content:center;width:15%;padding:0;color:#fff;text-align:center;background:none;border:0;opacity:.5;transition:opacity .15s ease}@media(prefers-reduced-motion: reduce){.carousel-control-prev,.carousel-control-next{transition:none}}.carousel-control-prev:hover,.carousel-control-prev:focus,.carousel-control-next:hover,.carousel-control-next:focus{color:#fff;text-decoration:none;outline:0;opacity:.9}.carousel-control-prev{left:0}.carousel-control-next{right:0}.carousel-control-prev-icon,.carousel-control-next-icon{display:inline-block;width:2rem;height:2rem;background-repeat:no-repeat;background-position:50%;background-size:100% 100%}.carousel-control-prev-icon{background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%23fff'%3e%3cpath d='M11.354 1.646a.5.5 0 0 1 0 .708L5.707 8l5.647 5.646a.5.5 0 0 1-.708.708l-6-6a.5.5 0 0 1 0-.708l6-6a.5.5 0 0 1 .708 0z'/%3e%3c/svg%3e")}.carousel-control-next-icon{background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%23fff'%3e%3cpath d='M4.646 1.646a.5.5 0 0 1 .708 0l6 6a.5.5 0 0 1 0 .708l-6 6a.5.5 0 0 1-.708-.708L10.293 8 4.646 2.354a.5.5 0 0 1 0-.708z'/%3e%3c/svg%3e")}.carousel-indicators{position:absolute;right:0;bottom:0;left:0;z-index:2;display:flex;display:-webkit-flex;justify-content:center;-webkit-justify-content:center;padding:0;margin-right:15%;margin-bottom:1rem;margin-left:15%;list-style:none}.carousel-indicators [data-bs-target]{box-sizing:content-box;flex:0 1 auto;-webkit-flex:0 1 auto;width:30px;height:3px;padding:0;margin-right:3px;margin-left:3px;text-indent:-999px;cursor:pointer;background-color:#fff;background-clip:padding-box;border:0;border-top:10px solid rgba(0,0,0,0);border-bottom:10px solid rgba(0,0,0,0);opacity:.5;transition:opacity .6s ease}@media(prefers-reduced-motion: reduce){.carousel-indicators [data-bs-target]{transition:none}}.carousel-indicators .active{opacity:1}.carousel-caption{position:absolute;right:15%;bottom:1.25rem;left:15%;padding-top:1.25rem;padding-bottom:1.25rem;color:#fff;text-align:center}.carousel-dark .carousel-control-prev-icon,.carousel-dark .carousel-control-next-icon{filter:invert(1) grayscale(100)}.carousel-dark .carousel-indicators [data-bs-target]{background-color:#000}.carousel-dark .carousel-caption{color:#000}@keyframes spinner-border{to{transform:rotate(360deg) /* rtl:ignore */}}.spinner-border{display:inline-block;width:2rem;height:2rem;vertical-align:-0.125em;border:.25em solid currentColor;border-right-color:rgba(0,0,0,0);border-radius:50%;animation:.75s linear infinite spinner-border}.spinner-border-sm{width:1rem;height:1rem;border-width:.2em}@keyframes spinner-grow{0%{transform:scale(0)}50%{opacity:1;transform:none}}.spinner-grow{display:inline-block;width:2rem;height:2rem;vertical-align:-0.125em;background-color:currentColor;border-radius:50%;opacity:0;animation:.75s linear infinite spinner-grow}.spinner-grow-sm{width:1rem;height:1rem}@media(prefers-reduced-motion: reduce){.spinner-border,.spinner-grow{animation-duration:1.5s;-webkit-animation-duration:1.5s;-moz-animation-duration:1.5s;-ms-animation-duration:1.5s;-o-animation-duration:1.5s}}.offcanvas{position:fixed;bottom:0;z-index:1045;display:flex;display:-webkit-flex;flex-direction:column;-webkit-flex-direction:column;max-width:100%;visibility:hidden;background-color:#fff;background-clip:padding-box;outline:0;transition:transform .3s ease-in-out}@media(prefers-reduced-motion: reduce){.offcanvas{transition:none}}.offcanvas-backdrop{position:fixed;top:0;left:0;z-index:1040;width:100vw;height:100vh;background-color:#000}.offcanvas-backdrop.fade{opacity:0}.offcanvas-backdrop.show{opacity:.5}.offcanvas-header{display:flex;display:-webkit-flex;align-items:center;-webkit-align-items:center;justify-content:space-between;-webkit-justify-content:space-between;padding:1rem 1rem}.offcanvas-header .btn-close{padding:.5rem .5rem;margin-top:-0.5rem;margin-right:-0.5rem;margin-bottom:-0.5rem}.offcanvas-title{margin-bottom:0;line-height:1.5}.offcanvas-body{flex-grow:1;-webkit-flex-grow:1;padding:1rem 1rem;overflow-y:auto}.offcanvas-start{top:0;left:0;width:400px;border-right:1px solid rgba(0,0,0,.2);transform:translateX(-100%)}.offcanvas-end{top:0;right:0;width:400px;border-left:1px solid rgba(0,0,0,.2);transform:translateX(100%)}.offcanvas-top{top:0;right:0;left:0;height:30vh;max-height:100%;border-bottom:1px solid rgba(0,0,0,.2);transform:translateY(-100%)}.offcanvas-bottom{right:0;left:0;height:30vh;max-height:100%;border-top:1px solid rgba(0,0,0,.2);transform:translateY(100%)}.offcanvas.show{transform:none}.placeholder{display:inline-block;min-height:1em;vertical-align:middle;cursor:wait;background-color:currentColor;opacity:.5}.placeholder.btn::before{display:inline-block;content:""}.placeholder-xs{min-height:.6em}.placeholder-sm{min-height:.8em}.placeholder-lg{min-height:1.2em}.placeholder-glow .placeholder{animation:placeholder-glow 2s ease-in-out infinite}@keyframes placeholder-glow{50%{opacity:.2}}.placeholder-wave{mask-image:linear-gradient(130deg, #000 55%, rgba(0, 0, 0, 0.8) 75%, #000 95%);-webkit-mask-image:linear-gradient(130deg, #000 55%, rgba(0, 0, 0, 0.8) 75%, #000 95%);mask-size:200% 100%;-webkit-mask-size:200% 100%;animation:placeholder-wave 2s linear infinite}@keyframes placeholder-wave{100%{mask-position:-200% 0%;-webkit-mask-position:-200% 0%}}.clearfix::after{display:block;clear:both;content:""}.link-default{color:#373a3c}.link-default:hover,.link-default:focus{color:#2c2e30}.link-primary{color:#2780e3}.link-primary:hover,.link-primary:focus{color:#1f66b6}.link-secondary{color:#373a3c}.link-secondary:hover,.link-secondary:focus{color:#2c2e30}.link-success{color:#3fb618}.link-success:hover,.link-success:focus{color:#329213}.link-info{color:#9954bb}.link-info:hover,.link-info:focus{color:#7a4396}.link-warning{color:#ff7518}.link-warning:hover,.link-warning:focus{color:#cc5e13}.link-danger{color:#ff0039}.link-danger:hover,.link-danger:focus{color:#cc002e}.link-light{color:#f8f9fa}.link-light:hover,.link-light:focus{color:#f9fafb}.link-dark{color:#373a3c}.link-dark:hover,.link-dark:focus{color:#2c2e30}.ratio{position:relative;width:100%}.ratio::before{display:block;padding-top:var(--bs-aspect-ratio);content:""}.ratio>*{position:absolute;top:0;left:0;width:100%;height:100%}.ratio-1x1{--bs-aspect-ratio: 100%}.ratio-4x3{--bs-aspect-ratio: 75%}.ratio-16x9{--bs-aspect-ratio: 56.25%}.ratio-21x9{--bs-aspect-ratio: 42.8571428571%}.fixed-top{position:fixed;top:0;right:0;left:0;z-index:1030}.fixed-bottom{position:fixed;right:0;bottom:0;left:0;z-index:1030}.sticky-top{position:sticky;top:0;z-index:1020}@media(min-width: 576px){.sticky-sm-top{position:sticky;top:0;z-index:1020}}@media(min-width: 768px){.sticky-md-top{position:sticky;top:0;z-index:1020}}@media(min-width: 992px){.sticky-lg-top{position:sticky;top:0;z-index:1020}}@media(min-width: 1200px){.sticky-xl-top{position:sticky;top:0;z-index:1020}}@media(min-width: 1400px){.sticky-xxl-top{position:sticky;top:0;z-index:1020}}.hstack{display:flex;display:-webkit-flex;flex-direction:row;-webkit-flex-direction:row;align-items:center;-webkit-align-items:center;align-self:stretch;-webkit-align-self:stretch}.vstack{display:flex;display:-webkit-flex;flex:1 1 auto;-webkit-flex:1 1 auto;flex-direction:column;-webkit-flex-direction:column;align-self:stretch;-webkit-align-self:stretch}.visually-hidden,.visually-hidden-focusable:not(:focus):not(:focus-within){position:absolute !important;width:1px !important;height:1px !important;padding:0 !important;margin:-1px !important;overflow:hidden !important;clip:rect(0, 0, 0, 0) !important;white-space:nowrap !important;border:0 !important}.stretched-link::after{position:absolute;top:0;right:0;bottom:0;left:0;z-index:1;content:""}.text-truncate{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.vr{display:inline-block;align-self:stretch;-webkit-align-self:stretch;width:1px;min-height:1em;background-color:currentColor;opacity:.25}.align-baseline{vertical-align:baseline !important}.align-top{vertical-align:top !important}.align-middle{vertical-align:middle !important}.align-bottom{vertical-align:bottom !important}.align-text-bottom{vertical-align:text-bottom !important}.align-text-top{vertical-align:text-top !important}.float-start{float:left !important}.float-end{float:right !important}.float-none{float:none !important}.opacity-0{opacity:0 !important}.opacity-25{opacity:.25 !important}.opacity-50{opacity:.5 !important}.opacity-75{opacity:.75 !important}.opacity-100{opacity:1 !important}.overflow-auto{overflow:auto !important}.overflow-hidden{overflow:hidden !important}.overflow-visible{overflow:visible !important}.overflow-scroll{overflow:scroll !important}.d-inline{display:inline !important}.d-inline-block{display:inline-block !important}.d-block{display:block !important}.d-grid{display:grid !important}.d-table{display:table !important}.d-table-row{display:table-row !important}.d-table-cell{display:table-cell !important}.d-flex{display:flex !important}.d-inline-flex{display:inline-flex !important}.d-none{display:none !important}.shadow{box-shadow:0 .5rem 1rem rgba(0,0,0,.15) !important}.shadow-sm{box-shadow:0 .125rem .25rem rgba(0,0,0,.075) !important}.shadow-lg{box-shadow:0 1rem 3rem rgba(0,0,0,.175) !important}.shadow-none{box-shadow:none !important}.position-static{position:static !important}.position-relative{position:relative !important}.position-absolute{position:absolute !important}.position-fixed{position:fixed !important}.position-sticky{position:sticky !important}.top-0{top:0 !important}.top-50{top:50% !important}.top-100{top:100% !important}.bottom-0{bottom:0 !important}.bottom-50{bottom:50% !important}.bottom-100{bottom:100% !important}.start-0{left:0 !important}.start-50{left:50% !important}.start-100{left:100% !important}.end-0{right:0 !important}.end-50{right:50% !important}.end-100{right:100% !important}.translate-middle{transform:translate(-50%, -50%) !important}.translate-middle-x{transform:translateX(-50%) !important}.translate-middle-y{transform:translateY(-50%) !important}.border{border:1px solid #dee2e6 !important}.border-0{border:0 !important}.border-top{border-top:1px solid #dee2e6 !important}.border-top-0{border-top:0 !important}.border-end{border-right:1px solid #dee2e6 !important}.border-end-0{border-right:0 !important}.border-bottom{border-bottom:1px solid #dee2e6 !important}.border-bottom-0{border-bottom:0 !important}.border-start{border-left:1px solid #dee2e6 !important}.border-start-0{border-left:0 !important}.border-default{border-color:#373a3c !important}.border-primary{border-color:#2780e3 !important}.border-secondary{border-color:#373a3c !important}.border-success{border-color:#3fb618 !important}.border-info{border-color:#9954bb !important}.border-warning{border-color:#ff7518 !important}.border-danger{border-color:#ff0039 !important}.border-light{border-color:#f8f9fa !important}.border-dark{border-color:#373a3c !important}.border-white{border-color:#fff !important}.border-1{border-width:1px !important}.border-2{border-width:2px !important}.border-3{border-width:3px !important}.border-4{border-width:4px !important}.border-5{border-width:5px !important}.w-25{width:25% !important}.w-50{width:50% !important}.w-75{width:75% !important}.w-100{width:100% !important}.w-auto{width:auto !important}.mw-100{max-width:100% !important}.vw-100{width:100vw !important}.min-vw-100{min-width:100vw !important}.h-25{height:25% !important}.h-50{height:50% !important}.h-75{height:75% !important}.h-100{height:100% !important}.h-auto{height:auto !important}.mh-100{max-height:100% !important}.vh-100{height:100vh !important}.min-vh-100{min-height:100vh !important}.flex-fill{flex:1 1 auto !important}.flex-row{flex-direction:row !important}.flex-column{flex-direction:column !important}.flex-row-reverse{flex-direction:row-reverse !important}.flex-column-reverse{flex-direction:column-reverse !important}.flex-grow-0{flex-grow:0 !important}.flex-grow-1{flex-grow:1 !important}.flex-shrink-0{flex-shrink:0 !important}.flex-shrink-1{flex-shrink:1 !important}.flex-wrap{flex-wrap:wrap !important}.flex-nowrap{flex-wrap:nowrap !important}.flex-wrap-reverse{flex-wrap:wrap-reverse !important}.gap-0{gap:0 !important}.gap-1{gap:.25rem !important}.gap-2{gap:.5rem !important}.gap-3{gap:1rem !important}.gap-4{gap:1.5rem !important}.gap-5{gap:3rem !important}.justify-content-start{justify-content:flex-start !important}.justify-content-end{justify-content:flex-end !important}.justify-content-center{justify-content:center !important}.justify-content-between{justify-content:space-between !important}.justify-content-around{justify-content:space-around !important}.justify-content-evenly{justify-content:space-evenly !important}.align-items-start{align-items:flex-start !important}.align-items-end{align-items:flex-end !important}.align-items-center{align-items:center !important}.align-items-baseline{align-items:baseline !important}.align-items-stretch{align-items:stretch !important}.align-content-start{align-content:flex-start !important}.align-content-end{align-content:flex-end !important}.align-content-center{align-content:center !important}.align-content-between{align-content:space-between !important}.align-content-around{align-content:space-around !important}.align-content-stretch{align-content:stretch !important}.align-self-auto{align-self:auto !important}.align-self-start{align-self:flex-start !important}.align-self-end{align-self:flex-end !important}.align-self-center{align-self:center !important}.align-self-baseline{align-self:baseline !important}.align-self-stretch{align-self:stretch !important}.order-first{order:-1 !important}.order-0{order:0 !important}.order-1{order:1 !important}.order-2{order:2 !important}.order-3{order:3 !important}.order-4{order:4 !important}.order-5{order:5 !important}.order-last{order:6 !important}.m-0{margin:0 !important}.m-1{margin:.25rem !important}.m-2{margin:.5rem !important}.m-3{margin:1rem !important}.m-4{margin:1.5rem !important}.m-5{margin:3rem !important}.m-auto{margin:auto !important}.mx-0{margin-right:0 !important;margin-left:0 !important}.mx-1{margin-right:.25rem !important;margin-left:.25rem !important}.mx-2{margin-right:.5rem !important;margin-left:.5rem !important}.mx-3{margin-right:1rem !important;margin-left:1rem !important}.mx-4{margin-right:1.5rem !important;margin-left:1.5rem !important}.mx-5{margin-right:3rem !important;margin-left:3rem !important}.mx-auto{margin-right:auto !important;margin-left:auto !important}.my-0{margin-top:0 !important;margin-bottom:0 !important}.my-1{margin-top:.25rem !important;margin-bottom:.25rem !important}.my-2{margin-top:.5rem !important;margin-bottom:.5rem !important}.my-3{margin-top:1rem !important;margin-bottom:1rem !important}.my-4{margin-top:1.5rem !important;margin-bottom:1.5rem !important}.my-5{margin-top:3rem !important;margin-bottom:3rem !important}.my-auto{margin-top:auto !important;margin-bottom:auto !important}.mt-0{margin-top:0 !important}.mt-1{margin-top:.25rem !important}.mt-2{margin-top:.5rem !important}.mt-3{margin-top:1rem !important}.mt-4{margin-top:1.5rem !important}.mt-5{margin-top:3rem !important}.mt-auto{margin-top:auto !important}.me-0{margin-right:0 !important}.me-1{margin-right:.25rem !important}.me-2{margin-right:.5rem !important}.me-3{margin-right:1rem !important}.me-4{margin-right:1.5rem !important}.me-5{margin-right:3rem !important}.me-auto{margin-right:auto !important}.mb-0{margin-bottom:0 !important}.mb-1{margin-bottom:.25rem !important}.mb-2{margin-bottom:.5rem !important}.mb-3{margin-bottom:1rem !important}.mb-4{margin-bottom:1.5rem !important}.mb-5{margin-bottom:3rem !important}.mb-auto{margin-bottom:auto !important}.ms-0{margin-left:0 !important}.ms-1{margin-left:.25rem !important}.ms-2{margin-left:.5rem !important}.ms-3{margin-left:1rem !important}.ms-4{margin-left:1.5rem !important}.ms-5{margin-left:3rem !important}.ms-auto{margin-left:auto !important}.p-0{padding:0 !important}.p-1{padding:.25rem !important}.p-2{padding:.5rem !important}.p-3{padding:1rem !important}.p-4{padding:1.5rem !important}.p-5{padding:3rem !important}.px-0{padding-right:0 !important;padding-left:0 !important}.px-1{padding-right:.25rem !important;padding-left:.25rem !important}.px-2{padding-right:.5rem !important;padding-left:.5rem !important}.px-3{padding-right:1rem !important;padding-left:1rem !important}.px-4{padding-right:1.5rem !important;padding-left:1.5rem !important}.px-5{padding-right:3rem !important;padding-left:3rem !important}.py-0{padding-top:0 !important;padding-bottom:0 !important}.py-1{padding-top:.25rem !important;padding-bottom:.25rem !important}.py-2{padding-top:.5rem !important;padding-bottom:.5rem !important}.py-3{padding-top:1rem !important;padding-bottom:1rem !important}.py-4{padding-top:1.5rem !important;padding-bottom:1.5rem !important}.py-5{padding-top:3rem !important;padding-bottom:3rem !important}.pt-0{padding-top:0 !important}.pt-1{padding-top:.25rem !important}.pt-2{padding-top:.5rem !important}.pt-3{padding-top:1rem !important}.pt-4{padding-top:1.5rem !important}.pt-5{padding-top:3rem !important}.pe-0{padding-right:0 !important}.pe-1{padding-right:.25rem !important}.pe-2{padding-right:.5rem !important}.pe-3{padding-right:1rem !important}.pe-4{padding-right:1.5rem !important}.pe-5{padding-right:3rem !important}.pb-0{padding-bottom:0 !important}.pb-1{padding-bottom:.25rem !important}.pb-2{padding-bottom:.5rem !important}.pb-3{padding-bottom:1rem !important}.pb-4{padding-bottom:1.5rem !important}.pb-5{padding-bottom:3rem !important}.ps-0{padding-left:0 !important}.ps-1{padding-left:.25rem !important}.ps-2{padding-left:.5rem !important}.ps-3{padding-left:1rem !important}.ps-4{padding-left:1.5rem !important}.ps-5{padding-left:3rem !important}.font-monospace{font-family:var(--bs-font-monospace) !important}.fs-1{font-size:calc(1.325rem + 0.9vw) !important}.fs-2{font-size:calc(1.29rem + 0.48vw) !important}.fs-3{font-size:calc(1.27rem + 0.24vw) !important}.fs-4{font-size:1.25rem !important}.fs-5{font-size:1.1rem !important}.fs-6{font-size:1rem !important}.fst-italic{font-style:italic !important}.fst-normal{font-style:normal !important}.fw-light{font-weight:300 !important}.fw-lighter{font-weight:lighter !important}.fw-normal{font-weight:400 !important}.fw-bold{font-weight:700 !important}.fw-bolder{font-weight:bolder !important}.lh-1{line-height:1 !important}.lh-sm{line-height:1.25 !important}.lh-base{line-height:1.7 !important}.lh-lg{line-height:2 !important}.text-start{text-align:left !important}.text-end{text-align:right !important}.text-center{text-align:center !important}.text-decoration-none{text-decoration:none !important}.text-decoration-underline{text-decoration:underline !important}.text-decoration-line-through{text-decoration:line-through !important}.text-lowercase{text-transform:lowercase !important}.text-uppercase{text-transform:uppercase !important}.text-capitalize{text-transform:capitalize !important}.text-wrap{white-space:normal !important}.text-nowrap{white-space:nowrap !important}.text-break{word-wrap:break-word !important;word-break:break-word !important}.text-default{--bs-text-opacity: 1;color:rgba(var(--bs-default-rgb), var(--bs-text-opacity)) !important}.text-primary{--bs-text-opacity: 1;color:rgba(var(--bs-primary-rgb), var(--bs-text-opacity)) !important}.text-secondary{--bs-text-opacity: 1;color:rgba(var(--bs-secondary-rgb), var(--bs-text-opacity)) !important}.text-success{--bs-text-opacity: 1;color:rgba(var(--bs-success-rgb), var(--bs-text-opacity)) !important}.text-info{--bs-text-opacity: 1;color:rgba(var(--bs-info-rgb), var(--bs-text-opacity)) !important}.text-warning{--bs-text-opacity: 1;color:rgba(var(--bs-warning-rgb), var(--bs-text-opacity)) !important}.text-danger{--bs-text-opacity: 1;color:rgba(var(--bs-danger-rgb), var(--bs-text-opacity)) !important}.text-light{--bs-text-opacity: 1;color:rgba(var(--bs-light-rgb), var(--bs-text-opacity)) !important}.text-dark{--bs-text-opacity: 1;color:rgba(var(--bs-dark-rgb), var(--bs-text-opacity)) !important}.text-black{--bs-text-opacity: 1;color:rgba(var(--bs-black-rgb), var(--bs-text-opacity)) !important}.text-white{--bs-text-opacity: 1;color:rgba(var(--bs-white-rgb), var(--bs-text-opacity)) !important}.text-body{--bs-text-opacity: 1;color:rgba(var(--bs-body-color-rgb), var(--bs-text-opacity)) !important}.text-muted{--bs-text-opacity: 1;color:#6c757d !important}.text-black-50{--bs-text-opacity: 1;color:rgba(0,0,0,.5) !important}.text-white-50{--bs-text-opacity: 1;color:rgba(255,255,255,.5) !important}.text-reset{--bs-text-opacity: 1;color:inherit !important}.text-opacity-25{--bs-text-opacity: 0.25}.text-opacity-50{--bs-text-opacity: 0.5}.text-opacity-75{--bs-text-opacity: 0.75}.text-opacity-100{--bs-text-opacity: 1}.bg-default{--bs-bg-opacity: 1;background-color:rgba(var(--bs-default-rgb), var(--bs-bg-opacity)) !important}.bg-primary{--bs-bg-opacity: 1;background-color:rgba(var(--bs-primary-rgb), var(--bs-bg-opacity)) !important}.bg-secondary{--bs-bg-opacity: 1;background-color:rgba(var(--bs-secondary-rgb), var(--bs-bg-opacity)) !important}.bg-success{--bs-bg-opacity: 1;background-color:rgba(var(--bs-success-rgb), var(--bs-bg-opacity)) !important}.bg-info{--bs-bg-opacity: 1;background-color:rgba(var(--bs-info-rgb), var(--bs-bg-opacity)) !important}.bg-warning{--bs-bg-opacity: 1;background-color:rgba(var(--bs-warning-rgb), var(--bs-bg-opacity)) !important}.bg-danger{--bs-bg-opacity: 1;background-color:rgba(var(--bs-danger-rgb), var(--bs-bg-opacity)) !important}.bg-light{--bs-bg-opacity: 1;background-color:rgba(var(--bs-light-rgb), var(--bs-bg-opacity)) !important}.bg-dark{--bs-bg-opacity: 1;background-color:rgba(var(--bs-dark-rgb), var(--bs-bg-opacity)) !important}.bg-black{--bs-bg-opacity: 1;background-color:rgba(var(--bs-black-rgb), var(--bs-bg-opacity)) !important}.bg-white{--bs-bg-opacity: 1;background-color:rgba(var(--bs-white-rgb), var(--bs-bg-opacity)) !important}.bg-body{--bs-bg-opacity: 1;background-color:rgba(var(--bs-body-bg-rgb), var(--bs-bg-opacity)) !important}.bg-transparent{--bs-bg-opacity: 1;background-color:rgba(0,0,0,0) !important}.bg-opacity-10{--bs-bg-opacity: 0.1}.bg-opacity-25{--bs-bg-opacity: 0.25}.bg-opacity-50{--bs-bg-opacity: 0.5}.bg-opacity-75{--bs-bg-opacity: 0.75}.bg-opacity-100{--bs-bg-opacity: 1}.bg-gradient{background-image:var(--bs-gradient) !important}.user-select-all{user-select:all !important}.user-select-auto{user-select:auto !important}.user-select-none{user-select:none !important}.pe-none{pointer-events:none !important}.pe-auto{pointer-events:auto !important}.rounded{border-radius:.25rem !important}.rounded-0{border-radius:0 !important}.rounded-1{border-radius:.2em !important}.rounded-2{border-radius:.25rem !important}.rounded-3{border-radius:.3rem !important}.rounded-circle{border-radius:50% !important}.rounded-pill{border-radius:50rem !important}.rounded-top{border-top-left-radius:.25rem !important;border-top-right-radius:.25rem !important}.rounded-end{border-top-right-radius:.25rem !important;border-bottom-right-radius:.25rem !important}.rounded-bottom{border-bottom-right-radius:.25rem !important;border-bottom-left-radius:.25rem !important}.rounded-start{border-bottom-left-radius:.25rem !important;border-top-left-radius:.25rem !important}.visible{visibility:visible !important}.invisible{visibility:hidden !important}@media(min-width: 576px){.float-sm-start{float:left !important}.float-sm-end{float:right !important}.float-sm-none{float:none !important}.d-sm-inline{display:inline !important}.d-sm-inline-block{display:inline-block !important}.d-sm-block{display:block !important}.d-sm-grid{display:grid !important}.d-sm-table{display:table !important}.d-sm-table-row{display:table-row !important}.d-sm-table-cell{display:table-cell !important}.d-sm-flex{display:flex !important}.d-sm-inline-flex{display:inline-flex !important}.d-sm-none{display:none !important}.flex-sm-fill{flex:1 1 auto !important}.flex-sm-row{flex-direction:row !important}.flex-sm-column{flex-direction:column !important}.flex-sm-row-reverse{flex-direction:row-reverse !important}.flex-sm-column-reverse{flex-direction:column-reverse !important}.flex-sm-grow-0{flex-grow:0 !important}.flex-sm-grow-1{flex-grow:1 !important}.flex-sm-shrink-0{flex-shrink:0 !important}.flex-sm-shrink-1{flex-shrink:1 !important}.flex-sm-wrap{flex-wrap:wrap !important}.flex-sm-nowrap{flex-wrap:nowrap !important}.flex-sm-wrap-reverse{flex-wrap:wrap-reverse !important}.gap-sm-0{gap:0 !important}.gap-sm-1{gap:.25rem !important}.gap-sm-2{gap:.5rem !important}.gap-sm-3{gap:1rem !important}.gap-sm-4{gap:1.5rem !important}.gap-sm-5{gap:3rem !important}.justify-content-sm-start{justify-content:flex-start !important}.justify-content-sm-end{justify-content:flex-end !important}.justify-content-sm-center{justify-content:center !important}.justify-content-sm-between{justify-content:space-between !important}.justify-content-sm-around{justify-content:space-around !important}.justify-content-sm-evenly{justify-content:space-evenly !important}.align-items-sm-start{align-items:flex-start !important}.align-items-sm-end{align-items:flex-end !important}.align-items-sm-center{align-items:center !important}.align-items-sm-baseline{align-items:baseline !important}.align-items-sm-stretch{align-items:stretch !important}.align-content-sm-start{align-content:flex-start !important}.align-content-sm-end{align-content:flex-end !important}.align-content-sm-center{align-content:center !important}.align-content-sm-between{align-content:space-between !important}.align-content-sm-around{align-content:space-around !important}.align-content-sm-stretch{align-content:stretch !important}.align-self-sm-auto{align-self:auto !important}.align-self-sm-start{align-self:flex-start !important}.align-self-sm-end{align-self:flex-end !important}.align-self-sm-center{align-self:center !important}.align-self-sm-baseline{align-self:baseline !important}.align-self-sm-stretch{align-self:stretch !important}.order-sm-first{order:-1 !important}.order-sm-0{order:0 !important}.order-sm-1{order:1 !important}.order-sm-2{order:2 !important}.order-sm-3{order:3 !important}.order-sm-4{order:4 !important}.order-sm-5{order:5 !important}.order-sm-last{order:6 !important}.m-sm-0{margin:0 !important}.m-sm-1{margin:.25rem !important}.m-sm-2{margin:.5rem !important}.m-sm-3{margin:1rem !important}.m-sm-4{margin:1.5rem !important}.m-sm-5{margin:3rem !important}.m-sm-auto{margin:auto !important}.mx-sm-0{margin-right:0 !important;margin-left:0 !important}.mx-sm-1{margin-right:.25rem !important;margin-left:.25rem !important}.mx-sm-2{margin-right:.5rem !important;margin-left:.5rem !important}.mx-sm-3{margin-right:1rem !important;margin-left:1rem !important}.mx-sm-4{margin-right:1.5rem !important;margin-left:1.5rem !important}.mx-sm-5{margin-right:3rem !important;margin-left:3rem !important}.mx-sm-auto{margin-right:auto !important;margin-left:auto !important}.my-sm-0{margin-top:0 !important;margin-bottom:0 !important}.my-sm-1{margin-top:.25rem !important;margin-bottom:.25rem !important}.my-sm-2{margin-top:.5rem !important;margin-bottom:.5rem !important}.my-sm-3{margin-top:1rem !important;margin-bottom:1rem !important}.my-sm-4{margin-top:1.5rem !important;margin-bottom:1.5rem !important}.my-sm-5{margin-top:3rem !important;margin-bottom:3rem !important}.my-sm-auto{margin-top:auto !important;margin-bottom:auto !important}.mt-sm-0{margin-top:0 !important}.mt-sm-1{margin-top:.25rem !important}.mt-sm-2{margin-top:.5rem !important}.mt-sm-3{margin-top:1rem !important}.mt-sm-4{margin-top:1.5rem !important}.mt-sm-5{margin-top:3rem !important}.mt-sm-auto{margin-top:auto !important}.me-sm-0{margin-right:0 !important}.me-sm-1{margin-right:.25rem !important}.me-sm-2{margin-right:.5rem !important}.me-sm-3{margin-right:1rem !important}.me-sm-4{margin-right:1.5rem !important}.me-sm-5{margin-right:3rem !important}.me-sm-auto{margin-right:auto !important}.mb-sm-0{margin-bottom:0 !important}.mb-sm-1{margin-bottom:.25rem !important}.mb-sm-2{margin-bottom:.5rem !important}.mb-sm-3{margin-bottom:1rem !important}.mb-sm-4{margin-bottom:1.5rem !important}.mb-sm-5{margin-bottom:3rem !important}.mb-sm-auto{margin-bottom:auto !important}.ms-sm-0{margin-left:0 !important}.ms-sm-1{margin-left:.25rem !important}.ms-sm-2{margin-left:.5rem !important}.ms-sm-3{margin-left:1rem !important}.ms-sm-4{margin-left:1.5rem !important}.ms-sm-5{margin-left:3rem !important}.ms-sm-auto{margin-left:auto !important}.p-sm-0{padding:0 !important}.p-sm-1{padding:.25rem !important}.p-sm-2{padding:.5rem !important}.p-sm-3{padding:1rem !important}.p-sm-4{padding:1.5rem !important}.p-sm-5{padding:3rem !important}.px-sm-0{padding-right:0 !important;padding-left:0 !important}.px-sm-1{padding-right:.25rem !important;padding-left:.25rem !important}.px-sm-2{padding-right:.5rem !important;padding-left:.5rem !important}.px-sm-3{padding-right:1rem !important;padding-left:1rem !important}.px-sm-4{padding-right:1.5rem !important;padding-left:1.5rem !important}.px-sm-5{padding-right:3rem !important;padding-left:3rem !important}.py-sm-0{padding-top:0 !important;padding-bottom:0 !important}.py-sm-1{padding-top:.25rem !important;padding-bottom:.25rem !important}.py-sm-2{padding-top:.5rem !important;padding-bottom:.5rem !important}.py-sm-3{padding-top:1rem !important;padding-bottom:1rem !important}.py-sm-4{padding-top:1.5rem !important;padding-bottom:1.5rem !important}.py-sm-5{padding-top:3rem !important;padding-bottom:3rem !important}.pt-sm-0{padding-top:0 !important}.pt-sm-1{padding-top:.25rem !important}.pt-sm-2{padding-top:.5rem !important}.pt-sm-3{padding-top:1rem !important}.pt-sm-4{padding-top:1.5rem !important}.pt-sm-5{padding-top:3rem !important}.pe-sm-0{padding-right:0 !important}.pe-sm-1{padding-right:.25rem !important}.pe-sm-2{padding-right:.5rem !important}.pe-sm-3{padding-right:1rem !important}.pe-sm-4{padding-right:1.5rem !important}.pe-sm-5{padding-right:3rem !important}.pb-sm-0{padding-bottom:0 !important}.pb-sm-1{padding-bottom:.25rem !important}.pb-sm-2{padding-bottom:.5rem !important}.pb-sm-3{padding-bottom:1rem !important}.pb-sm-4{padding-bottom:1.5rem !important}.pb-sm-5{padding-bottom:3rem !important}.ps-sm-0{padding-left:0 !important}.ps-sm-1{padding-left:.25rem !important}.ps-sm-2{padding-left:.5rem !important}.ps-sm-3{padding-left:1rem !important}.ps-sm-4{padding-left:1.5rem !important}.ps-sm-5{padding-left:3rem !important}.text-sm-start{text-align:left !important}.text-sm-end{text-align:right !important}.text-sm-center{text-align:center !important}}@media(min-width: 768px){.float-md-start{float:left !important}.float-md-end{float:right !important}.float-md-none{float:none !important}.d-md-inline{display:inline !important}.d-md-inline-block{display:inline-block !important}.d-md-block{display:block !important}.d-md-grid{display:grid !important}.d-md-table{display:table !important}.d-md-table-row{display:table-row !important}.d-md-table-cell{display:table-cell !important}.d-md-flex{display:flex !important}.d-md-inline-flex{display:inline-flex !important}.d-md-none{display:none !important}.flex-md-fill{flex:1 1 auto !important}.flex-md-row{flex-direction:row !important}.flex-md-column{flex-direction:column !important}.flex-md-row-reverse{flex-direction:row-reverse !important}.flex-md-column-reverse{flex-direction:column-reverse !important}.flex-md-grow-0{flex-grow:0 !important}.flex-md-grow-1{flex-grow:1 !important}.flex-md-shrink-0{flex-shrink:0 !important}.flex-md-shrink-1{flex-shrink:1 !important}.flex-md-wrap{flex-wrap:wrap !important}.flex-md-nowrap{flex-wrap:nowrap !important}.flex-md-wrap-reverse{flex-wrap:wrap-reverse !important}.gap-md-0{gap:0 !important}.gap-md-1{gap:.25rem !important}.gap-md-2{gap:.5rem !important}.gap-md-3{gap:1rem !important}.gap-md-4{gap:1.5rem !important}.gap-md-5{gap:3rem !important}.justify-content-md-start{justify-content:flex-start !important}.justify-content-md-end{justify-content:flex-end !important}.justify-content-md-center{justify-content:center !important}.justify-content-md-between{justify-content:space-between !important}.justify-content-md-around{justify-content:space-around !important}.justify-content-md-evenly{justify-content:space-evenly !important}.align-items-md-start{align-items:flex-start !important}.align-items-md-end{align-items:flex-end !important}.align-items-md-center{align-items:center !important}.align-items-md-baseline{align-items:baseline !important}.align-items-md-stretch{align-items:stretch !important}.align-content-md-start{align-content:flex-start !important}.align-content-md-end{align-content:flex-end !important}.align-content-md-center{align-content:center !important}.align-content-md-between{align-content:space-between !important}.align-content-md-around{align-content:space-around !important}.align-content-md-stretch{align-content:stretch !important}.align-self-md-auto{align-self:auto !important}.align-self-md-start{align-self:flex-start !important}.align-self-md-end{align-self:flex-end !important}.align-self-md-center{align-self:center !important}.align-self-md-baseline{align-self:baseline !important}.align-self-md-stretch{align-self:stretch !important}.order-md-first{order:-1 !important}.order-md-0{order:0 !important}.order-md-1{order:1 !important}.order-md-2{order:2 !important}.order-md-3{order:3 !important}.order-md-4{order:4 !important}.order-md-5{order:5 !important}.order-md-last{order:6 !important}.m-md-0{margin:0 !important}.m-md-1{margin:.25rem !important}.m-md-2{margin:.5rem !important}.m-md-3{margin:1rem !important}.m-md-4{margin:1.5rem !important}.m-md-5{margin:3rem !important}.m-md-auto{margin:auto !important}.mx-md-0{margin-right:0 !important;margin-left:0 !important}.mx-md-1{margin-right:.25rem !important;margin-left:.25rem !important}.mx-md-2{margin-right:.5rem !important;margin-left:.5rem !important}.mx-md-3{margin-right:1rem !important;margin-left:1rem !important}.mx-md-4{margin-right:1.5rem !important;margin-left:1.5rem !important}.mx-md-5{margin-right:3rem !important;margin-left:3rem !important}.mx-md-auto{margin-right:auto !important;margin-left:auto !important}.my-md-0{margin-top:0 !important;margin-bottom:0 !important}.my-md-1{margin-top:.25rem !important;margin-bottom:.25rem !important}.my-md-2{margin-top:.5rem !important;margin-bottom:.5rem !important}.my-md-3{margin-top:1rem !important;margin-bottom:1rem !important}.my-md-4{margin-top:1.5rem !important;margin-bottom:1.5rem !important}.my-md-5{margin-top:3rem !important;margin-bottom:3rem !important}.my-md-auto{margin-top:auto !important;margin-bottom:auto !important}.mt-md-0{margin-top:0 !important}.mt-md-1{margin-top:.25rem !important}.mt-md-2{margin-top:.5rem !important}.mt-md-3{margin-top:1rem !important}.mt-md-4{margin-top:1.5rem !important}.mt-md-5{margin-top:3rem !important}.mt-md-auto{margin-top:auto !important}.me-md-0{margin-right:0 !important}.me-md-1{margin-right:.25rem !important}.me-md-2{margin-right:.5rem !important}.me-md-3{margin-right:1rem !important}.me-md-4{margin-right:1.5rem !important}.me-md-5{margin-right:3rem !important}.me-md-auto{margin-right:auto !important}.mb-md-0{margin-bottom:0 !important}.mb-md-1{margin-bottom:.25rem !important}.mb-md-2{margin-bottom:.5rem !important}.mb-md-3{margin-bottom:1rem !important}.mb-md-4{margin-bottom:1.5rem !important}.mb-md-5{margin-bottom:3rem !important}.mb-md-auto{margin-bottom:auto !important}.ms-md-0{margin-left:0 !important}.ms-md-1{margin-left:.25rem !important}.ms-md-2{margin-left:.5rem !important}.ms-md-3{margin-left:1rem !important}.ms-md-4{margin-left:1.5rem !important}.ms-md-5{margin-left:3rem !important}.ms-md-auto{margin-left:auto !important}.p-md-0{padding:0 !important}.p-md-1{padding:.25rem !important}.p-md-2{padding:.5rem !important}.p-md-3{padding:1rem !important}.p-md-4{padding:1.5rem !important}.p-md-5{padding:3rem !important}.px-md-0{padding-right:0 !important;padding-left:0 !important}.px-md-1{padding-right:.25rem !important;padding-left:.25rem !important}.px-md-2{padding-right:.5rem !important;padding-left:.5rem !important}.px-md-3{padding-right:1rem !important;padding-left:1rem !important}.px-md-4{padding-right:1.5rem !important;padding-left:1.5rem !important}.px-md-5{padding-right:3rem !important;padding-left:3rem !important}.py-md-0{padding-top:0 !important;padding-bottom:0 !important}.py-md-1{padding-top:.25rem !important;padding-bottom:.25rem !important}.py-md-2{padding-top:.5rem !important;padding-bottom:.5rem !important}.py-md-3{padding-top:1rem !important;padding-bottom:1rem !important}.py-md-4{padding-top:1.5rem !important;padding-bottom:1.5rem !important}.py-md-5{padding-top:3rem !important;padding-bottom:3rem !important}.pt-md-0{padding-top:0 !important}.pt-md-1{padding-top:.25rem !important}.pt-md-2{padding-top:.5rem !important}.pt-md-3{padding-top:1rem !important}.pt-md-4{padding-top:1.5rem !important}.pt-md-5{padding-top:3rem !important}.pe-md-0{padding-right:0 !important}.pe-md-1{padding-right:.25rem !important}.pe-md-2{padding-right:.5rem !important}.pe-md-3{padding-right:1rem !important}.pe-md-4{padding-right:1.5rem !important}.pe-md-5{padding-right:3rem !important}.pb-md-0{padding-bottom:0 !important}.pb-md-1{padding-bottom:.25rem !important}.pb-md-2{padding-bottom:.5rem !important}.pb-md-3{padding-bottom:1rem !important}.pb-md-4{padding-bottom:1.5rem !important}.pb-md-5{padding-bottom:3rem !important}.ps-md-0{padding-left:0 !important}.ps-md-1{padding-left:.25rem !important}.ps-md-2{padding-left:.5rem !important}.ps-md-3{padding-left:1rem !important}.ps-md-4{padding-left:1.5rem !important}.ps-md-5{padding-left:3rem !important}.text-md-start{text-align:left !important}.text-md-end{text-align:right !important}.text-md-center{text-align:center !important}}@media(min-width: 992px){.float-lg-start{float:left !important}.float-lg-end{float:right !important}.float-lg-none{float:none !important}.d-lg-inline{display:inline !important}.d-lg-inline-block{display:inline-block !important}.d-lg-block{display:block !important}.d-lg-grid{display:grid !important}.d-lg-table{display:table !important}.d-lg-table-row{display:table-row !important}.d-lg-table-cell{display:table-cell !important}.d-lg-flex{display:flex !important}.d-lg-inline-flex{display:inline-flex !important}.d-lg-none{display:none !important}.flex-lg-fill{flex:1 1 auto !important}.flex-lg-row{flex-direction:row !important}.flex-lg-column{flex-direction:column !important}.flex-lg-row-reverse{flex-direction:row-reverse !important}.flex-lg-column-reverse{flex-direction:column-reverse !important}.flex-lg-grow-0{flex-grow:0 !important}.flex-lg-grow-1{flex-grow:1 !important}.flex-lg-shrink-0{flex-shrink:0 !important}.flex-lg-shrink-1{flex-shrink:1 !important}.flex-lg-wrap{flex-wrap:wrap !important}.flex-lg-nowrap{flex-wrap:nowrap !important}.flex-lg-wrap-reverse{flex-wrap:wrap-reverse !important}.gap-lg-0{gap:0 !important}.gap-lg-1{gap:.25rem !important}.gap-lg-2{gap:.5rem !important}.gap-lg-3{gap:1rem !important}.gap-lg-4{gap:1.5rem !important}.gap-lg-5{gap:3rem !important}.justify-content-lg-start{justify-content:flex-start !important}.justify-content-lg-end{justify-content:flex-end !important}.justify-content-lg-center{justify-content:center !important}.justify-content-lg-between{justify-content:space-between !important}.justify-content-lg-around{justify-content:space-around !important}.justify-content-lg-evenly{justify-content:space-evenly !important}.align-items-lg-start{align-items:flex-start !important}.align-items-lg-end{align-items:flex-end !important}.align-items-lg-center{align-items:center !important}.align-items-lg-baseline{align-items:baseline !important}.align-items-lg-stretch{align-items:stretch !important}.align-content-lg-start{align-content:flex-start !important}.align-content-lg-end{align-content:flex-end !important}.align-content-lg-center{align-content:center !important}.align-content-lg-between{align-content:space-between !important}.align-content-lg-around{align-content:space-around !important}.align-content-lg-stretch{align-content:stretch !important}.align-self-lg-auto{align-self:auto !important}.align-self-lg-start{align-self:flex-start !important}.align-self-lg-end{align-self:flex-end !important}.align-self-lg-center{align-self:center !important}.align-self-lg-baseline{align-self:baseline !important}.align-self-lg-stretch{align-self:stretch !important}.order-lg-first{order:-1 !important}.order-lg-0{order:0 !important}.order-lg-1{order:1 !important}.order-lg-2{order:2 !important}.order-lg-3{order:3 !important}.order-lg-4{order:4 !important}.order-lg-5{order:5 !important}.order-lg-last{order:6 !important}.m-lg-0{margin:0 !important}.m-lg-1{margin:.25rem !important}.m-lg-2{margin:.5rem !important}.m-lg-3{margin:1rem !important}.m-lg-4{margin:1.5rem !important}.m-lg-5{margin:3rem !important}.m-lg-auto{margin:auto !important}.mx-lg-0{margin-right:0 !important;margin-left:0 !important}.mx-lg-1{margin-right:.25rem !important;margin-left:.25rem !important}.mx-lg-2{margin-right:.5rem !important;margin-left:.5rem !important}.mx-lg-3{margin-right:1rem !important;margin-left:1rem !important}.mx-lg-4{margin-right:1.5rem !important;margin-left:1.5rem !important}.mx-lg-5{margin-right:3rem !important;margin-left:3rem !important}.mx-lg-auto{margin-right:auto !important;margin-left:auto !important}.my-lg-0{margin-top:0 !important;margin-bottom:0 !important}.my-lg-1{margin-top:.25rem !important;margin-bottom:.25rem !important}.my-lg-2{margin-top:.5rem !important;margin-bottom:.5rem !important}.my-lg-3{margin-top:1rem !important;margin-bottom:1rem !important}.my-lg-4{margin-top:1.5rem !important;margin-bottom:1.5rem !important}.my-lg-5{margin-top:3rem !important;margin-bottom:3rem !important}.my-lg-auto{margin-top:auto !important;margin-bottom:auto !important}.mt-lg-0{margin-top:0 !important}.mt-lg-1{margin-top:.25rem !important}.mt-lg-2{margin-top:.5rem !important}.mt-lg-3{margin-top:1rem !important}.mt-lg-4{margin-top:1.5rem !important}.mt-lg-5{margin-top:3rem !important}.mt-lg-auto{margin-top:auto !important}.me-lg-0{margin-right:0 !important}.me-lg-1{margin-right:.25rem !important}.me-lg-2{margin-right:.5rem !important}.me-lg-3{margin-right:1rem !important}.me-lg-4{margin-right:1.5rem !important}.me-lg-5{margin-right:3rem !important}.me-lg-auto{margin-right:auto !important}.mb-lg-0{margin-bottom:0 !important}.mb-lg-1{margin-bottom:.25rem !important}.mb-lg-2{margin-bottom:.5rem !important}.mb-lg-3{margin-bottom:1rem !important}.mb-lg-4{margin-bottom:1.5rem !important}.mb-lg-5{margin-bottom:3rem !important}.mb-lg-auto{margin-bottom:auto !important}.ms-lg-0{margin-left:0 !important}.ms-lg-1{margin-left:.25rem !important}.ms-lg-2{margin-left:.5rem !important}.ms-lg-3{margin-left:1rem !important}.ms-lg-4{margin-left:1.5rem !important}.ms-lg-5{margin-left:3rem !important}.ms-lg-auto{margin-left:auto !important}.p-lg-0{padding:0 !important}.p-lg-1{padding:.25rem !important}.p-lg-2{padding:.5rem !important}.p-lg-3{padding:1rem !important}.p-lg-4{padding:1.5rem !important}.p-lg-5{padding:3rem !important}.px-lg-0{padding-right:0 !important;padding-left:0 !important}.px-lg-1{padding-right:.25rem !important;padding-left:.25rem !important}.px-lg-2{padding-right:.5rem !important;padding-left:.5rem !important}.px-lg-3{padding-right:1rem !important;padding-left:1rem !important}.px-lg-4{padding-right:1.5rem !important;padding-left:1.5rem !important}.px-lg-5{padding-right:3rem !important;padding-left:3rem !important}.py-lg-0{padding-top:0 !important;padding-bottom:0 !important}.py-lg-1{padding-top:.25rem !important;padding-bottom:.25rem !important}.py-lg-2{padding-top:.5rem !important;padding-bottom:.5rem !important}.py-lg-3{padding-top:1rem !important;padding-bottom:1rem !important}.py-lg-4{padding-top:1.5rem !important;padding-bottom:1.5rem !important}.py-lg-5{padding-top:3rem !important;padding-bottom:3rem !important}.pt-lg-0{padding-top:0 !important}.pt-lg-1{padding-top:.25rem !important}.pt-lg-2{padding-top:.5rem !important}.pt-lg-3{padding-top:1rem !important}.pt-lg-4{padding-top:1.5rem !important}.pt-lg-5{padding-top:3rem !important}.pe-lg-0{padding-right:0 !important}.pe-lg-1{padding-right:.25rem !important}.pe-lg-2{padding-right:.5rem !important}.pe-lg-3{padding-right:1rem !important}.pe-lg-4{padding-right:1.5rem !important}.pe-lg-5{padding-right:3rem !important}.pb-lg-0{padding-bottom:0 !important}.pb-lg-1{padding-bottom:.25rem !important}.pb-lg-2{padding-bottom:.5rem !important}.pb-lg-3{padding-bottom:1rem !important}.pb-lg-4{padding-bottom:1.5rem !important}.pb-lg-5{padding-bottom:3rem !important}.ps-lg-0{padding-left:0 !important}.ps-lg-1{padding-left:.25rem !important}.ps-lg-2{padding-left:.5rem !important}.ps-lg-3{padding-left:1rem !important}.ps-lg-4{padding-left:1.5rem !important}.ps-lg-5{padding-left:3rem !important}.text-lg-start{text-align:left !important}.text-lg-end{text-align:right !important}.text-lg-center{text-align:center !important}}@media(min-width: 1200px){.float-xl-start{float:left !important}.float-xl-end{float:right !important}.float-xl-none{float:none !important}.d-xl-inline{display:inline !important}.d-xl-inline-block{display:inline-block !important}.d-xl-block{display:block !important}.d-xl-grid{display:grid !important}.d-xl-table{display:table !important}.d-xl-table-row{display:table-row !important}.d-xl-table-cell{display:table-cell !important}.d-xl-flex{display:flex !important}.d-xl-inline-flex{display:inline-flex !important}.d-xl-none{display:none !important}.flex-xl-fill{flex:1 1 auto !important}.flex-xl-row{flex-direction:row !important}.flex-xl-column{flex-direction:column !important}.flex-xl-row-reverse{flex-direction:row-reverse !important}.flex-xl-column-reverse{flex-direction:column-reverse !important}.flex-xl-grow-0{flex-grow:0 !important}.flex-xl-grow-1{flex-grow:1 !important}.flex-xl-shrink-0{flex-shrink:0 !important}.flex-xl-shrink-1{flex-shrink:1 !important}.flex-xl-wrap{flex-wrap:wrap !important}.flex-xl-nowrap{flex-wrap:nowrap !important}.flex-xl-wrap-reverse{flex-wrap:wrap-reverse !important}.gap-xl-0{gap:0 !important}.gap-xl-1{gap:.25rem !important}.gap-xl-2{gap:.5rem !important}.gap-xl-3{gap:1rem !important}.gap-xl-4{gap:1.5rem !important}.gap-xl-5{gap:3rem !important}.justify-content-xl-start{justify-content:flex-start !important}.justify-content-xl-end{justify-content:flex-end !important}.justify-content-xl-center{justify-content:center !important}.justify-content-xl-between{justify-content:space-between !important}.justify-content-xl-around{justify-content:space-around !important}.justify-content-xl-evenly{justify-content:space-evenly !important}.align-items-xl-start{align-items:flex-start !important}.align-items-xl-end{align-items:flex-end !important}.align-items-xl-center{align-items:center !important}.align-items-xl-baseline{align-items:baseline !important}.align-items-xl-stretch{align-items:stretch !important}.align-content-xl-start{align-content:flex-start !important}.align-content-xl-end{align-content:flex-end !important}.align-content-xl-center{align-content:center !important}.align-content-xl-between{align-content:space-between !important}.align-content-xl-around{align-content:space-around !important}.align-content-xl-stretch{align-content:stretch !important}.align-self-xl-auto{align-self:auto !important}.align-self-xl-start{align-self:flex-start !important}.align-self-xl-end{align-self:flex-end !important}.align-self-xl-center{align-self:center !important}.align-self-xl-baseline{align-self:baseline !important}.align-self-xl-stretch{align-self:stretch !important}.order-xl-first{order:-1 !important}.order-xl-0{order:0 !important}.order-xl-1{order:1 !important}.order-xl-2{order:2 !important}.order-xl-3{order:3 !important}.order-xl-4{order:4 !important}.order-xl-5{order:5 !important}.order-xl-last{order:6 !important}.m-xl-0{margin:0 !important}.m-xl-1{margin:.25rem !important}.m-xl-2{margin:.5rem !important}.m-xl-3{margin:1rem !important}.m-xl-4{margin:1.5rem !important}.m-xl-5{margin:3rem !important}.m-xl-auto{margin:auto !important}.mx-xl-0{margin-right:0 !important;margin-left:0 !important}.mx-xl-1{margin-right:.25rem !important;margin-left:.25rem !important}.mx-xl-2{margin-right:.5rem !important;margin-left:.5rem !important}.mx-xl-3{margin-right:1rem !important;margin-left:1rem !important}.mx-xl-4{margin-right:1.5rem !important;margin-left:1.5rem !important}.mx-xl-5{margin-right:3rem !important;margin-left:3rem !important}.mx-xl-auto{margin-right:auto !important;margin-left:auto !important}.my-xl-0{margin-top:0 !important;margin-bottom:0 !important}.my-xl-1{margin-top:.25rem !important;margin-bottom:.25rem !important}.my-xl-2{margin-top:.5rem !important;margin-bottom:.5rem !important}.my-xl-3{margin-top:1rem !important;margin-bottom:1rem !important}.my-xl-4{margin-top:1.5rem !important;margin-bottom:1.5rem !important}.my-xl-5{margin-top:3rem !important;margin-bottom:3rem !important}.my-xl-auto{margin-top:auto !important;margin-bottom:auto !important}.mt-xl-0{margin-top:0 !important}.mt-xl-1{margin-top:.25rem !important}.mt-xl-2{margin-top:.5rem !important}.mt-xl-3{margin-top:1rem !important}.mt-xl-4{margin-top:1.5rem !important}.mt-xl-5{margin-top:3rem !important}.mt-xl-auto{margin-top:auto !important}.me-xl-0{margin-right:0 !important}.me-xl-1{margin-right:.25rem !important}.me-xl-2{margin-right:.5rem !important}.me-xl-3{margin-right:1rem !important}.me-xl-4{margin-right:1.5rem !important}.me-xl-5{margin-right:3rem !important}.me-xl-auto{margin-right:auto !important}.mb-xl-0{margin-bottom:0 !important}.mb-xl-1{margin-bottom:.25rem !important}.mb-xl-2{margin-bottom:.5rem !important}.mb-xl-3{margin-bottom:1rem !important}.mb-xl-4{margin-bottom:1.5rem !important}.mb-xl-5{margin-bottom:3rem !important}.mb-xl-auto{margin-bottom:auto !important}.ms-xl-0{margin-left:0 !important}.ms-xl-1{margin-left:.25rem !important}.ms-xl-2{margin-left:.5rem !important}.ms-xl-3{margin-left:1rem !important}.ms-xl-4{margin-left:1.5rem !important}.ms-xl-5{margin-left:3rem !important}.ms-xl-auto{margin-left:auto !important}.p-xl-0{padding:0 !important}.p-xl-1{padding:.25rem !important}.p-xl-2{padding:.5rem !important}.p-xl-3{padding:1rem !important}.p-xl-4{padding:1.5rem !important}.p-xl-5{padding:3rem !important}.px-xl-0{padding-right:0 !important;padding-left:0 !important}.px-xl-1{padding-right:.25rem !important;padding-left:.25rem !important}.px-xl-2{padding-right:.5rem !important;padding-left:.5rem !important}.px-xl-3{padding-right:1rem !important;padding-left:1rem !important}.px-xl-4{padding-right:1.5rem !important;padding-left:1.5rem !important}.px-xl-5{padding-right:3rem !important;padding-left:3rem !important}.py-xl-0{padding-top:0 !important;padding-bottom:0 !important}.py-xl-1{padding-top:.25rem !important;padding-bottom:.25rem !important}.py-xl-2{padding-top:.5rem !important;padding-bottom:.5rem !important}.py-xl-3{padding-top:1rem !important;padding-bottom:1rem !important}.py-xl-4{padding-top:1.5rem !important;padding-bottom:1.5rem !important}.py-xl-5{padding-top:3rem !important;padding-bottom:3rem !important}.pt-xl-0{padding-top:0 !important}.pt-xl-1{padding-top:.25rem !important}.pt-xl-2{padding-top:.5rem !important}.pt-xl-3{padding-top:1rem !important}.pt-xl-4{padding-top:1.5rem !important}.pt-xl-5{padding-top:3rem !important}.pe-xl-0{padding-right:0 !important}.pe-xl-1{padding-right:.25rem !important}.pe-xl-2{padding-right:.5rem !important}.pe-xl-3{padding-right:1rem !important}.pe-xl-4{padding-right:1.5rem !important}.pe-xl-5{padding-right:3rem !important}.pb-xl-0{padding-bottom:0 !important}.pb-xl-1{padding-bottom:.25rem !important}.pb-xl-2{padding-bottom:.5rem !important}.pb-xl-3{padding-bottom:1rem !important}.pb-xl-4{padding-bottom:1.5rem !important}.pb-xl-5{padding-bottom:3rem !important}.ps-xl-0{padding-left:0 !important}.ps-xl-1{padding-left:.25rem !important}.ps-xl-2{padding-left:.5rem !important}.ps-xl-3{padding-left:1rem !important}.ps-xl-4{padding-left:1.5rem !important}.ps-xl-5{padding-left:3rem !important}.text-xl-start{text-align:left !important}.text-xl-end{text-align:right !important}.text-xl-center{text-align:center !important}}@media(min-width: 1400px){.float-xxl-start{float:left !important}.float-xxl-end{float:right !important}.float-xxl-none{float:none !important}.d-xxl-inline{display:inline !important}.d-xxl-inline-block{display:inline-block !important}.d-xxl-block{display:block !important}.d-xxl-grid{display:grid !important}.d-xxl-table{display:table !important}.d-xxl-table-row{display:table-row !important}.d-xxl-table-cell{display:table-cell !important}.d-xxl-flex{display:flex !important}.d-xxl-inline-flex{display:inline-flex !important}.d-xxl-none{display:none !important}.flex-xxl-fill{flex:1 1 auto !important}.flex-xxl-row{flex-direction:row !important}.flex-xxl-column{flex-direction:column !important}.flex-xxl-row-reverse{flex-direction:row-reverse !important}.flex-xxl-column-reverse{flex-direction:column-reverse !important}.flex-xxl-grow-0{flex-grow:0 !important}.flex-xxl-grow-1{flex-grow:1 !important}.flex-xxl-shrink-0{flex-shrink:0 !important}.flex-xxl-shrink-1{flex-shrink:1 !important}.flex-xxl-wrap{flex-wrap:wrap !important}.flex-xxl-nowrap{flex-wrap:nowrap !important}.flex-xxl-wrap-reverse{flex-wrap:wrap-reverse !important}.gap-xxl-0{gap:0 !important}.gap-xxl-1{gap:.25rem !important}.gap-xxl-2{gap:.5rem !important}.gap-xxl-3{gap:1rem !important}.gap-xxl-4{gap:1.5rem !important}.gap-xxl-5{gap:3rem !important}.justify-content-xxl-start{justify-content:flex-start !important}.justify-content-xxl-end{justify-content:flex-end !important}.justify-content-xxl-center{justify-content:center !important}.justify-content-xxl-between{justify-content:space-between !important}.justify-content-xxl-around{justify-content:space-around !important}.justify-content-xxl-evenly{justify-content:space-evenly !important}.align-items-xxl-start{align-items:flex-start !important}.align-items-xxl-end{align-items:flex-end !important}.align-items-xxl-center{align-items:center !important}.align-items-xxl-baseline{align-items:baseline !important}.align-items-xxl-stretch{align-items:stretch !important}.align-content-xxl-start{align-content:flex-start !important}.align-content-xxl-end{align-content:flex-end !important}.align-content-xxl-center{align-content:center !important}.align-content-xxl-between{align-content:space-between !important}.align-content-xxl-around{align-content:space-around !important}.align-content-xxl-stretch{align-content:stretch !important}.align-self-xxl-auto{align-self:auto !important}.align-self-xxl-start{align-self:flex-start !important}.align-self-xxl-end{align-self:flex-end !important}.align-self-xxl-center{align-self:center !important}.align-self-xxl-baseline{align-self:baseline !important}.align-self-xxl-stretch{align-self:stretch !important}.order-xxl-first{order:-1 !important}.order-xxl-0{order:0 !important}.order-xxl-1{order:1 !important}.order-xxl-2{order:2 !important}.order-xxl-3{order:3 !important}.order-xxl-4{order:4 !important}.order-xxl-5{order:5 !important}.order-xxl-last{order:6 !important}.m-xxl-0{margin:0 !important}.m-xxl-1{margin:.25rem !important}.m-xxl-2{margin:.5rem !important}.m-xxl-3{margin:1rem !important}.m-xxl-4{margin:1.5rem !important}.m-xxl-5{margin:3rem !important}.m-xxl-auto{margin:auto !important}.mx-xxl-0{margin-right:0 !important;margin-left:0 !important}.mx-xxl-1{margin-right:.25rem !important;margin-left:.25rem !important}.mx-xxl-2{margin-right:.5rem !important;margin-left:.5rem !important}.mx-xxl-3{margin-right:1rem !important;margin-left:1rem !important}.mx-xxl-4{margin-right:1.5rem !important;margin-left:1.5rem !important}.mx-xxl-5{margin-right:3rem !important;margin-left:3rem !important}.mx-xxl-auto{margin-right:auto !important;margin-left:auto !important}.my-xxl-0{margin-top:0 !important;margin-bottom:0 !important}.my-xxl-1{margin-top:.25rem !important;margin-bottom:.25rem !important}.my-xxl-2{margin-top:.5rem !important;margin-bottom:.5rem !important}.my-xxl-3{margin-top:1rem !important;margin-bottom:1rem !important}.my-xxl-4{margin-top:1.5rem !important;margin-bottom:1.5rem !important}.my-xxl-5{margin-top:3rem !important;margin-bottom:3rem !important}.my-xxl-auto{margin-top:auto !important;margin-bottom:auto !important}.mt-xxl-0{margin-top:0 !important}.mt-xxl-1{margin-top:.25rem !important}.mt-xxl-2{margin-top:.5rem !important}.mt-xxl-3{margin-top:1rem !important}.mt-xxl-4{margin-top:1.5rem !important}.mt-xxl-5{margin-top:3rem !important}.mt-xxl-auto{margin-top:auto !important}.me-xxl-0{margin-right:0 !important}.me-xxl-1{margin-right:.25rem !important}.me-xxl-2{margin-right:.5rem !important}.me-xxl-3{margin-right:1rem !important}.me-xxl-4{margin-right:1.5rem !important}.me-xxl-5{margin-right:3rem !important}.me-xxl-auto{margin-right:auto !important}.mb-xxl-0{margin-bottom:0 !important}.mb-xxl-1{margin-bottom:.25rem !important}.mb-xxl-2{margin-bottom:.5rem !important}.mb-xxl-3{margin-bottom:1rem !important}.mb-xxl-4{margin-bottom:1.5rem !important}.mb-xxl-5{margin-bottom:3rem !important}.mb-xxl-auto{margin-bottom:auto !important}.ms-xxl-0{margin-left:0 !important}.ms-xxl-1{margin-left:.25rem !important}.ms-xxl-2{margin-left:.5rem !important}.ms-xxl-3{margin-left:1rem !important}.ms-xxl-4{margin-left:1.5rem !important}.ms-xxl-5{margin-left:3rem !important}.ms-xxl-auto{margin-left:auto !important}.p-xxl-0{padding:0 !important}.p-xxl-1{padding:.25rem !important}.p-xxl-2{padding:.5rem !important}.p-xxl-3{padding:1rem !important}.p-xxl-4{padding:1.5rem !important}.p-xxl-5{padding:3rem !important}.px-xxl-0{padding-right:0 !important;padding-left:0 !important}.px-xxl-1{padding-right:.25rem !important;padding-left:.25rem !important}.px-xxl-2{padding-right:.5rem !important;padding-left:.5rem !important}.px-xxl-3{padding-right:1rem !important;padding-left:1rem !important}.px-xxl-4{padding-right:1.5rem !important;padding-left:1.5rem !important}.px-xxl-5{padding-right:3rem !important;padding-left:3rem !important}.py-xxl-0{padding-top:0 !important;padding-bottom:0 !important}.py-xxl-1{padding-top:.25rem !important;padding-bottom:.25rem !important}.py-xxl-2{padding-top:.5rem !important;padding-bottom:.5rem !important}.py-xxl-3{padding-top:1rem !important;padding-bottom:1rem !important}.py-xxl-4{padding-top:1.5rem !important;padding-bottom:1.5rem !important}.py-xxl-5{padding-top:3rem !important;padding-bottom:3rem !important}.pt-xxl-0{padding-top:0 !important}.pt-xxl-1{padding-top:.25rem !important}.pt-xxl-2{padding-top:.5rem !important}.pt-xxl-3{padding-top:1rem !important}.pt-xxl-4{padding-top:1.5rem !important}.pt-xxl-5{padding-top:3rem !important}.pe-xxl-0{padding-right:0 !important}.pe-xxl-1{padding-right:.25rem !important}.pe-xxl-2{padding-right:.5rem !important}.pe-xxl-3{padding-right:1rem !important}.pe-xxl-4{padding-right:1.5rem !important}.pe-xxl-5{padding-right:3rem !important}.pb-xxl-0{padding-bottom:0 !important}.pb-xxl-1{padding-bottom:.25rem !important}.pb-xxl-2{padding-bottom:.5rem !important}.pb-xxl-3{padding-bottom:1rem !important}.pb-xxl-4{padding-bottom:1.5rem !important}.pb-xxl-5{padding-bottom:3rem !important}.ps-xxl-0{padding-left:0 !important}.ps-xxl-1{padding-left:.25rem !important}.ps-xxl-2{padding-left:.5rem !important}.ps-xxl-3{padding-left:1rem !important}.ps-xxl-4{padding-left:1.5rem !important}.ps-xxl-5{padding-left:3rem !important}.text-xxl-start{text-align:left !important}.text-xxl-end{text-align:right !important}.text-xxl-center{text-align:center !important}}.bg-default{color:#fff}.bg-primary{color:#fff}.bg-secondary{color:#fff}.bg-success{color:#fff}.bg-info{color:#fff}.bg-warning{color:#fff}.bg-danger{color:#fff}.bg-light{color:#000}.bg-dark{color:#fff}@media(min-width: 1200px){.fs-1{font-size:2rem !important}.fs-2{font-size:1.65rem !important}.fs-3{font-size:1.45rem !important}}@media print{.d-print-inline{display:inline !important}.d-print-inline-block{display:inline-block !important}.d-print-block{display:block !important}.d-print-grid{display:grid !important}.d-print-table{display:table !important}.d-print-table-row{display:table-row !important}.d-print-table-cell{display:table-cell !important}.d-print-flex{display:flex !important}.d-print-inline-flex{display:inline-flex !important}.d-print-none{display:none !important}}.quarto-container{min-height:calc(100vh - 132px)}footer.footer .nav-footer,#quarto-header>nav{padding-left:1em;padding-right:1em}nav[role=doc-toc]{padding-left:.5em}#quarto-content>*{padding-top:14px}@media(max-width: 991.98px){#quarto-content>*{padding-top:0}#quarto-content .subtitle{padding-top:14px}#quarto-content section:first-of-type h2:first-of-type,#quarto-content section:first-of-type .h2:first-of-type{margin-top:1rem}}.headroom-target,header.headroom{will-change:transform;transition:position 200ms linear;transition:all 200ms linear}header.headroom--pinned{transform:translateY(0%)}header.headroom--unpinned{transform:translateY(-100%)}.navbar-container{width:100%}.navbar-brand{overflow:hidden;text-overflow:ellipsis}.navbar-brand-container{max-width:calc(100% - 115px);min-width:0;display:flex;align-items:center}@media(min-width: 992px){.navbar-brand-container{margin-right:1em}}.navbar-brand.navbar-brand-logo{margin-right:4px;display:inline-flex}.navbar-toggler{flex-basis:content;flex-shrink:0}.navbar .navbar-toggler{order:-1;margin-right:.5em}.navbar-logo{max-height:24px;width:auto;padding-right:4px}nav .nav-item:not(.compact){padding-top:1px}nav .nav-link i,nav .dropdown-item i{padding-right:1px}.navbar-expand-lg .navbar-nav .nav-link{padding-left:.6rem;padding-right:.6rem}nav .nav-item.compact .nav-link{padding-left:.5rem;padding-right:.5rem;font-size:1.1rem}.navbar .quarto-navbar-tools div.dropdown{display:inline-block}.navbar .quarto-navbar-tools .quarto-navigation-tool{color:#fdfeff}.navbar .quarto-navbar-tools .quarto-navigation-tool:hover{color:#fdfeff}@media(max-width: 991.98px){.navbar .quarto-navbar-tools{margin-top:.25em;padding-top:.75em;display:block;color:solid #007ffd 1px;text-align:center;vertical-align:middle;margin-right:auto}}.navbar-nav .dropdown-menu{min-width:220px;font-size:.9rem}.navbar .navbar-nav .nav-link.dropdown-toggle::after{opacity:.75;vertical-align:.175em}.navbar ul.dropdown-menu{padding-top:0;padding-bottom:0}.navbar .dropdown-header{text-transform:uppercase;font-size:.8rem;padding:0 .5rem}.navbar .dropdown-item{padding:.4rem .5rem}.navbar .dropdown-item>i.bi{margin-left:.1rem;margin-right:.25em}.sidebar #quarto-search{margin-top:-1px}.sidebar #quarto-search svg.aa-SubmitIcon{width:16px;height:16px}.sidebar-navigation a{color:inherit}.sidebar-title{margin-top:.25rem;padding-bottom:.5rem;font-size:1.3rem;line-height:1.6rem;visibility:visible}.sidebar-title>a{font-size:inherit;text-decoration:none}.sidebar-title .sidebar-tools-main{margin-top:-6px}@media(max-width: 991.98px){#quarto-sidebar div.sidebar-header{padding-top:.2em}}.sidebar-header-stacked .sidebar-title{margin-top:.6rem}.sidebar-logo{max-width:90%;padding-bottom:.5rem}.sidebar-logo-link{text-decoration:none}.sidebar-navigation li a{text-decoration:none}.sidebar-navigation .quarto-navigation-tool{opacity:.7;font-size:.875rem}#quarto-sidebar>nav>.sidebar-tools-main{margin-left:14px}.sidebar-tools-main{display:inline-flex;margin-left:0px;order:2}.sidebar-tools-main:not(.tools-wide){vertical-align:middle}.sidebar-navigation .quarto-navigation-tool.dropdown-toggle::after{display:none}.sidebar.sidebar-navigation>*{padding-top:1em}.sidebar-item{margin-bottom:.2em}.sidebar-section{margin-top:.2em;padding-left:.5em;padding-bottom:.2em}.sidebar-item .sidebar-item-container{display:flex;justify-content:space-between}.sidebar-item-toggle:hover{cursor:pointer}.sidebar-item .sidebar-item-toggle .bi{font-size:.7rem;text-align:center}.sidebar-item .sidebar-item-toggle .bi-chevron-right::before{transition:transform 200ms ease}.sidebar-item .sidebar-item-toggle[aria-expanded=false] .bi-chevron-right::before{transform:none}.sidebar-item .sidebar-item-toggle[aria-expanded=true] .bi-chevron-right::before{transform:rotate(90deg)}.sidebar-navigation .sidebar-divider{margin-left:0;margin-right:0;margin-top:.5rem;margin-bottom:.5rem}@media(max-width: 991.98px){.quarto-secondary-nav{display:block}.quarto-secondary-nav button.quarto-search-button{padding-right:0em;padding-left:2em}.quarto-secondary-nav button.quarto-btn-toggle{margin-left:-0.75rem;margin-right:.15rem}.quarto-secondary-nav nav.quarto-page-breadcrumbs{display:flex;align-items:center;padding-right:1em;margin-left:-0.25em}.quarto-secondary-nav nav.quarto-page-breadcrumbs a{text-decoration:none}.quarto-secondary-nav nav.quarto-page-breadcrumbs ol.breadcrumb{margin-bottom:0}}@media(min-width: 992px){.quarto-secondary-nav{display:none}}.quarto-secondary-nav .quarto-btn-toggle{color:#595959}.quarto-secondary-nav[aria-expanded=false] .quarto-btn-toggle .bi-chevron-right::before{transform:none}.quarto-secondary-nav[aria-expanded=true] .quarto-btn-toggle .bi-chevron-right::before{transform:rotate(90deg)}.quarto-secondary-nav .quarto-btn-toggle .bi-chevron-right::before{transition:transform 200ms ease}.quarto-secondary-nav{cursor:pointer}.quarto-secondary-nav-title{margin-top:.3em;color:#595959;padding-top:4px}.quarto-secondary-nav nav.quarto-page-breadcrumbs{color:#595959}.quarto-secondary-nav nav.quarto-page-breadcrumbs a{color:#595959}.quarto-secondary-nav nav.quarto-page-breadcrumbs a:hover{color:rgba(27,88,157,.8)}.quarto-secondary-nav nav.quarto-page-breadcrumbs .breadcrumb-item::before{color:#8c8c8c}div.sidebar-item-container{color:#595959}div.sidebar-item-container:hover,div.sidebar-item-container:focus{color:rgba(27,88,157,.8)}div.sidebar-item-container.disabled{color:rgba(89,89,89,.75)}div.sidebar-item-container .active,div.sidebar-item-container .show>.nav-link,div.sidebar-item-container .sidebar-link>code{color:#1b589d}div.sidebar.sidebar-navigation.rollup.quarto-sidebar-toggle-contents,nav.sidebar.sidebar-navigation:not(.rollup){background-color:#fff}@media(max-width: 991.98px){.sidebar-navigation .sidebar-item a,.nav-page .nav-page-text,.sidebar-navigation{font-size:1rem}.sidebar-navigation ul.sidebar-section.depth1 .sidebar-section-item{font-size:1.1rem}.sidebar-logo{display:none}.sidebar.sidebar-navigation{position:static;border-bottom:1px solid #dee2e6}.sidebar.sidebar-navigation.collapsing{position:fixed;z-index:1000}.sidebar.sidebar-navigation.show{position:fixed;z-index:1000}.sidebar.sidebar-navigation{min-height:100%}nav.quarto-secondary-nav{background-color:#fff;border-bottom:1px solid #dee2e6}.sidebar .sidebar-footer{visibility:visible;padding-top:1rem;position:inherit}.sidebar-tools-collapse{display:block}}#quarto-sidebar{transition:width .15s ease-in}#quarto-sidebar>*{padding-right:1em}@media(max-width: 991.98px){#quarto-sidebar .sidebar-menu-container{white-space:nowrap;min-width:225px}#quarto-sidebar.show{transition:width .15s ease-out}}@media(min-width: 992px){#quarto-sidebar{display:flex;flex-direction:column}.nav-page .nav-page-text,.sidebar-navigation .sidebar-section .sidebar-item{font-size:.875rem}.sidebar-navigation .sidebar-item{font-size:.925rem}.sidebar.sidebar-navigation{display:block;position:sticky}.sidebar-search{width:100%}.sidebar .sidebar-footer{visibility:visible}}@media(max-width: 991.98px){#quarto-sidebar-glass{position:fixed;top:0;bottom:0;left:0;right:0;background-color:rgba(255,255,255,0);transition:background-color .15s ease-in;z-index:-1}#quarto-sidebar-glass.collapsing{z-index:1000}#quarto-sidebar-glass.show{transition:background-color .15s ease-out;background-color:rgba(102,102,102,.4);z-index:1000}}.sidebar .sidebar-footer{padding:.5rem 1rem;align-self:flex-end;color:#6c757d;width:100%}.quarto-page-breadcrumbs .breadcrumb-item+.breadcrumb-item,.quarto-page-breadcrumbs .breadcrumb-item{padding-right:.33em;padding-left:0}.quarto-page-breadcrumbs .breadcrumb-item::before{padding-right:.33em}.quarto-sidebar-footer{font-size:.875em}.sidebar-section .bi-chevron-right{vertical-align:middle}.sidebar-section .bi-chevron-right::before{font-size:.9em}.notransition{-webkit-transition:none !important;-moz-transition:none !important;-o-transition:none !important;transition:none !important}.btn:focus:not(:focus-visible){box-shadow:none}.page-navigation{display:flex;justify-content:space-between}.nav-page{padding-bottom:.75em}.nav-page .bi{font-size:1.8rem;vertical-align:middle}.nav-page .nav-page-text{padding-left:.25em;padding-right:.25em}.nav-page a{color:#6c757d;text-decoration:none;display:flex;align-items:center}.nav-page a:hover{color:#1f66b6}.toc-actions{display:flex}.toc-actions p{margin-block-start:0;margin-block-end:0}.toc-actions a{text-decoration:none;color:inherit;font-weight:400}.toc-actions a:hover{color:#1f66b6}.toc-actions .action-links{margin-left:4px}.sidebar nav[role=doc-toc] .toc-actions .bi{margin-left:-4px;font-size:.7rem;color:#6c757d}.sidebar nav[role=doc-toc] .toc-actions .bi:before{padding-top:3px}#quarto-margin-sidebar .toc-actions .bi:before{margin-top:.3rem;font-size:.7rem;color:#6c757d;vertical-align:top}.sidebar nav[role=doc-toc] .toc-actions>div:first-of-type{margin-top:-3px}#quarto-margin-sidebar .toc-actions p,.sidebar nav[role=doc-toc] .toc-actions p{font-size:.875rem}.nav-footer .toc-actions{padding-bottom:.5em;padding-top:.5em}.nav-footer .toc-actions :first-child{margin-left:auto}.nav-footer .toc-actions :last-child{margin-right:auto}.nav-footer .toc-actions .action-links{display:flex}.nav-footer .toc-actions .action-links p{padding-right:1.5em}.nav-footer .toc-actions .action-links p:last-of-type{padding-right:0}.nav-footer{display:flex;flex-direction:row;flex-wrap:wrap;justify-content:space-between;align-items:baseline;text-align:center;padding-top:.5rem;padding-bottom:.5rem;background-color:#fff}body.nav-fixed{padding-top:64px}.nav-footer-contents{color:#6c757d;margin-top:.25rem}.nav-footer{min-height:3.5em;color:#757575}.nav-footer a{color:#757575}.nav-footer .nav-footer-left{font-size:.825em}.nav-footer .nav-footer-center{font-size:.825em}.nav-footer .nav-footer-right{font-size:.825em}.nav-footer-left .footer-items,.nav-footer-center .footer-items,.nav-footer-right .footer-items{display:inline-flex;padding-top:.3em;padding-bottom:.3em;margin-bottom:0em}.nav-footer-left .footer-items .nav-link,.nav-footer-center .footer-items .nav-link,.nav-footer-right .footer-items .nav-link{padding-left:.6em;padding-right:.6em}.nav-footer-left{flex:1 1 0px;text-align:left}.nav-footer-right{flex:1 1 0px;text-align:right}.nav-footer-center{flex:1 1 0px;min-height:3em;text-align:center}.nav-footer-center .footer-items{justify-content:center}@media(max-width: 767.98px){.nav-footer-center{margin-top:3em}}.navbar .quarto-reader-toggle.reader .quarto-reader-toggle-btn{background-color:#fdfeff;border-radius:3px}.quarto-reader-toggle.reader.quarto-navigation-tool .quarto-reader-toggle-btn{background-color:#595959;border-radius:3px}.quarto-reader-toggle .quarto-reader-toggle-btn{display:inline-flex;padding-left:.2em;padding-right:.2em;margin-left:-0.2em;margin-right:-0.2em;text-align:center}.navbar .quarto-reader-toggle:not(.reader) .bi::before{background-image:url('data:image/svg+xml,')}.navbar .quarto-reader-toggle.reader .bi::before{background-image:url('data:image/svg+xml,')}.sidebar-navigation .quarto-reader-toggle:not(.reader) .bi::before{background-image:url('data:image/svg+xml,')}.sidebar-navigation .quarto-reader-toggle.reader .bi::before{background-image:url('data:image/svg+xml,')}#quarto-back-to-top{display:none;position:fixed;bottom:50px;background-color:#fff;border-radius:.25rem;box-shadow:0 .2rem .5rem #6c757d,0 0 .05rem #6c757d;color:#6c757d;text-decoration:none;font-size:.9em;text-align:center;left:50%;padding:.4rem .8rem;transform:translate(-50%, 0)}.aa-DetachedOverlay ul.aa-List,#quarto-search-results ul.aa-List{list-style:none;padding-left:0}.aa-DetachedOverlay .aa-Panel,#quarto-search-results .aa-Panel{background-color:#fff;position:absolute;z-index:2000}#quarto-search-results .aa-Panel{max-width:400px}#quarto-search input{font-size:.925rem}@media(min-width: 992px){.navbar #quarto-search{margin-left:.25rem;order:999}}@media(max-width: 991.98px){#quarto-sidebar .sidebar-search{display:none}}#quarto-sidebar .sidebar-search .aa-Autocomplete{width:100%}.navbar .aa-Autocomplete .aa-Form{width:180px}.navbar #quarto-search.type-overlay .aa-Autocomplete{width:40px}.navbar #quarto-search.type-overlay .aa-Autocomplete .aa-Form{background-color:inherit;border:none}.navbar #quarto-search.type-overlay .aa-Autocomplete .aa-Form:focus-within{box-shadow:none;outline:none}.navbar #quarto-search.type-overlay .aa-Autocomplete .aa-Form .aa-InputWrapper{display:none}.navbar #quarto-search.type-overlay .aa-Autocomplete .aa-Form .aa-InputWrapper:focus-within{display:inherit}.navbar #quarto-search.type-overlay .aa-Autocomplete .aa-Form .aa-Label svg,.navbar #quarto-search.type-overlay .aa-Autocomplete .aa-Form .aa-LoadingIndicator svg{width:26px;height:26px;color:#fdfeff;opacity:1}.navbar #quarto-search.type-overlay .aa-Autocomplete svg.aa-SubmitIcon{width:26px;height:26px;color:#fdfeff;opacity:1}.aa-Autocomplete .aa-Form,.aa-DetachedFormContainer .aa-Form{align-items:center;background-color:#fff;border:1px solid #ced4da;border-radius:.25rem;color:#373a3c;display:flex;line-height:1em;margin:0;position:relative;width:100%}.aa-Autocomplete .aa-Form:focus-within,.aa-DetachedFormContainer .aa-Form:focus-within{box-shadow:rgba(39,128,227,.6) 0 0 0 1px;outline:currentColor none medium}.aa-Autocomplete .aa-Form .aa-InputWrapperPrefix,.aa-DetachedFormContainer .aa-Form .aa-InputWrapperPrefix{align-items:center;display:flex;flex-shrink:0;order:1}.aa-Autocomplete .aa-Form .aa-InputWrapperPrefix .aa-Label,.aa-Autocomplete .aa-Form .aa-InputWrapperPrefix .aa-LoadingIndicator,.aa-DetachedFormContainer .aa-Form .aa-InputWrapperPrefix .aa-Label,.aa-DetachedFormContainer .aa-Form .aa-InputWrapperPrefix .aa-LoadingIndicator{cursor:initial;flex-shrink:0;padding:0;text-align:left}.aa-Autocomplete .aa-Form .aa-InputWrapperPrefix .aa-Label svg,.aa-Autocomplete .aa-Form .aa-InputWrapperPrefix .aa-LoadingIndicator svg,.aa-DetachedFormContainer .aa-Form .aa-InputWrapperPrefix .aa-Label svg,.aa-DetachedFormContainer .aa-Form .aa-InputWrapperPrefix .aa-LoadingIndicator svg{color:#373a3c;opacity:.5}.aa-Autocomplete .aa-Form .aa-InputWrapperPrefix .aa-SubmitButton,.aa-DetachedFormContainer .aa-Form .aa-InputWrapperPrefix .aa-SubmitButton{appearance:none;background:none;border:0;margin:0}.aa-Autocomplete .aa-Form .aa-InputWrapperPrefix .aa-LoadingIndicator,.aa-DetachedFormContainer .aa-Form .aa-InputWrapperPrefix .aa-LoadingIndicator{align-items:center;display:flex;justify-content:center}.aa-Autocomplete .aa-Form .aa-InputWrapperPrefix .aa-LoadingIndicator[hidden],.aa-DetachedFormContainer .aa-Form .aa-InputWrapperPrefix .aa-LoadingIndicator[hidden]{display:none}.aa-Autocomplete .aa-Form .aa-InputWrapper,.aa-DetachedFormContainer .aa-Form .aa-InputWrapper{order:3;position:relative;width:100%}.aa-Autocomplete .aa-Form .aa-InputWrapper .aa-Input,.aa-DetachedFormContainer .aa-Form .aa-InputWrapper .aa-Input{appearance:none;background:none;border:0;color:#373a3c;font:inherit;height:calc(1.5em + .1rem + 2px);padding:0;width:100%}.aa-Autocomplete .aa-Form .aa-InputWrapper .aa-Input::placeholder,.aa-DetachedFormContainer .aa-Form .aa-InputWrapper .aa-Input::placeholder{color:#373a3c;opacity:.8}.aa-Autocomplete .aa-Form .aa-InputWrapper .aa-Input:focus,.aa-DetachedFormContainer .aa-Form .aa-InputWrapper .aa-Input:focus{border-color:none;box-shadow:none;outline:none}.aa-Autocomplete .aa-Form .aa-InputWrapper .aa-Input::-webkit-search-decoration,.aa-Autocomplete .aa-Form .aa-InputWrapper .aa-Input::-webkit-search-cancel-button,.aa-Autocomplete .aa-Form .aa-InputWrapper .aa-Input::-webkit-search-results-button,.aa-Autocomplete .aa-Form .aa-InputWrapper .aa-Input::-webkit-search-results-decoration,.aa-DetachedFormContainer .aa-Form .aa-InputWrapper .aa-Input::-webkit-search-decoration,.aa-DetachedFormContainer .aa-Form .aa-InputWrapper .aa-Input::-webkit-search-cancel-button,.aa-DetachedFormContainer .aa-Form .aa-InputWrapper .aa-Input::-webkit-search-results-button,.aa-DetachedFormContainer .aa-Form .aa-InputWrapper .aa-Input::-webkit-search-results-decoration{display:none}.aa-Autocomplete .aa-Form .aa-InputWrapperSuffix,.aa-DetachedFormContainer .aa-Form .aa-InputWrapperSuffix{align-items:center;display:flex;order:4}.aa-Autocomplete .aa-Form .aa-InputWrapperSuffix .aa-ClearButton,.aa-DetachedFormContainer .aa-Form .aa-InputWrapperSuffix .aa-ClearButton{align-items:center;background:none;border:0;color:#373a3c;opacity:.8;cursor:pointer;display:flex;margin:0;width:calc(1.5em + .1rem + 2px)}.aa-Autocomplete .aa-Form .aa-InputWrapperSuffix .aa-ClearButton:hover,.aa-Autocomplete .aa-Form .aa-InputWrapperSuffix .aa-ClearButton:focus,.aa-DetachedFormContainer .aa-Form .aa-InputWrapperSuffix .aa-ClearButton:hover,.aa-DetachedFormContainer .aa-Form .aa-InputWrapperSuffix .aa-ClearButton:focus{color:#373a3c;opacity:.8}.aa-Autocomplete .aa-Form .aa-InputWrapperSuffix .aa-ClearButton[hidden],.aa-DetachedFormContainer .aa-Form .aa-InputWrapperSuffix .aa-ClearButton[hidden]{display:none}.aa-Autocomplete .aa-Form .aa-InputWrapperSuffix .aa-ClearButton svg,.aa-DetachedFormContainer .aa-Form .aa-InputWrapperSuffix .aa-ClearButton svg{width:calc(1.5em + 0.75rem + 2px)}.aa-Autocomplete .aa-Form .aa-InputWrapperSuffix .aa-CopyButton,.aa-DetachedFormContainer .aa-Form .aa-InputWrapperSuffix .aa-CopyButton{border:none;align-items:center;background:none;color:#373a3c;opacity:.4;font-size:.7rem;cursor:pointer;display:none;margin:0;width:calc(1em + .1rem + 2px)}.aa-Autocomplete .aa-Form .aa-InputWrapperSuffix .aa-CopyButton:hover,.aa-Autocomplete .aa-Form .aa-InputWrapperSuffix .aa-CopyButton:focus,.aa-DetachedFormContainer .aa-Form .aa-InputWrapperSuffix .aa-CopyButton:hover,.aa-DetachedFormContainer .aa-Form .aa-InputWrapperSuffix .aa-CopyButton:focus{color:#373a3c;opacity:.8}.aa-Autocomplete .aa-Form .aa-InputWrapperSuffix .aa-CopyButton[hidden],.aa-DetachedFormContainer .aa-Form .aa-InputWrapperSuffix .aa-CopyButton[hidden]{display:none}.aa-PanelLayout:empty{display:none}.quarto-search-no-results.no-query{display:none}.aa-Source:has(.no-query){display:none}#quarto-search-results .aa-Panel{border:solid #ced4da 1px}#quarto-search-results .aa-SourceNoResults{width:398px}.aa-DetachedOverlay .aa-Panel,#quarto-search-results .aa-Panel{max-height:65vh;overflow-y:auto;font-size:.925rem}.aa-DetachedOverlay .aa-SourceNoResults,#quarto-search-results .aa-SourceNoResults{height:60px;display:flex;justify-content:center;align-items:center}.aa-DetachedOverlay .search-error,#quarto-search-results .search-error{padding-top:10px;padding-left:20px;padding-right:20px;cursor:default}.aa-DetachedOverlay .search-error .search-error-title,#quarto-search-results .search-error .search-error-title{font-size:1.1rem;margin-bottom:.5rem}.aa-DetachedOverlay .search-error .search-error-title .search-error-icon,#quarto-search-results .search-error .search-error-title .search-error-icon{margin-right:8px}.aa-DetachedOverlay .search-error .search-error-text,#quarto-search-results .search-error .search-error-text{font-weight:300}.aa-DetachedOverlay .search-result-text,#quarto-search-results .search-result-text{font-weight:300;overflow:hidden;text-overflow:ellipsis;display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;line-height:1.2rem;max-height:2.4rem}.aa-DetachedOverlay .aa-SourceHeader .search-result-header,#quarto-search-results .aa-SourceHeader .search-result-header{font-size:.875rem;background-color:#f2f2f2;padding-left:14px;padding-bottom:4px;padding-top:4px}.aa-DetachedOverlay .aa-SourceHeader .search-result-header-no-results,#quarto-search-results .aa-SourceHeader .search-result-header-no-results{display:none}.aa-DetachedOverlay .aa-SourceFooter .algolia-search-logo,#quarto-search-results .aa-SourceFooter .algolia-search-logo{width:110px;opacity:.85;margin:8px;float:right}.aa-DetachedOverlay .search-result-section,#quarto-search-results .search-result-section{font-size:.925em}.aa-DetachedOverlay a.search-result-link,#quarto-search-results a.search-result-link{color:inherit;text-decoration:none}.aa-DetachedOverlay li.aa-Item[aria-selected=true] .search-item,#quarto-search-results li.aa-Item[aria-selected=true] .search-item{background-color:#2780e3}.aa-DetachedOverlay li.aa-Item[aria-selected=true] .search-item.search-result-more,.aa-DetachedOverlay li.aa-Item[aria-selected=true] .search-item .search-result-section,.aa-DetachedOverlay li.aa-Item[aria-selected=true] .search-item .search-result-text,.aa-DetachedOverlay li.aa-Item[aria-selected=true] .search-item .search-result-title-container,.aa-DetachedOverlay li.aa-Item[aria-selected=true] .search-item .search-result-text-container,#quarto-search-results li.aa-Item[aria-selected=true] .search-item.search-result-more,#quarto-search-results li.aa-Item[aria-selected=true] .search-item .search-result-section,#quarto-search-results li.aa-Item[aria-selected=true] .search-item .search-result-text,#quarto-search-results li.aa-Item[aria-selected=true] .search-item .search-result-title-container,#quarto-search-results li.aa-Item[aria-selected=true] .search-item .search-result-text-container{color:#fff;background-color:#2780e3}.aa-DetachedOverlay li.aa-Item[aria-selected=true] .search-item mark.search-match,.aa-DetachedOverlay li.aa-Item[aria-selected=true] .search-item .search-match.mark,#quarto-search-results li.aa-Item[aria-selected=true] .search-item mark.search-match,#quarto-search-results li.aa-Item[aria-selected=true] .search-item .search-match.mark{color:#fff;background-color:#4b95e8}.aa-DetachedOverlay li.aa-Item[aria-selected=false] .search-item,#quarto-search-results li.aa-Item[aria-selected=false] .search-item{background-color:#fff}.aa-DetachedOverlay li.aa-Item[aria-selected=false] .search-item.search-result-more,.aa-DetachedOverlay li.aa-Item[aria-selected=false] .search-item .search-result-section,.aa-DetachedOverlay li.aa-Item[aria-selected=false] .search-item .search-result-text,.aa-DetachedOverlay li.aa-Item[aria-selected=false] .search-item .search-result-title-container,.aa-DetachedOverlay li.aa-Item[aria-selected=false] .search-item .search-result-text-container,#quarto-search-results li.aa-Item[aria-selected=false] .search-item.search-result-more,#quarto-search-results li.aa-Item[aria-selected=false] .search-item .search-result-section,#quarto-search-results li.aa-Item[aria-selected=false] .search-item .search-result-text,#quarto-search-results li.aa-Item[aria-selected=false] .search-item .search-result-title-container,#quarto-search-results li.aa-Item[aria-selected=false] .search-item .search-result-text-container{color:#373a3c}.aa-DetachedOverlay li.aa-Item[aria-selected=false] .search-item mark.search-match,.aa-DetachedOverlay li.aa-Item[aria-selected=false] .search-item .search-match.mark,#quarto-search-results li.aa-Item[aria-selected=false] .search-item mark.search-match,#quarto-search-results li.aa-Item[aria-selected=false] .search-item .search-match.mark{color:inherit;background-color:#e5effc}.aa-DetachedOverlay .aa-Item .search-result-doc:not(.document-selectable) .search-result-title-container,#quarto-search-results .aa-Item .search-result-doc:not(.document-selectable) .search-result-title-container{background-color:#fff;color:#373a3c}.aa-DetachedOverlay .aa-Item .search-result-doc:not(.document-selectable) .search-result-text-container,#quarto-search-results .aa-Item .search-result-doc:not(.document-selectable) .search-result-text-container{padding-top:0px}.aa-DetachedOverlay li.aa-Item .search-result-doc.document-selectable .search-result-text-container,#quarto-search-results li.aa-Item .search-result-doc.document-selectable .search-result-text-container{margin-top:-4px}.aa-DetachedOverlay .aa-Item,#quarto-search-results .aa-Item{cursor:pointer}.aa-DetachedOverlay .aa-Item .search-item,#quarto-search-results .aa-Item .search-item{border-left:none;border-right:none;border-top:none;background-color:#fff;border-color:#ced4da;color:#373a3c}.aa-DetachedOverlay .aa-Item .search-item p,#quarto-search-results .aa-Item .search-item p{margin-top:0;margin-bottom:0}.aa-DetachedOverlay .aa-Item .search-item i.bi,#quarto-search-results .aa-Item .search-item i.bi{padding-left:8px;padding-right:8px;font-size:1.3em}.aa-DetachedOverlay .aa-Item .search-item .search-result-title,#quarto-search-results .aa-Item .search-item .search-result-title{margin-top:.3em;margin-bottom:.1rem}.aa-DetachedOverlay .aa-Item .search-result-title-container,#quarto-search-results .aa-Item .search-result-title-container{font-size:1em;display:flex;padding:6px 4px 6px 4px}.aa-DetachedOverlay .aa-Item .search-result-text-container,#quarto-search-results .aa-Item .search-result-text-container{padding-bottom:8px;padding-right:8px;margin-left:44px}.aa-DetachedOverlay .aa-Item .search-result-doc-section,.aa-DetachedOverlay .aa-Item .search-result-more,#quarto-search-results .aa-Item .search-result-doc-section,#quarto-search-results .aa-Item .search-result-more{padding-top:8px;padding-bottom:8px;padding-left:44px}.aa-DetachedOverlay .aa-Item .search-result-more,#quarto-search-results .aa-Item .search-result-more{font-size:.8em;font-weight:400}.aa-DetachedOverlay .aa-Item .search-result-doc,#quarto-search-results .aa-Item .search-result-doc{border-top:1px solid #ced4da}.aa-DetachedSearchButton{background:none;border:none}.aa-DetachedSearchButton .aa-DetachedSearchButtonPlaceholder{display:none}.navbar .aa-DetachedSearchButton .aa-DetachedSearchButtonIcon{color:#fdfeff}.sidebar-tools-collapse #quarto-search,.sidebar-tools-main #quarto-search{display:inline}.sidebar-tools-collapse #quarto-search .aa-Autocomplete,.sidebar-tools-main #quarto-search .aa-Autocomplete{display:inline}.sidebar-tools-collapse #quarto-search .aa-DetachedSearchButton,.sidebar-tools-main #quarto-search .aa-DetachedSearchButton{padding-left:4px;padding-right:4px}.sidebar-tools-collapse #quarto-search .aa-DetachedSearchButton .aa-DetachedSearchButtonIcon,.sidebar-tools-main #quarto-search .aa-DetachedSearchButton .aa-DetachedSearchButtonIcon{color:#595959}.sidebar-tools-collapse #quarto-search .aa-DetachedSearchButton .aa-DetachedSearchButtonIcon .aa-SubmitIcon,.sidebar-tools-main #quarto-search .aa-DetachedSearchButton .aa-DetachedSearchButtonIcon .aa-SubmitIcon{margin-top:-3px}.aa-DetachedContainer{background:rgba(255,255,255,.65);width:90%;bottom:0;box-shadow:rgba(206,212,218,.6) 0 0 0 1px;outline:currentColor none medium;display:flex;flex-direction:column;left:0;margin:0;overflow:hidden;padding:0;position:fixed;right:0;top:0;z-index:1101}.aa-DetachedContainer::after{height:32px}.aa-DetachedContainer .aa-SourceHeader{margin:var(--aa-spacing-half) 0 var(--aa-spacing-half) 2px}.aa-DetachedContainer .aa-Panel{background-color:#fff;border-radius:0;box-shadow:none;flex-grow:1;margin:0;padding:0;position:relative}.aa-DetachedContainer .aa-PanelLayout{bottom:0;box-shadow:none;left:0;margin:0;max-height:none;overflow-y:auto;position:absolute;right:0;top:0;width:100%}.aa-DetachedFormContainer{background-color:#fff;border-bottom:1px solid #ced4da;display:flex;flex-direction:row;justify-content:space-between;margin:0;padding:.5em}.aa-DetachedCancelButton{background:none;font-size:.8em;border:0;border-radius:3px;color:#373a3c;cursor:pointer;margin:0 0 0 .5em;padding:0 .5em}.aa-DetachedCancelButton:hover,.aa-DetachedCancelButton:focus{box-shadow:rgba(39,128,227,.6) 0 0 0 1px;outline:currentColor none medium}.aa-DetachedContainer--modal{bottom:inherit;height:auto;margin:0 auto;position:absolute;top:100px;border-radius:6px;max-width:850px}@media(max-width: 575.98px){.aa-DetachedContainer--modal{width:100%;top:0px;border-radius:0px;border:none}}.aa-DetachedContainer--modal .aa-PanelLayout{max-height:var(--aa-detached-modal-max-height);padding-bottom:var(--aa-spacing-half);position:static}.aa-Detached{height:100vh;overflow:hidden}.aa-DetachedOverlay{background-color:rgba(55,58,60,.4);position:fixed;left:0;right:0;top:0;margin:0;padding:0;height:100vh;z-index:1100}.quarto-listing{padding-bottom:1em}.listing-pagination{padding-top:.5em}ul.pagination{float:right;padding-left:8px;padding-top:.5em}ul.pagination li{padding-right:.75em}ul.pagination li.disabled a,ul.pagination li.active a{color:#373a3c;text-decoration:none}ul.pagination li:last-of-type{padding-right:0}.listing-actions-group{display:flex}.quarto-listing-filter{margin-bottom:1em;width:200px;margin-left:auto}.quarto-listing-sort{margin-bottom:1em;margin-right:auto;width:auto}.quarto-listing-sort .input-group-text{font-size:.8em}.input-group-text{border-right:none}.quarto-listing-sort select.form-select{font-size:.8em}.listing-no-matching{text-align:center;padding-top:2em;padding-bottom:3em;font-size:1em}#quarto-margin-sidebar .quarto-listing-category{padding-top:0;font-size:1rem}#quarto-margin-sidebar .quarto-listing-category-title{cursor:pointer;font-weight:600;font-size:1rem}.quarto-listing-category .category{cursor:pointer}.quarto-listing-category .category.active{font-weight:600}.quarto-listing-category.category-cloud{display:flex;flex-wrap:wrap;align-items:baseline}.quarto-listing-category.category-cloud .category{padding-right:5px}.quarto-listing-category.category-cloud .category-cloud-1{font-size:.75em}.quarto-listing-category.category-cloud .category-cloud-2{font-size:.95em}.quarto-listing-category.category-cloud .category-cloud-3{font-size:1.15em}.quarto-listing-category.category-cloud .category-cloud-4{font-size:1.35em}.quarto-listing-category.category-cloud .category-cloud-5{font-size:1.55em}.quarto-listing-category.category-cloud .category-cloud-6{font-size:1.75em}.quarto-listing-category.category-cloud .category-cloud-7{font-size:1.95em}.quarto-listing-category.category-cloud .category-cloud-8{font-size:2.15em}.quarto-listing-category.category-cloud .category-cloud-9{font-size:2.35em}.quarto-listing-category.category-cloud .category-cloud-10{font-size:2.55em}.quarto-listing-cols-1{grid-template-columns:repeat(1, minmax(0, 1fr));gap:1.5em}@media(max-width: 767.98px){.quarto-listing-cols-1{grid-template-columns:repeat(1, minmax(0, 1fr));gap:1.5em}}@media(max-width: 575.98px){.quarto-listing-cols-1{grid-template-columns:minmax(0, 1fr);gap:1.5em}}.quarto-listing-cols-2{grid-template-columns:repeat(2, minmax(0, 1fr));gap:1.5em}@media(max-width: 767.98px){.quarto-listing-cols-2{grid-template-columns:repeat(2, minmax(0, 1fr));gap:1.5em}}@media(max-width: 575.98px){.quarto-listing-cols-2{grid-template-columns:minmax(0, 1fr);gap:1.5em}}.quarto-listing-cols-3{grid-template-columns:repeat(3, minmax(0, 1fr));gap:1.5em}@media(max-width: 767.98px){.quarto-listing-cols-3{grid-template-columns:repeat(2, minmax(0, 1fr));gap:1.5em}}@media(max-width: 575.98px){.quarto-listing-cols-3{grid-template-columns:minmax(0, 1fr);gap:1.5em}}.quarto-listing-cols-4{grid-template-columns:repeat(4, minmax(0, 1fr));gap:1.5em}@media(max-width: 767.98px){.quarto-listing-cols-4{grid-template-columns:repeat(2, minmax(0, 1fr));gap:1.5em}}@media(max-width: 575.98px){.quarto-listing-cols-4{grid-template-columns:minmax(0, 1fr);gap:1.5em}}.quarto-listing-cols-5{grid-template-columns:repeat(5, minmax(0, 1fr));gap:1.5em}@media(max-width: 767.98px){.quarto-listing-cols-5{grid-template-columns:repeat(2, minmax(0, 1fr));gap:1.5em}}@media(max-width: 575.98px){.quarto-listing-cols-5{grid-template-columns:minmax(0, 1fr);gap:1.5em}}.quarto-listing-cols-6{grid-template-columns:repeat(6, minmax(0, 1fr));gap:1.5em}@media(max-width: 767.98px){.quarto-listing-cols-6{grid-template-columns:repeat(2, minmax(0, 1fr));gap:1.5em}}@media(max-width: 575.98px){.quarto-listing-cols-6{grid-template-columns:minmax(0, 1fr);gap:1.5em}}.quarto-listing-cols-7{grid-template-columns:repeat(7, minmax(0, 1fr));gap:1.5em}@media(max-width: 767.98px){.quarto-listing-cols-7{grid-template-columns:repeat(2, minmax(0, 1fr));gap:1.5em}}@media(max-width: 575.98px){.quarto-listing-cols-7{grid-template-columns:minmax(0, 1fr);gap:1.5em}}.quarto-listing-cols-8{grid-template-columns:repeat(8, minmax(0, 1fr));gap:1.5em}@media(max-width: 767.98px){.quarto-listing-cols-8{grid-template-columns:repeat(2, minmax(0, 1fr));gap:1.5em}}@media(max-width: 575.98px){.quarto-listing-cols-8{grid-template-columns:minmax(0, 1fr);gap:1.5em}}.quarto-listing-cols-9{grid-template-columns:repeat(9, minmax(0, 1fr));gap:1.5em}@media(max-width: 767.98px){.quarto-listing-cols-9{grid-template-columns:repeat(2, minmax(0, 1fr));gap:1.5em}}@media(max-width: 575.98px){.quarto-listing-cols-9{grid-template-columns:minmax(0, 1fr);gap:1.5em}}.quarto-listing-cols-10{grid-template-columns:repeat(10, minmax(0, 1fr));gap:1.5em}@media(max-width: 767.98px){.quarto-listing-cols-10{grid-template-columns:repeat(2, minmax(0, 1fr));gap:1.5em}}@media(max-width: 575.98px){.quarto-listing-cols-10{grid-template-columns:minmax(0, 1fr);gap:1.5em}}.quarto-listing-cols-11{grid-template-columns:repeat(11, minmax(0, 1fr));gap:1.5em}@media(max-width: 767.98px){.quarto-listing-cols-11{grid-template-columns:repeat(2, minmax(0, 1fr));gap:1.5em}}@media(max-width: 575.98px){.quarto-listing-cols-11{grid-template-columns:minmax(0, 1fr);gap:1.5em}}.quarto-listing-cols-12{grid-template-columns:repeat(12, minmax(0, 1fr));gap:1.5em}@media(max-width: 767.98px){.quarto-listing-cols-12{grid-template-columns:repeat(2, minmax(0, 1fr));gap:1.5em}}@media(max-width: 575.98px){.quarto-listing-cols-12{grid-template-columns:minmax(0, 1fr);gap:1.5em}}.quarto-listing-grid{gap:1.5em}.quarto-grid-item.borderless{border:none}.quarto-grid-item.borderless .listing-categories .listing-category:last-of-type,.quarto-grid-item.borderless .listing-categories .listing-category:first-of-type{padding-left:0}.quarto-grid-item.borderless .listing-categories .listing-category{border:0}.quarto-grid-link{text-decoration:none;color:inherit}.quarto-grid-link:hover{text-decoration:none;color:inherit}.quarto-grid-item h5.title,.quarto-grid-item .title.h5{margin-top:0;margin-bottom:0}.quarto-grid-item .card-footer{display:flex;justify-content:space-between;font-size:.8em}.quarto-grid-item .card-footer p{margin-bottom:0}.quarto-grid-item p.card-img-top{margin-bottom:0}.quarto-grid-item p.card-img-top>img{object-fit:cover}.quarto-grid-item .card-other-values{margin-top:.5em;font-size:.8em}.quarto-grid-item .card-other-values tr{margin-bottom:.5em}.quarto-grid-item .card-other-values tr>td:first-of-type{font-weight:600;padding-right:1em;padding-left:1em;vertical-align:top}.quarto-grid-item div.post-contents{display:flex;flex-direction:column;text-decoration:none;height:100%}.quarto-grid-item .listing-item-img-placeholder{background-color:#adb5bd;flex-shrink:0}.quarto-grid-item .card-attribution{padding-top:1em;display:flex;gap:1em;text-transform:uppercase;color:#6c757d;font-weight:500;flex-grow:10;align-items:flex-end}.quarto-grid-item .description{padding-bottom:1em}.quarto-grid-item .card-attribution .date{align-self:flex-end}.quarto-grid-item .card-attribution.justify{justify-content:space-between}.quarto-grid-item .card-attribution.start{justify-content:flex-start}.quarto-grid-item .card-attribution.end{justify-content:flex-end}.quarto-grid-item .card-title{margin-bottom:.1em}.quarto-grid-item .card-subtitle{padding-top:.25em}.quarto-grid-item .card-text{font-size:.9em}.quarto-grid-item .listing-reading-time{padding-bottom:.25em}.quarto-grid-item .card-text-small{font-size:.8em}.quarto-grid-item .card-subtitle.subtitle{font-size:.9em;font-weight:600;padding-bottom:.5em}.quarto-grid-item .listing-categories{display:flex;flex-wrap:wrap;padding-bottom:5px}.quarto-grid-item .listing-categories .listing-category{color:#6c757d;border:solid 1px #dee2e6;border-radius:.25rem;text-transform:uppercase;font-size:.65em;padding-left:.5em;padding-right:.5em;padding-top:.15em;padding-bottom:.15em;cursor:pointer;margin-right:4px;margin-bottom:4px}.quarto-grid-item.card-right{text-align:right}.quarto-grid-item.card-right .listing-categories{justify-content:flex-end}.quarto-grid-item.card-left{text-align:left}.quarto-grid-item.card-center{text-align:center}.quarto-grid-item.card-center .listing-description{text-align:justify}.quarto-grid-item.card-center .listing-categories{justify-content:center}table.quarto-listing-table td.image{padding:0px}table.quarto-listing-table td.image img{width:100%;max-width:50px;object-fit:contain}table.quarto-listing-table a{text-decoration:none}table.quarto-listing-table th a{color:inherit}table.quarto-listing-table th a.asc:after{margin-bottom:-2px;margin-left:5px;display:inline-block;height:1rem;width:1rem;background-repeat:no-repeat;background-size:1rem 1rem;background-image:url('data:image/svg+xml,');content:""}table.quarto-listing-table th a.desc:after{margin-bottom:-2px;margin-left:5px;display:inline-block;height:1rem;width:1rem;background-repeat:no-repeat;background-size:1rem 1rem;background-image:url('data:image/svg+xml,');content:""}table.quarto-listing-table.table-hover td{cursor:pointer}.quarto-post.image-left{flex-direction:row}.quarto-post.image-right{flex-direction:row-reverse}@media(max-width: 767.98px){.quarto-post.image-right,.quarto-post.image-left{gap:0em;flex-direction:column}.quarto-post .metadata{padding-bottom:1em;order:2}.quarto-post .body{order:1}.quarto-post .thumbnail{order:3}}.list.quarto-listing-default div:last-of-type{border-bottom:none}@media(min-width: 992px){.quarto-listing-container-default{margin-right:2em}}div.quarto-post{display:flex;gap:2em;margin-bottom:1.5em;border-bottom:1px solid #dee2e6}@media(max-width: 767.98px){div.quarto-post{padding-bottom:1em}}div.quarto-post .metadata{flex-basis:20%;flex-grow:0;margin-top:.2em;flex-shrink:10}div.quarto-post .thumbnail{flex-basis:30%;flex-grow:0;flex-shrink:0}div.quarto-post .thumbnail img{margin-top:.4em;width:100%;object-fit:cover}div.quarto-post .body{flex-basis:45%;flex-grow:1;flex-shrink:0}div.quarto-post .body h3.listing-title,div.quarto-post .body .listing-title.h3{margin-top:0px;margin-bottom:0px;border-bottom:none}div.quarto-post .body .listing-subtitle{font-size:.875em;margin-bottom:.5em;margin-top:.2em}div.quarto-post .body .description{font-size:.9em}div.quarto-post a{color:#373a3c;display:flex;flex-direction:column;text-decoration:none}div.quarto-post a div.description{flex-shrink:0}div.quarto-post .metadata{display:flex;flex-direction:column;font-size:.8em;font-family:var(--bs-font-sans-serif);flex-basis:33%}div.quarto-post .listing-categories{display:flex;flex-wrap:wrap;padding-bottom:5px}div.quarto-post .listing-categories .listing-category{color:#6c757d;border:solid 1px #dee2e6;border-radius:.25rem;text-transform:uppercase;font-size:.65em;padding-left:.5em;padding-right:.5em;padding-top:.15em;padding-bottom:.15em;cursor:pointer;margin-right:4px;margin-bottom:4px}div.quarto-post .listing-description{margin-bottom:.5em}div.quarto-about-jolla{display:flex !important;flex-direction:column;align-items:center;margin-top:10%;padding-bottom:1em}div.quarto-about-jolla .about-image{object-fit:cover;margin-left:auto;margin-right:auto;margin-bottom:1.5em}div.quarto-about-jolla img.round{border-radius:50%}div.quarto-about-jolla img.rounded{border-radius:10px}div.quarto-about-jolla .quarto-title h1.title,div.quarto-about-jolla .quarto-title .title.h1{text-align:center}div.quarto-about-jolla .quarto-title .description{text-align:center}div.quarto-about-jolla h2,div.quarto-about-jolla .h2{border-bottom:none}div.quarto-about-jolla .about-sep{width:60%}div.quarto-about-jolla main{text-align:center}div.quarto-about-jolla .about-links{display:flex}@media(min-width: 992px){div.quarto-about-jolla .about-links{flex-direction:row;column-gap:.8em;row-gap:15px;flex-wrap:wrap}}@media(max-width: 991.98px){div.quarto-about-jolla .about-links{flex-direction:column;row-gap:1em;width:100%;padding-bottom:1.5em}}div.quarto-about-jolla .about-link{color:#686d71;text-decoration:none;border:solid 1px}@media(min-width: 992px){div.quarto-about-jolla .about-link{font-size:.8em;padding:.25em .5em;border-radius:4px}}@media(max-width: 991.98px){div.quarto-about-jolla .about-link{font-size:1.1em;padding:.5em .5em;text-align:center;border-radius:6px}}div.quarto-about-jolla .about-link:hover{color:#2780e3}div.quarto-about-jolla .about-link i.bi{margin-right:.15em}div.quarto-about-solana{display:flex !important;flex-direction:column;padding-top:3em !important;padding-bottom:1em}div.quarto-about-solana .about-entity{display:flex !important;align-items:start;justify-content:space-between}@media(min-width: 992px){div.quarto-about-solana .about-entity{flex-direction:row}}@media(max-width: 991.98px){div.quarto-about-solana .about-entity{flex-direction:column-reverse;align-items:center;text-align:center}}div.quarto-about-solana .about-entity .entity-contents{display:flex;flex-direction:column}@media(max-width: 767.98px){div.quarto-about-solana .about-entity .entity-contents{width:100%}}div.quarto-about-solana .about-entity .about-image{object-fit:cover}@media(max-width: 991.98px){div.quarto-about-solana .about-entity .about-image{margin-bottom:1.5em}}div.quarto-about-solana .about-entity img.round{border-radius:50%}div.quarto-about-solana .about-entity img.rounded{border-radius:10px}div.quarto-about-solana .about-entity .about-links{display:flex;justify-content:left;padding-bottom:1.2em}@media(min-width: 992px){div.quarto-about-solana .about-entity .about-links{flex-direction:row;column-gap:.8em;row-gap:15px;flex-wrap:wrap}}@media(max-width: 991.98px){div.quarto-about-solana .about-entity .about-links{flex-direction:column;row-gap:1em;width:100%;padding-bottom:1.5em}}div.quarto-about-solana .about-entity .about-link{color:#686d71;text-decoration:none;border:solid 1px}@media(min-width: 992px){div.quarto-about-solana .about-entity .about-link{font-size:.8em;padding:.25em .5em;border-radius:4px}}@media(max-width: 991.98px){div.quarto-about-solana .about-entity .about-link{font-size:1.1em;padding:.5em .5em;text-align:center;border-radius:6px}}div.quarto-about-solana .about-entity .about-link:hover{color:#2780e3}div.quarto-about-solana .about-entity .about-link i.bi{margin-right:.15em}div.quarto-about-solana .about-contents{padding-right:1.5em;flex-basis:0;flex-grow:1}div.quarto-about-solana .about-contents main.content{margin-top:0}div.quarto-about-solana .about-contents h2,div.quarto-about-solana .about-contents .h2{border-bottom:none}div.quarto-about-trestles{display:flex !important;flex-direction:row;padding-top:3em !important;padding-bottom:1em}@media(max-width: 991.98px){div.quarto-about-trestles{flex-direction:column;padding-top:0em !important}}div.quarto-about-trestles .about-entity{display:flex !important;flex-direction:column;align-items:center;text-align:center;padding-right:1em}@media(min-width: 992px){div.quarto-about-trestles .about-entity{flex:0 0 42%}}div.quarto-about-trestles .about-entity .about-image{object-fit:cover;margin-bottom:1.5em}div.quarto-about-trestles .about-entity img.round{border-radius:50%}div.quarto-about-trestles .about-entity img.rounded{border-radius:10px}div.quarto-about-trestles .about-entity .about-links{display:flex;justify-content:center}@media(min-width: 992px){div.quarto-about-trestles .about-entity .about-links{flex-direction:row;column-gap:.8em;row-gap:15px;flex-wrap:wrap}}@media(max-width: 991.98px){div.quarto-about-trestles .about-entity .about-links{flex-direction:column;row-gap:1em;width:100%;padding-bottom:1.5em}}div.quarto-about-trestles .about-entity .about-link{color:#686d71;text-decoration:none;border:solid 1px}@media(min-width: 992px){div.quarto-about-trestles .about-entity .about-link{font-size:.8em;padding:.25em .5em;border-radius:4px}}@media(max-width: 991.98px){div.quarto-about-trestles .about-entity .about-link{font-size:1.1em;padding:.5em .5em;text-align:center;border-radius:6px}}div.quarto-about-trestles .about-entity .about-link:hover{color:#2780e3}div.quarto-about-trestles .about-entity .about-link i.bi{margin-right:.15em}div.quarto-about-trestles .about-contents{flex-basis:0;flex-grow:1}div.quarto-about-trestles .about-contents h2,div.quarto-about-trestles .about-contents .h2{border-bottom:none}@media(min-width: 992px){div.quarto-about-trestles .about-contents{border-left:solid 1px #dee2e6;padding-left:1.5em}}div.quarto-about-trestles .about-contents main.content{margin-top:0}div.quarto-about-marquee{padding-bottom:1em}div.quarto-about-marquee .about-contents{display:flex;flex-direction:column}div.quarto-about-marquee .about-image{max-height:550px;margin-bottom:1.5em;object-fit:cover}div.quarto-about-marquee img.round{border-radius:50%}div.quarto-about-marquee img.rounded{border-radius:10px}div.quarto-about-marquee h2,div.quarto-about-marquee .h2{border-bottom:none}div.quarto-about-marquee .about-links{display:flex;justify-content:center;padding-top:1.5em}@media(min-width: 992px){div.quarto-about-marquee .about-links{flex-direction:row;column-gap:.8em;row-gap:15px;flex-wrap:wrap}}@media(max-width: 991.98px){div.quarto-about-marquee .about-links{flex-direction:column;row-gap:1em;width:100%;padding-bottom:1.5em}}div.quarto-about-marquee .about-link{color:#686d71;text-decoration:none;border:solid 1px}@media(min-width: 992px){div.quarto-about-marquee .about-link{font-size:.8em;padding:.25em .5em;border-radius:4px}}@media(max-width: 991.98px){div.quarto-about-marquee .about-link{font-size:1.1em;padding:.5em .5em;text-align:center;border-radius:6px}}div.quarto-about-marquee .about-link:hover{color:#2780e3}div.quarto-about-marquee .about-link i.bi{margin-right:.15em}@media(min-width: 992px){div.quarto-about-marquee .about-link{border:none}}div.quarto-about-broadside{display:flex;flex-direction:column;padding-bottom:1em}div.quarto-about-broadside .about-main{display:flex !important;padding-top:0 !important}@media(min-width: 992px){div.quarto-about-broadside .about-main{flex-direction:row;align-items:flex-start}}@media(max-width: 991.98px){div.quarto-about-broadside .about-main{flex-direction:column}}@media(max-width: 991.98px){div.quarto-about-broadside .about-main .about-entity{flex-shrink:0;width:100%;height:450px;margin-bottom:1.5em;background-size:cover;background-repeat:no-repeat}}@media(min-width: 992px){div.quarto-about-broadside .about-main .about-entity{flex:0 10 50%;margin-right:1.5em;width:100%;height:100%;background-size:100%;background-repeat:no-repeat}}div.quarto-about-broadside .about-main .about-contents{padding-top:14px;flex:0 0 50%}div.quarto-about-broadside h2,div.quarto-about-broadside .h2{border-bottom:none}div.quarto-about-broadside .about-sep{margin-top:1.5em;width:60%;align-self:center}div.quarto-about-broadside .about-links{display:flex;justify-content:center;column-gap:20px;padding-top:1.5em}@media(min-width: 992px){div.quarto-about-broadside .about-links{flex-direction:row;column-gap:.8em;row-gap:15px;flex-wrap:wrap}}@media(max-width: 991.98px){div.quarto-about-broadside .about-links{flex-direction:column;row-gap:1em;width:100%;padding-bottom:1.5em}}div.quarto-about-broadside .about-link{color:#686d71;text-decoration:none;border:solid 1px}@media(min-width: 992px){div.quarto-about-broadside .about-link{font-size:.8em;padding:.25em .5em;border-radius:4px}}@media(max-width: 991.98px){div.quarto-about-broadside .about-link{font-size:1.1em;padding:.5em .5em;text-align:center;border-radius:6px}}div.quarto-about-broadside .about-link:hover{color:#2780e3}div.quarto-about-broadside .about-link i.bi{margin-right:.15em}@media(min-width: 992px){div.quarto-about-broadside .about-link{border:none}}.tippy-box[data-theme~=quarto]{background-color:#fff;border:solid 1px #dee2e6;border-radius:.25rem;color:#373a3c;font-size:.875rem}.tippy-box[data-theme~=quarto]>.tippy-backdrop{background-color:#fff}.tippy-box[data-theme~=quarto]>.tippy-arrow:after,.tippy-box[data-theme~=quarto]>.tippy-svg-arrow:after{content:"";position:absolute;z-index:-1}.tippy-box[data-theme~=quarto]>.tippy-arrow:after{border-color:rgba(0,0,0,0);border-style:solid}.tippy-box[data-placement^=top]>.tippy-arrow:before{bottom:-6px}.tippy-box[data-placement^=bottom]>.tippy-arrow:before{top:-6px}.tippy-box[data-placement^=right]>.tippy-arrow:before{left:-6px}.tippy-box[data-placement^=left]>.tippy-arrow:before{right:-6px}.tippy-box[data-theme~=quarto][data-placement^=top]>.tippy-arrow:before{border-top-color:#fff}.tippy-box[data-theme~=quarto][data-placement^=top]>.tippy-arrow:after{border-top-color:#dee2e6;border-width:7px 7px 0;top:17px;left:1px}.tippy-box[data-theme~=quarto][data-placement^=top]>.tippy-svg-arrow>svg{top:16px}.tippy-box[data-theme~=quarto][data-placement^=top]>.tippy-svg-arrow:after{top:17px}.tippy-box[data-theme~=quarto][data-placement^=bottom]>.tippy-arrow:before{border-bottom-color:#fff;bottom:16px}.tippy-box[data-theme~=quarto][data-placement^=bottom]>.tippy-arrow:after{border-bottom-color:#dee2e6;border-width:0 7px 7px;bottom:17px;left:1px}.tippy-box[data-theme~=quarto][data-placement^=bottom]>.tippy-svg-arrow>svg{bottom:15px}.tippy-box[data-theme~=quarto][data-placement^=bottom]>.tippy-svg-arrow:after{bottom:17px}.tippy-box[data-theme~=quarto][data-placement^=left]>.tippy-arrow:before{border-left-color:#fff}.tippy-box[data-theme~=quarto][data-placement^=left]>.tippy-arrow:after{border-left-color:#dee2e6;border-width:7px 0 7px 7px;left:17px;top:1px}.tippy-box[data-theme~=quarto][data-placement^=left]>.tippy-svg-arrow>svg{left:11px}.tippy-box[data-theme~=quarto][data-placement^=left]>.tippy-svg-arrow:after{left:12px}.tippy-box[data-theme~=quarto][data-placement^=right]>.tippy-arrow:before{border-right-color:#fff;right:16px}.tippy-box[data-theme~=quarto][data-placement^=right]>.tippy-arrow:after{border-width:7px 7px 7px 0;right:17px;top:1px;border-right-color:#dee2e6}.tippy-box[data-theme~=quarto][data-placement^=right]>.tippy-svg-arrow>svg{right:11px}.tippy-box[data-theme~=quarto][data-placement^=right]>.tippy-svg-arrow:after{right:12px}.tippy-box[data-theme~=quarto]>.tippy-svg-arrow{fill:#373a3c}.tippy-box[data-theme~=quarto]>.tippy-svg-arrow:after{background-image:url(data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTYiIGhlaWdodD0iNiIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj48cGF0aCBkPSJNMCA2czEuNzk2LS4wMTMgNC42Ny0zLjYxNUM1Ljg1MS45IDYuOTMuMDA2IDggMGMxLjA3LS4wMDYgMi4xNDguODg3IDMuMzQzIDIuMzg1QzE0LjIzMyA2LjAwNSAxNiA2IDE2IDZIMHoiIGZpbGw9InJnYmEoMCwgOCwgMTYsIDAuMikiLz48L3N2Zz4=);background-size:16px 6px;width:16px;height:6px}.top-right{position:absolute;top:1em;right:1em}.hidden{display:none !important}.zindex-bottom{z-index:-1 !important}.quarto-layout-panel{margin-bottom:1em}.quarto-layout-panel>figure{width:100%}.quarto-layout-panel>figure>figcaption,.quarto-layout-panel>.panel-caption{margin-top:10pt}.quarto-layout-panel>.table-caption{margin-top:0px}.table-caption p{margin-bottom:.5em}.quarto-layout-row{display:flex;flex-direction:row;align-items:flex-start}.quarto-layout-valign-top{align-items:flex-start}.quarto-layout-valign-bottom{align-items:flex-end}.quarto-layout-valign-center{align-items:center}.quarto-layout-cell{position:relative;margin-right:20px}.quarto-layout-cell:last-child{margin-right:0}.quarto-layout-cell figure,.quarto-layout-cell>p{margin:.2em}.quarto-layout-cell img{max-width:100%}.quarto-layout-cell .html-widget{width:100% !important}.quarto-layout-cell div figure p{margin:0}.quarto-layout-cell figure{display:inline-block;margin-inline-start:0;margin-inline-end:0}.quarto-layout-cell table{display:inline-table}.quarto-layout-cell-subref figcaption,figure .quarto-layout-row figure figcaption{text-align:center;font-style:italic}.quarto-figure{position:relative;margin-bottom:1em}.quarto-figure>figure{width:100%;margin-bottom:0}.quarto-figure-left>figure>p,.quarto-figure-left>figure>div{text-align:left}.quarto-figure-center>figure>p,.quarto-figure-center>figure>div{text-align:center}.quarto-figure-right>figure>p,.quarto-figure-right>figure>div{text-align:right}figure>p:empty{display:none}figure>p:first-child{margin-top:0;margin-bottom:0}figure>figcaption{margin-top:.5em}div[id^=tbl-]{position:relative}.quarto-figure>.anchorjs-link{position:absolute;top:.6em;right:.5em}div[id^=tbl-]>.anchorjs-link{position:absolute;top:.7em;right:.3em}.quarto-figure:hover>.anchorjs-link,div[id^=tbl-]:hover>.anchorjs-link,h2:hover>.anchorjs-link,.h2:hover>.anchorjs-link,h3:hover>.anchorjs-link,.h3:hover>.anchorjs-link,h4:hover>.anchorjs-link,.h4:hover>.anchorjs-link,h5:hover>.anchorjs-link,.h5:hover>.anchorjs-link,h6:hover>.anchorjs-link,.h6:hover>.anchorjs-link,.reveal-anchorjs-link>.anchorjs-link{opacity:1}#title-block-header{margin-block-end:1rem;position:relative;margin-top:-1px}#title-block-header .abstract{margin-block-start:1rem}#title-block-header .abstract .abstract-title{font-weight:600}#title-block-header a{text-decoration:none}#title-block-header .author,#title-block-header .date,#title-block-header .doi{margin-block-end:.2rem}#title-block-header .quarto-title-block>div{display:flex}#title-block-header .quarto-title-block>div>h1,#title-block-header .quarto-title-block>div>.h1{flex-grow:1}#title-block-header .quarto-title-block>div>button{flex-shrink:0;height:2.25rem;margin-top:0}@media(min-width: 992px){#title-block-header .quarto-title-block>div>button{margin-top:5px}}tr.header>th>p:last-of-type{margin-bottom:0px}table,.table{caption-side:top;margin-bottom:1.5rem}caption,.table-caption{padding-top:.5rem;padding-bottom:.5rem;text-align:center}.utterances{max-width:none;margin-left:-8px}iframe{margin-bottom:1em}details{margin-bottom:1em}details[show]{margin-bottom:0}details>summary{color:#6c757d}details>summary>p:only-child{display:inline}pre.sourceCode,code.sourceCode{position:relative}p code:not(.sourceCode){white-space:pre-wrap}code{white-space:pre}@media print{code{white-space:pre-wrap}}pre>code{display:block}pre>code.sourceCode{white-space:pre}pre>code.sourceCode>span>a:first-child::before{text-decoration:none}pre.code-overflow-wrap>code.sourceCode{white-space:pre-wrap}pre.code-overflow-scroll>code.sourceCode{white-space:pre}code a:any-link{color:inherit;text-decoration:none}code a:hover{color:inherit;text-decoration:underline}ul.task-list{padding-left:1em}[data-tippy-root]{display:inline-block}.tippy-content .footnote-back{display:none}.quarto-embedded-source-code{display:none}.quarto-unresolved-ref{font-weight:600}.quarto-cover-image{max-width:35%;float:right;margin-left:30px}.cell-output-display .widget-subarea{margin-bottom:1em}.cell-output-display:not(.no-overflow-x),.knitsql-table:not(.no-overflow-x){overflow-x:auto}.panel-input{margin-bottom:1em}.panel-input>div,.panel-input>div>div{display:inline-block;vertical-align:top;padding-right:12px}.panel-input>p:last-child{margin-bottom:0}.layout-sidebar{margin-bottom:1em}.layout-sidebar .tab-content{border:none}.tab-content>.page-columns.active{display:grid}div.sourceCode>iframe{width:100%;height:300px;margin-bottom:-0.5em}div.ansi-escaped-output{font-family:monospace;display:block}/*!
+*
+* ansi colors from IPython notebook's
+*
+*/.ansi-black-fg{color:#3e424d}.ansi-black-bg{background-color:#3e424d}.ansi-black-intense-fg{color:#282c36}.ansi-black-intense-bg{background-color:#282c36}.ansi-red-fg{color:#e75c58}.ansi-red-bg{background-color:#e75c58}.ansi-red-intense-fg{color:#b22b31}.ansi-red-intense-bg{background-color:#b22b31}.ansi-green-fg{color:#00a250}.ansi-green-bg{background-color:#00a250}.ansi-green-intense-fg{color:#007427}.ansi-green-intense-bg{background-color:#007427}.ansi-yellow-fg{color:#ddb62b}.ansi-yellow-bg{background-color:#ddb62b}.ansi-yellow-intense-fg{color:#b27d12}.ansi-yellow-intense-bg{background-color:#b27d12}.ansi-blue-fg{color:#208ffb}.ansi-blue-bg{background-color:#208ffb}.ansi-blue-intense-fg{color:#0065ca}.ansi-blue-intense-bg{background-color:#0065ca}.ansi-magenta-fg{color:#d160c4}.ansi-magenta-bg{background-color:#d160c4}.ansi-magenta-intense-fg{color:#a03196}.ansi-magenta-intense-bg{background-color:#a03196}.ansi-cyan-fg{color:#60c6c8}.ansi-cyan-bg{background-color:#60c6c8}.ansi-cyan-intense-fg{color:#258f8f}.ansi-cyan-intense-bg{background-color:#258f8f}.ansi-white-fg{color:#c5c1b4}.ansi-white-bg{background-color:#c5c1b4}.ansi-white-intense-fg{color:#a1a6b2}.ansi-white-intense-bg{background-color:#a1a6b2}.ansi-default-inverse-fg{color:#fff}.ansi-default-inverse-bg{background-color:#000}.ansi-bold{font-weight:bold}.ansi-underline{text-decoration:underline}:root{--quarto-body-bg: #fff;--quarto-body-color: #373a3c;--quarto-text-muted: #6c757d;--quarto-border-color: #dee2e6;--quarto-border-width: 1px;--quarto-border-radius: 0.25rem}table.gt_table{color:var(--quarto-body-color);font-size:1em;width:100%;background-color:rgba(0,0,0,0);border-top-width:inherit;border-bottom-width:inherit;border-color:var(--quarto-border-color)}table.gt_table th.gt_column_spanner_outer{color:var(--quarto-body-color);background-color:rgba(0,0,0,0);border-top-width:inherit;border-bottom-width:inherit;border-color:var(--quarto-border-color)}table.gt_table th.gt_col_heading{color:var(--quarto-body-color);font-weight:bold;background-color:rgba(0,0,0,0)}table.gt_table thead.gt_col_headings{border-bottom:1px solid currentColor;border-top-width:inherit;border-top-color:var(--quarto-border-color)}table.gt_table thead.gt_col_headings:not(:first-child){border-top-width:1px;border-top-color:var(--quarto-border-color)}table.gt_table td.gt_row{border-bottom-width:1px;border-bottom-color:var(--quarto-border-color);border-top-width:0px}table.gt_table tbody.gt_table_body{border-top-width:1px;border-bottom-width:1px;border-bottom-color:var(--quarto-border-color);border-top-color:currentColor}div.columns{display:initial;gap:initial}div.column{display:inline-block;overflow-x:initial;vertical-align:top;width:50%}.code-annotation-tip-content{word-wrap:break-word}.code-annotation-container-hidden{display:none !important}dl.code-annotation-container-grid{display:grid;grid-template-columns:min-content auto}dl.code-annotation-container-grid dt{grid-column:1}dl.code-annotation-container-grid dd{grid-column:2}pre.sourceCode.code-annotation-code{padding-right:0}code.sourceCode .code-annotation-anchor{z-index:100;position:absolute;right:.5em;left:inherit;background-color:rgba(0,0,0,0)}:root{--mermaid-bg-color: #fff;--mermaid-edge-color: #373a3c;--mermaid-node-fg-color: #373a3c;--mermaid-fg-color: #373a3c;--mermaid-fg-color--lighter: #4f5457;--mermaid-fg-color--lightest: #686d71;--mermaid-font-family: Source Sans Pro, -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Helvetica Neue, Arial, sans-serif, Apple Color Emoji, Segoe UI Emoji, Segoe UI Symbol;--mermaid-label-bg-color: #fff;--mermaid-label-fg-color: #2780e3;--mermaid-node-bg-color: rgba(39, 128, 227, 0.1);--mermaid-node-fg-color: #373a3c}@media print{:root{font-size:11pt}#quarto-sidebar,#TOC,.nav-page{display:none}.page-columns .content{grid-column-start:page-start}.fixed-top{position:relative}.panel-caption,.figure-caption,figcaption{color:#666}}.code-copy-button{position:absolute;top:0;right:0;border:0;margin-top:5px;margin-right:5px;background-color:rgba(0,0,0,0);z-index:3}.code-copy-button:focus{outline:none}.code-copy-button-tooltip{font-size:.75em}pre.sourceCode:hover>.code-copy-button>.bi::before{display:inline-block;height:1rem;width:1rem;content:"";vertical-align:-0.125em;background-image:url('data:image/svg+xml,');background-repeat:no-repeat;background-size:1rem 1rem}pre.sourceCode:hover>.code-copy-button-checked>.bi::before{background-image:url('data:image/svg+xml,')}pre.sourceCode:hover>.code-copy-button:hover>.bi::before{background-image:url('data:image/svg+xml,')}pre.sourceCode:hover>.code-copy-button-checked:hover>.bi::before{background-image:url('data:image/svg+xml,')}main ol ol,main ul ul,main ol ul,main ul ol{margin-bottom:1em}ul>li:not(:has(>p))>ul,ol>li:not(:has(>p))>ul,ul>li:not(:has(>p))>ol,ol>li:not(:has(>p))>ol{margin-bottom:0}ul>li:not(:has(>p))>ul>li:has(>p),ol>li:not(:has(>p))>ul>li:has(>p),ul>li:not(:has(>p))>ol>li:has(>p),ol>li:not(:has(>p))>ol>li:has(>p){margin-top:1rem}body{margin:0}main.page-columns>header>h1.title,main.page-columns>header>.title.h1{margin-bottom:0}@media(min-width: 992px){body .page-columns{display:grid;gap:0;grid-template-columns:[screen-start] 1.5em [screen-start-inset] 5fr [page-start page-start-inset] 35px [body-start-outset] 35px [body-start] 1.5em [body-content-start] minmax(500px, calc( 850px - 3em )) [body-content-end] 1.5em [body-end] 35px [body-end-outset] minmax(75px, 145px) [page-end-inset] 35px [page-end] 5fr [screen-end-inset] 1.5em [screen-end]}body.fullcontent:not(.floating):not(.docked) .page-columns{display:grid;gap:0;grid-template-columns:[screen-start] 1.5em [screen-start-inset] 5fr [page-start page-start-inset] 35px [body-start-outset] 35px [body-start] 1.5em [body-content-start] minmax(500px, calc( 850px - 3em )) [body-content-end] 1.5em [body-end] 35px [body-end-outset] 35px [page-end-inset page-end] 5fr [screen-end-inset] 1.5em}body.slimcontent:not(.floating):not(.docked) .page-columns{display:grid;gap:0;grid-template-columns:[screen-start] 1.5em [screen-start-inset] 5fr [page-start page-start-inset] 35px [body-start-outset] 35px [body-start] 1.5em [body-content-start] minmax(500px, calc( 850px - 3em )) [body-content-end] 1.5em [body-end] 50px [body-end-outset] minmax(0px, 200px) [page-end-inset] 35px [page-end] 5fr [screen-end-inset] 1.5em [screen-end]}body.listing:not(.floating):not(.docked) .page-columns{display:grid;gap:0;grid-template-columns:[screen-start] 1.5em [screen-start-inset page-start] minmax(50px, 100px) [page-start-inset] 50px [body-start-outset] 50px [body-start] 1.5em [body-content-start] minmax(500px, calc( 850px - 3em )) [body-content-end] 3em [body-end] 50px [body-end-outset] minmax(0px, 250px) [page-end-inset] minmax(50px, 100px) [page-end] 1fr [screen-end-inset] 1.5em [screen-end]}body:not(.floating):not(.docked) .page-columns.toc-left{display:grid;gap:0;grid-template-columns:[screen-start] 1.5em [screen-start-inset] 5fr [page-start] 35px [page-start-inset] minmax(0px, 175px) [body-start-outset] 35px [body-start] 1.5em [body-content-start] minmax(450px, calc( 800px - 3em )) [body-content-end] 1.5em [body-end] 50px [body-end-outset] minmax(0px, 200px) [page-end-inset] 50px [page-end] 5fr [screen-end-inset] 1.5em [screen-end]}body:not(.floating):not(.docked) .page-columns.toc-left .page-columns{display:grid;gap:0;grid-template-columns:[screen-start] 1.5em [screen-start-inset] 5fr [page-start] 35px [page-start-inset] minmax(0px, 175px) [body-start-outset] 35px [body-start] 1.5em [body-content-start] minmax(450px, calc( 800px - 3em )) [body-content-end] 1.5em [body-end] 50px [body-end-outset] minmax(0px, 200px) [page-end-inset] 50px [page-end] 5fr [screen-end-inset] 1.5em [screen-end]}body.floating .page-columns{display:grid;gap:0;grid-template-columns:[screen-start] 1.5em [screen-start-inset] 5fr [page-start] minmax(25px, 50px) [page-start-inset] minmax(50px, 150px) [body-start-outset] minmax(25px, 50px) [body-start] 1.5em [body-content-start] minmax(500px, calc( 800px - 3em )) [body-content-end] 1.5em [body-end] minmax(25px, 50px) [body-end-outset] minmax(50px, 150px) [page-end-inset] minmax(25px, 50px) [page-end] 5fr [screen-end-inset] 1.5em [screen-end]}body.docked .page-columns{display:grid;gap:0;grid-template-columns:[screen-start] 1.5em [screen-start-inset page-start] minmax(50px, 100px) [page-start-inset] 50px [body-start-outset] 50px [body-start] 1.5em [body-content-start] minmax(500px, calc( 1000px - 3em )) [body-content-end] 1.5em [body-end] 50px [body-end-outset] minmax(50px, 100px) [page-end-inset] 50px [page-end] 5fr [screen-end-inset] 1.5em [screen-end]}body.docked.fullcontent .page-columns{display:grid;gap:0;grid-template-columns:[screen-start] 1.5em [screen-start-inset page-start] minmax(50px, 100px) [page-start-inset] 50px [body-start-outset] 50px [body-start] 1.5em [body-content-start] minmax(500px, calc( 1000px - 3em )) [body-content-end] 1.5em [body-end body-end-outset page-end-inset page-end] 5fr [screen-end-inset] 1.5em [screen-end]}body.floating.fullcontent .page-columns{display:grid;gap:0;grid-template-columns:[screen-start] 1.5em [screen-start-inset] 5fr [page-start] 50px [page-start-inset] minmax(50px, 150px) [body-start-outset] 50px [body-start] 1.5em [body-content-start] minmax(500px, calc( 800px - 3em )) [body-content-end] 1.5em [body-end body-end-outset page-end-inset page-end] 5fr [screen-end-inset] 1.5em [screen-end]}body.docked.slimcontent .page-columns{display:grid;gap:0;grid-template-columns:[screen-start] 1.5em [screen-start-inset page-start] minmax(50px, 100px) [page-start-inset] 50px [body-start-outset] 50px [body-start] 1.5em [body-content-start] minmax(450px, calc( 750px - 3em )) [body-content-end] 1.5em [body-end] 50px [body-end-outset] minmax(0px, 200px) [page-end-inset] 50px [page-end] 5fr [screen-end-inset] 1.5em [screen-end]}body.docked.listing .page-columns{display:grid;gap:0;grid-template-columns:[screen-start] 1.5em [screen-start-inset page-start] minmax(50px, 100px) [page-start-inset] 50px [body-start-outset] 50px [body-start] 1.5em [body-content-start] minmax(500px, calc( 1000px - 3em )) [body-content-end] 1.5em [body-end] 50px [body-end-outset] minmax(0px, 200px) [page-end-inset] 50px [page-end] 5fr [screen-end-inset] 1.5em [screen-end]}body.floating.slimcontent .page-columns{display:grid;gap:0;grid-template-columns:[screen-start] 1.5em [screen-start-inset] 5fr [page-start] 50px [page-start-inset] minmax(50px, 150px) [body-start-outset] 50px [body-start] 1.5em [body-content-start] minmax(450px, calc( 750px - 3em )) [body-content-end] 1.5em [body-end] 50px [body-end-outset] minmax(50px, 150px) [page-end-inset] 50px [page-end] 5fr [screen-end-inset] 1.5em [screen-end]}body.floating.listing .page-columns{display:grid;gap:0;grid-template-columns:[screen-start] 1.5em [screen-start-inset] 5fr [page-start] minmax(25px, 50px) [page-start-inset] minmax(50px, 150px) [body-start-outset] minmax(25px, 50px) [body-start] 1.5em [body-content-start] minmax(500px, calc( 800px - 3em )) [body-content-end] 1.5em [body-end] minmax(25px, 50px) [body-end-outset] minmax(50px, 150px) [page-end-inset] minmax(25px, 50px) [page-end] 5fr [screen-end-inset] 1.5em [screen-end]}}@media(max-width: 991.98px){body .page-columns{display:grid;gap:0;grid-template-columns:[screen-start] 1.5em [screen-start-inset page-start page-start-inset body-start-outset] 5fr [body-start] 1.5em [body-content-start] minmax(500px, calc( 800px - 3em )) [body-content-end] 1.5em [body-end] 35px [body-end-outset] minmax(75px, 145px) [page-end-inset] 35px [page-end] 5fr [screen-end-inset] 1.5em [screen-end]}body.fullcontent:not(.floating):not(.docked) .page-columns{display:grid;gap:0;grid-template-columns:[screen-start] 1.5em [screen-start-inset page-start page-start-inset body-start-outset] 5fr [body-start] 1.5em [body-content-start] minmax(500px, calc( 800px - 3em )) [body-content-end] 1.5em [body-end body-end-outset page-end-inset page-end] 5fr [screen-end-inset] 1.5em [screen-end]}body.slimcontent:not(.floating):not(.docked) .page-columns{display:grid;gap:0;grid-template-columns:[screen-start] 1.5em [screen-start-inset page-start page-start-inset body-start-outset] 5fr [body-start] 1.5em [body-content-start] minmax(500px, calc( 800px - 3em )) [body-content-end] 1.5em [body-end] 35px [body-end-outset] minmax(75px, 145px) [page-end-inset] 35px [page-end] 5fr [screen-end-inset] 1.5em [screen-end]}body.listing:not(.floating):not(.docked) .page-columns{display:grid;gap:0;grid-template-columns:[screen-start] 1.5em [screen-start-inset page-start page-start-inset body-start-outset] 5fr [body-start] 1.5em [body-content-start] minmax(500px, calc( 1250px - 3em )) [body-content-end body-end body-end-outset page-end-inset page-end] 5fr [screen-end-inset] 1.5em [screen-end]}body:not(.floating):not(.docked) .page-columns.toc-left{display:grid;gap:0;grid-template-columns:[screen-start] 1.5em [screen-start-inset] 5fr [page-start] 35px [page-start-inset] minmax(0px, 145px) [body-start-outset] 35px [body-start] 1.5em [body-content-start] minmax(450px, calc( 800px - 3em )) [body-content-end] 1.5em [body-end body-end-outset page-end-inset page-end] 5fr [screen-end-inset] 1.5em [screen-end]}body:not(.floating):not(.docked) .page-columns.toc-left .page-columns{display:grid;gap:0;grid-template-columns:[screen-start] 1.5em [screen-start-inset] 5fr [page-start] 35px [page-start-inset] minmax(0px, 145px) [body-start-outset] 35px [body-start] 1.5em [body-content-start] minmax(450px, calc( 800px - 3em )) [body-content-end] 1.5em [body-end body-end-outset page-end-inset page-end] 5fr [screen-end-inset] 1.5em [screen-end]}body.floating .page-columns{display:grid;gap:0;grid-template-columns:[screen-start] 1.5em [screen-start-inset] 5fr [page-start page-start-inset body-start-outset body-start] 1.5em [body-content-start] minmax(500px, calc( 750px - 3em )) [body-content-end] 1.5em [body-end] 50px [body-end-outset] minmax(75px, 150px) [page-end-inset] 25px [page-end] 5fr [screen-end-inset] 1.5em [screen-end]}body.docked .page-columns{display:grid;gap:0;grid-template-columns:[screen-start] 1.5em [screen-start-inset page-start page-start-inset body-start-outset body-start body-content-start] minmax(500px, calc( 750px - 3em )) [body-content-end] 1.5em [body-end] 50px [body-end-outset] minmax(25px, 50px) [page-end-inset] 50px [page-end] 5fr [screen-end-inset] 1.5em [screen-end]}body.docked.fullcontent .page-columns{display:grid;gap:0;grid-template-columns:[screen-start] 1.5em [screen-start-inset page-start page-start-inset body-start-outset body-start body-content-start] minmax(500px, calc( 1000px - 3em )) [body-content-end] 1.5em [body-end body-end-outset page-end-inset page-end] 5fr [screen-end-inset] 1.5em [screen-end]}body.floating.fullcontent .page-columns{display:grid;gap:0;grid-template-columns:[screen-start] 1.5em [screen-start-inset] 5fr [page-start page-start-inset body-start-outset body-start] 1em [body-content-start] minmax(500px, calc( 800px - 3em )) [body-content-end] 1.5em [body-end body-end-outset page-end-inset page-end] 4fr [screen-end-inset] 1.5em [screen-end]}body.docked.slimcontent .page-columns{display:grid;gap:0;grid-template-columns:[screen-start] 1.5em [screen-start-inset page-start page-start-inset body-start-outset body-start body-content-start] minmax(500px, calc( 750px - 3em )) [body-content-end] 1.5em [body-end] 50px [body-end-outset] minmax(25px, 50px) [page-end-inset] 50px [page-end] 5fr [screen-end-inset] 1.5em [screen-end]}body.docked.listing .page-columns{display:grid;gap:0;grid-template-columns:[screen-start] 1.5em [screen-start-inset page-start page-start-inset body-start-outset body-start body-content-start] minmax(500px, calc( 750px - 3em )) [body-content-end] 1.5em [body-end] 50px [body-end-outset] minmax(25px, 50px) [page-end-inset] 50px [page-end] 5fr [screen-end-inset] 1.5em [screen-end]}body.floating.slimcontent .page-columns{display:grid;gap:0;grid-template-columns:[screen-start] 1.5em [screen-start-inset] 5fr [page-start page-start-inset body-start-outset body-start] 1em [body-content-start] minmax(500px, calc( 750px - 3em )) [body-content-end] 1.5em [body-end] 35px [body-end-outset] minmax(75px, 145px) [page-end-inset] 35px [page-end] 4fr [screen-end-inset] 1.5em [screen-end]}body.floating.listing .page-columns{display:grid;gap:0;grid-template-columns:[screen-start] 1.5em [screen-start-inset] 5fr [page-start page-start-inset body-start-outset body-start] 1em [body-content-start] minmax(500px, calc( 750px - 3em )) [body-content-end] 1.5em [body-end] 50px [body-end-outset] minmax(75px, 150px) [page-end-inset] 25px [page-end] 4fr [screen-end-inset] 1.5em [screen-end]}}@media(max-width: 767.98px){body .page-columns,body.fullcontent:not(.floating):not(.docked) .page-columns,body.slimcontent:not(.floating):not(.docked) .page-columns,body.docked .page-columns,body.docked.slimcontent .page-columns,body.docked.fullcontent .page-columns,body.floating .page-columns,body.floating.slimcontent .page-columns,body.floating.fullcontent .page-columns{display:grid;gap:0;grid-template-columns:[screen-start] 1.5em [screen-start-inset page-start page-start-inset body-start-outset body-start body-content-start] minmax(0px, 1fr) [body-content-end body-end body-end-outset page-end-inset page-end screen-end-inset] 1.5em [screen-end]}body:not(.floating):not(.docked) .page-columns.toc-left{display:grid;gap:0;grid-template-columns:[screen-start] 1.5em [screen-start-inset page-start page-start-inset body-start-outset body-start body-content-start] minmax(0px, 1fr) [body-content-end body-end body-end-outset page-end-inset page-end screen-end-inset] 1.5em [screen-end]}body:not(.floating):not(.docked) .page-columns.toc-left .page-columns{display:grid;gap:0;grid-template-columns:[screen-start] 1.5em [screen-start-inset page-start page-start-inset body-start-outset body-start body-content-start] minmax(0px, 1fr) [body-content-end body-end body-end-outset page-end-inset page-end screen-end-inset] 1.5em [screen-end]}nav[role=doc-toc]{display:none}}body,.page-row-navigation{grid-template-rows:[page-top] max-content [contents-top] max-content [contents-bottom] max-content [page-bottom]}.page-rows-contents{grid-template-rows:[content-top] minmax(max-content, 1fr) [content-bottom] minmax(60px, max-content) [page-bottom]}.page-full{grid-column:screen-start/screen-end !important}.page-columns>*{grid-column:body-content-start/body-content-end}.page-columns.column-page>*{grid-column:page-start/page-end}.page-columns.column-page-left>*{grid-column:page-start/body-content-end}.page-columns.column-page-right>*{grid-column:body-content-start/page-end}.page-rows{grid-auto-rows:auto}.header{grid-column:screen-start/screen-end;grid-row:page-top/contents-top}#quarto-content{padding:0;grid-column:screen-start/screen-end;grid-row:contents-top/contents-bottom}body.floating .sidebar.sidebar-navigation{grid-column:page-start/body-start;grid-row:content-top/page-bottom}body.docked .sidebar.sidebar-navigation{grid-column:screen-start/body-start;grid-row:content-top/page-bottom}.sidebar.toc-left{grid-column:page-start/body-start;grid-row:content-top/page-bottom}.sidebar.margin-sidebar{grid-column:body-end/page-end;grid-row:content-top/page-bottom}.page-columns .content{grid-column:body-content-start/body-content-end;grid-row:content-top/content-bottom;align-content:flex-start}.page-columns .page-navigation{grid-column:body-content-start/body-content-end;grid-row:content-bottom/page-bottom}.page-columns .footer{grid-column:screen-start/screen-end;grid-row:contents-bottom/page-bottom}.page-columns .column-body{grid-column:body-content-start/body-content-end}.page-columns .column-body-fullbleed{grid-column:body-start/body-end}.page-columns .column-body-outset{grid-column:body-start-outset/body-end-outset;z-index:998;transform:translate3d(0, 0, 0)}.page-columns .column-body-outset table{background:#fff}.page-columns .column-body-outset-left{grid-column:body-start-outset/body-content-end;z-index:998;transform:translate3d(0, 0, 0)}.page-columns .column-body-outset-left table{background:#fff}.page-columns .column-body-outset-right{grid-column:body-content-start/body-end-outset;z-index:998;transform:translate3d(0, 0, 0)}.page-columns .column-body-outset-right table{background:#fff}.page-columns .column-page{grid-column:page-start/page-end;z-index:998;transform:translate3d(0, 0, 0)}.page-columns .column-page table{background:#fff}.page-columns .column-page-inset{grid-column:page-start-inset/page-end-inset;z-index:998;transform:translate3d(0, 0, 0)}.page-columns .column-page-inset table{background:#fff}.page-columns .column-page-inset-left{grid-column:page-start-inset/body-content-end;z-index:998;transform:translate3d(0, 0, 0)}.page-columns .column-page-inset-left table{background:#fff}.page-columns .column-page-inset-right{grid-column:body-content-start/page-end-inset;z-index:998;transform:translate3d(0, 0, 0)}.page-columns .column-page-inset-right figcaption table{background:#fff}.page-columns .column-page-left{grid-column:page-start/body-content-end;z-index:998;transform:translate3d(0, 0, 0)}.page-columns .column-page-left table{background:#fff}.page-columns .column-page-right{grid-column:body-content-start/page-end;z-index:998;transform:translate3d(0, 0, 0)}.page-columns .column-page-right figcaption table{background:#fff}#quarto-content.page-columns #quarto-margin-sidebar,#quarto-content.page-columns #quarto-sidebar{z-index:1}@media(max-width: 991.98px){#quarto-content.page-columns #quarto-margin-sidebar.collapse,#quarto-content.page-columns #quarto-sidebar.collapse,#quarto-content.page-columns #quarto-margin-sidebar.collapsing,#quarto-content.page-columns #quarto-sidebar.collapsing{z-index:1055}}#quarto-content.page-columns main.column-page,#quarto-content.page-columns main.column-page-right,#quarto-content.page-columns main.column-page-left{z-index:0}.page-columns .column-screen-inset{grid-column:screen-start-inset/screen-end-inset;z-index:998;transform:translate3d(0, 0, 0)}.page-columns .column-screen-inset table{background:#fff}.page-columns .column-screen-inset-left{grid-column:screen-start-inset/body-content-end;z-index:998;transform:translate3d(0, 0, 0)}.page-columns .column-screen-inset-left table{background:#fff}.page-columns .column-screen-inset-right{grid-column:body-content-start/screen-end-inset;z-index:998;transform:translate3d(0, 0, 0)}.page-columns .column-screen-inset-right table{background:#fff}.page-columns .column-screen{grid-column:screen-start/screen-end;z-index:998;transform:translate3d(0, 0, 0)}.page-columns .column-screen table{background:#fff}.page-columns .column-screen-left{grid-column:screen-start/body-content-end;z-index:998;transform:translate3d(0, 0, 0)}.page-columns .column-screen-left table{background:#fff}.page-columns .column-screen-right{grid-column:body-content-start/screen-end;z-index:998;transform:translate3d(0, 0, 0)}.page-columns .column-screen-right table{background:#fff}.page-columns .column-screen-inset-shaded{grid-column:screen-start/screen-end;padding:1em;background:#f8f9fa;z-index:998;transform:translate3d(0, 0, 0);margin-bottom:1em}.zindex-content{z-index:998;transform:translate3d(0, 0, 0)}.zindex-modal{z-index:1055;transform:translate3d(0, 0, 0)}.zindex-over-content{z-index:999;transform:translate3d(0, 0, 0)}img.img-fluid.column-screen,img.img-fluid.column-screen-inset-shaded,img.img-fluid.column-screen-inset,img.img-fluid.column-screen-inset-left,img.img-fluid.column-screen-inset-right,img.img-fluid.column-screen-left,img.img-fluid.column-screen-right{width:100%}@media(min-width: 992px){.margin-caption,div.aside,aside,.column-margin{grid-column:body-end/page-end !important;z-index:998}.column-sidebar{grid-column:page-start/body-start !important;z-index:998}.column-leftmargin{grid-column:screen-start-inset/body-start !important;z-index:998}.no-row-height{height:1em;overflow:visible}}@media(max-width: 991.98px){.margin-caption,div.aside,aside,.column-margin{grid-column:body-end/page-end !important;z-index:998}.no-row-height{height:1em;overflow:visible}.page-columns.page-full{overflow:visible}.page-columns.toc-left .margin-caption,.page-columns.toc-left div.aside,.page-columns.toc-left aside,.page-columns.toc-left .column-margin{grid-column:body-content-start/body-content-end !important;z-index:998;transform:translate3d(0, 0, 0)}.page-columns.toc-left .no-row-height{height:initial;overflow:initial}}@media(max-width: 767.98px){.margin-caption,div.aside,aside,.column-margin{grid-column:body-content-start/body-content-end !important;z-index:998;transform:translate3d(0, 0, 0)}.no-row-height{height:initial;overflow:initial}#quarto-margin-sidebar{display:none}#quarto-sidebar-toc-left{display:none}.hidden-sm{display:none}}.panel-grid{display:grid;grid-template-rows:repeat(1, 1fr);grid-template-columns:repeat(24, 1fr);gap:1em}.panel-grid .g-col-1{grid-column:auto/span 1}.panel-grid .g-col-2{grid-column:auto/span 2}.panel-grid .g-col-3{grid-column:auto/span 3}.panel-grid .g-col-4{grid-column:auto/span 4}.panel-grid .g-col-5{grid-column:auto/span 5}.panel-grid .g-col-6{grid-column:auto/span 6}.panel-grid .g-col-7{grid-column:auto/span 7}.panel-grid .g-col-8{grid-column:auto/span 8}.panel-grid .g-col-9{grid-column:auto/span 9}.panel-grid .g-col-10{grid-column:auto/span 10}.panel-grid .g-col-11{grid-column:auto/span 11}.panel-grid .g-col-12{grid-column:auto/span 12}.panel-grid .g-col-13{grid-column:auto/span 13}.panel-grid .g-col-14{grid-column:auto/span 14}.panel-grid .g-col-15{grid-column:auto/span 15}.panel-grid .g-col-16{grid-column:auto/span 16}.panel-grid .g-col-17{grid-column:auto/span 17}.panel-grid .g-col-18{grid-column:auto/span 18}.panel-grid .g-col-19{grid-column:auto/span 19}.panel-grid .g-col-20{grid-column:auto/span 20}.panel-grid .g-col-21{grid-column:auto/span 21}.panel-grid .g-col-22{grid-column:auto/span 22}.panel-grid .g-col-23{grid-column:auto/span 23}.panel-grid .g-col-24{grid-column:auto/span 24}.panel-grid .g-start-1{grid-column-start:1}.panel-grid .g-start-2{grid-column-start:2}.panel-grid .g-start-3{grid-column-start:3}.panel-grid .g-start-4{grid-column-start:4}.panel-grid .g-start-5{grid-column-start:5}.panel-grid .g-start-6{grid-column-start:6}.panel-grid .g-start-7{grid-column-start:7}.panel-grid .g-start-8{grid-column-start:8}.panel-grid .g-start-9{grid-column-start:9}.panel-grid .g-start-10{grid-column-start:10}.panel-grid .g-start-11{grid-column-start:11}.panel-grid .g-start-12{grid-column-start:12}.panel-grid .g-start-13{grid-column-start:13}.panel-grid .g-start-14{grid-column-start:14}.panel-grid .g-start-15{grid-column-start:15}.panel-grid .g-start-16{grid-column-start:16}.panel-grid .g-start-17{grid-column-start:17}.panel-grid .g-start-18{grid-column-start:18}.panel-grid .g-start-19{grid-column-start:19}.panel-grid .g-start-20{grid-column-start:20}.panel-grid .g-start-21{grid-column-start:21}.panel-grid .g-start-22{grid-column-start:22}.panel-grid .g-start-23{grid-column-start:23}@media(min-width: 576px){.panel-grid .g-col-sm-1{grid-column:auto/span 1}.panel-grid .g-col-sm-2{grid-column:auto/span 2}.panel-grid .g-col-sm-3{grid-column:auto/span 3}.panel-grid .g-col-sm-4{grid-column:auto/span 4}.panel-grid .g-col-sm-5{grid-column:auto/span 5}.panel-grid .g-col-sm-6{grid-column:auto/span 6}.panel-grid .g-col-sm-7{grid-column:auto/span 7}.panel-grid .g-col-sm-8{grid-column:auto/span 8}.panel-grid .g-col-sm-9{grid-column:auto/span 9}.panel-grid .g-col-sm-10{grid-column:auto/span 10}.panel-grid .g-col-sm-11{grid-column:auto/span 11}.panel-grid .g-col-sm-12{grid-column:auto/span 12}.panel-grid .g-col-sm-13{grid-column:auto/span 13}.panel-grid .g-col-sm-14{grid-column:auto/span 14}.panel-grid .g-col-sm-15{grid-column:auto/span 15}.panel-grid .g-col-sm-16{grid-column:auto/span 16}.panel-grid .g-col-sm-17{grid-column:auto/span 17}.panel-grid .g-col-sm-18{grid-column:auto/span 18}.panel-grid .g-col-sm-19{grid-column:auto/span 19}.panel-grid .g-col-sm-20{grid-column:auto/span 20}.panel-grid .g-col-sm-21{grid-column:auto/span 21}.panel-grid .g-col-sm-22{grid-column:auto/span 22}.panel-grid .g-col-sm-23{grid-column:auto/span 23}.panel-grid .g-col-sm-24{grid-column:auto/span 24}.panel-grid .g-start-sm-1{grid-column-start:1}.panel-grid .g-start-sm-2{grid-column-start:2}.panel-grid .g-start-sm-3{grid-column-start:3}.panel-grid .g-start-sm-4{grid-column-start:4}.panel-grid .g-start-sm-5{grid-column-start:5}.panel-grid .g-start-sm-6{grid-column-start:6}.panel-grid .g-start-sm-7{grid-column-start:7}.panel-grid .g-start-sm-8{grid-column-start:8}.panel-grid .g-start-sm-9{grid-column-start:9}.panel-grid .g-start-sm-10{grid-column-start:10}.panel-grid .g-start-sm-11{grid-column-start:11}.panel-grid .g-start-sm-12{grid-column-start:12}.panel-grid .g-start-sm-13{grid-column-start:13}.panel-grid .g-start-sm-14{grid-column-start:14}.panel-grid .g-start-sm-15{grid-column-start:15}.panel-grid .g-start-sm-16{grid-column-start:16}.panel-grid .g-start-sm-17{grid-column-start:17}.panel-grid .g-start-sm-18{grid-column-start:18}.panel-grid .g-start-sm-19{grid-column-start:19}.panel-grid .g-start-sm-20{grid-column-start:20}.panel-grid .g-start-sm-21{grid-column-start:21}.panel-grid .g-start-sm-22{grid-column-start:22}.panel-grid .g-start-sm-23{grid-column-start:23}}@media(min-width: 768px){.panel-grid .g-col-md-1{grid-column:auto/span 1}.panel-grid .g-col-md-2{grid-column:auto/span 2}.panel-grid .g-col-md-3{grid-column:auto/span 3}.panel-grid .g-col-md-4{grid-column:auto/span 4}.panel-grid .g-col-md-5{grid-column:auto/span 5}.panel-grid .g-col-md-6{grid-column:auto/span 6}.panel-grid .g-col-md-7{grid-column:auto/span 7}.panel-grid .g-col-md-8{grid-column:auto/span 8}.panel-grid .g-col-md-9{grid-column:auto/span 9}.panel-grid .g-col-md-10{grid-column:auto/span 10}.panel-grid .g-col-md-11{grid-column:auto/span 11}.panel-grid .g-col-md-12{grid-column:auto/span 12}.panel-grid .g-col-md-13{grid-column:auto/span 13}.panel-grid .g-col-md-14{grid-column:auto/span 14}.panel-grid .g-col-md-15{grid-column:auto/span 15}.panel-grid .g-col-md-16{grid-column:auto/span 16}.panel-grid .g-col-md-17{grid-column:auto/span 17}.panel-grid .g-col-md-18{grid-column:auto/span 18}.panel-grid .g-col-md-19{grid-column:auto/span 19}.panel-grid .g-col-md-20{grid-column:auto/span 20}.panel-grid .g-col-md-21{grid-column:auto/span 21}.panel-grid .g-col-md-22{grid-column:auto/span 22}.panel-grid .g-col-md-23{grid-column:auto/span 23}.panel-grid .g-col-md-24{grid-column:auto/span 24}.panel-grid .g-start-md-1{grid-column-start:1}.panel-grid .g-start-md-2{grid-column-start:2}.panel-grid .g-start-md-3{grid-column-start:3}.panel-grid .g-start-md-4{grid-column-start:4}.panel-grid .g-start-md-5{grid-column-start:5}.panel-grid .g-start-md-6{grid-column-start:6}.panel-grid .g-start-md-7{grid-column-start:7}.panel-grid .g-start-md-8{grid-column-start:8}.panel-grid .g-start-md-9{grid-column-start:9}.panel-grid .g-start-md-10{grid-column-start:10}.panel-grid .g-start-md-11{grid-column-start:11}.panel-grid .g-start-md-12{grid-column-start:12}.panel-grid .g-start-md-13{grid-column-start:13}.panel-grid .g-start-md-14{grid-column-start:14}.panel-grid .g-start-md-15{grid-column-start:15}.panel-grid .g-start-md-16{grid-column-start:16}.panel-grid .g-start-md-17{grid-column-start:17}.panel-grid .g-start-md-18{grid-column-start:18}.panel-grid .g-start-md-19{grid-column-start:19}.panel-grid .g-start-md-20{grid-column-start:20}.panel-grid .g-start-md-21{grid-column-start:21}.panel-grid .g-start-md-22{grid-column-start:22}.panel-grid .g-start-md-23{grid-column-start:23}}@media(min-width: 992px){.panel-grid .g-col-lg-1{grid-column:auto/span 1}.panel-grid .g-col-lg-2{grid-column:auto/span 2}.panel-grid .g-col-lg-3{grid-column:auto/span 3}.panel-grid .g-col-lg-4{grid-column:auto/span 4}.panel-grid .g-col-lg-5{grid-column:auto/span 5}.panel-grid .g-col-lg-6{grid-column:auto/span 6}.panel-grid .g-col-lg-7{grid-column:auto/span 7}.panel-grid .g-col-lg-8{grid-column:auto/span 8}.panel-grid .g-col-lg-9{grid-column:auto/span 9}.panel-grid .g-col-lg-10{grid-column:auto/span 10}.panel-grid .g-col-lg-11{grid-column:auto/span 11}.panel-grid .g-col-lg-12{grid-column:auto/span 12}.panel-grid .g-col-lg-13{grid-column:auto/span 13}.panel-grid .g-col-lg-14{grid-column:auto/span 14}.panel-grid .g-col-lg-15{grid-column:auto/span 15}.panel-grid .g-col-lg-16{grid-column:auto/span 16}.panel-grid .g-col-lg-17{grid-column:auto/span 17}.panel-grid .g-col-lg-18{grid-column:auto/span 18}.panel-grid .g-col-lg-19{grid-column:auto/span 19}.panel-grid .g-col-lg-20{grid-column:auto/span 20}.panel-grid .g-col-lg-21{grid-column:auto/span 21}.panel-grid .g-col-lg-22{grid-column:auto/span 22}.panel-grid .g-col-lg-23{grid-column:auto/span 23}.panel-grid .g-col-lg-24{grid-column:auto/span 24}.panel-grid .g-start-lg-1{grid-column-start:1}.panel-grid .g-start-lg-2{grid-column-start:2}.panel-grid .g-start-lg-3{grid-column-start:3}.panel-grid .g-start-lg-4{grid-column-start:4}.panel-grid .g-start-lg-5{grid-column-start:5}.panel-grid .g-start-lg-6{grid-column-start:6}.panel-grid .g-start-lg-7{grid-column-start:7}.panel-grid .g-start-lg-8{grid-column-start:8}.panel-grid .g-start-lg-9{grid-column-start:9}.panel-grid .g-start-lg-10{grid-column-start:10}.panel-grid .g-start-lg-11{grid-column-start:11}.panel-grid .g-start-lg-12{grid-column-start:12}.panel-grid .g-start-lg-13{grid-column-start:13}.panel-grid .g-start-lg-14{grid-column-start:14}.panel-grid .g-start-lg-15{grid-column-start:15}.panel-grid .g-start-lg-16{grid-column-start:16}.panel-grid .g-start-lg-17{grid-column-start:17}.panel-grid .g-start-lg-18{grid-column-start:18}.panel-grid .g-start-lg-19{grid-column-start:19}.panel-grid .g-start-lg-20{grid-column-start:20}.panel-grid .g-start-lg-21{grid-column-start:21}.panel-grid .g-start-lg-22{grid-column-start:22}.panel-grid .g-start-lg-23{grid-column-start:23}}@media(min-width: 1200px){.panel-grid .g-col-xl-1{grid-column:auto/span 1}.panel-grid .g-col-xl-2{grid-column:auto/span 2}.panel-grid .g-col-xl-3{grid-column:auto/span 3}.panel-grid .g-col-xl-4{grid-column:auto/span 4}.panel-grid .g-col-xl-5{grid-column:auto/span 5}.panel-grid .g-col-xl-6{grid-column:auto/span 6}.panel-grid .g-col-xl-7{grid-column:auto/span 7}.panel-grid .g-col-xl-8{grid-column:auto/span 8}.panel-grid .g-col-xl-9{grid-column:auto/span 9}.panel-grid .g-col-xl-10{grid-column:auto/span 10}.panel-grid .g-col-xl-11{grid-column:auto/span 11}.panel-grid .g-col-xl-12{grid-column:auto/span 12}.panel-grid .g-col-xl-13{grid-column:auto/span 13}.panel-grid .g-col-xl-14{grid-column:auto/span 14}.panel-grid .g-col-xl-15{grid-column:auto/span 15}.panel-grid .g-col-xl-16{grid-column:auto/span 16}.panel-grid .g-col-xl-17{grid-column:auto/span 17}.panel-grid .g-col-xl-18{grid-column:auto/span 18}.panel-grid .g-col-xl-19{grid-column:auto/span 19}.panel-grid .g-col-xl-20{grid-column:auto/span 20}.panel-grid .g-col-xl-21{grid-column:auto/span 21}.panel-grid .g-col-xl-22{grid-column:auto/span 22}.panel-grid .g-col-xl-23{grid-column:auto/span 23}.panel-grid .g-col-xl-24{grid-column:auto/span 24}.panel-grid .g-start-xl-1{grid-column-start:1}.panel-grid .g-start-xl-2{grid-column-start:2}.panel-grid .g-start-xl-3{grid-column-start:3}.panel-grid .g-start-xl-4{grid-column-start:4}.panel-grid .g-start-xl-5{grid-column-start:5}.panel-grid .g-start-xl-6{grid-column-start:6}.panel-grid .g-start-xl-7{grid-column-start:7}.panel-grid .g-start-xl-8{grid-column-start:8}.panel-grid .g-start-xl-9{grid-column-start:9}.panel-grid .g-start-xl-10{grid-column-start:10}.panel-grid .g-start-xl-11{grid-column-start:11}.panel-grid .g-start-xl-12{grid-column-start:12}.panel-grid .g-start-xl-13{grid-column-start:13}.panel-grid .g-start-xl-14{grid-column-start:14}.panel-grid .g-start-xl-15{grid-column-start:15}.panel-grid .g-start-xl-16{grid-column-start:16}.panel-grid .g-start-xl-17{grid-column-start:17}.panel-grid .g-start-xl-18{grid-column-start:18}.panel-grid .g-start-xl-19{grid-column-start:19}.panel-grid .g-start-xl-20{grid-column-start:20}.panel-grid .g-start-xl-21{grid-column-start:21}.panel-grid .g-start-xl-22{grid-column-start:22}.panel-grid .g-start-xl-23{grid-column-start:23}}@media(min-width: 1400px){.panel-grid .g-col-xxl-1{grid-column:auto/span 1}.panel-grid .g-col-xxl-2{grid-column:auto/span 2}.panel-grid .g-col-xxl-3{grid-column:auto/span 3}.panel-grid .g-col-xxl-4{grid-column:auto/span 4}.panel-grid .g-col-xxl-5{grid-column:auto/span 5}.panel-grid .g-col-xxl-6{grid-column:auto/span 6}.panel-grid .g-col-xxl-7{grid-column:auto/span 7}.panel-grid .g-col-xxl-8{grid-column:auto/span 8}.panel-grid .g-col-xxl-9{grid-column:auto/span 9}.panel-grid .g-col-xxl-10{grid-column:auto/span 10}.panel-grid .g-col-xxl-11{grid-column:auto/span 11}.panel-grid .g-col-xxl-12{grid-column:auto/span 12}.panel-grid .g-col-xxl-13{grid-column:auto/span 13}.panel-grid .g-col-xxl-14{grid-column:auto/span 14}.panel-grid .g-col-xxl-15{grid-column:auto/span 15}.panel-grid .g-col-xxl-16{grid-column:auto/span 16}.panel-grid .g-col-xxl-17{grid-column:auto/span 17}.panel-grid .g-col-xxl-18{grid-column:auto/span 18}.panel-grid .g-col-xxl-19{grid-column:auto/span 19}.panel-grid .g-col-xxl-20{grid-column:auto/span 20}.panel-grid .g-col-xxl-21{grid-column:auto/span 21}.panel-grid .g-col-xxl-22{grid-column:auto/span 22}.panel-grid .g-col-xxl-23{grid-column:auto/span 23}.panel-grid .g-col-xxl-24{grid-column:auto/span 24}.panel-grid .g-start-xxl-1{grid-column-start:1}.panel-grid .g-start-xxl-2{grid-column-start:2}.panel-grid .g-start-xxl-3{grid-column-start:3}.panel-grid .g-start-xxl-4{grid-column-start:4}.panel-grid .g-start-xxl-5{grid-column-start:5}.panel-grid .g-start-xxl-6{grid-column-start:6}.panel-grid .g-start-xxl-7{grid-column-start:7}.panel-grid .g-start-xxl-8{grid-column-start:8}.panel-grid .g-start-xxl-9{grid-column-start:9}.panel-grid .g-start-xxl-10{grid-column-start:10}.panel-grid .g-start-xxl-11{grid-column-start:11}.panel-grid .g-start-xxl-12{grid-column-start:12}.panel-grid .g-start-xxl-13{grid-column-start:13}.panel-grid .g-start-xxl-14{grid-column-start:14}.panel-grid .g-start-xxl-15{grid-column-start:15}.panel-grid .g-start-xxl-16{grid-column-start:16}.panel-grid .g-start-xxl-17{grid-column-start:17}.panel-grid .g-start-xxl-18{grid-column-start:18}.panel-grid .g-start-xxl-19{grid-column-start:19}.panel-grid .g-start-xxl-20{grid-column-start:20}.panel-grid .g-start-xxl-21{grid-column-start:21}.panel-grid .g-start-xxl-22{grid-column-start:22}.panel-grid .g-start-xxl-23{grid-column-start:23}}main{margin-top:1em;margin-bottom:1em}h1,.h1,h2,.h2{opacity:.9;margin-top:2rem;margin-bottom:1rem;font-weight:600}h1.title,.title.h1{margin-top:0}h2,.h2{border-bottom:1px solid #dee2e6;padding-bottom:.5rem}h3,.h3{font-weight:600}h3,.h3,h4,.h4{opacity:.9;margin-top:1.5rem}h5,.h5,h6,.h6{opacity:.9}.header-section-number{color:#747a7f}.nav-link.active .header-section-number{color:inherit}mark,.mark{padding:0em}.panel-caption,caption,.figure-caption{font-size:.9rem}.panel-caption,.figure-caption,figcaption{color:#747a7f}.table-caption,caption{color:#373a3c}.quarto-layout-cell[data-ref-parent] caption{color:#747a7f}.column-margin figcaption,.margin-caption,div.aside,aside,.column-margin{color:#747a7f;font-size:.825rem}.panel-caption.margin-caption{text-align:inherit}.column-margin.column-container p{margin-bottom:0}.column-margin.column-container>*:not(.collapse){padding-top:.5em;padding-bottom:.5em;display:block}.column-margin.column-container>*.collapse:not(.show){display:none}@media(min-width: 768px){.column-margin.column-container .callout-margin-content:first-child{margin-top:4.5em}.column-margin.column-container .callout-margin-content-simple:first-child{margin-top:3.5em}}.margin-caption>*{padding-top:.5em;padding-bottom:.5em}@media(max-width: 767.98px){.quarto-layout-row{flex-direction:column}}.nav-tabs .nav-item{margin-top:1px;cursor:pointer}.tab-content{margin-top:0px;border-left:#dee2e6 1px solid;border-right:#dee2e6 1px solid;border-bottom:#dee2e6 1px solid;margin-left:0;padding:1em;margin-bottom:1em}@media(max-width: 767.98px){.layout-sidebar{margin-left:0;margin-right:0}}.panel-sidebar,.panel-sidebar .form-control,.panel-input,.panel-input .form-control,.selectize-dropdown{font-size:.9rem}.panel-sidebar .form-control,.panel-input .form-control{padding-top:.1rem}.tab-pane div.sourceCode{margin-top:0px}.tab-pane>p{padding-top:1em}.tab-content>.tab-pane:not(.active){display:none !important}div.sourceCode{background-color:rgba(233,236,239,.65);border:1px solid rgba(233,236,239,.65);border-radius:.25rem}pre.sourceCode{background-color:rgba(0,0,0,0)}pre.sourceCode{border:none;font-size:.875em;overflow:visible !important;padding:.4em}.callout pre.sourceCode{padding-left:0}div.sourceCode{overflow-y:hidden}.callout div.sourceCode{margin-left:initial}.blockquote{font-size:inherit;padding-left:1rem;padding-right:1.5rem;color:#747a7f}.blockquote h1:first-child,.blockquote .h1:first-child,.blockquote h2:first-child,.blockquote .h2:first-child,.blockquote h3:first-child,.blockquote .h3:first-child,.blockquote h4:first-child,.blockquote .h4:first-child,.blockquote h5:first-child,.blockquote .h5:first-child{margin-top:0}pre{background-color:initial;padding:initial;border:initial}p code:not(.sourceCode),li code:not(.sourceCode),td code:not(.sourceCode){background-color:#f7f7f7;padding:.2em}nav p code:not(.sourceCode),nav li code:not(.sourceCode),nav td code:not(.sourceCode){background-color:rgba(0,0,0,0);padding:0}td code:not(.sourceCode){white-space:pre-wrap}#quarto-embedded-source-code-modal>.modal-dialog{max-width:1000px;padding-left:1.75rem;padding-right:1.75rem}#quarto-embedded-source-code-modal>.modal-dialog>.modal-content>.modal-body{padding:0}#quarto-embedded-source-code-modal>.modal-dialog>.modal-content>.modal-body div.sourceCode{margin:0;padding:.2rem .2rem;border-radius:0px;border:none}#quarto-embedded-source-code-modal>.modal-dialog>.modal-content>.modal-header{padding:.7rem}.code-tools-button{font-size:1rem;padding:.15rem .15rem;margin-left:5px;color:#6c757d;background-color:rgba(0,0,0,0);transition:initial;cursor:pointer}.code-tools-button>.bi::before{display:inline-block;height:1rem;width:1rem;content:"";vertical-align:-0.125em;background-image:url('data:image/svg+xml,');background-repeat:no-repeat;background-size:1rem 1rem}.code-tools-button:hover>.bi::before{background-image:url('data:image/svg+xml,')}#quarto-embedded-source-code-modal .code-copy-button>.bi::before{background-image:url('data:image/svg+xml,')}#quarto-embedded-source-code-modal .code-copy-button-checked>.bi::before{background-image:url('data:image/svg+xml,')}.sidebar{will-change:top;transition:top 200ms linear;position:sticky;overflow-y:auto;padding-top:1.2em;max-height:100vh}.sidebar.toc-left,.sidebar.margin-sidebar{top:0px;padding-top:1em}.sidebar.toc-left>*,.sidebar.margin-sidebar>*{padding-top:.5em}.sidebar.quarto-banner-title-block-sidebar>*{padding-top:1.65em}figure .quarto-notebook-link{margin-top:.5em}.quarto-notebook-link{font-size:.75em;color:#6c757d;margin-bottom:1em;text-decoration:none;display:block}.quarto-notebook-link:hover{text-decoration:underline;color:#2780e3}.quarto-notebook-link::before{display:inline-block;height:.75rem;width:.75rem;margin-bottom:0em;margin-right:.25em;content:"";vertical-align:-0.125em;background-image:url('data:image/svg+xml,');background-repeat:no-repeat;background-size:.75rem .75rem}.quarto-alternate-notebooks i.bi,.quarto-alternate-formats i.bi{margin-right:.4em}.quarto-notebook .cell-container{display:flex}.quarto-notebook .cell-container .cell{flex-grow:4}.quarto-notebook .cell-container .cell-decorator{padding-top:1.5em;padding-right:1em;text-align:right}.quarto-notebook h2,.quarto-notebook .h2{border-bottom:none}.sidebar .quarto-alternate-formats a,.sidebar .quarto-alternate-notebooks a{text-decoration:none}.sidebar .quarto-alternate-formats a:hover,.sidebar .quarto-alternate-notebooks a:hover{color:#2780e3}.sidebar .quarto-alternate-notebooks h2,.sidebar .quarto-alternate-notebooks .h2,.sidebar .quarto-alternate-formats h2,.sidebar .quarto-alternate-formats .h2,.sidebar nav[role=doc-toc]>h2,.sidebar nav[role=doc-toc]>.h2{font-size:.875rem;font-weight:400;margin-bottom:.5rem;margin-top:.3rem;font-family:inherit;border-bottom:0;padding-bottom:0;padding-top:0px}.sidebar .quarto-alternate-notebooks h2,.sidebar .quarto-alternate-notebooks .h2,.sidebar .quarto-alternate-formats h2,.sidebar .quarto-alternate-formats .h2{margin-top:1rem}.sidebar nav[role=doc-toc]>ul a{border-left:1px solid #e9ecef;padding-left:.6rem}.sidebar .quarto-alternate-notebooks h2>ul a,.sidebar .quarto-alternate-notebooks .h2>ul a,.sidebar .quarto-alternate-formats h2>ul a,.sidebar .quarto-alternate-formats .h2>ul a{border-left:none;padding-left:.6rem}.sidebar .quarto-alternate-notebooks ul a:empty,.sidebar .quarto-alternate-formats ul a:empty,.sidebar nav[role=doc-toc]>ul a:empty{display:none}.sidebar .quarto-alternate-notebooks ul,.sidebar .quarto-alternate-formats ul,.sidebar nav[role=doc-toc] ul{padding-left:0;list-style:none;font-size:.875rem;font-weight:300}.sidebar .quarto-alternate-notebooks ul li a,.sidebar .quarto-alternate-formats ul li a,.sidebar nav[role=doc-toc]>ul li a{line-height:1.1rem;padding-bottom:.2rem;padding-top:.2rem;color:inherit}.sidebar nav[role=doc-toc] ul>li>ul>li>a{padding-left:1.2em}.sidebar nav[role=doc-toc] ul>li>ul>li>ul>li>a{padding-left:2.4em}.sidebar nav[role=doc-toc] ul>li>ul>li>ul>li>ul>li>a{padding-left:3.6em}.sidebar nav[role=doc-toc] ul>li>ul>li>ul>li>ul>li>ul>li>a{padding-left:4.8em}.sidebar nav[role=doc-toc] ul>li>ul>li>ul>li>ul>li>ul>li>ul>li>a{padding-left:6em}.sidebar nav[role=doc-toc] ul>li>a.active,.sidebar nav[role=doc-toc] ul>li>ul>li>a.active{border-left:1px solid #2780e3;color:#2780e3 !important}.sidebar nav[role=doc-toc] ul>li>a:hover,.sidebar nav[role=doc-toc] ul>li>ul>li>a:hover{color:#2780e3 !important}kbd,.kbd{color:#373a3c;background-color:#f8f9fa;border:1px solid;border-radius:5px;border-color:#dee2e6}div.hanging-indent{margin-left:1em;text-indent:-1em}.citation a,.footnote-ref{text-decoration:none}.footnotes ol{padding-left:1em}.tippy-content>*{margin-bottom:.7em}.tippy-content>*:last-child{margin-bottom:0}.table a{word-break:break-word}.table>thead{border-top-width:1px;border-top-color:#dee2e6;border-bottom:1px solid #b6babc}.callout{margin-top:1.25rem;margin-bottom:1.25rem;border-radius:.25rem;overflow-wrap:break-word}.callout .callout-title-container{overflow-wrap:anywhere}.callout.callout-style-simple{padding:.4em .7em;border-left:5px solid;border-right:1px solid #dee2e6;border-top:1px solid #dee2e6;border-bottom:1px solid #dee2e6}.callout.callout-style-default{border-left:5px solid;border-right:1px solid #dee2e6;border-top:1px solid #dee2e6;border-bottom:1px solid #dee2e6}.callout .callout-body-container{flex-grow:1}.callout.callout-style-simple .callout-body{font-size:.9rem;font-weight:400}.callout.callout-style-default .callout-body{font-size:.9rem;font-weight:400}.callout.callout-titled .callout-body{margin-top:.2em}.callout:not(.no-icon).callout-titled.callout-style-simple .callout-body{padding-left:1.6em}.callout.callout-titled>.callout-header{padding-top:.2em;margin-bottom:-0.2em}.callout.callout-style-simple>div.callout-header{border-bottom:none;font-size:.9rem;font-weight:600;opacity:75%}.callout.callout-style-default>div.callout-header{border-bottom:none;font-weight:600;opacity:85%;font-size:.9rem;padding-left:.5em;padding-right:.5em}.callout.callout-style-default div.callout-body{padding-left:.5em;padding-right:.5em}.callout.callout-style-default div.callout-body>:first-child{margin-top:.5em}.callout>div.callout-header[data-bs-toggle=collapse]{cursor:pointer}.callout.callout-style-default .callout-header[aria-expanded=false],.callout.callout-style-default .callout-header[aria-expanded=true]{padding-top:0px;margin-bottom:0px;align-items:center}.callout.callout-titled .callout-body>:last-child:not(.sourceCode),.callout.callout-titled .callout-body>div>:last-child:not(.sourceCode){margin-bottom:.5rem}.callout:not(.callout-titled) .callout-body>:first-child,.callout:not(.callout-titled) .callout-body>div>:first-child{margin-top:.25rem}.callout:not(.callout-titled) .callout-body>:last-child,.callout:not(.callout-titled) .callout-body>div>:last-child{margin-bottom:.2rem}.callout.callout-style-simple .callout-icon::before,.callout.callout-style-simple .callout-toggle::before{height:1rem;width:1rem;display:inline-block;content:"";background-repeat:no-repeat;background-size:1rem 1rem}.callout.callout-style-default .callout-icon::before,.callout.callout-style-default .callout-toggle::before{height:.9rem;width:.9rem;display:inline-block;content:"";background-repeat:no-repeat;background-size:.9rem .9rem}.callout.callout-style-default .callout-toggle::before{margin-top:5px}.callout .callout-btn-toggle .callout-toggle::before{transition:transform .2s linear}.callout .callout-header[aria-expanded=false] .callout-toggle::before{transform:rotate(-90deg)}.callout .callout-header[aria-expanded=true] .callout-toggle::before{transform:none}.callout.callout-style-simple:not(.no-icon) div.callout-icon-container{padding-top:.2em;padding-right:.55em}.callout.callout-style-default:not(.no-icon) div.callout-icon-container{padding-top:.1em;padding-right:.35em}.callout.callout-style-default:not(.no-icon) div.callout-title-container{margin-top:-1px}.callout.callout-style-default.callout-caution:not(.no-icon) div.callout-icon-container{padding-top:.3em;padding-right:.35em}.callout>.callout-body>.callout-icon-container>.no-icon,.callout>.callout-header>.callout-icon-container>.no-icon{display:none}div.callout.callout{border-left-color:#6c757d}div.callout.callout-style-default>.callout-header{background-color:#6c757d}div.callout-note.callout{border-left-color:#2780e3}div.callout-note.callout-style-default>.callout-header{background-color:#e9f2fc}div.callout-note:not(.callout-titled) .callout-icon::before{background-image:url('data:image/svg+xml,');}div.callout-note.callout-titled .callout-icon::before{background-image:url('data:image/svg+xml,');}div.callout-note .callout-toggle::before{background-image:url('data:image/svg+xml,')}div.callout-tip.callout{border-left-color:#3fb618}div.callout-tip.callout-style-default>.callout-header{background-color:#ecf8e8}div.callout-tip:not(.callout-titled) .callout-icon::before{background-image:url('data:image/svg+xml,');}div.callout-tip.callout-titled .callout-icon::before{background-image:url('data:image/svg+xml,');}div.callout-tip .callout-toggle::before{background-image:url('data:image/svg+xml,')}div.callout-warning.callout{border-left-color:#ff7518}div.callout-warning.callout-style-default>.callout-header{background-color:#fff1e8}div.callout-warning:not(.callout-titled) .callout-icon::before{background-image:url('data:image/svg+xml,');}div.callout-warning.callout-titled .callout-icon::before{background-image:url('data:image/svg+xml,');}div.callout-warning .callout-toggle::before{background-image:url('data:image/svg+xml,')}div.callout-caution.callout{border-left-color:#f0ad4e}div.callout-caution.callout-style-default>.callout-header{background-color:#fef7ed}div.callout-caution:not(.callout-titled) .callout-icon::before{background-image:url('data:image/svg+xml,');}div.callout-caution.callout-titled .callout-icon::before{background-image:url('data:image/svg+xml,');}div.callout-caution .callout-toggle::before{background-image:url('data:image/svg+xml,')}div.callout-important.callout{border-left-color:#ff0039}div.callout-important.callout-style-default>.callout-header{background-color:#ffe6eb}div.callout-important:not(.callout-titled) .callout-icon::before{background-image:url('data:image/svg+xml,');}div.callout-important.callout-titled .callout-icon::before{background-image:url('data:image/svg+xml,');}div.callout-important .callout-toggle::before{background-image:url('data:image/svg+xml,')}.quarto-toggle-container{display:flex;align-items:center}.quarto-reader-toggle .bi::before,.quarto-color-scheme-toggle .bi::before{display:inline-block;height:1rem;width:1rem;content:"";background-repeat:no-repeat;background-size:1rem 1rem}.sidebar-navigation{padding-left:20px}.navbar .quarto-color-scheme-toggle:not(.alternate) .bi::before{background-image:url('data:image/svg+xml,')}.navbar .quarto-color-scheme-toggle.alternate .bi::before{background-image:url('data:image/svg+xml,')}.sidebar-navigation .quarto-color-scheme-toggle:not(.alternate) .bi::before{background-image:url('data:image/svg+xml,')}.sidebar-navigation .quarto-color-scheme-toggle.alternate .bi::before{background-image:url('data:image/svg+xml,')}.quarto-sidebar-toggle{border-color:#dee2e6;border-bottom-left-radius:.25rem;border-bottom-right-radius:.25rem;border-style:solid;border-width:1px;overflow:hidden;border-top-width:0px;padding-top:0px !important}.quarto-sidebar-toggle-title{cursor:pointer;padding-bottom:2px;margin-left:.25em;text-align:center;font-weight:400;font-size:.775em}#quarto-content .quarto-sidebar-toggle{background:#fafafa}#quarto-content .quarto-sidebar-toggle-title{color:#373a3c}.quarto-sidebar-toggle-icon{color:#dee2e6;margin-right:.5em;float:right;transition:transform .2s ease}.quarto-sidebar-toggle-icon::before{padding-top:5px}.quarto-sidebar-toggle.expanded .quarto-sidebar-toggle-icon{transform:rotate(-180deg)}.quarto-sidebar-toggle.expanded .quarto-sidebar-toggle-title{border-bottom:solid #dee2e6 1px}.quarto-sidebar-toggle-contents{background-color:#fff;padding-right:10px;padding-left:10px;margin-top:0px !important;transition:max-height .5s ease}.quarto-sidebar-toggle.expanded .quarto-sidebar-toggle-contents{padding-top:1em;padding-bottom:10px}.quarto-sidebar-toggle:not(.expanded) .quarto-sidebar-toggle-contents{padding-top:0px !important;padding-bottom:0px}nav[role=doc-toc]{z-index:1020}#quarto-sidebar>*,nav[role=doc-toc]>*{transition:opacity .1s ease,border .1s ease}#quarto-sidebar.slow>*,nav[role=doc-toc].slow>*{transition:opacity .4s ease,border .4s ease}.quarto-color-scheme-toggle:not(.alternate).top-right .bi::before{background-image:url('data:image/svg+xml,')}.quarto-color-scheme-toggle.alternate.top-right .bi::before{background-image:url('data:image/svg+xml,')}#quarto-appendix.default{border-top:1px solid #dee2e6}#quarto-appendix.default{background-color:#fff;padding-top:1.5em;margin-top:2em;z-index:998}#quarto-appendix.default .quarto-appendix-heading{margin-top:0;line-height:1.4em;font-weight:600;opacity:.9;border-bottom:none;margin-bottom:0}#quarto-appendix.default .footnotes ol,#quarto-appendix.default .footnotes ol li>p:last-of-type,#quarto-appendix.default .quarto-appendix-contents>p:last-of-type{margin-bottom:0}#quarto-appendix.default .quarto-appendix-secondary-label{margin-bottom:.4em}#quarto-appendix.default .quarto-appendix-bibtex{font-size:.7em;padding:1em;border:solid 1px #dee2e6;margin-bottom:1em}#quarto-appendix.default .quarto-appendix-bibtex code.sourceCode{white-space:pre-wrap}#quarto-appendix.default .quarto-appendix-citeas{font-size:.9em;padding:1em;border:solid 1px #dee2e6;margin-bottom:1em}#quarto-appendix.default .quarto-appendix-heading{font-size:1em !important}#quarto-appendix.default *[role=doc-endnotes]>ol,#quarto-appendix.default .quarto-appendix-contents>*:not(h2):not(.h2){font-size:.9em}#quarto-appendix.default section{padding-bottom:1.5em}#quarto-appendix.default section *[role=doc-endnotes],#quarto-appendix.default section>*:not(a){opacity:.9;word-wrap:break-word}.btn.btn-quarto,div.cell-output-display .btn-quarto{color:#cbcccc;background-color:#373a3c;border-color:#373a3c}.btn.btn-quarto:hover,div.cell-output-display .btn-quarto:hover{color:#cbcccc;background-color:#555859;border-color:#4b4e50}.btn-check:focus+.btn.btn-quarto,.btn.btn-quarto:focus,.btn-check:focus+div.cell-output-display .btn-quarto,div.cell-output-display .btn-quarto:focus{color:#cbcccc;background-color:#555859;border-color:#4b4e50;box-shadow:0 0 0 .25rem rgba(77,80,82,.5)}.btn-check:checked+.btn.btn-quarto,.btn-check:active+.btn.btn-quarto,.btn.btn-quarto:active,.btn.btn-quarto.active,.show>.btn.btn-quarto.dropdown-toggle,.btn-check:checked+div.cell-output-display .btn-quarto,.btn-check:active+div.cell-output-display .btn-quarto,div.cell-output-display .btn-quarto:active,div.cell-output-display .btn-quarto.active,.show>div.cell-output-display .btn-quarto.dropdown-toggle{color:#fff;background-color:#5f6163;border-color:#4b4e50}.btn-check:checked+.btn.btn-quarto:focus,.btn-check:active+.btn.btn-quarto:focus,.btn.btn-quarto:active:focus,.btn.btn-quarto.active:focus,.show>.btn.btn-quarto.dropdown-toggle:focus,.btn-check:checked+div.cell-output-display .btn-quarto:focus,.btn-check:active+div.cell-output-display .btn-quarto:focus,div.cell-output-display .btn-quarto:active:focus,div.cell-output-display .btn-quarto.active:focus,.show>div.cell-output-display .btn-quarto.dropdown-toggle:focus{box-shadow:0 0 0 .25rem rgba(77,80,82,.5)}.btn.btn-quarto:disabled,.btn.btn-quarto.disabled,div.cell-output-display .btn-quarto:disabled,div.cell-output-display .btn-quarto.disabled{color:#fff;background-color:#373a3c;border-color:#373a3c}nav.quarto-secondary-nav.color-navbar{background-color:#2780e3;color:#fdfeff}nav.quarto-secondary-nav.color-navbar h1,nav.quarto-secondary-nav.color-navbar .h1,nav.quarto-secondary-nav.color-navbar .quarto-btn-toggle{color:#fdfeff}@media(max-width: 991.98px){body.nav-sidebar .quarto-title-banner{margin-bottom:0;padding-bottom:0}body.nav-sidebar #title-block-header{margin-block-end:0}}p.subtitle{margin-top:.25em;margin-bottom:.5em}code a:any-link{color:inherit;text-decoration-color:#6c757d}/*! light */div.observablehq table thead tr th{background-color:var(--bs-body-bg)}input,button,select,optgroup,textarea{background-color:var(--bs-body-bg)}.code-annotated .code-copy-button{margin-right:1.25em;margin-top:0;padding-bottom:0;padding-top:3px}.code-annotation-gutter-bg{background-color:#fff}.code-annotation-gutter{background-color:rgba(233,236,239,.65)}.code-annotation-gutter,.code-annotation-gutter-bg{height:100%;width:calc(20px + .5em);position:absolute;top:0;right:0}dl.code-annotation-container-grid dt{margin-right:1em;margin-top:.25rem}dl.code-annotation-container-grid dt{font-family:var(--bs-font-monospace);color:#4f5457;border:solid #4f5457 1px;border-radius:50%;height:22px;width:22px;line-height:22px;font-size:11px;text-align:center;vertical-align:middle;text-decoration:none}dl.code-annotation-container-grid dt[data-target-cell]{cursor:pointer}dl.code-annotation-container-grid dt[data-target-cell].code-annotation-active{color:#fff;border:solid #aaa 1px;background-color:#aaa}pre.code-annotation-code{padding-top:0;padding-bottom:0}pre.code-annotation-code code{z-index:3}#code-annotation-line-highlight-gutter{width:100%;border-top:solid rgba(170,170,170,.2666666667) 1px;border-bottom:solid rgba(170,170,170,.2666666667) 1px;z-index:2;background-color:rgba(170,170,170,.1333333333)}#code-annotation-line-highlight{margin-left:-4em;width:calc(100% + 4em);border-top:solid rgba(170,170,170,.2666666667) 1px;border-bottom:solid rgba(170,170,170,.2666666667) 1px;z-index:2;background-color:rgba(170,170,170,.1333333333)}code.sourceCode .code-annotation-anchor.code-annotation-active{background-color:var(--quarto-hl-normal-color, #aaaaaa);border:solid var(--quarto-hl-normal-color, #aaaaaa) 1px;color:#e9ecef;font-weight:bolder}code.sourceCode .code-annotation-anchor{font-family:var(--bs-font-monospace);color:var(--quarto-hl-co-color);border:solid var(--quarto-hl-co-color) 1px;border-radius:50%;height:18px;width:18px;font-size:9px;margin-top:2px}code.sourceCode button.code-annotation-anchor{padding:2px}code.sourceCode a.code-annotation-anchor{line-height:18px;text-align:center;vertical-align:middle;cursor:default;text-decoration:none}@media print{.page-columns .column-screen-inset{grid-column:page-start-inset/page-end-inset;z-index:998;transform:translate3d(0, 0, 0)}.page-columns .column-screen-inset table{background:#fff}.page-columns .column-screen-inset-left{grid-column:page-start-inset/body-content-end;z-index:998;transform:translate3d(0, 0, 0)}.page-columns .column-screen-inset-left table{background:#fff}.page-columns .column-screen-inset-right{grid-column:body-content-start/page-end-inset;z-index:998;transform:translate3d(0, 0, 0)}.page-columns .column-screen-inset-right table{background:#fff}.page-columns .column-screen{grid-column:page-start/page-end;z-index:998;transform:translate3d(0, 0, 0)}.page-columns .column-screen table{background:#fff}.page-columns .column-screen-left{grid-column:page-start/body-content-end;z-index:998;transform:translate3d(0, 0, 0)}.page-columns .column-screen-left table{background:#fff}.page-columns .column-screen-right{grid-column:body-content-start/page-end;z-index:998;transform:translate3d(0, 0, 0)}.page-columns .column-screen-right table{background:#fff}.page-columns .column-screen-inset-shaded{grid-column:page-start-inset/page-end-inset;padding:1em;background:#f8f9fa;z-index:998;transform:translate3d(0, 0, 0);margin-bottom:1em}}.quarto-video{margin-bottom:1em}.table>thead{border-top-width:0}.table>:not(caption)>*:not(:last-child)>*{border-bottom-color:#ebeced;border-bottom-style:solid;border-bottom-width:1px}.table>:not(:first-child){border-top:1px solid #b6babc;border-bottom:1px solid inherit}.table tbody{border-bottom-color:#b6babc}a.external:after{display:inline-block;height:.75rem;width:.75rem;margin-bottom:.15em;margin-left:.25em;content:"";vertical-align:-0.125em;background-image:url('data:image/svg+xml,');background-repeat:no-repeat;background-size:.75rem .75rem}div.sourceCode code a.external:after{content:none}a.external:after:hover{cursor:pointer}.quarto-ext-icon{display:inline-block;font-size:.75em;padding-left:.3em}.code-with-filename .code-with-filename-file{margin-bottom:0;padding-bottom:2px;padding-top:2px;padding-left:.7em;border:var(--quarto-border-width) solid var(--quarto-border-color);border-radius:var(--quarto-border-radius);border-bottom:0;border-bottom-left-radius:0%;border-bottom-right-radius:0%}.code-with-filename div.sourceCode,.reveal .code-with-filename div.sourceCode{margin-top:0;border-top-left-radius:0%;border-top-right-radius:0%}.code-with-filename .code-with-filename-file pre{margin-bottom:0}.code-with-filename .code-with-filename-file,.code-with-filename .code-with-filename-file pre{background-color:rgba(219,219,219,.8)}.quarto-dark .code-with-filename .code-with-filename-file,.quarto-dark .code-with-filename .code-with-filename-file pre{background-color:#555}.code-with-filename .code-with-filename-file strong{font-weight:400}.quarto-title-banner{margin-bottom:1em;color:#fdfeff;background:#2780e3}.quarto-title-banner .code-tools-button{color:#97cbff}.quarto-title-banner .code-tools-button:hover{color:#fdfeff}.quarto-title-banner .code-tools-button>.bi::before{background-image:url('data:image/svg+xml,')}.quarto-title-banner .code-tools-button:hover>.bi::before{background-image:url('data:image/svg+xml,')}.quarto-title-banner .quarto-title .title{font-weight:600}.quarto-title-banner .quarto-categories{margin-top:.75em}@media(min-width: 992px){.quarto-title-banner{padding-top:2.5em;padding-bottom:2.5em}}@media(max-width: 991.98px){.quarto-title-banner{padding-top:1em;padding-bottom:1em}}main.quarto-banner-title-block>section:first-child>h2,main.quarto-banner-title-block>section:first-child>.h2,main.quarto-banner-title-block>section:first-child>h3,main.quarto-banner-title-block>section:first-child>.h3,main.quarto-banner-title-block>section:first-child>h4,main.quarto-banner-title-block>section:first-child>.h4{margin-top:0}.quarto-title .quarto-categories{display:flex;flex-wrap:wrap;row-gap:.5em;column-gap:.4em;padding-bottom:.5em;margin-top:.75em}.quarto-title .quarto-categories .quarto-category{padding:.25em .75em;font-size:.65em;text-transform:uppercase;border:solid 1px;border-radius:.25rem;opacity:.6}.quarto-title .quarto-categories .quarto-category a{color:inherit}#title-block-header.quarto-title-block.default .quarto-title-meta{display:grid;grid-template-columns:repeat(2, 1fr)}#title-block-header.quarto-title-block.default .quarto-title .title{margin-bottom:0}#title-block-header.quarto-title-block.default .quarto-title-author-orcid img{margin-top:-5px}#title-block-header.quarto-title-block.default .quarto-description p:last-of-type{margin-bottom:0}#title-block-header.quarto-title-block.default .quarto-title-meta-contents p,#title-block-header.quarto-title-block.default .quarto-title-authors p,#title-block-header.quarto-title-block.default .quarto-title-affiliations p{margin-bottom:.1em}#title-block-header.quarto-title-block.default .quarto-title-meta-heading{text-transform:uppercase;margin-top:1em;font-size:.8em;opacity:.8;font-weight:400}#title-block-header.quarto-title-block.default .quarto-title-meta-contents{font-size:.9em}#title-block-header.quarto-title-block.default .quarto-title-meta-contents a{color:#373a3c}#title-block-header.quarto-title-block.default .quarto-title-meta-contents p.affiliation:last-of-type{margin-bottom:.7em}#title-block-header.quarto-title-block.default p.affiliation{margin-bottom:.1em}#title-block-header.quarto-title-block.default .description,#title-block-header.quarto-title-block.default .abstract{margin-top:0}#title-block-header.quarto-title-block.default .description>p,#title-block-header.quarto-title-block.default .abstract>p{font-size:.9em}#title-block-header.quarto-title-block.default .description>p:last-of-type,#title-block-header.quarto-title-block.default .abstract>p:last-of-type{margin-bottom:0}#title-block-header.quarto-title-block.default .description .abstract-title,#title-block-header.quarto-title-block.default .abstract .abstract-title{margin-top:1em;text-transform:uppercase;font-size:.8em;opacity:.8;font-weight:400}#title-block-header.quarto-title-block.default .quarto-title-meta-author{display:grid;grid-template-columns:1fr 1fr}.quarto-title-tools-only{display:flex;justify-content:right}body{-webkit-font-smoothing:antialiased}.badge.bg-light{color:#373a3c}.progress .progress-bar{font-size:8px;line-height:8px}/*# sourceMappingURL=9161419e6f82ea4435380a70856fa72b.css.map */
diff --git a/site_libs/bootstrap/bootstrap.min.js b/site_libs/bootstrap/bootstrap.min.js
new file mode 100644
index 00000000..cc0a2556
--- /dev/null
+++ b/site_libs/bootstrap/bootstrap.min.js
@@ -0,0 +1,7 @@
+/*!
+ * Bootstrap v5.1.3 (https://getbootstrap.com/)
+ * Copyright 2011-2021 The Bootstrap Authors (https://github.com/twbs/bootstrap/graphs/contributors)
+ * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)
+ */
+!function(t,e){"object"==typeof exports&&"undefined"!=typeof module?module.exports=e():"function"==typeof define&&define.amd?define(e):(t="undefined"!=typeof globalThis?globalThis:t||self).bootstrap=e()}(this,(function(){"use strict";const t="transitionend",e=t=>{let e=t.getAttribute("data-bs-target");if(!e||"#"===e){let i=t.getAttribute("href");if(!i||!i.includes("#")&&!i.startsWith("."))return null;i.includes("#")&&!i.startsWith("#")&&(i=`#${i.split("#")[1]}`),e=i&&"#"!==i?i.trim():null}return e},i=t=>{const i=e(t);return i&&document.querySelector(i)?i:null},n=t=>{const i=e(t);return i?document.querySelector(i):null},s=e=>{e.dispatchEvent(new Event(t))},o=t=>!(!t||"object"!=typeof t)&&(void 0!==t.jquery&&(t=t[0]),void 0!==t.nodeType),r=t=>o(t)?t.jquery?t[0]:t:"string"==typeof t&&t.length>0?document.querySelector(t):null,a=(t,e,i)=>{Object.keys(i).forEach((n=>{const s=i[n],r=e[n],a=r&&o(r)?"element":null==(l=r)?`${l}`:{}.toString.call(l).match(/\s([a-z]+)/i)[1].toLowerCase();var l;if(!new RegExp(s).test(a))throw new TypeError(`${t.toUpperCase()}: Option "${n}" provided type "${a}" but expected type "${s}".`)}))},l=t=>!(!o(t)||0===t.getClientRects().length)&&"visible"===getComputedStyle(t).getPropertyValue("visibility"),c=t=>!t||t.nodeType!==Node.ELEMENT_NODE||!!t.classList.contains("disabled")||(void 0!==t.disabled?t.disabled:t.hasAttribute("disabled")&&"false"!==t.getAttribute("disabled")),h=t=>{if(!document.documentElement.attachShadow)return null;if("function"==typeof t.getRootNode){const e=t.getRootNode();return e instanceof ShadowRoot?e:null}return t instanceof ShadowRoot?t:t.parentNode?h(t.parentNode):null},d=()=>{},u=t=>{t.offsetHeight},f=()=>{const{jQuery:t}=window;return t&&!document.body.hasAttribute("data-bs-no-jquery")?t:null},p=[],m=()=>"rtl"===document.documentElement.dir,g=t=>{var e;e=()=>{const e=f();if(e){const i=t.NAME,n=e.fn[i];e.fn[i]=t.jQueryInterface,e.fn[i].Constructor=t,e.fn[i].noConflict=()=>(e.fn[i]=n,t.jQueryInterface)}},"loading"===document.readyState?(p.length||document.addEventListener("DOMContentLoaded",(()=>{p.forEach((t=>t()))})),p.push(e)):e()},_=t=>{"function"==typeof t&&t()},b=(e,i,n=!0)=>{if(!n)return void _(e);const o=(t=>{if(!t)return 0;let{transitionDuration:e,transitionDelay:i}=window.getComputedStyle(t);const n=Number.parseFloat(e),s=Number.parseFloat(i);return n||s?(e=e.split(",")[0],i=i.split(",")[0],1e3*(Number.parseFloat(e)+Number.parseFloat(i))):0})(i)+5;let r=!1;const a=({target:n})=>{n===i&&(r=!0,i.removeEventListener(t,a),_(e))};i.addEventListener(t,a),setTimeout((()=>{r||s(i)}),o)},v=(t,e,i,n)=>{let s=t.indexOf(e);if(-1===s)return t[!i&&n?t.length-1:0];const o=t.length;return s+=i?1:-1,n&&(s=(s+o)%o),t[Math.max(0,Math.min(s,o-1))]},y=/[^.]*(?=\..*)\.|.*/,w=/\..*/,E=/::\d+$/,A={};let T=1;const O={mouseenter:"mouseover",mouseleave:"mouseout"},C=/^(mouseenter|mouseleave)/i,k=new Set(["click","dblclick","mouseup","mousedown","contextmenu","mousewheel","DOMMouseScroll","mouseover","mouseout","mousemove","selectstart","selectend","keydown","keypress","keyup","orientationchange","touchstart","touchmove","touchend","touchcancel","pointerdown","pointermove","pointerup","pointerleave","pointercancel","gesturestart","gesturechange","gestureend","focus","blur","change","reset","select","submit","focusin","focusout","load","unload","beforeunload","resize","move","DOMContentLoaded","readystatechange","error","abort","scroll"]);function L(t,e){return e&&`${e}::${T++}`||t.uidEvent||T++}function x(t){const e=L(t);return t.uidEvent=e,A[e]=A[e]||{},A[e]}function D(t,e,i=null){const n=Object.keys(t);for(let s=0,o=n.length;sfunction(e){if(!e.relatedTarget||e.relatedTarget!==e.delegateTarget&&!e.delegateTarget.contains(e.relatedTarget))return t.call(this,e)};n?n=t(n):i=t(i)}const[o,r,a]=S(e,i,n),l=x(t),c=l[a]||(l[a]={}),h=D(c,r,o?i:null);if(h)return void(h.oneOff=h.oneOff&&s);const d=L(r,e.replace(y,"")),u=o?function(t,e,i){return function n(s){const o=t.querySelectorAll(e);for(let{target:r}=s;r&&r!==this;r=r.parentNode)for(let a=o.length;a--;)if(o[a]===r)return s.delegateTarget=r,n.oneOff&&j.off(t,s.type,e,i),i.apply(r,[s]);return null}}(t,i,n):function(t,e){return function i(n){return n.delegateTarget=t,i.oneOff&&j.off(t,n.type,e),e.apply(t,[n])}}(t,i);u.delegationSelector=o?i:null,u.originalHandler=r,u.oneOff=s,u.uidEvent=d,c[d]=u,t.addEventListener(a,u,o)}function I(t,e,i,n,s){const o=D(e[i],n,s);o&&(t.removeEventListener(i,o,Boolean(s)),delete e[i][o.uidEvent])}function P(t){return t=t.replace(w,""),O[t]||t}const j={on(t,e,i,n){N(t,e,i,n,!1)},one(t,e,i,n){N(t,e,i,n,!0)},off(t,e,i,n){if("string"!=typeof e||!t)return;const[s,o,r]=S(e,i,n),a=r!==e,l=x(t),c=e.startsWith(".");if(void 0!==o){if(!l||!l[r])return;return void I(t,l,r,o,s?i:null)}c&&Object.keys(l).forEach((i=>{!function(t,e,i,n){const s=e[i]||{};Object.keys(s).forEach((o=>{if(o.includes(n)){const n=s[o];I(t,e,i,n.originalHandler,n.delegationSelector)}}))}(t,l,i,e.slice(1))}));const h=l[r]||{};Object.keys(h).forEach((i=>{const n=i.replace(E,"");if(!a||e.includes(n)){const e=h[i];I(t,l,r,e.originalHandler,e.delegationSelector)}}))},trigger(t,e,i){if("string"!=typeof e||!t)return null;const n=f(),s=P(e),o=e!==s,r=k.has(s);let a,l=!0,c=!0,h=!1,d=null;return o&&n&&(a=n.Event(e,i),n(t).trigger(a),l=!a.isPropagationStopped(),c=!a.isImmediatePropagationStopped(),h=a.isDefaultPrevented()),r?(d=document.createEvent("HTMLEvents"),d.initEvent(s,l,!0)):d=new CustomEvent(e,{bubbles:l,cancelable:!0}),void 0!==i&&Object.keys(i).forEach((t=>{Object.defineProperty(d,t,{get:()=>i[t]})})),h&&d.preventDefault(),c&&t.dispatchEvent(d),d.defaultPrevented&&void 0!==a&&a.preventDefault(),d}},M=new Map,H={set(t,e,i){M.has(t)||M.set(t,new Map);const n=M.get(t);n.has(e)||0===n.size?n.set(e,i):console.error(`Bootstrap doesn't allow more than one instance per element. Bound instance: ${Array.from(n.keys())[0]}.`)},get:(t,e)=>M.has(t)&&M.get(t).get(e)||null,remove(t,e){if(!M.has(t))return;const i=M.get(t);i.delete(e),0===i.size&&M.delete(t)}};class B{constructor(t){(t=r(t))&&(this._element=t,H.set(this._element,this.constructor.DATA_KEY,this))}dispose(){H.remove(this._element,this.constructor.DATA_KEY),j.off(this._element,this.constructor.EVENT_KEY),Object.getOwnPropertyNames(this).forEach((t=>{this[t]=null}))}_queueCallback(t,e,i=!0){b(t,e,i)}static getInstance(t){return H.get(r(t),this.DATA_KEY)}static getOrCreateInstance(t,e={}){return this.getInstance(t)||new this(t,"object"==typeof e?e:null)}static get VERSION(){return"5.1.3"}static get NAME(){throw new Error('You have to implement the static method "NAME", for each component!')}static get DATA_KEY(){return`bs.${this.NAME}`}static get EVENT_KEY(){return`.${this.DATA_KEY}`}}const R=(t,e="hide")=>{const i=`click.dismiss${t.EVENT_KEY}`,s=t.NAME;j.on(document,i,`[data-bs-dismiss="${s}"]`,(function(i){if(["A","AREA"].includes(this.tagName)&&i.preventDefault(),c(this))return;const o=n(this)||this.closest(`.${s}`);t.getOrCreateInstance(o)[e]()}))};class W extends B{static get NAME(){return"alert"}close(){if(j.trigger(this._element,"close.bs.alert").defaultPrevented)return;this._element.classList.remove("show");const t=this._element.classList.contains("fade");this._queueCallback((()=>this._destroyElement()),this._element,t)}_destroyElement(){this._element.remove(),j.trigger(this._element,"closed.bs.alert"),this.dispose()}static jQueryInterface(t){return this.each((function(){const e=W.getOrCreateInstance(this);if("string"==typeof t){if(void 0===e[t]||t.startsWith("_")||"constructor"===t)throw new TypeError(`No method named "${t}"`);e[t](this)}}))}}R(W,"close"),g(W);const $='[data-bs-toggle="button"]';class z extends B{static get NAME(){return"button"}toggle(){this._element.setAttribute("aria-pressed",this._element.classList.toggle("active"))}static jQueryInterface(t){return this.each((function(){const e=z.getOrCreateInstance(this);"toggle"===t&&e[t]()}))}}function q(t){return"true"===t||"false"!==t&&(t===Number(t).toString()?Number(t):""===t||"null"===t?null:t)}function F(t){return t.replace(/[A-Z]/g,(t=>`-${t.toLowerCase()}`))}j.on(document,"click.bs.button.data-api",$,(t=>{t.preventDefault();const e=t.target.closest($);z.getOrCreateInstance(e).toggle()})),g(z);const U={setDataAttribute(t,e,i){t.setAttribute(`data-bs-${F(e)}`,i)},removeDataAttribute(t,e){t.removeAttribute(`data-bs-${F(e)}`)},getDataAttributes(t){if(!t)return{};const e={};return Object.keys(t.dataset).filter((t=>t.startsWith("bs"))).forEach((i=>{let n=i.replace(/^bs/,"");n=n.charAt(0).toLowerCase()+n.slice(1,n.length),e[n]=q(t.dataset[i])})),e},getDataAttribute:(t,e)=>q(t.getAttribute(`data-bs-${F(e)}`)),offset(t){const e=t.getBoundingClientRect();return{top:e.top+window.pageYOffset,left:e.left+window.pageXOffset}},position:t=>({top:t.offsetTop,left:t.offsetLeft})},V={find:(t,e=document.documentElement)=>[].concat(...Element.prototype.querySelectorAll.call(e,t)),findOne:(t,e=document.documentElement)=>Element.prototype.querySelector.call(e,t),children:(t,e)=>[].concat(...t.children).filter((t=>t.matches(e))),parents(t,e){const i=[];let n=t.parentNode;for(;n&&n.nodeType===Node.ELEMENT_NODE&&3!==n.nodeType;)n.matches(e)&&i.push(n),n=n.parentNode;return i},prev(t,e){let i=t.previousElementSibling;for(;i;){if(i.matches(e))return[i];i=i.previousElementSibling}return[]},next(t,e){let i=t.nextElementSibling;for(;i;){if(i.matches(e))return[i];i=i.nextElementSibling}return[]},focusableChildren(t){const e=["a","button","input","textarea","select","details","[tabindex]",'[contenteditable="true"]'].map((t=>`${t}:not([tabindex^="-"])`)).join(", ");return this.find(e,t).filter((t=>!c(t)&&l(t)))}},K="carousel",X={interval:5e3,keyboard:!0,slide:!1,pause:"hover",wrap:!0,touch:!0},Y={interval:"(number|boolean)",keyboard:"boolean",slide:"(boolean|string)",pause:"(string|boolean)",wrap:"boolean",touch:"boolean"},Q="next",G="prev",Z="left",J="right",tt={ArrowLeft:J,ArrowRight:Z},et="slid.bs.carousel",it="active",nt=".active.carousel-item";class st extends B{constructor(t,e){super(t),this._items=null,this._interval=null,this._activeElement=null,this._isPaused=!1,this._isSliding=!1,this.touchTimeout=null,this.touchStartX=0,this.touchDeltaX=0,this._config=this._getConfig(e),this._indicatorsElement=V.findOne(".carousel-indicators",this._element),this._touchSupported="ontouchstart"in document.documentElement||navigator.maxTouchPoints>0,this._pointerEvent=Boolean(window.PointerEvent),this._addEventListeners()}static get Default(){return X}static get NAME(){return K}next(){this._slide(Q)}nextWhenVisible(){!document.hidden&&l(this._element)&&this.next()}prev(){this._slide(G)}pause(t){t||(this._isPaused=!0),V.findOne(".carousel-item-next, .carousel-item-prev",this._element)&&(s(this._element),this.cycle(!0)),clearInterval(this._interval),this._interval=null}cycle(t){t||(this._isPaused=!1),this._interval&&(clearInterval(this._interval),this._interval=null),this._config&&this._config.interval&&!this._isPaused&&(this._updateInterval(),this._interval=setInterval((document.visibilityState?this.nextWhenVisible:this.next).bind(this),this._config.interval))}to(t){this._activeElement=V.findOne(nt,this._element);const e=this._getItemIndex(this._activeElement);if(t>this._items.length-1||t<0)return;if(this._isSliding)return void j.one(this._element,et,(()=>this.to(t)));if(e===t)return this.pause(),void this.cycle();const i=t>e?Q:G;this._slide(i,this._items[t])}_getConfig(t){return t={...X,...U.getDataAttributes(this._element),..."object"==typeof t?t:{}},a(K,t,Y),t}_handleSwipe(){const t=Math.abs(this.touchDeltaX);if(t<=40)return;const e=t/this.touchDeltaX;this.touchDeltaX=0,e&&this._slide(e>0?J:Z)}_addEventListeners(){this._config.keyboard&&j.on(this._element,"keydown.bs.carousel",(t=>this._keydown(t))),"hover"===this._config.pause&&(j.on(this._element,"mouseenter.bs.carousel",(t=>this.pause(t))),j.on(this._element,"mouseleave.bs.carousel",(t=>this.cycle(t)))),this._config.touch&&this._touchSupported&&this._addTouchEventListeners()}_addTouchEventListeners(){const t=t=>this._pointerEvent&&("pen"===t.pointerType||"touch"===t.pointerType),e=e=>{t(e)?this.touchStartX=e.clientX:this._pointerEvent||(this.touchStartX=e.touches[0].clientX)},i=t=>{this.touchDeltaX=t.touches&&t.touches.length>1?0:t.touches[0].clientX-this.touchStartX},n=e=>{t(e)&&(this.touchDeltaX=e.clientX-this.touchStartX),this._handleSwipe(),"hover"===this._config.pause&&(this.pause(),this.touchTimeout&&clearTimeout(this.touchTimeout),this.touchTimeout=setTimeout((t=>this.cycle(t)),500+this._config.interval))};V.find(".carousel-item img",this._element).forEach((t=>{j.on(t,"dragstart.bs.carousel",(t=>t.preventDefault()))})),this._pointerEvent?(j.on(this._element,"pointerdown.bs.carousel",(t=>e(t))),j.on(this._element,"pointerup.bs.carousel",(t=>n(t))),this._element.classList.add("pointer-event")):(j.on(this._element,"touchstart.bs.carousel",(t=>e(t))),j.on(this._element,"touchmove.bs.carousel",(t=>i(t))),j.on(this._element,"touchend.bs.carousel",(t=>n(t))))}_keydown(t){if(/input|textarea/i.test(t.target.tagName))return;const e=tt[t.key];e&&(t.preventDefault(),this._slide(e))}_getItemIndex(t){return this._items=t&&t.parentNode?V.find(".carousel-item",t.parentNode):[],this._items.indexOf(t)}_getItemByOrder(t,e){const i=t===Q;return v(this._items,e,i,this._config.wrap)}_triggerSlideEvent(t,e){const i=this._getItemIndex(t),n=this._getItemIndex(V.findOne(nt,this._element));return j.trigger(this._element,"slide.bs.carousel",{relatedTarget:t,direction:e,from:n,to:i})}_setActiveIndicatorElement(t){if(this._indicatorsElement){const e=V.findOne(".active",this._indicatorsElement);e.classList.remove(it),e.removeAttribute("aria-current");const i=V.find("[data-bs-target]",this._indicatorsElement);for(let e=0;e{j.trigger(this._element,et,{relatedTarget:o,direction:d,from:s,to:r})};if(this._element.classList.contains("slide")){o.classList.add(h),u(o),n.classList.add(c),o.classList.add(c);const t=()=>{o.classList.remove(c,h),o.classList.add(it),n.classList.remove(it,h,c),this._isSliding=!1,setTimeout(f,0)};this._queueCallback(t,n,!0)}else n.classList.remove(it),o.classList.add(it),this._isSliding=!1,f();a&&this.cycle()}_directionToOrder(t){return[J,Z].includes(t)?m()?t===Z?G:Q:t===Z?Q:G:t}_orderToDirection(t){return[Q,G].includes(t)?m()?t===G?Z:J:t===G?J:Z:t}static carouselInterface(t,e){const i=st.getOrCreateInstance(t,e);let{_config:n}=i;"object"==typeof e&&(n={...n,...e});const s="string"==typeof e?e:n.slide;if("number"==typeof e)i.to(e);else if("string"==typeof s){if(void 0===i[s])throw new TypeError(`No method named "${s}"`);i[s]()}else n.interval&&n.ride&&(i.pause(),i.cycle())}static jQueryInterface(t){return this.each((function(){st.carouselInterface(this,t)}))}static dataApiClickHandler(t){const e=n(this);if(!e||!e.classList.contains("carousel"))return;const i={...U.getDataAttributes(e),...U.getDataAttributes(this)},s=this.getAttribute("data-bs-slide-to");s&&(i.interval=!1),st.carouselInterface(e,i),s&&st.getInstance(e).to(s),t.preventDefault()}}j.on(document,"click.bs.carousel.data-api","[data-bs-slide], [data-bs-slide-to]",st.dataApiClickHandler),j.on(window,"load.bs.carousel.data-api",(()=>{const t=V.find('[data-bs-ride="carousel"]');for(let e=0,i=t.length;et===this._element));null!==s&&o.length&&(this._selector=s,this._triggerArray.push(e))}this._initializeChildren(),this._config.parent||this._addAriaAndCollapsedClass(this._triggerArray,this._isShown()),this._config.toggle&&this.toggle()}static get Default(){return rt}static get NAME(){return ot}toggle(){this._isShown()?this.hide():this.show()}show(){if(this._isTransitioning||this._isShown())return;let t,e=[];if(this._config.parent){const t=V.find(ut,this._config.parent);e=V.find(".collapse.show, .collapse.collapsing",this._config.parent).filter((e=>!t.includes(e)))}const i=V.findOne(this._selector);if(e.length){const n=e.find((t=>i!==t));if(t=n?pt.getInstance(n):null,t&&t._isTransitioning)return}if(j.trigger(this._element,"show.bs.collapse").defaultPrevented)return;e.forEach((e=>{i!==e&&pt.getOrCreateInstance(e,{toggle:!1}).hide(),t||H.set(e,"bs.collapse",null)}));const n=this._getDimension();this._element.classList.remove(ct),this._element.classList.add(ht),this._element.style[n]=0,this._addAriaAndCollapsedClass(this._triggerArray,!0),this._isTransitioning=!0;const s=`scroll${n[0].toUpperCase()+n.slice(1)}`;this._queueCallback((()=>{this._isTransitioning=!1,this._element.classList.remove(ht),this._element.classList.add(ct,lt),this._element.style[n]="",j.trigger(this._element,"shown.bs.collapse")}),this._element,!0),this._element.style[n]=`${this._element[s]}px`}hide(){if(this._isTransitioning||!this._isShown())return;if(j.trigger(this._element,"hide.bs.collapse").defaultPrevented)return;const t=this._getDimension();this._element.style[t]=`${this._element.getBoundingClientRect()[t]}px`,u(this._element),this._element.classList.add(ht),this._element.classList.remove(ct,lt);const e=this._triggerArray.length;for(let t=0;t{this._isTransitioning=!1,this._element.classList.remove(ht),this._element.classList.add(ct),j.trigger(this._element,"hidden.bs.collapse")}),this._element,!0)}_isShown(t=this._element){return t.classList.contains(lt)}_getConfig(t){return(t={...rt,...U.getDataAttributes(this._element),...t}).toggle=Boolean(t.toggle),t.parent=r(t.parent),a(ot,t,at),t}_getDimension(){return this._element.classList.contains("collapse-horizontal")?"width":"height"}_initializeChildren(){if(!this._config.parent)return;const t=V.find(ut,this._config.parent);V.find(ft,this._config.parent).filter((e=>!t.includes(e))).forEach((t=>{const e=n(t);e&&this._addAriaAndCollapsedClass([t],this._isShown(e))}))}_addAriaAndCollapsedClass(t,e){t.length&&t.forEach((t=>{e?t.classList.remove(dt):t.classList.add(dt),t.setAttribute("aria-expanded",e)}))}static jQueryInterface(t){return this.each((function(){const e={};"string"==typeof t&&/show|hide/.test(t)&&(e.toggle=!1);const i=pt.getOrCreateInstance(this,e);if("string"==typeof t){if(void 0===i[t])throw new TypeError(`No method named "${t}"`);i[t]()}}))}}j.on(document,"click.bs.collapse.data-api",ft,(function(t){("A"===t.target.tagName||t.delegateTarget&&"A"===t.delegateTarget.tagName)&&t.preventDefault();const e=i(this);V.find(e).forEach((t=>{pt.getOrCreateInstance(t,{toggle:!1}).toggle()}))})),g(pt);var mt="top",gt="bottom",_t="right",bt="left",vt="auto",yt=[mt,gt,_t,bt],wt="start",Et="end",At="clippingParents",Tt="viewport",Ot="popper",Ct="reference",kt=yt.reduce((function(t,e){return t.concat([e+"-"+wt,e+"-"+Et])}),[]),Lt=[].concat(yt,[vt]).reduce((function(t,e){return t.concat([e,e+"-"+wt,e+"-"+Et])}),[]),xt="beforeRead",Dt="read",St="afterRead",Nt="beforeMain",It="main",Pt="afterMain",jt="beforeWrite",Mt="write",Ht="afterWrite",Bt=[xt,Dt,St,Nt,It,Pt,jt,Mt,Ht];function Rt(t){return t?(t.nodeName||"").toLowerCase():null}function Wt(t){if(null==t)return window;if("[object Window]"!==t.toString()){var e=t.ownerDocument;return e&&e.defaultView||window}return t}function $t(t){return t instanceof Wt(t).Element||t instanceof Element}function zt(t){return t instanceof Wt(t).HTMLElement||t instanceof HTMLElement}function qt(t){return"undefined"!=typeof ShadowRoot&&(t instanceof Wt(t).ShadowRoot||t instanceof ShadowRoot)}const Ft={name:"applyStyles",enabled:!0,phase:"write",fn:function(t){var e=t.state;Object.keys(e.elements).forEach((function(t){var i=e.styles[t]||{},n=e.attributes[t]||{},s=e.elements[t];zt(s)&&Rt(s)&&(Object.assign(s.style,i),Object.keys(n).forEach((function(t){var e=n[t];!1===e?s.removeAttribute(t):s.setAttribute(t,!0===e?"":e)})))}))},effect:function(t){var e=t.state,i={popper:{position:e.options.strategy,left:"0",top:"0",margin:"0"},arrow:{position:"absolute"},reference:{}};return Object.assign(e.elements.popper.style,i.popper),e.styles=i,e.elements.arrow&&Object.assign(e.elements.arrow.style,i.arrow),function(){Object.keys(e.elements).forEach((function(t){var n=e.elements[t],s=e.attributes[t]||{},o=Object.keys(e.styles.hasOwnProperty(t)?e.styles[t]:i[t]).reduce((function(t,e){return t[e]="",t}),{});zt(n)&&Rt(n)&&(Object.assign(n.style,o),Object.keys(s).forEach((function(t){n.removeAttribute(t)})))}))}},requires:["computeStyles"]};function Ut(t){return t.split("-")[0]}function Vt(t,e){var i=t.getBoundingClientRect();return{width:i.width/1,height:i.height/1,top:i.top/1,right:i.right/1,bottom:i.bottom/1,left:i.left/1,x:i.left/1,y:i.top/1}}function Kt(t){var e=Vt(t),i=t.offsetWidth,n=t.offsetHeight;return Math.abs(e.width-i)<=1&&(i=e.width),Math.abs(e.height-n)<=1&&(n=e.height),{x:t.offsetLeft,y:t.offsetTop,width:i,height:n}}function Xt(t,e){var i=e.getRootNode&&e.getRootNode();if(t.contains(e))return!0;if(i&&qt(i)){var n=e;do{if(n&&t.isSameNode(n))return!0;n=n.parentNode||n.host}while(n)}return!1}function Yt(t){return Wt(t).getComputedStyle(t)}function Qt(t){return["table","td","th"].indexOf(Rt(t))>=0}function Gt(t){return(($t(t)?t.ownerDocument:t.document)||window.document).documentElement}function Zt(t){return"html"===Rt(t)?t:t.assignedSlot||t.parentNode||(qt(t)?t.host:null)||Gt(t)}function Jt(t){return zt(t)&&"fixed"!==Yt(t).position?t.offsetParent:null}function te(t){for(var e=Wt(t),i=Jt(t);i&&Qt(i)&&"static"===Yt(i).position;)i=Jt(i);return i&&("html"===Rt(i)||"body"===Rt(i)&&"static"===Yt(i).position)?e:i||function(t){var e=-1!==navigator.userAgent.toLowerCase().indexOf("firefox");if(-1!==navigator.userAgent.indexOf("Trident")&&zt(t)&&"fixed"===Yt(t).position)return null;for(var i=Zt(t);zt(i)&&["html","body"].indexOf(Rt(i))<0;){var n=Yt(i);if("none"!==n.transform||"none"!==n.perspective||"paint"===n.contain||-1!==["transform","perspective"].indexOf(n.willChange)||e&&"filter"===n.willChange||e&&n.filter&&"none"!==n.filter)return i;i=i.parentNode}return null}(t)||e}function ee(t){return["top","bottom"].indexOf(t)>=0?"x":"y"}var ie=Math.max,ne=Math.min,se=Math.round;function oe(t,e,i){return ie(t,ne(e,i))}function re(t){return Object.assign({},{top:0,right:0,bottom:0,left:0},t)}function ae(t,e){return e.reduce((function(e,i){return e[i]=t,e}),{})}const le={name:"arrow",enabled:!0,phase:"main",fn:function(t){var e,i=t.state,n=t.name,s=t.options,o=i.elements.arrow,r=i.modifiersData.popperOffsets,a=Ut(i.placement),l=ee(a),c=[bt,_t].indexOf(a)>=0?"height":"width";if(o&&r){var h=function(t,e){return re("number"!=typeof(t="function"==typeof t?t(Object.assign({},e.rects,{placement:e.placement})):t)?t:ae(t,yt))}(s.padding,i),d=Kt(o),u="y"===l?mt:bt,f="y"===l?gt:_t,p=i.rects.reference[c]+i.rects.reference[l]-r[l]-i.rects.popper[c],m=r[l]-i.rects.reference[l],g=te(o),_=g?"y"===l?g.clientHeight||0:g.clientWidth||0:0,b=p/2-m/2,v=h[u],y=_-d[c]-h[f],w=_/2-d[c]/2+b,E=oe(v,w,y),A=l;i.modifiersData[n]=((e={})[A]=E,e.centerOffset=E-w,e)}},effect:function(t){var e=t.state,i=t.options.element,n=void 0===i?"[data-popper-arrow]":i;null!=n&&("string"!=typeof n||(n=e.elements.popper.querySelector(n)))&&Xt(e.elements.popper,n)&&(e.elements.arrow=n)},requires:["popperOffsets"],requiresIfExists:["preventOverflow"]};function ce(t){return t.split("-")[1]}var he={top:"auto",right:"auto",bottom:"auto",left:"auto"};function de(t){var e,i=t.popper,n=t.popperRect,s=t.placement,o=t.variation,r=t.offsets,a=t.position,l=t.gpuAcceleration,c=t.adaptive,h=t.roundOffsets,d=!0===h?function(t){var e=t.x,i=t.y,n=window.devicePixelRatio||1;return{x:se(se(e*n)/n)||0,y:se(se(i*n)/n)||0}}(r):"function"==typeof h?h(r):r,u=d.x,f=void 0===u?0:u,p=d.y,m=void 0===p?0:p,g=r.hasOwnProperty("x"),_=r.hasOwnProperty("y"),b=bt,v=mt,y=window;if(c){var w=te(i),E="clientHeight",A="clientWidth";w===Wt(i)&&"static"!==Yt(w=Gt(i)).position&&"absolute"===a&&(E="scrollHeight",A="scrollWidth"),w=w,s!==mt&&(s!==bt&&s!==_t||o!==Et)||(v=gt,m-=w[E]-n.height,m*=l?1:-1),s!==bt&&(s!==mt&&s!==gt||o!==Et)||(b=_t,f-=w[A]-n.width,f*=l?1:-1)}var T,O=Object.assign({position:a},c&&he);return l?Object.assign({},O,((T={})[v]=_?"0":"",T[b]=g?"0":"",T.transform=(y.devicePixelRatio||1)<=1?"translate("+f+"px, "+m+"px)":"translate3d("+f+"px, "+m+"px, 0)",T)):Object.assign({},O,((e={})[v]=_?m+"px":"",e[b]=g?f+"px":"",e.transform="",e))}const ue={name:"computeStyles",enabled:!0,phase:"beforeWrite",fn:function(t){var e=t.state,i=t.options,n=i.gpuAcceleration,s=void 0===n||n,o=i.adaptive,r=void 0===o||o,a=i.roundOffsets,l=void 0===a||a,c={placement:Ut(e.placement),variation:ce(e.placement),popper:e.elements.popper,popperRect:e.rects.popper,gpuAcceleration:s};null!=e.modifiersData.popperOffsets&&(e.styles.popper=Object.assign({},e.styles.popper,de(Object.assign({},c,{offsets:e.modifiersData.popperOffsets,position:e.options.strategy,adaptive:r,roundOffsets:l})))),null!=e.modifiersData.arrow&&(e.styles.arrow=Object.assign({},e.styles.arrow,de(Object.assign({},c,{offsets:e.modifiersData.arrow,position:"absolute",adaptive:!1,roundOffsets:l})))),e.attributes.popper=Object.assign({},e.attributes.popper,{"data-popper-placement":e.placement})},data:{}};var fe={passive:!0};const pe={name:"eventListeners",enabled:!0,phase:"write",fn:function(){},effect:function(t){var e=t.state,i=t.instance,n=t.options,s=n.scroll,o=void 0===s||s,r=n.resize,a=void 0===r||r,l=Wt(e.elements.popper),c=[].concat(e.scrollParents.reference,e.scrollParents.popper);return o&&c.forEach((function(t){t.addEventListener("scroll",i.update,fe)})),a&&l.addEventListener("resize",i.update,fe),function(){o&&c.forEach((function(t){t.removeEventListener("scroll",i.update,fe)})),a&&l.removeEventListener("resize",i.update,fe)}},data:{}};var me={left:"right",right:"left",bottom:"top",top:"bottom"};function ge(t){return t.replace(/left|right|bottom|top/g,(function(t){return me[t]}))}var _e={start:"end",end:"start"};function be(t){return t.replace(/start|end/g,(function(t){return _e[t]}))}function ve(t){var e=Wt(t);return{scrollLeft:e.pageXOffset,scrollTop:e.pageYOffset}}function ye(t){return Vt(Gt(t)).left+ve(t).scrollLeft}function we(t){var e=Yt(t),i=e.overflow,n=e.overflowX,s=e.overflowY;return/auto|scroll|overlay|hidden/.test(i+s+n)}function Ee(t){return["html","body","#document"].indexOf(Rt(t))>=0?t.ownerDocument.body:zt(t)&&we(t)?t:Ee(Zt(t))}function Ae(t,e){var i;void 0===e&&(e=[]);var n=Ee(t),s=n===(null==(i=t.ownerDocument)?void 0:i.body),o=Wt(n),r=s?[o].concat(o.visualViewport||[],we(n)?n:[]):n,a=e.concat(r);return s?a:a.concat(Ae(Zt(r)))}function Te(t){return Object.assign({},t,{left:t.x,top:t.y,right:t.x+t.width,bottom:t.y+t.height})}function Oe(t,e){return e===Tt?Te(function(t){var e=Wt(t),i=Gt(t),n=e.visualViewport,s=i.clientWidth,o=i.clientHeight,r=0,a=0;return n&&(s=n.width,o=n.height,/^((?!chrome|android).)*safari/i.test(navigator.userAgent)||(r=n.offsetLeft,a=n.offsetTop)),{width:s,height:o,x:r+ye(t),y:a}}(t)):zt(e)?function(t){var e=Vt(t);return e.top=e.top+t.clientTop,e.left=e.left+t.clientLeft,e.bottom=e.top+t.clientHeight,e.right=e.left+t.clientWidth,e.width=t.clientWidth,e.height=t.clientHeight,e.x=e.left,e.y=e.top,e}(e):Te(function(t){var e,i=Gt(t),n=ve(t),s=null==(e=t.ownerDocument)?void 0:e.body,o=ie(i.scrollWidth,i.clientWidth,s?s.scrollWidth:0,s?s.clientWidth:0),r=ie(i.scrollHeight,i.clientHeight,s?s.scrollHeight:0,s?s.clientHeight:0),a=-n.scrollLeft+ye(t),l=-n.scrollTop;return"rtl"===Yt(s||i).direction&&(a+=ie(i.clientWidth,s?s.clientWidth:0)-o),{width:o,height:r,x:a,y:l}}(Gt(t)))}function Ce(t){var e,i=t.reference,n=t.element,s=t.placement,o=s?Ut(s):null,r=s?ce(s):null,a=i.x+i.width/2-n.width/2,l=i.y+i.height/2-n.height/2;switch(o){case mt:e={x:a,y:i.y-n.height};break;case gt:e={x:a,y:i.y+i.height};break;case _t:e={x:i.x+i.width,y:l};break;case bt:e={x:i.x-n.width,y:l};break;default:e={x:i.x,y:i.y}}var c=o?ee(o):null;if(null!=c){var h="y"===c?"height":"width";switch(r){case wt:e[c]=e[c]-(i[h]/2-n[h]/2);break;case Et:e[c]=e[c]+(i[h]/2-n[h]/2)}}return e}function ke(t,e){void 0===e&&(e={});var i=e,n=i.placement,s=void 0===n?t.placement:n,o=i.boundary,r=void 0===o?At:o,a=i.rootBoundary,l=void 0===a?Tt:a,c=i.elementContext,h=void 0===c?Ot:c,d=i.altBoundary,u=void 0!==d&&d,f=i.padding,p=void 0===f?0:f,m=re("number"!=typeof p?p:ae(p,yt)),g=h===Ot?Ct:Ot,_=t.rects.popper,b=t.elements[u?g:h],v=function(t,e,i){var n="clippingParents"===e?function(t){var e=Ae(Zt(t)),i=["absolute","fixed"].indexOf(Yt(t).position)>=0&&zt(t)?te(t):t;return $t(i)?e.filter((function(t){return $t(t)&&Xt(t,i)&&"body"!==Rt(t)})):[]}(t):[].concat(e),s=[].concat(n,[i]),o=s[0],r=s.reduce((function(e,i){var n=Oe(t,i);return e.top=ie(n.top,e.top),e.right=ne(n.right,e.right),e.bottom=ne(n.bottom,e.bottom),e.left=ie(n.left,e.left),e}),Oe(t,o));return r.width=r.right-r.left,r.height=r.bottom-r.top,r.x=r.left,r.y=r.top,r}($t(b)?b:b.contextElement||Gt(t.elements.popper),r,l),y=Vt(t.elements.reference),w=Ce({reference:y,element:_,strategy:"absolute",placement:s}),E=Te(Object.assign({},_,w)),A=h===Ot?E:y,T={top:v.top-A.top+m.top,bottom:A.bottom-v.bottom+m.bottom,left:v.left-A.left+m.left,right:A.right-v.right+m.right},O=t.modifiersData.offset;if(h===Ot&&O){var C=O[s];Object.keys(T).forEach((function(t){var e=[_t,gt].indexOf(t)>=0?1:-1,i=[mt,gt].indexOf(t)>=0?"y":"x";T[t]+=C[i]*e}))}return T}function Le(t,e){void 0===e&&(e={});var i=e,n=i.placement,s=i.boundary,o=i.rootBoundary,r=i.padding,a=i.flipVariations,l=i.allowedAutoPlacements,c=void 0===l?Lt:l,h=ce(n),d=h?a?kt:kt.filter((function(t){return ce(t)===h})):yt,u=d.filter((function(t){return c.indexOf(t)>=0}));0===u.length&&(u=d);var f=u.reduce((function(e,i){return e[i]=ke(t,{placement:i,boundary:s,rootBoundary:o,padding:r})[Ut(i)],e}),{});return Object.keys(f).sort((function(t,e){return f[t]-f[e]}))}const xe={name:"flip",enabled:!0,phase:"main",fn:function(t){var e=t.state,i=t.options,n=t.name;if(!e.modifiersData[n]._skip){for(var s=i.mainAxis,o=void 0===s||s,r=i.altAxis,a=void 0===r||r,l=i.fallbackPlacements,c=i.padding,h=i.boundary,d=i.rootBoundary,u=i.altBoundary,f=i.flipVariations,p=void 0===f||f,m=i.allowedAutoPlacements,g=e.options.placement,_=Ut(g),b=l||(_!==g&&p?function(t){if(Ut(t)===vt)return[];var e=ge(t);return[be(t),e,be(e)]}(g):[ge(g)]),v=[g].concat(b).reduce((function(t,i){return t.concat(Ut(i)===vt?Le(e,{placement:i,boundary:h,rootBoundary:d,padding:c,flipVariations:p,allowedAutoPlacements:m}):i)}),[]),y=e.rects.reference,w=e.rects.popper,E=new Map,A=!0,T=v[0],O=0;O=0,D=x?"width":"height",S=ke(e,{placement:C,boundary:h,rootBoundary:d,altBoundary:u,padding:c}),N=x?L?_t:bt:L?gt:mt;y[D]>w[D]&&(N=ge(N));var I=ge(N),P=[];if(o&&P.push(S[k]<=0),a&&P.push(S[N]<=0,S[I]<=0),P.every((function(t){return t}))){T=C,A=!1;break}E.set(C,P)}if(A)for(var j=function(t){var e=v.find((function(e){var i=E.get(e);if(i)return i.slice(0,t).every((function(t){return t}))}));if(e)return T=e,"break"},M=p?3:1;M>0&&"break"!==j(M);M--);e.placement!==T&&(e.modifiersData[n]._skip=!0,e.placement=T,e.reset=!0)}},requiresIfExists:["offset"],data:{_skip:!1}};function De(t,e,i){return void 0===i&&(i={x:0,y:0}),{top:t.top-e.height-i.y,right:t.right-e.width+i.x,bottom:t.bottom-e.height+i.y,left:t.left-e.width-i.x}}function Se(t){return[mt,_t,gt,bt].some((function(e){return t[e]>=0}))}const Ne={name:"hide",enabled:!0,phase:"main",requiresIfExists:["preventOverflow"],fn:function(t){var e=t.state,i=t.name,n=e.rects.reference,s=e.rects.popper,o=e.modifiersData.preventOverflow,r=ke(e,{elementContext:"reference"}),a=ke(e,{altBoundary:!0}),l=De(r,n),c=De(a,s,o),h=Se(l),d=Se(c);e.modifiersData[i]={referenceClippingOffsets:l,popperEscapeOffsets:c,isReferenceHidden:h,hasPopperEscaped:d},e.attributes.popper=Object.assign({},e.attributes.popper,{"data-popper-reference-hidden":h,"data-popper-escaped":d})}},Ie={name:"offset",enabled:!0,phase:"main",requires:["popperOffsets"],fn:function(t){var e=t.state,i=t.options,n=t.name,s=i.offset,o=void 0===s?[0,0]:s,r=Lt.reduce((function(t,i){return t[i]=function(t,e,i){var n=Ut(t),s=[bt,mt].indexOf(n)>=0?-1:1,o="function"==typeof i?i(Object.assign({},e,{placement:t})):i,r=o[0],a=o[1];return r=r||0,a=(a||0)*s,[bt,_t].indexOf(n)>=0?{x:a,y:r}:{x:r,y:a}}(i,e.rects,o),t}),{}),a=r[e.placement],l=a.x,c=a.y;null!=e.modifiersData.popperOffsets&&(e.modifiersData.popperOffsets.x+=l,e.modifiersData.popperOffsets.y+=c),e.modifiersData[n]=r}},Pe={name:"popperOffsets",enabled:!0,phase:"read",fn:function(t){var e=t.state,i=t.name;e.modifiersData[i]=Ce({reference:e.rects.reference,element:e.rects.popper,strategy:"absolute",placement:e.placement})},data:{}},je={name:"preventOverflow",enabled:!0,phase:"main",fn:function(t){var e=t.state,i=t.options,n=t.name,s=i.mainAxis,o=void 0===s||s,r=i.altAxis,a=void 0!==r&&r,l=i.boundary,c=i.rootBoundary,h=i.altBoundary,d=i.padding,u=i.tether,f=void 0===u||u,p=i.tetherOffset,m=void 0===p?0:p,g=ke(e,{boundary:l,rootBoundary:c,padding:d,altBoundary:h}),_=Ut(e.placement),b=ce(e.placement),v=!b,y=ee(_),w="x"===y?"y":"x",E=e.modifiersData.popperOffsets,A=e.rects.reference,T=e.rects.popper,O="function"==typeof m?m(Object.assign({},e.rects,{placement:e.placement})):m,C={x:0,y:0};if(E){if(o||a){var k="y"===y?mt:bt,L="y"===y?gt:_t,x="y"===y?"height":"width",D=E[y],S=E[y]+g[k],N=E[y]-g[L],I=f?-T[x]/2:0,P=b===wt?A[x]:T[x],j=b===wt?-T[x]:-A[x],M=e.elements.arrow,H=f&&M?Kt(M):{width:0,height:0},B=e.modifiersData["arrow#persistent"]?e.modifiersData["arrow#persistent"].padding:{top:0,right:0,bottom:0,left:0},R=B[k],W=B[L],$=oe(0,A[x],H[x]),z=v?A[x]/2-I-$-R-O:P-$-R-O,q=v?-A[x]/2+I+$+W+O:j+$+W+O,F=e.elements.arrow&&te(e.elements.arrow),U=F?"y"===y?F.clientTop||0:F.clientLeft||0:0,V=e.modifiersData.offset?e.modifiersData.offset[e.placement][y]:0,K=E[y]+z-V-U,X=E[y]+q-V;if(o){var Y=oe(f?ne(S,K):S,D,f?ie(N,X):N);E[y]=Y,C[y]=Y-D}if(a){var Q="x"===y?mt:bt,G="x"===y?gt:_t,Z=E[w],J=Z+g[Q],tt=Z-g[G],et=oe(f?ne(J,K):J,Z,f?ie(tt,X):tt);E[w]=et,C[w]=et-Z}}e.modifiersData[n]=C}},requiresIfExists:["offset"]};function Me(t,e,i){void 0===i&&(i=!1);var n=zt(e);zt(e)&&function(t){var e=t.getBoundingClientRect();e.width,t.offsetWidth,e.height,t.offsetHeight}(e);var s,o,r=Gt(e),a=Vt(t),l={scrollLeft:0,scrollTop:0},c={x:0,y:0};return(n||!n&&!i)&&(("body"!==Rt(e)||we(r))&&(l=(s=e)!==Wt(s)&&zt(s)?{scrollLeft:(o=s).scrollLeft,scrollTop:o.scrollTop}:ve(s)),zt(e)?((c=Vt(e)).x+=e.clientLeft,c.y+=e.clientTop):r&&(c.x=ye(r))),{x:a.left+l.scrollLeft-c.x,y:a.top+l.scrollTop-c.y,width:a.width,height:a.height}}function He(t){var e=new Map,i=new Set,n=[];function s(t){i.add(t.name),[].concat(t.requires||[],t.requiresIfExists||[]).forEach((function(t){if(!i.has(t)){var n=e.get(t);n&&s(n)}})),n.push(t)}return t.forEach((function(t){e.set(t.name,t)})),t.forEach((function(t){i.has(t.name)||s(t)})),n}var Be={placement:"bottom",modifiers:[],strategy:"absolute"};function Re(){for(var t=arguments.length,e=new Array(t),i=0;ij.on(t,"mouseover",d))),this._element.focus(),this._element.setAttribute("aria-expanded",!0),this._menu.classList.add(Je),this._element.classList.add(Je),j.trigger(this._element,"shown.bs.dropdown",t)}hide(){if(c(this._element)||!this._isShown(this._menu))return;const t={relatedTarget:this._element};this._completeHide(t)}dispose(){this._popper&&this._popper.destroy(),super.dispose()}update(){this._inNavbar=this._detectNavbar(),this._popper&&this._popper.update()}_completeHide(t){j.trigger(this._element,"hide.bs.dropdown",t).defaultPrevented||("ontouchstart"in document.documentElement&&[].concat(...document.body.children).forEach((t=>j.off(t,"mouseover",d))),this._popper&&this._popper.destroy(),this._menu.classList.remove(Je),this._element.classList.remove(Je),this._element.setAttribute("aria-expanded","false"),U.removeDataAttribute(this._menu,"popper"),j.trigger(this._element,"hidden.bs.dropdown",t))}_getConfig(t){if(t={...this.constructor.Default,...U.getDataAttributes(this._element),...t},a(Ue,t,this.constructor.DefaultType),"object"==typeof t.reference&&!o(t.reference)&&"function"!=typeof t.reference.getBoundingClientRect)throw new TypeError(`${Ue.toUpperCase()}: Option "reference" provided type "object" without a required "getBoundingClientRect" method.`);return t}_createPopper(t){if(void 0===Fe)throw new TypeError("Bootstrap's dropdowns require Popper (https://popper.js.org)");let e=this._element;"parent"===this._config.reference?e=t:o(this._config.reference)?e=r(this._config.reference):"object"==typeof this._config.reference&&(e=this._config.reference);const i=this._getPopperConfig(),n=i.modifiers.find((t=>"applyStyles"===t.name&&!1===t.enabled));this._popper=qe(e,this._menu,i),n&&U.setDataAttribute(this._menu,"popper","static")}_isShown(t=this._element){return t.classList.contains(Je)}_getMenuElement(){return V.next(this._element,ei)[0]}_getPlacement(){const t=this._element.parentNode;if(t.classList.contains("dropend"))return ri;if(t.classList.contains("dropstart"))return ai;const e="end"===getComputedStyle(this._menu).getPropertyValue("--bs-position").trim();return t.classList.contains("dropup")?e?ni:ii:e?oi:si}_detectNavbar(){return null!==this._element.closest(".navbar")}_getOffset(){const{offset:t}=this._config;return"string"==typeof t?t.split(",").map((t=>Number.parseInt(t,10))):"function"==typeof t?e=>t(e,this._element):t}_getPopperConfig(){const t={placement:this._getPlacement(),modifiers:[{name:"preventOverflow",options:{boundary:this._config.boundary}},{name:"offset",options:{offset:this._getOffset()}}]};return"static"===this._config.display&&(t.modifiers=[{name:"applyStyles",enabled:!1}]),{...t,..."function"==typeof this._config.popperConfig?this._config.popperConfig(t):this._config.popperConfig}}_selectMenuItem({key:t,target:e}){const i=V.find(".dropdown-menu .dropdown-item:not(.disabled):not(:disabled)",this._menu).filter(l);i.length&&v(i,e,t===Ye,!i.includes(e)).focus()}static jQueryInterface(t){return this.each((function(){const e=hi.getOrCreateInstance(this,t);if("string"==typeof t){if(void 0===e[t])throw new TypeError(`No method named "${t}"`);e[t]()}}))}static clearMenus(t){if(t&&(2===t.button||"keyup"===t.type&&"Tab"!==t.key))return;const e=V.find(ti);for(let i=0,n=e.length;ie+t)),this._setElementAttributes(di,"paddingRight",(e=>e+t)),this._setElementAttributes(ui,"marginRight",(e=>e-t))}_disableOverFlow(){this._saveInitialAttribute(this._element,"overflow"),this._element.style.overflow="hidden"}_setElementAttributes(t,e,i){const n=this.getWidth();this._applyManipulationCallback(t,(t=>{if(t!==this._element&&window.innerWidth>t.clientWidth+n)return;this._saveInitialAttribute(t,e);const s=window.getComputedStyle(t)[e];t.style[e]=`${i(Number.parseFloat(s))}px`}))}reset(){this._resetElementAttributes(this._element,"overflow"),this._resetElementAttributes(this._element,"paddingRight"),this._resetElementAttributes(di,"paddingRight"),this._resetElementAttributes(ui,"marginRight")}_saveInitialAttribute(t,e){const i=t.style[e];i&&U.setDataAttribute(t,e,i)}_resetElementAttributes(t,e){this._applyManipulationCallback(t,(t=>{const i=U.getDataAttribute(t,e);void 0===i?t.style.removeProperty(e):(U.removeDataAttribute(t,e),t.style[e]=i)}))}_applyManipulationCallback(t,e){o(t)?e(t):V.find(t,this._element).forEach(e)}isOverflowing(){return this.getWidth()>0}}const pi={className:"modal-backdrop",isVisible:!0,isAnimated:!1,rootElement:"body",clickCallback:null},mi={className:"string",isVisible:"boolean",isAnimated:"boolean",rootElement:"(element|string)",clickCallback:"(function|null)"},gi="show",_i="mousedown.bs.backdrop";class bi{constructor(t){this._config=this._getConfig(t),this._isAppended=!1,this._element=null}show(t){this._config.isVisible?(this._append(),this._config.isAnimated&&u(this._getElement()),this._getElement().classList.add(gi),this._emulateAnimation((()=>{_(t)}))):_(t)}hide(t){this._config.isVisible?(this._getElement().classList.remove(gi),this._emulateAnimation((()=>{this.dispose(),_(t)}))):_(t)}_getElement(){if(!this._element){const t=document.createElement("div");t.className=this._config.className,this._config.isAnimated&&t.classList.add("fade"),this._element=t}return this._element}_getConfig(t){return(t={...pi,..."object"==typeof t?t:{}}).rootElement=r(t.rootElement),a("backdrop",t,mi),t}_append(){this._isAppended||(this._config.rootElement.append(this._getElement()),j.on(this._getElement(),_i,(()=>{_(this._config.clickCallback)})),this._isAppended=!0)}dispose(){this._isAppended&&(j.off(this._element,_i),this._element.remove(),this._isAppended=!1)}_emulateAnimation(t){b(t,this._getElement(),this._config.isAnimated)}}const vi={trapElement:null,autofocus:!0},yi={trapElement:"element",autofocus:"boolean"},wi=".bs.focustrap",Ei="backward";class Ai{constructor(t){this._config=this._getConfig(t),this._isActive=!1,this._lastTabNavDirection=null}activate(){const{trapElement:t,autofocus:e}=this._config;this._isActive||(e&&t.focus(),j.off(document,wi),j.on(document,"focusin.bs.focustrap",(t=>this._handleFocusin(t))),j.on(document,"keydown.tab.bs.focustrap",(t=>this._handleKeydown(t))),this._isActive=!0)}deactivate(){this._isActive&&(this._isActive=!1,j.off(document,wi))}_handleFocusin(t){const{target:e}=t,{trapElement:i}=this._config;if(e===document||e===i||i.contains(e))return;const n=V.focusableChildren(i);0===n.length?i.focus():this._lastTabNavDirection===Ei?n[n.length-1].focus():n[0].focus()}_handleKeydown(t){"Tab"===t.key&&(this._lastTabNavDirection=t.shiftKey?Ei:"forward")}_getConfig(t){return t={...vi,..."object"==typeof t?t:{}},a("focustrap",t,yi),t}}const Ti="modal",Oi="Escape",Ci={backdrop:!0,keyboard:!0,focus:!0},ki={backdrop:"(boolean|string)",keyboard:"boolean",focus:"boolean"},Li="hidden.bs.modal",xi="show.bs.modal",Di="resize.bs.modal",Si="click.dismiss.bs.modal",Ni="keydown.dismiss.bs.modal",Ii="mousedown.dismiss.bs.modal",Pi="modal-open",ji="show",Mi="modal-static";class Hi extends B{constructor(t,e){super(t),this._config=this._getConfig(e),this._dialog=V.findOne(".modal-dialog",this._element),this._backdrop=this._initializeBackDrop(),this._focustrap=this._initializeFocusTrap(),this._isShown=!1,this._ignoreBackdropClick=!1,this._isTransitioning=!1,this._scrollBar=new fi}static get Default(){return Ci}static get NAME(){return Ti}toggle(t){return this._isShown?this.hide():this.show(t)}show(t){this._isShown||this._isTransitioning||j.trigger(this._element,xi,{relatedTarget:t}).defaultPrevented||(this._isShown=!0,this._isAnimated()&&(this._isTransitioning=!0),this._scrollBar.hide(),document.body.classList.add(Pi),this._adjustDialog(),this._setEscapeEvent(),this._setResizeEvent(),j.on(this._dialog,Ii,(()=>{j.one(this._element,"mouseup.dismiss.bs.modal",(t=>{t.target===this._element&&(this._ignoreBackdropClick=!0)}))})),this._showBackdrop((()=>this._showElement(t))))}hide(){if(!this._isShown||this._isTransitioning)return;if(j.trigger(this._element,"hide.bs.modal").defaultPrevented)return;this._isShown=!1;const t=this._isAnimated();t&&(this._isTransitioning=!0),this._setEscapeEvent(),this._setResizeEvent(),this._focustrap.deactivate(),this._element.classList.remove(ji),j.off(this._element,Si),j.off(this._dialog,Ii),this._queueCallback((()=>this._hideModal()),this._element,t)}dispose(){[window,this._dialog].forEach((t=>j.off(t,".bs.modal"))),this._backdrop.dispose(),this._focustrap.deactivate(),super.dispose()}handleUpdate(){this._adjustDialog()}_initializeBackDrop(){return new bi({isVisible:Boolean(this._config.backdrop),isAnimated:this._isAnimated()})}_initializeFocusTrap(){return new Ai({trapElement:this._element})}_getConfig(t){return t={...Ci,...U.getDataAttributes(this._element),..."object"==typeof t?t:{}},a(Ti,t,ki),t}_showElement(t){const e=this._isAnimated(),i=V.findOne(".modal-body",this._dialog);this._element.parentNode&&this._element.parentNode.nodeType===Node.ELEMENT_NODE||document.body.append(this._element),this._element.style.display="block",this._element.removeAttribute("aria-hidden"),this._element.setAttribute("aria-modal",!0),this._element.setAttribute("role","dialog"),this._element.scrollTop=0,i&&(i.scrollTop=0),e&&u(this._element),this._element.classList.add(ji),this._queueCallback((()=>{this._config.focus&&this._focustrap.activate(),this._isTransitioning=!1,j.trigger(this._element,"shown.bs.modal",{relatedTarget:t})}),this._dialog,e)}_setEscapeEvent(){this._isShown?j.on(this._element,Ni,(t=>{this._config.keyboard&&t.key===Oi?(t.preventDefault(),this.hide()):this._config.keyboard||t.key!==Oi||this._triggerBackdropTransition()})):j.off(this._element,Ni)}_setResizeEvent(){this._isShown?j.on(window,Di,(()=>this._adjustDialog())):j.off(window,Di)}_hideModal(){this._element.style.display="none",this._element.setAttribute("aria-hidden",!0),this._element.removeAttribute("aria-modal"),this._element.removeAttribute("role"),this._isTransitioning=!1,this._backdrop.hide((()=>{document.body.classList.remove(Pi),this._resetAdjustments(),this._scrollBar.reset(),j.trigger(this._element,Li)}))}_showBackdrop(t){j.on(this._element,Si,(t=>{this._ignoreBackdropClick?this._ignoreBackdropClick=!1:t.target===t.currentTarget&&(!0===this._config.backdrop?this.hide():"static"===this._config.backdrop&&this._triggerBackdropTransition())})),this._backdrop.show(t)}_isAnimated(){return this._element.classList.contains("fade")}_triggerBackdropTransition(){if(j.trigger(this._element,"hidePrevented.bs.modal").defaultPrevented)return;const{classList:t,scrollHeight:e,style:i}=this._element,n=e>document.documentElement.clientHeight;!n&&"hidden"===i.overflowY||t.contains(Mi)||(n||(i.overflowY="hidden"),t.add(Mi),this._queueCallback((()=>{t.remove(Mi),n||this._queueCallback((()=>{i.overflowY=""}),this._dialog)}),this._dialog),this._element.focus())}_adjustDialog(){const t=this._element.scrollHeight>document.documentElement.clientHeight,e=this._scrollBar.getWidth(),i=e>0;(!i&&t&&!m()||i&&!t&&m())&&(this._element.style.paddingLeft=`${e}px`),(i&&!t&&!m()||!i&&t&&m())&&(this._element.style.paddingRight=`${e}px`)}_resetAdjustments(){this._element.style.paddingLeft="",this._element.style.paddingRight=""}static jQueryInterface(t,e){return this.each((function(){const i=Hi.getOrCreateInstance(this,t);if("string"==typeof t){if(void 0===i[t])throw new TypeError(`No method named "${t}"`);i[t](e)}}))}}j.on(document,"click.bs.modal.data-api",'[data-bs-toggle="modal"]',(function(t){const e=n(this);["A","AREA"].includes(this.tagName)&&t.preventDefault(),j.one(e,xi,(t=>{t.defaultPrevented||j.one(e,Li,(()=>{l(this)&&this.focus()}))}));const i=V.findOne(".modal.show");i&&Hi.getInstance(i).hide(),Hi.getOrCreateInstance(e).toggle(this)})),R(Hi),g(Hi);const Bi="offcanvas",Ri={backdrop:!0,keyboard:!0,scroll:!1},Wi={backdrop:"boolean",keyboard:"boolean",scroll:"boolean"},$i="show",zi=".offcanvas.show",qi="hidden.bs.offcanvas";class Fi extends B{constructor(t,e){super(t),this._config=this._getConfig(e),this._isShown=!1,this._backdrop=this._initializeBackDrop(),this._focustrap=this._initializeFocusTrap(),this._addEventListeners()}static get NAME(){return Bi}static get Default(){return Ri}toggle(t){return this._isShown?this.hide():this.show(t)}show(t){this._isShown||j.trigger(this._element,"show.bs.offcanvas",{relatedTarget:t}).defaultPrevented||(this._isShown=!0,this._element.style.visibility="visible",this._backdrop.show(),this._config.scroll||(new fi).hide(),this._element.removeAttribute("aria-hidden"),this._element.setAttribute("aria-modal",!0),this._element.setAttribute("role","dialog"),this._element.classList.add($i),this._queueCallback((()=>{this._config.scroll||this._focustrap.activate(),j.trigger(this._element,"shown.bs.offcanvas",{relatedTarget:t})}),this._element,!0))}hide(){this._isShown&&(j.trigger(this._element,"hide.bs.offcanvas").defaultPrevented||(this._focustrap.deactivate(),this._element.blur(),this._isShown=!1,this._element.classList.remove($i),this._backdrop.hide(),this._queueCallback((()=>{this._element.setAttribute("aria-hidden",!0),this._element.removeAttribute("aria-modal"),this._element.removeAttribute("role"),this._element.style.visibility="hidden",this._config.scroll||(new fi).reset(),j.trigger(this._element,qi)}),this._element,!0)))}dispose(){this._backdrop.dispose(),this._focustrap.deactivate(),super.dispose()}_getConfig(t){return t={...Ri,...U.getDataAttributes(this._element),..."object"==typeof t?t:{}},a(Bi,t,Wi),t}_initializeBackDrop(){return new bi({className:"offcanvas-backdrop",isVisible:this._config.backdrop,isAnimated:!0,rootElement:this._element.parentNode,clickCallback:()=>this.hide()})}_initializeFocusTrap(){return new Ai({trapElement:this._element})}_addEventListeners(){j.on(this._element,"keydown.dismiss.bs.offcanvas",(t=>{this._config.keyboard&&"Escape"===t.key&&this.hide()}))}static jQueryInterface(t){return this.each((function(){const e=Fi.getOrCreateInstance(this,t);if("string"==typeof t){if(void 0===e[t]||t.startsWith("_")||"constructor"===t)throw new TypeError(`No method named "${t}"`);e[t](this)}}))}}j.on(document,"click.bs.offcanvas.data-api",'[data-bs-toggle="offcanvas"]',(function(t){const e=n(this);if(["A","AREA"].includes(this.tagName)&&t.preventDefault(),c(this))return;j.one(e,qi,(()=>{l(this)&&this.focus()}));const i=V.findOne(zi);i&&i!==e&&Fi.getInstance(i).hide(),Fi.getOrCreateInstance(e).toggle(this)})),j.on(window,"load.bs.offcanvas.data-api",(()=>V.find(zi).forEach((t=>Fi.getOrCreateInstance(t).show())))),R(Fi),g(Fi);const Ui=new Set(["background","cite","href","itemtype","longdesc","poster","src","xlink:href"]),Vi=/^(?:(?:https?|mailto|ftp|tel|file|sms):|[^#&/:?]*(?:[#/?]|$))/i,Ki=/^data:(?:image\/(?:bmp|gif|jpeg|jpg|png|tiff|webp)|video\/(?:mpeg|mp4|ogg|webm)|audio\/(?:mp3|oga|ogg|opus));base64,[\d+/a-z]+=*$/i,Xi=(t,e)=>{const i=t.nodeName.toLowerCase();if(e.includes(i))return!Ui.has(i)||Boolean(Vi.test(t.nodeValue)||Ki.test(t.nodeValue));const n=e.filter((t=>t instanceof RegExp));for(let t=0,e=n.length;t{Xi(t,r)||i.removeAttribute(t.nodeName)}))}return n.body.innerHTML}const Qi="tooltip",Gi=new Set(["sanitize","allowList","sanitizeFn"]),Zi={animation:"boolean",template:"string",title:"(string|element|function)",trigger:"string",delay:"(number|object)",html:"boolean",selector:"(string|boolean)",placement:"(string|function)",offset:"(array|string|function)",container:"(string|element|boolean)",fallbackPlacements:"array",boundary:"(string|element)",customClass:"(string|function)",sanitize:"boolean",sanitizeFn:"(null|function)",allowList:"object",popperConfig:"(null|object|function)"},Ji={AUTO:"auto",TOP:"top",RIGHT:m()?"left":"right",BOTTOM:"bottom",LEFT:m()?"right":"left"},tn={animation:!0,template:'
',trigger:"hover focus",title:"",delay:0,html:!1,selector:!1,placement:"top",offset:[0,0],container:!1,fallbackPlacements:["top","right","bottom","left"],boundary:"clippingParents",customClass:"",sanitize:!0,sanitizeFn:null,allowList:{"*":["class","dir","id","lang","role",/^aria-[\w-]*$/i],a:["target","href","title","rel"],area:[],b:[],br:[],col:[],code:[],div:[],em:[],hr:[],h1:[],h2:[],h3:[],h4:[],h5:[],h6:[],i:[],img:["src","srcset","alt","title","width","height"],li:[],ol:[],p:[],pre:[],s:[],small:[],span:[],sub:[],sup:[],strong:[],u:[],ul:[]},popperConfig:null},en={HIDE:"hide.bs.tooltip",HIDDEN:"hidden.bs.tooltip",SHOW:"show.bs.tooltip",SHOWN:"shown.bs.tooltip",INSERTED:"inserted.bs.tooltip",CLICK:"click.bs.tooltip",FOCUSIN:"focusin.bs.tooltip",FOCUSOUT:"focusout.bs.tooltip",MOUSEENTER:"mouseenter.bs.tooltip",MOUSELEAVE:"mouseleave.bs.tooltip"},nn="fade",sn="show",on="show",rn="out",an=".tooltip-inner",ln=".modal",cn="hide.bs.modal",hn="hover",dn="focus";class un extends B{constructor(t,e){if(void 0===Fe)throw new TypeError("Bootstrap's tooltips require Popper (https://popper.js.org)");super(t),this._isEnabled=!0,this._timeout=0,this._hoverState="",this._activeTrigger={},this._popper=null,this._config=this._getConfig(e),this.tip=null,this._setListeners()}static get Default(){return tn}static get NAME(){return Qi}static get Event(){return en}static get DefaultType(){return Zi}enable(){this._isEnabled=!0}disable(){this._isEnabled=!1}toggleEnabled(){this._isEnabled=!this._isEnabled}toggle(t){if(this._isEnabled)if(t){const e=this._initializeOnDelegatedTarget(t);e._activeTrigger.click=!e._activeTrigger.click,e._isWithActiveTrigger()?e._enter(null,e):e._leave(null,e)}else{if(this.getTipElement().classList.contains(sn))return void this._leave(null,this);this._enter(null,this)}}dispose(){clearTimeout(this._timeout),j.off(this._element.closest(ln),cn,this._hideModalHandler),this.tip&&this.tip.remove(),this._disposePopper(),super.dispose()}show(){if("none"===this._element.style.display)throw new Error("Please use show on visible elements");if(!this.isWithContent()||!this._isEnabled)return;const t=j.trigger(this._element,this.constructor.Event.SHOW),e=h(this._element),i=null===e?this._element.ownerDocument.documentElement.contains(this._element):e.contains(this._element);if(t.defaultPrevented||!i)return;"tooltip"===this.constructor.NAME&&this.tip&&this.getTitle()!==this.tip.querySelector(an).innerHTML&&(this._disposePopper(),this.tip.remove(),this.tip=null);const n=this.getTipElement(),s=(t=>{do{t+=Math.floor(1e6*Math.random())}while(document.getElementById(t));return t})(this.constructor.NAME);n.setAttribute("id",s),this._element.setAttribute("aria-describedby",s),this._config.animation&&n.classList.add(nn);const o="function"==typeof this._config.placement?this._config.placement.call(this,n,this._element):this._config.placement,r=this._getAttachment(o);this._addAttachmentClass(r);const{container:a}=this._config;H.set(n,this.constructor.DATA_KEY,this),this._element.ownerDocument.documentElement.contains(this.tip)||(a.append(n),j.trigger(this._element,this.constructor.Event.INSERTED)),this._popper?this._popper.update():this._popper=qe(this._element,n,this._getPopperConfig(r)),n.classList.add(sn);const l=this._resolvePossibleFunction(this._config.customClass);l&&n.classList.add(...l.split(" ")),"ontouchstart"in document.documentElement&&[].concat(...document.body.children).forEach((t=>{j.on(t,"mouseover",d)}));const c=this.tip.classList.contains(nn);this._queueCallback((()=>{const t=this._hoverState;this._hoverState=null,j.trigger(this._element,this.constructor.Event.SHOWN),t===rn&&this._leave(null,this)}),this.tip,c)}hide(){if(!this._popper)return;const t=this.getTipElement();if(j.trigger(this._element,this.constructor.Event.HIDE).defaultPrevented)return;t.classList.remove(sn),"ontouchstart"in document.documentElement&&[].concat(...document.body.children).forEach((t=>j.off(t,"mouseover",d))),this._activeTrigger.click=!1,this._activeTrigger.focus=!1,this._activeTrigger.hover=!1;const e=this.tip.classList.contains(nn);this._queueCallback((()=>{this._isWithActiveTrigger()||(this._hoverState!==on&&t.remove(),this._cleanTipClass(),this._element.removeAttribute("aria-describedby"),j.trigger(this._element,this.constructor.Event.HIDDEN),this._disposePopper())}),this.tip,e),this._hoverState=""}update(){null!==this._popper&&this._popper.update()}isWithContent(){return Boolean(this.getTitle())}getTipElement(){if(this.tip)return this.tip;const t=document.createElement("div");t.innerHTML=this._config.template;const e=t.children[0];return this.setContent(e),e.classList.remove(nn,sn),this.tip=e,this.tip}setContent(t){this._sanitizeAndSetContent(t,this.getTitle(),an)}_sanitizeAndSetContent(t,e,i){const n=V.findOne(i,t);e||!n?this.setElementContent(n,e):n.remove()}setElementContent(t,e){if(null!==t)return o(e)?(e=r(e),void(this._config.html?e.parentNode!==t&&(t.innerHTML="",t.append(e)):t.textContent=e.textContent)):void(this._config.html?(this._config.sanitize&&(e=Yi(e,this._config.allowList,this._config.sanitizeFn)),t.innerHTML=e):t.textContent=e)}getTitle(){const t=this._element.getAttribute("data-bs-original-title")||this._config.title;return this._resolvePossibleFunction(t)}updateAttachment(t){return"right"===t?"end":"left"===t?"start":t}_initializeOnDelegatedTarget(t,e){return e||this.constructor.getOrCreateInstance(t.delegateTarget,this._getDelegateConfig())}_getOffset(){const{offset:t}=this._config;return"string"==typeof t?t.split(",").map((t=>Number.parseInt(t,10))):"function"==typeof t?e=>t(e,this._element):t}_resolvePossibleFunction(t){return"function"==typeof t?t.call(this._element):t}_getPopperConfig(t){const e={placement:t,modifiers:[{name:"flip",options:{fallbackPlacements:this._config.fallbackPlacements}},{name:"offset",options:{offset:this._getOffset()}},{name:"preventOverflow",options:{boundary:this._config.boundary}},{name:"arrow",options:{element:`.${this.constructor.NAME}-arrow`}},{name:"onChange",enabled:!0,phase:"afterWrite",fn:t=>this._handlePopperPlacementChange(t)}],onFirstUpdate:t=>{t.options.placement!==t.placement&&this._handlePopperPlacementChange(t)}};return{...e,..."function"==typeof this._config.popperConfig?this._config.popperConfig(e):this._config.popperConfig}}_addAttachmentClass(t){this.getTipElement().classList.add(`${this._getBasicClassPrefix()}-${this.updateAttachment(t)}`)}_getAttachment(t){return Ji[t.toUpperCase()]}_setListeners(){this._config.trigger.split(" ").forEach((t=>{if("click"===t)j.on(this._element,this.constructor.Event.CLICK,this._config.selector,(t=>this.toggle(t)));else if("manual"!==t){const e=t===hn?this.constructor.Event.MOUSEENTER:this.constructor.Event.FOCUSIN,i=t===hn?this.constructor.Event.MOUSELEAVE:this.constructor.Event.FOCUSOUT;j.on(this._element,e,this._config.selector,(t=>this._enter(t))),j.on(this._element,i,this._config.selector,(t=>this._leave(t)))}})),this._hideModalHandler=()=>{this._element&&this.hide()},j.on(this._element.closest(ln),cn,this._hideModalHandler),this._config.selector?this._config={...this._config,trigger:"manual",selector:""}:this._fixTitle()}_fixTitle(){const t=this._element.getAttribute("title"),e=typeof this._element.getAttribute("data-bs-original-title");(t||"string"!==e)&&(this._element.setAttribute("data-bs-original-title",t||""),!t||this._element.getAttribute("aria-label")||this._element.textContent||this._element.setAttribute("aria-label",t),this._element.setAttribute("title",""))}_enter(t,e){e=this._initializeOnDelegatedTarget(t,e),t&&(e._activeTrigger["focusin"===t.type?dn:hn]=!0),e.getTipElement().classList.contains(sn)||e._hoverState===on?e._hoverState=on:(clearTimeout(e._timeout),e._hoverState=on,e._config.delay&&e._config.delay.show?e._timeout=setTimeout((()=>{e._hoverState===on&&e.show()}),e._config.delay.show):e.show())}_leave(t,e){e=this._initializeOnDelegatedTarget(t,e),t&&(e._activeTrigger["focusout"===t.type?dn:hn]=e._element.contains(t.relatedTarget)),e._isWithActiveTrigger()||(clearTimeout(e._timeout),e._hoverState=rn,e._config.delay&&e._config.delay.hide?e._timeout=setTimeout((()=>{e._hoverState===rn&&e.hide()}),e._config.delay.hide):e.hide())}_isWithActiveTrigger(){for(const t in this._activeTrigger)if(this._activeTrigger[t])return!0;return!1}_getConfig(t){const e=U.getDataAttributes(this._element);return Object.keys(e).forEach((t=>{Gi.has(t)&&delete e[t]})),(t={...this.constructor.Default,...e,..."object"==typeof t&&t?t:{}}).container=!1===t.container?document.body:r(t.container),"number"==typeof t.delay&&(t.delay={show:t.delay,hide:t.delay}),"number"==typeof t.title&&(t.title=t.title.toString()),"number"==typeof t.content&&(t.content=t.content.toString()),a(Qi,t,this.constructor.DefaultType),t.sanitize&&(t.template=Yi(t.template,t.allowList,t.sanitizeFn)),t}_getDelegateConfig(){const t={};for(const e in this._config)this.constructor.Default[e]!==this._config[e]&&(t[e]=this._config[e]);return t}_cleanTipClass(){const t=this.getTipElement(),e=new RegExp(`(^|\\s)${this._getBasicClassPrefix()}\\S+`,"g"),i=t.getAttribute("class").match(e);null!==i&&i.length>0&&i.map((t=>t.trim())).forEach((e=>t.classList.remove(e)))}_getBasicClassPrefix(){return"bs-tooltip"}_handlePopperPlacementChange(t){const{state:e}=t;e&&(this.tip=e.elements.popper,this._cleanTipClass(),this._addAttachmentClass(this._getAttachment(e.placement)))}_disposePopper(){this._popper&&(this._popper.destroy(),this._popper=null)}static jQueryInterface(t){return this.each((function(){const e=un.getOrCreateInstance(this,t);if("string"==typeof t){if(void 0===e[t])throw new TypeError(`No method named "${t}"`);e[t]()}}))}}g(un);const fn={...un.Default,placement:"right",offset:[0,8],trigger:"click",content:"",template:'
We can also add static features to each serie (these can be things like product_id or store_id). Only the first static feature (static_0) is relevant to the target.