diff --git a/.nojekyll b/.nojekyll new file mode 100644 index 00000000..e69de29b diff --git a/core.html b/core.html new file mode 100644 index 00000000..6e9f5aaa --- /dev/null +++ b/core.html @@ -0,0 +1,1180 @@ + + + + + + + + + +mlforecast - Core + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+ + +
+ +
+ + +
+ + + +
+ +
+
+

Core

+
+ + + +
+ + + + +
+ + +
+ + +
+
import copy
+
+from nbdev import show_doc
+from fastcore.test import test_eq, test_fail, test_warns
+from window_ops.expanding import expanding_mean
+from window_ops.rolling import rolling_mean
+from window_ops.shift import shift_array
+
+from mlforecast.target_transforms import Differences, LocalStandardScaler
+from mlforecast.utils import generate_daily_series, generate_prices_for_series
+
+
+

Data format

+

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_iddsystatic_0static_1
0id_002000-01-017.4045292753
1id_002000-01-0235.9526242753
2id_002000-01-0368.9583532753
3id_002000-01-0484.9945052753
4id_002000-01-05113.2198102753
..................
4869id_192000-03-25400.6068079745
4870id_192000-03-26538.7948249745
4871id_192000-03-27620.2021049745
4872id_192000-03-2820.6254269745
4873id_192000-03-29141.5131699745
+ +

4874 rows × 5 columns

+
+
+
+

For simplicity we’ll just take one time serie here.

+
+
uids = series['unique_id'].unique()
+serie = series[series['unique_id'].eq(uids[0])]
+serie
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
unique_iddsystatic_0static_1
0id_002000-01-017.4045292753
1id_002000-01-0235.9526242753
2id_002000-01-0368.9583532753
3id_002000-01-0484.9945052753
4id_002000-01-05113.2198102753
..................
217id_002000-08-0513.2631882753
218id_002000-08-0638.2319812753
219id_002000-08-0759.5551832753
220id_002000-08-0886.9863682753
221id_002000-08-09119.2548102753
+ +

222 rows × 5 columns

+
+
+
+
+
+

TimeSeries

+
+
 TimeSeries (freq:Union[int,str,pandas._libs.tslibs.offsets.BaseOffset,Non
+             eType]=None, lags:Optional[Iterable[int]]=None, lag_transform
+             s:Optional[Dict[int,List[Union[Callable,Tuple[Callable,Any]]]
+             ]]=None,
+             date_features:Optional[Iterable[Union[str,Callable]]]=None,
+             num_threads:int=1, target_transforms:Optional[List[mlforecast
+             .target_transforms.BaseTargetTransform]]=None)
+
+

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.

+
+
def month_start_or_end(dates):
+    return dates.is_month_start | dates.is_month_end
+
+flow_config = dict(
+    freq='W-THU',
+    lags=[7],
+    lag_transforms={
+        1: [expanding_mean, (rolling_mean, 7)]
+    },
+    date_features=['dayofweek', 'week', month_start_or_end]
+)
+
+ts = TimeSeries(**flow_config)
+ts
+
+
TimeSeries(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)
+
+
+

The frequency is converted to an offset.

+
+
test_eq(ts.freq, pd.tseries.frequencies.to_offset(flow_config['freq']))
+
+

The date features are stored as they were passed to the constructor.

+
+
test_eq(ts.date_features, flow_config['date_features'])
+
+

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.

+
+
test_eq(
+    ts.transforms, 
+    {
+        'lag7': (7, _identity),
+        'expanding_mean_lag1': (1, expanding_mean), 
+        'rolling_mean_lag1_window_size7': (1, rolling_mean, 7)
+        
+    }
+)
+
+

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.

+
+
+
+
+

TimeSeries.fit_transform

+
+
 TimeSeries.fit_transform (data:pandas.core.frame.DataFrame, id_col:str,
+                           time_col:str, target_col:str,
+                           static_features:Optional[List[str]]=None,
+                           dropna:bool=True,
+                           keep_last_n:Optional[int]=None,
+                           max_horizon:Optional[int]=None,
+                           return_X_y:bool=False)
+
+

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.

+
+
flow_config = dict(
+    freq='D',
+    lags=[7, 14],
+    lag_transforms={
+        2: [
+            (rolling_mean, 7),
+            (rolling_mean, 14),
+        ]
+    },
+    date_features=['dayofweek', 'month', 'year'],
+    num_threads=2
+)
+
+ts = TimeSeries(**flow_config)
+_ = ts.fit_transform(series, id_col='unique_id', time_col='ds', target_col='y')
+
+

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.

+
+
np.testing.assert_equal(ts.ga.data, series.y.values)
+
+

The series ids are stored in an uids attribute.

+
+
test_eq(ts.uids, series['unique_id'].unique())
+
+

For each time serie, the last observed date is stored so that predictions start from the last date + the frequency.

+
+
test_eq(ts.last_dates, series.groupby('unique_id')['ds'].max().values)
+
+

The last row of every serie without the y and ds columns are taken as static features.

+
+
pd.testing.assert_frame_equal(
+    ts.static_features_,
+    series.groupby('unique_id').tail(1).drop(columns=['ds', 'y']).reset_index(drop=True),
+)
+
+

If you pass static_features to TimeSeries.fit_transform then only these are kept.

+
+
ts.fit_transform(series, id_col='unique_id', time_col='ds', target_col='y', static_features=['static_0'])
+
+pd.testing.assert_frame_equal(
+    ts.static_features_,
+    series.groupby('unique_id').tail(1)[['unique_id', 'static_0']].reset_index(drop=True),
+)
+
+

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.

+
+
series_with_nulls = series.copy()
+series_with_nulls.loc[1, 'y'] = np.nan
+test_fail(
+    lambda: ts.fit_transform(series_with_nulls, id_col='unique_id', time_col='ds', target_col='y'),
+    contains='y column contains null values'
+)
+
+
+
+
+

TimeSeries.predict

+
+
 TimeSeries.predict (models:Dict[str,Union[sklearn.base.BaseEstimator,List
+                     [sklearn.base.BaseEstimator]]], horizon:int, dynamic_
+                     dfs:Optional[List[pandas.core.frame.DataFrame]]=None,
+                     before_predict_callback:Optional[Callable]=None,
+                     after_predict_callback:Optional[Callable]=None,
+                     X_df:Optional[pandas.core.frame.DataFrame]=None,
+                     ids:Optional[List[str]]=None)
+
+

Once we have a trained model we can use TimeSeries.predict passing the model and the horizon to get the predictions back.

+
+
class DummyModel:
+    def predict(self, X: pd.DataFrame) -> np.ndarray:
+        return X['lag7'].values
+
+horizon = 7
+model = DummyModel()
+ts = TimeSeries(**flow_config)
+ts.fit_transform(series, id_col='unique_id', time_col='ds', target_col='y')
+predictions = ts.predict({'DummyModel': model}, horizon)
+
+grouped_series = series.groupby('unique_id')
+expected_preds = grouped_series['y'].tail(7)  # the model predicts the lag-7
+last_dates = grouped_series['ds'].max()
+expected_dsmin = last_dates + ts.freq
+expected_dsmax = last_dates + horizon * ts.freq
+grouped_preds = predictions.groupby('unique_id')
+
+np.testing.assert_allclose(predictions['DummyModel'], expected_preds)
+pd.testing.assert_series_equal(grouped_preds['ds'].min(), expected_dsmin)
+pd.testing.assert_series_equal(grouped_preds['ds'].max(), expected_dsmax)
+
+

If we have dynamic features we can pass them to X_df.

+
+
class PredictPrice:
+    def predict(self, X):
+        return X['price']
+
+series = generate_daily_series(20, n_static_features=2, equal_ends=True)
+dynamic_series = series.rename(columns={'static_1': 'product_id'})
+prices_catalog = generate_prices_for_series(dynamic_series)
+series_with_prices = dynamic_series.merge(prices_catalog, how='left')
+
+model = PredictPrice()
+ts = TimeSeries(**flow_config)
+ts.fit_transform(
+    series_with_prices,
+    id_col='unique_id',
+    time_col='ds',
+    target_col='y',
+    static_features=['static_0', 'product_id'],
+)
+predictions = ts.predict({'PredictPrice': model}, horizon=1, X_df=prices_catalog)
+pd.testing.assert_frame_equal(
+    predictions.rename(columns={'PredictPrice': 'price'}),
+    prices_catalog.merge(predictions[['unique_id', 'ds']])[['unique_id', 'ds', 'price']]
+)
+
+
+
+
+

TimeSeries.update

+
+
 TimeSeries.update (df:pandas.core.frame.DataFrame)
+
+

Update the values of the stored series.

+ + +
+ +

Give us a ⭐ on Github

+ +
+ + + + \ No newline at end of file diff --git a/distributed.forecast.html b/distributed.forecast.html new file mode 100644 index 00000000..c5d2ea93 --- /dev/null +++ b/distributed.forecast.html @@ -0,0 +1,2521 @@ + + + + + + + + + + +mlforecast - DistributedMLForecast + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+ + +
+ +
+ + +
+ + + +
+ +
+
+

DistributedMLForecast

+
+ +
+
+ Distributed pipeline encapsulation +
+
+ + +
+ + + + +
+ + +
+ + +
+
+

DistributedMLForecast

+
+
 DistributedMLForecast (models,
+                        freq:Union[int,str,pandas._libs.tslibs.offsets.Bas
+                        eOffset,NoneType]=None,
+                        lags:Optional[Iterable[int]]=None, lag_transforms:
+                        Optional[Dict[int,List[Union[Callable,Tuple[Callab
+                        le,Any]]]]]=None, date_features:Optional[Iterable[
+                        Union[str,Callable]]]=None, num_threads:int=1, tar
+                        get_transforms:Optional[List[mlforecast.target_tra
+                        nsforms.BaseTargetTransform]]=None, engine=None,
+                        num_partitions:Optional[int]=None)
+
+

Multi backend distributed pipeline

+

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:

+
    +
  1. You need to set up a cluster. We currently support dask and spark (ray is on the roadmap).
  2. +
  3. Your data needs to be a distributed collection. We currently support dask and spark dataframes.
  4. +
  5. You need to use a model that implements distributed training in your framework of choice, e.g. SynapseML for LightGBM in spark.
  6. +
+
+
import matplotlib.pyplot as plt
+import numpy as np
+import pandas as pd
+from dask.distributed import Client
+from window_ops.expanding import expanding_mean
+from window_ops.rolling import rolling_mean
+
+from mlforecast.target_transforms import Differences
+from mlforecast.utils import backtest_splits, generate_daily_series, generate_prices_for_series
+from mlforecast.distributed.models.dask.lgb import DaskLGBMForecast
+from mlforecast.distributed.models.dask.xgb import DaskXGBForecast
+
+
+
+

Dask

+
+

Client setup

+
+
client = Client(n_workers=2, threads_per_worker=1)
+
+

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_iddsystatic_0static_1
npartitions=10
id_00objectdatetime64[ns]float64int64int64
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.

+
+
models = [DaskXGBForecast(random_state=0), DaskLGBMForecast(random_state=0)]
+
+
+
+

Training

+

Once we have our models we instantiate a DistributedMLForecast object defining our features.

+
+
fcst = DistributedMLForecast(
+    models=models,
+    freq='D',
+    lags=[7],
+    lag_transforms={
+        1: [expanding_mean],
+        7: [(rolling_mean, 14)]
+    },
+    date_features=['dayofweek', 'month'],
+    num_threads=1,
+    engine=client,
+)
+fcst
+
+
DistributedMLForecast(models=[DaskXGBForecast, DaskLGBMForecast], freq=<Day>, lag_features=['lag7', 'expanding_mean_lag1', 'rolling_mean_lag7_window_size14'], date_features=['dayofweek', 'month'], num_threads=1, engine=<Client: 'tcp://127.0.0.1:42319' processes=2 threads=2, memory=15.48 GiB>)
+
+
+

Here where we say that:

+
    +
  • Our series have daily frequency.
  • +
  • We want to use lag 7 as a feature
  • +
  • We want the lag transformations to be: +
      +
    • expanding mean of the lag 1
    • +
    • rolling mean of the lag 7 over a window of size 14
    • +
  • +
  • We want to use dayofweek and month as date features.
  • +
  • We want to perform the preprocessing and the forecasting steps using 1 thread, because we have 10 partitions and 2 workers.
  • +
+

From this point we have two options:

+
    +
  1. Compute the features and fit our models.
  2. +
  3. Compute the features and get them back as a dataframe to do some custom splitting or adding additional features, then training the models.
  4. +
+
+
+

1. Using all the data

+
+
+
+
+

DistributedMLForecast.fit

+
+
 DistributedMLForecast.fit (df:~AnyDataFrame, id_col:str='unique_id',
+                            time_col:str='ds', target_col:str='y',
+                            static_features:Optional[List[str]]=None,
+                            dropna:bool=True,
+                            keep_last_n:Optional[int]=None,
+                            data:Optional[~AnyDataFrame]=None)
+
+

Apply the feature engineering and train the models.

+ ++++++ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
TypeDefaultDetails
dfAnyDataFrameSeries data in long format.
id_colstrunique_idColumn that identifies each serie.
time_colstrdsColumn that identifies each timestep, its values can be timestamps or integers.
target_colstryColumn that contains the target.
static_featurestyping.Optional[typing.List[str]]NoneNames of the features that are static and will be repeated when forecasting.
dropnaboolTrueDrop rows with missing values produced by the transformations.
keep_last_ntyping.Optional[int]NoneKeep only these many records from each serie for the forecasting step. Can save time and memory if your features allow it.
datatyping.Optional[~AnyDataFrame]None
ReturnsDistributedMLForecastnoqa: ARG002
+

Calling fit on our data computes the features independently for each partition and performs distributed training.

+
+
fcst.fit(partitioned_series)
+
+
+

Forecasting

+
+
+
+
+

DistributedMLForecast.predict

+
+
 DistributedMLForecast.predict (h:int,
+                                dynamic_dfs:Optional[List[pandas.core.fram
+                                e.DataFrame]]=None, before_predict_callbac
+                                k:Optional[Callable]=None, after_predict_c
+                                allback:Optional[Callable]=None,
+                                new_df:Optional[~AnyDataFrame]=None,
+                                horizon:Optional[int]=None,
+                                new_data:Optional[~AnyDataFrame]=None)
+
+

Compute the predictions for the next horizon steps.

+ ++++++ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
TypeDefaultDetails
hintForecast horizon.
dynamic_dfstyping.Optional[typing.List[pandas.core.frame.DataFrame]]NoneFuture values of the dynamic features, e.g. prices.
before_predict_callbacktyping.Optional[typing.Callable]NoneFunction 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_callbacktyping.Optional[typing.Callable]NoneFunction 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_dftyping.Optional[~AnyDataFrame]NoneSeries 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.
horizontyping.Optional[int]None
new_datatyping.Optional[~AnyDataFrame]None
ReturnsAnyDataFramePredictions for each serie and timestep, with one column per model.
+

Once we have our fitted models we can compute the predictions for the next 7 timesteps.

+
+
preds = fcst.predict(7)
+preds
+
+
Dask DataFrame Structure:
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
unique_iddsDaskXGBForecastDaskLGBMForecast
npartitions=10
id_00objectdatetime64[ns]float64float64
id_10............
...............
id_89............
id_99............
+ +
+
Dask Name: map, 17 graph layers
+
+
+
+

2. Preprocess and train

+

If we only want to perform the preprocessing step we call preprocess with our data.

+
+
+
+
+

DistributedMLForecast.preprocess

+
+
 DistributedMLForecast.preprocess (df:~AnyDataFrame,
+                                   id_col:str='unique_id',
+                                   time_col:str='ds', target_col:str='y', 
+                                   static_features:Optional[List[str]]=Non
+                                   e, dropna:bool=True,
+                                   keep_last_n:Optional[int]=None,
+                                   data:Optional[~AnyDataFrame]=None)
+
+

Add the features to data.

+ ++++++ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
TypeDefaultDetails
dfAnyDataFrameSeries data in long format.
id_colstrunique_idColumn that identifies each serie.
time_colstrdsColumn that identifies each timestep, its values can be timestamps or integers.
target_colstryColumn that contains the target.
static_featurestyping.Optional[typing.List[str]]NoneNames of the features that are static and will be repeated when forecasting.
dropnaboolTrueDrop rows with missing values produced by the transformations.
keep_last_ntyping.Optional[int]NoneKeep only these many records from each serie for the forecasting step. Can save time and memory if your features allow it.
datatyping.Optional[~AnyDataFrame]None
ReturnsAnyDataFramenoqa: ARG002
+
+
features_ddf = fcst.preprocess(partitioned_series)
+features_ddf.head()
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
unique_iddsystatic_0static_1lag7expanding_mean_lag1rolling_mean_lag7_window_size14
20id_002000-10-2549.766844794550.69463925.00136726.320060
21id_002000-10-263.91834779453.88778026.18067526.313387
22id_002000-10-279.437778794511.51277425.16875126.398056
23id_002000-10-2817.923574794518.03849824.48479626.425272
24id_002000-10-2926.754645794524.22285924.21141126.305563
+ +
+
+
+

This is useful if we want to inspect the data the model will be trained. If we do this we must manually train our models and add a local version of them to the models_ attribute.

+
+
X, y = features_ddf.drop(columns=['unique_id', 'ds', 'y']), features_ddf['y']
+model = DaskXGBForecast(random_state=0).fit(X, y)
+fcst.models_ = {'DaskXGBForecast': model.model_}
+fcst.predict(7)
+
+
+

Dynamic features

+

By default the predict method repeats the static features and updates the transformations and the date features. If you have dynamic features like prices or a calendar with holidays you can pass them as a list to the dynamic_dfs argument of DistributedMLForecast.predict, which will call pd.DataFrame.merge on each of them in order.

+

Here’s an example:

+

Suppose that we have a product_id column and we have a catalog for prices based on that product_id and the date.

+
+
dynamic_series = series.rename(columns={'static_1': 'product_id'})
+prices_catalog = generate_prices_for_series(dynamic_series)
+prices_catalog
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
dsproduct_idprice
02000-06-0910.548814
12000-06-1010.715189
22000-06-1110.602763
32000-06-1210.544883
42000-06-1310.423655
............
201802001-05-17990.223520
201812001-05-18990.446104
201822001-05-19990.044783
201832001-05-20990.483216
201842001-05-21990.799660
+ +

20185 rows × 3 columns

+
+
+
+

And you have already merged these prices into your series dataframe.

+
+
dynamic_series = partitioned_series.rename(columns={'static_1': 'product_id'})
+dynamic_series = dynamic_series
+series_with_prices = dynamic_series.merge(prices_catalog, how='left')
+series_with_prices.head()
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
unique_iddsystatic_0product_idprice
0id_002000-10-053.98119879450.570826
1id_002000-10-0610.32740179450.260562
2id_002000-10-0717.65747479450.274048
3id_002000-10-0825.89879079450.433878
4id_002000-10-0934.49404079450.653738
+ +
+
+
+

This dataframe will be passed to DistributedMLForecast.fit (or DistributedMLForecast.preprocess), however since the price is dynamic we have to tell that method that only static_0 and product_id are static and we’ll have to update price in every timestep, which basically involves merging the updated features with the prices catalog.

+
+
fcst = DistributedMLForecast(
+    models,
+    freq='D',
+    lags=[7],
+    lag_transforms={
+        1: [expanding_mean],
+        7: [(rolling_mean, 14)]
+    },
+    date_features=['dayofweek', 'month'],
+    num_threads=1,
+)
+series_with_prices = series_with_prices
+fcst.fit(
+    series_with_prices,
+    static_features=['static_0', 'product_id'],
+)
+
+

So in order to update the price in each timestep we just call DistributedMLForecast.predict with our forecast horizon and pass the prices catalog as a dynamic dataframe.

+
+
preds = fcst.predict(7, dynamic_dfs=[prices_catalog])
+preds.compute()
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
unique_iddsDaskXGBForecastDaskLGBMForecast
0id_002001-05-1542.40400343.094384
1id_002001-05-1650.45744749.880064
2id_002001-05-172.0443981.938665
3id_002001-05-1810.10240310.250496
4id_002001-05-1918.24554318.473560
...............
72id_992001-05-1743.53634644.494822
73id_992001-05-182.0635842.093080
74id_992001-05-199.0162129.148367
75id_992001-05-2015.63031615.048958
76id_992001-05-2122.42023323.037681
+ +

700 rows × 4 columns

+
+
+
+
+
+

Custom predictions

+

If you want to do something like scaling the predictions you can define a function and pass it to DistributedMLForecast.predict as described in Custom predictions.

+
+

Cross validation

+

Refer to MLForecast.cross_validation.

+
+
+
+
+
+

DistributedMLForecast.cross_validation

+
+
 DistributedMLForecast.cross_validation (df:~AnyDataFrame, n_windows:int,
+                                         h:int, id_col:str='unique_id',
+                                         time_col:str='ds',
+                                         target_col:str='y',
+                                         step_size:Optional[int]=None, sta
+                                         tic_features:Optional[List[str]]=
+                                         None, dropna:bool=True,
+                                         keep_last_n:Optional[int]=None,
+                                         refit:bool=True, before_predict_c
+                                         allback:Optional[Callable]=None, 
+                                         after_predict_callback:Optional[C
+                                         allable]=None,
+                                         input_size:Optional[int]=None, da
+                                         ta:Optional[~AnyDataFrame]=None,
+                                         window_size:Optional[int]=None)
+
+

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.

+ ++++++ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
TypeDefaultDetails
dfAnyDataFrameSeries data in long format.
n_windowsintNumber of windows to evaluate.
hintNumber of test periods in each window.
id_colstrunique_idColumn that identifies each serie.
time_colstrdsColumn that identifies each timestep, its values can be timestamps or integers.
target_colstryColumn that contains the target.
step_sizetyping.Optional[int]NoneStep size between each cross validation window. If None it will be equal to h.
static_featurestyping.Optional[typing.List[str]]NoneNames of the features that are static and will be repeated when forecasting.
dropnaboolTrueDrop rows with missing values produced by the transformations.
keep_last_ntyping.Optional[int]NoneKeep only these many records from each serie for the forecasting step. Can save time and memory if your features allow it.
refitboolTrueRetrain model for each cross validation window.
If False, the models are trained at the beginning and then used to predict each window.
before_predict_callbacktyping.Optional[typing.Callable]NoneFunction 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_callbacktyping.Optional[typing.Callable]NoneFunction 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_sizetyping.Optional[int]NoneMaximum training samples per serie in each window. If None, will use an expanding window.
datatyping.Optional[~AnyDataFrame]None
window_sizetyping.Optional[int]None
ReturnsAnyDataFramenoqa: ARG002
noqa: ARG002
+
+
fcst = DistributedMLForecast(
+    models=[DaskLGBMForecast(), DaskXGBForecast()],
+    freq='D',
+    lags=[7],
+    lag_transforms={
+        1: [expanding_mean],
+        7: [(rolling_mean, 14)]
+    },
+    date_features=['dayofweek', 'month'],
+    num_threads=1,
+)
+
+
+
n_windows = 2
+window_size = 14
+
+cv_results = fcst.cross_validation(
+    partitioned_series,
+    n_windows,
+    window_size,
+)
+cv_results
+
+

We can aggregate these by date to get a rough estimate of how our model is doing.

+
+
agg_results = cv_results_df.drop(columns='cutoff').groupby('ds').mean()
+agg_results.head()
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
DaskLGBMForecastDaskXGBForecasty
ds
2001-04-1716.19523016.16870916.123231
2001-04-1815.14531815.13573415.213920
2001-04-1917.14911917.08715016.985699
2001-04-2018.00278118.04509218.068340
2001-04-2118.13661218.14214418.200609
+ +
+
+
+

We can also compute the error for each model.

+
+
def mse_from_dask_dataframe(ddf):
+    mses = {}
+    for model_name in ddf.columns.drop(['unique_id', 'ds', 'y', 'cutoff']):
+        mses[model_name] = (ddf['y'] - ddf[model_name]).pow(2).mean()
+    return client.gather(client.compute(mses))
+
+{k: round(v, 2) for k, v in mse_from_dask_dataframe(cv_results).items()}
+
+
{'DaskLGBMForecast': 0.92, 'DaskXGBForecast': 0.86}
+
+
+
+
client.close()
+
+
+
+

Spark

+
+

Session setup

+
+
from pyspark.sql import SparkSession
+
+
+
spark = (
+    SparkSession.builder.appName("MyApp")
+    .config("spark.jars.packages", "com.microsoft.azure:synapseml_2.12:0.10.2")
+    .config("spark.jars.repositories", "https://mmlspark.azureedge.net/maven")
+    .getOrCreate()
+)
+
+
+
+

Data setup

+

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.

+
+
numPartitions = 4
+series = generate_daily_series(100, n_static_features=2, equal_ends=True, static_as_categorical=False)
+spark_series = spark.createDataFrame(series).repartitionByRange(numPartitions, 'unique_id')
+
+
+
+

Models

+

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.

+
+
from mlforecast.distributed.models.spark.lgb import SparkLGBMForecast
+
+models = [SparkLGBMForecast()]
+try:
+    from xgboost.spark import SparkXGBRegressor
+    from mlforecast.distributed.models.spark.xgb import SparkXGBForecast
+    models.append(SparkXGBForecast())
+except ModuleNotFoundError:  # py < 38
+    pass
+
+
+
+

Training

+
+
fcst = DistributedMLForecast(
+    models,
+    freq='D',
+    lags=[1],
+    lag_transforms={
+        1: [expanding_mean]
+    },
+    date_features=['dayofweek'],
+)
+fcst.fit(
+    spark_series,
+    static_features=['static_0', 'static_1'],
+)
+
+
+
+

Forecasting

+
+
preds = fcst.predict(14)
+
+
+
preds.toPandas()
+
+
                                                                                
+
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
unique_iddsSparkLGBMForecastSparkXGBForecast
0id_002001-05-1542.21398442.305004
1id_002001-05-1649.71802150.262386
2id_002001-05-171.3062481.912686
3id_002001-05-1810.06010410.240939
4id_002001-05-1918.07078518.265749
...............
1395id_992001-05-2443.42690143.780163
1396id_992001-05-251.3616802.097803
1397id_992001-05-268.7872838.593580
1398id_992001-05-2715.55196515.622238
1399id_992001-05-2822.51851822.943216
+ +

1400 rows × 4 columns

+
+
+
+
+
+

Cross validation

+
+
cv_res = fcst.cross_validation(
+    spark_series,
+    n_windows=2,
+    window_size=14,
+).toPandas()
+
+
+
cv_res
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
unique_iddsSparkLGBMForecastSparkXGBForecastcutoffy
0id_172001-04-3031.46784931.6763362001-04-1630.832464
1id_072001-04-171.0154291.0393122001-04-161.034871
2id_062001-04-2921.1339191.3680222001-04-160.944155
3id_112001-04-1757.06901357.5915262001-04-1657.406090
4id_122001-04-277.9655857.7412582001-04-168.498222
.....................
2795id_962001-05-129.0695988.9251492001-04-307.983343
2796id_842001-05-0410.4746239.9598462001-04-3010.683266
2797id_872001-05-072.1623162.0654322001-04-301.277810
2798id_802001-05-1122.67955220.5477852001-04-3019.823192
2799id_902001-05-0840.22544840.2934192001-04-3039.215204
+ +

2800 rows × 6 columns

+
+
+
+
+
spark.stop()
+
+
+
+
+

Ray

+
+

Session setup

+
+
import ray
+from ray.cluster_utils import Cluster
+
+
+
ray_cluster = Cluster(
+    initialize_head=True,
+    head_node_args={"num_cpus": 2}
+)
+ray.init(address=ray_cluster.address, ignore_reinit_error=True)
+# add mock node to simulate a cluster
+mock_node = ray_cluster.add_node(num_cpus=2)
+
+
+
+

Data setup

+

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
+
+models = [
+    RayLGBMForecast(),
+    RayXGBForecast(),
+]
+
+
+
+

Training

+

To control the number of partitions to use using Ray, we have to include num_partitions to DistributedMLForecast.

+
+
num_partitions = 4
+
+
+
fcst = DistributedMLForecast(
+    models,
+    freq='D',
+    lags=[1],
+    lag_transforms={
+        1: [expanding_mean]
+    },
+    date_features=['dayofweek'],
+    num_partitions=num_partitions, # Use num_partitions to reduce overhead
+)
+fcst.fit(
+    ray_series,
+    static_features=['static_0', 'static_1'],
+)
+
+
+
+

Forecasting

+
+
preds = fcst.predict(14)
+
+
+
preds.to_pandas()
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
unique_iddsRayLGBMForecastRayXGBForecast
0id_002001-05-1542.21398441.992321
1id_002001-05-1649.71802150.999878
2id_002001-05-171.3062481.712625
3id_002001-05-1810.06010410.157331
4id_002001-05-1918.07078518.163649
...............
1395id_992001-05-2443.42690142.060478
1396id_992001-05-251.3616802.587303
1397id_992001-05-268.7872838.652343
1398id_992001-05-2715.55196515.278493
1399id_992001-05-2822.51851822.898369
+ +

1400 rows × 4 columns

+
+
+
+
+
+

Cross validation

+
+
cv_res = fcst.cross_validation(
+    ray_series,
+    n_windows=2,
+    window_size=14,
+).to_pandas()
+
+
+
cv_res
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
unique_iddsRayLGBMForecastRayXGBForecastcutoffy
0id_002001-04-1741.39594841.9682012001-04-1640.499332
1id_002001-04-1850.00467050.1917042001-04-1650.888323
2id_002001-04-191.8211051.9786452001-04-160.121812
3id_002001-04-2010.26645910.2116972001-04-1610.987977
4id_002001-04-2118.28540017.9443682001-04-1616.370385
.....................
2795id_692001-05-072.1517521.9717452001-04-300.768383
2796id_822001-05-0929.73349229.8446852001-04-3029.584063
2797id_802001-05-0314.97861115.5644032001-04-3014.888339
2798id_282001-05-1018.20401716.6448822001-04-3016.512652
2799id_932001-05-0129.75617329.0355992001-04-3029.340027
+ +

2800 rows × 6 columns

+
+
+
+
+
ray.shutdown()
+
+ + +
+
+ +

Give us a ⭐ on Github

+ +
+ + + + \ No newline at end of file diff --git a/distributed.models.dask.lgb.html b/distributed.models.dask.lgb.html new file mode 100644 index 00000000..b8eee20a --- /dev/null +++ b/distributed.models.dask.lgb.html @@ -0,0 +1,708 @@ + + + + + + + + + + +mlforecast - DaskLGBMForecast + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+ + +
+ +
+ + +
+ + + +
+ +
+
+

DaskLGBMForecast

+
+ +
+
+ dask LightGBM forecaster +
+
+ + +
+ + + + +
+ + +
+ + +

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.

+
+
+

DaskLGBMForecast

+
+
 DaskLGBMForecast (boosting_type:str='gbdt', num_leaves:int=31,
+                   max_depth:int=-1, learning_rate:float=0.1,
+                   n_estimators:int=100, subsample_for_bin:int=200000, obj
+                   ective:Union[str,Callable[[Optional[numpy.ndarray],nump
+                   y.ndarray],Tuple[numpy.ndarray,numpy.ndarray]],Callable
+                   [[Optional[numpy.ndarray],numpy.ndarray,Optional[numpy.
+                   ndarray]],Tuple[numpy.ndarray,numpy.ndarray]],Callable[
+                   [Optional[numpy.ndarray],numpy.ndarray,Optional[numpy.n
+                   darray],Optional[numpy.ndarray]],Tuple[numpy.ndarray,nu
+                   mpy.ndarray]],NoneType]=None,
+                   class_weight:Union[dict,str,NoneType]=None,
+                   min_split_gain:float=0.0, min_child_weight:float=0.001,
+                   min_child_samples:int=20, subsample:float=1.0,
+                   subsample_freq:int=0, colsample_bytree:float=1.0,
+                   reg_alpha:float=0.0, reg_lambda:float=0.0, random_state
+                   :Union[int,numpy.random.mtrand.RandomState,NoneType]=No
+                   ne, n_jobs:Optional[int]=None,
+                   importance_type:str='split',
+                   client:Optional[distributed.client.Client]=None,
+                   **kwargs:Any)
+
+

Distributed version of lightgbm.LGBMRegressor.

+ + +
+ +

Give us a ⭐ on Github

+ +
+ + + + \ No newline at end of file diff --git a/distributed.models.dask.xgb.html b/distributed.models.dask.xgb.html new file mode 100644 index 00000000..2135f798 --- /dev/null +++ b/distributed.models.dask.xgb.html @@ -0,0 +1,991 @@ + + + + + + + + + + +mlforecast - DaskXGBForecast + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+ + +
+ +
+ + +
+ + + +
+ +
+
+

DaskXGBForecast

+
+ +
+
+ dask XGBoost forecaster +
+
+ + +
+ + + + +
+ + +
+ + +

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.

+
+
+

DaskXGBForecast

+
+
 DaskXGBForecast (max_depth:Optional[int]=None,
+                  max_leaves:Optional[int]=None,
+                  max_bin:Optional[int]=None,
+                  grow_policy:Optional[str]=None,
+                  learning_rate:Optional[float]=None,
+                  n_estimators:Optional[int]=None,
+                  verbosity:Optional[int]=None, objective:Union[str,Callab
+                  le[[numpy.ndarray,numpy.ndarray],Tuple[numpy.ndarray,num
+                  py.ndarray]],NoneType]=None, booster:Optional[str]=None,
+                  tree_method:Optional[str]=None,
+                  n_jobs:Optional[int]=None, gamma:Optional[float]=None,
+                  min_child_weight:Optional[float]=None,
+                  max_delta_step:Optional[float]=None,
+                  subsample:Optional[float]=None,
+                  sampling_method:Optional[str]=None,
+                  colsample_bytree:Optional[float]=None,
+                  colsample_bylevel:Optional[float]=None,
+                  colsample_bynode:Optional[float]=None,
+                  reg_alpha:Optional[float]=None,
+                  reg_lambda:Optional[float]=None,
+                  scale_pos_weight:Optional[float]=None,
+                  base_score:Optional[float]=None, random_state:Union[nump
+                  y.random.mtrand.RandomState,int,NoneType]=None,
+                  missing:float=nan, num_parallel_tree:Optional[int]=None,
+                  monotone_constraints:Union[Dict[str,int],str,NoneType]=N
+                  one, interaction_constraints:Union[str,Sequence[Sequence
+                  [str]],NoneType]=None,
+                  importance_type:Optional[str]=None,
+                  device:Optional[str]=None,
+                  validate_parameters:Optional[bool]=None,
+                  enable_categorical:bool=False,
+                  feature_types:Optional[Sequence[str]]=None,
+                  max_cat_to_onehot:Optional[int]=None,
+                  max_cat_threshold:Optional[int]=None,
+                  multi_strategy:Optional[str]=None,
+                  eval_metric:Union[str,List[str],Callable,NoneType]=None,
+                  early_stopping_rounds:Optional[int]=None, callbacks:Opti
+                  onal[List[xgboost.callback.TrainingCallback]]=None,
+                  **kwargs:Any)
+
+

Implementation of the Scikit-Learn API for XGBoost. See :doc:/python/sklearn_estimator for more information.

+ ++++++ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
TypeDefaultDetails
max_depthtyping.Optional[int]NoneMaximum tree depth for base learners.
max_leavestyping.Optional[int]NoneMaximum number of leaves; 0 indicates no limit.
max_bintyping.Optional[int]NoneIf using histogram-based algorithm, maximum number of bins per feature
grow_policytyping.Optional[str]NoneTree growing policy. 0: favor splitting at nodes closest to the node, i.e. grow
depth-wise. 1: favor splitting at nodes with highest loss change.
learning_ratetyping.Optional[float]NoneBoosting learning rate (xgb’s “eta”)
n_estimatorstyping.Optional[int]NoneNumber of gradient boosted trees. Equivalent to number of boosting
rounds.
verbositytyping.Optional[int]NoneThe degree of verbosity. Valid values are 0 (silent) - 3 (debug).
objectivetyping.Union[str, typing.Callable[[numpy.ndarray, numpy.ndarray], typing.Tuple[numpy.ndarray, numpy.ndarray]], NoneType]NoneSpecify the learning task and the corresponding learning objective or
a custom objective function to be used (see note below).
boostertyping.Optional[str]None
tree_methodtyping.Optional[str]None
n_jobstyping.Optional[int]NoneNumber 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.
gammatyping.Optional[float]None(min_split_loss) Minimum loss reduction required to make a further partition on a
leaf node of the tree.
min_child_weighttyping.Optional[float]NoneMinimum sum of instance weight(hessian) needed in a child.
max_delta_steptyping.Optional[float]NoneMaximum delta step we allow each tree’s weight estimation to be.
subsampletyping.Optional[float]NoneSubsample ratio of the training instance.
sampling_methodtyping.Optional[str]NoneSampling 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_bytreetyping.Optional[float]NoneSubsample ratio of columns when constructing each tree.
colsample_byleveltyping.Optional[float]NoneSubsample ratio of columns for each level.
colsample_bynodetyping.Optional[float]NoneSubsample ratio of columns for each split.
reg_alphatyping.Optional[float]NoneL1 regularization term on weights (xgb’s alpha).
reg_lambdatyping.Optional[float]NoneL2 regularization term on weights (xgb’s lambda).
scale_pos_weighttyping.Optional[float]NoneBalancing of positive and negative weights.
base_scoretyping.Optional[float]NoneThe initial prediction score of all instances, global bias.
random_statetyping.Union[numpy.random.mtrand.RandomState, int, NoneType]NoneRandom number seed.

.. note::

Using gblinear booster with shotgun updater is nondeterministic as
it uses Hogwild algorithm.
missingfloatnanValue in the data which needs to be present as a missing value.
num_parallel_treetyping.Optional[int]None
monotone_constraintstyping.Union[typing.Dict[str, int], str, NoneType]NoneConstraint of variable monotonicity. See :doc:tutorial </tutorials/monotonic>
for more information.
interaction_constraintstyping.Union[str, typing.Sequence[typing.Sequence[str]], NoneType]NoneConstraints 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_typetyping.Optional[str]None
devicetyping.Optional[str]None.. versionadded:: 2.0.0

Device ordinal, available options are cpu, cuda, and gpu.
validate_parameterstyping.Optional[bool]NoneGive warnings for unknown parameter.
enable_categoricalboolFalse.. 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_typestyping.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_onehottyping.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_thresholdtyping.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_strategytyping.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.
eval_metrictyping.Union[str, typing.List[str], typing.Callable, NoneType]None.. versionadded:: 1.6.0

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_roundstyping.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.
callbackstyping.Optional[typing.List[xgboost.callback.TrainingCallback]]NoneList 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)
kwargstyping.AnyKeyword 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.
ReturnsNone
+ + +
+ +

Give us a ⭐ on Github

+ +
+ + + + \ No newline at end of file diff --git a/distributed.models.ray.lgb.html b/distributed.models.ray.lgb.html new file mode 100644 index 00000000..650a81b7 --- /dev/null +++ b/distributed.models.ray.lgb.html @@ -0,0 +1,706 @@ + + + + + + + + + + +mlforecast - RayLGBMForecast + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+ + +
+ +
+ + +
+ + + +
+ +
+
+

RayLGBMForecast

+
+ +
+
+ ray LightGBM forecaster +
+
+ + +
+ + + + +
+ + +
+ + +

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.

+
+
+

RayLGBMForecast

+
+
 RayLGBMForecast (boosting_type:str='gbdt', num_leaves:int=31,
+                  max_depth:int=-1, learning_rate:float=0.1,
+                  n_estimators:int=100, subsample_for_bin:int=200000, obje
+                  ctive:Union[str,Callable[[Optional[numpy.ndarray],numpy.
+                  ndarray],Tuple[numpy.ndarray,numpy.ndarray]],Callable[[O
+                  ptional[numpy.ndarray],numpy.ndarray,Optional[numpy.ndar
+                  ray]],Tuple[numpy.ndarray,numpy.ndarray]],Callable[[Opti
+                  onal[numpy.ndarray],numpy.ndarray,Optional[numpy.ndarray
+                  ],Optional[numpy.ndarray]],Tuple[numpy.ndarray,numpy.nda
+                  rray]],NoneType]=None,
+                  class_weight:Union[Dict,str,NoneType]=None,
+                  min_split_gain:float=0.0, min_child_weight:float=0.001,
+                  min_child_samples:int=20, subsample:float=1.0,
+                  subsample_freq:int=0, colsample_bytree:float=1.0,
+                  reg_alpha:float=0.0, reg_lambda:float=0.0, random_state:
+                  Union[int,numpy.random.mtrand.RandomState,NoneType]=None
+                  , n_jobs:Optional[int]=None,
+                  importance_type:str='split', **kwargs)
+
+

PublicAPI (beta): This API is in beta and may change before becoming stable.

+ + +
+ +

Give us a ⭐ on Github

+ +
+ + + + \ No newline at end of file diff --git a/distributed.models.ray.xgb.html b/distributed.models.ray.xgb.html new file mode 100644 index 00000000..12b65d39 --- /dev/null +++ b/distributed.models.ray.xgb.html @@ -0,0 +1,727 @@ + + + + + + + + + + +mlforecast - RayXGBForecast + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+ + +
+ +
+ + +
+ + + +
+ +
+
+

RayXGBForecast

+
+ +
+
+ ray XGBoost forecaster +
+
+ + +
+ + + + +
+ + +
+ + +

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.

+
+
+

RayXGBForecast

+
+
 RayXGBForecast (objective:Union[str,Callable[[numpy.ndarray,numpy.ndarray
+                 ],Tuple[numpy.ndarray,numpy.ndarray]],NoneType]='reg:squa
+                 rederror', **kwargs:Any)
+
+

Implementation of the scikit-learn API for Ray-distributed XGBoost regression. See :doc:/python/sklearn_estimator for more information.

+ ++++++ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
TypeDefaultDetails
objectivetyping.Union[str, typing.Callable[[numpy.ndarray, numpy.ndarray], typing.Tuple[numpy.ndarray, numpy.ndarray]], NoneType]reg:squarederrorSpecify the learning task and the corresponding learning objective or
a custom objective function to be used (see note below).
kwargstyping.AnyKeyword 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
ReturnsNone
+ + +
+ +

Give us a ⭐ on Github

+ +
+ + + + \ No newline at end of file diff --git a/distributed.models.spark.lgb.html b/distributed.models.spark.lgb.html new file mode 100644 index 00000000..cfef2a9d --- /dev/null +++ b/distributed.models.spark.lgb.html @@ -0,0 +1,689 @@ + + + + + + + + + + +mlforecast - SparkLGBMForecast + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+ + +
+ +
+ + +
+ + + +
+ +
+
+

SparkLGBMForecast

+
+ +
+
+ spark LightGBM forecaster +
+
+ + +
+ + + + +
+ + +
+ + +

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.

+ + +
+ +

Give us a ⭐ on Github

+ +
+ + + + \ No newline at end of file diff --git a/distributed.models.spark.xgb.html b/distributed.models.spark.xgb.html new file mode 100644 index 00000000..37b4ff53 --- /dev/null +++ b/distributed.models.spark.xgb.html @@ -0,0 +1,707 @@ + + + + + + + + + + +mlforecast - SparkXGBForecast + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+ + +
+ +
+ + +
+ + + +
+ +
+
+

SparkXGBForecast

+
+ +
+
+ spark XGBoost forecaster +
+
+ + +
+ + + + +
+ + +
+ + +

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.

+
/opt/hostedtoolcache/Python/3.9.18/x64/lib/python3.9/site-packages/fastcore/docscrape.py:225: UserWarning: Unknown section Note
+  else: warn(msg)
+/opt/hostedtoolcache/Python/3.9.18/x64/lib/python3.9/site-packages/fastcore/docscrape.py:225: UserWarning: Unknown section Examples
+  else: warn(msg)
+
+
+

SparkXGBForecast

+
+
 SparkXGBForecast (features_col:Union[str,List[str]]='features',
+                   label_col:str='label', prediction_col:str='prediction',
+                   pred_contrib_col:Optional[str]=None,
+                   validation_indicator_col:Optional[str]=None,
+                   weight_col:Optional[str]=None,
+                   base_margin_col:Optional[str]=None, num_workers:int=1,
+                   use_gpu:Optional[bool]=None, device:Optional[str]=None,
+                   force_repartition:bool=False,
+                   repartition_random_shuffle:bool=False,
+                   enable_sparse_data_optim:bool=False, **kwargs:Any)
+
+

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.

+ + +
+ +

Give us a ⭐ on Github

+ +
+ + + + \ No newline at end of file diff --git a/docs/getting-started/end_to_end_walkthrough.html b/docs/getting-started/end_to_end_walkthrough.html new file mode 100644 index 00000000..87e7cfb9 --- /dev/null +++ b/docs/getting-started/end_to_end_walkthrough.html @@ -0,0 +1,2251 @@ + + + + + + + + + + +mlforecast - End to end walkthrough + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+ + +
+ +
+ + +
+ + + +
+ +
+
+

End to end walkthrough

+
+ +
+
+ Detailed description of all the functionalities that MLForecast provides. +
+
+ + +
+ + + + +
+ + +
+ + +
+

Data setup

+

For this example we’ll use a subset of the M4 hourly dataset. You can find the a notebook with the full dataset here.

+
+
import random
+
+import pandas as pd
+from datasetsforecast.m4 import M4
+from utilsforecast.plotting import plot_series
+
+
+
await M4.async_download('data', group='Hourly')
+df, *_ = M4.load('data', 'Hourly')
+uids = df['unique_id'].unique()
+random.seed(0)
+sample_uids = random.choices(uids, k=4)
+df = df[df['unique_id'].isin(sample_uids)].reset_index(drop=True)
+df['ds'] = df['ds'].astype('int64')
+df
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
unique_iddsy
0H196111.8
1H196211.4
2H196311.1
3H196410.8
4H196510.6
............
4027H413100499.0
4028H413100588.0
4029H413100647.0
4030H413100741.0
4031H413100834.0
+ +

4032 rows × 3 columns

+
+
+
+
+
+

EDA

+

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_iddsy
24H196250.3
25H196260.3
26H196270.1
27H196280.2
28H196290.2
............
4027H413100439.0
4028H413100555.0
4029H413100614.0
4030H41310073.0
4031H41310084.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.

+
+
fcst = MLForecast(
+    models=[],
+    freq=1,
+    lags=[1, 24],
+    target_transforms=[Differences([24])],    
+)
+prep = fcst.preprocess(df)
+prep
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
unique_iddsylag1lag24
48H196490.10.10.3
49H196500.10.10.3
50H196510.20.10.1
51H196520.10.20.2
52H196530.10.10.2
..................
4027H413100439.029.01.0
4028H413100555.039.0-25.0
4029H413100614.055.0-20.0
4030H41310073.014.00.0
4031H41310084.03.0-16.0
+ +

3840 rows × 5 columns

+
+
+
+
+
prep.drop(columns=['unique_id', 'ds']).corr()['y']
+
+
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:

+
    +
  • supply a tuple (tfm_func, arg1, arg2, …)
  • +
  • define a new function fixing the arguments
  • +
+
+
from numba import njit
+from window_ops.expanding import expanding_mean
+from window_ops.rolling import rolling_mean
+
+
+
@njit
+def rolling_mean_48(x):
+    return rolling_mean(x, window_size=48)
+
+
+fcst = MLForecast(
+    models=[],
+    freq=1,
+    target_transforms=[Differences([24])],    
+    lag_transforms={
+        1: [expanding_mean],
+        24: [(rolling_mean, 48), rolling_mean_48],
+    },
+)
+prep = fcst.preprocess(df)
+prep
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
unique_iddsyexpanding_mean_lag1rolling_mean_lag24_window_size48rolling_mean_48_lag24
95H196960.10.1746480.1500000.150000
96H196970.30.1736110.1458330.145833
97H196980.30.1753420.1416670.141667
98H196990.30.1770270.1416670.141667
99H1961000.30.1786670.1416670.141667
.....................
4027H413100439.00.2420843.4375003.437500
4028H413100555.00.2816332.7083332.708333
4029H413100614.00.3374112.1250002.125000
4030H41310073.00.3513241.7708331.770833
4031H41310084.00.3540181.2083331.208333
+ +

3652 rows × 6 columns

+
+
+
+

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.

+
+
def hour_index(times):
+    return times % 24
+
+fcst = MLForecast(
+    models=[],
+    freq=1,
+    target_transforms=[Differences([24])],
+    date_features=[hour_index],
+)
+fcst.preprocess(df)
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
unique_iddsyhour_index
24H196250.31
25H196260.32
26H196270.13
27H196280.24
28H196290.25
...............
4027H413100439.020
4028H413100555.021
4029H413100614.022
4030H41310073.023
4031H41310084.00
+ +

3936 rows × 4 columns

+
+
+
+
+
+

Target transformations

+

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
+
+
+
fcst = MLForecast(
+    models=[],
+    freq=1,
+    lags=[1],
+    target_transforms=[LocalStandardScaler()]
+)
+fcst.preprocess(df)
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
unique_iddsylag1
1H1962-1.493026-1.383286
2H1963-1.575331-1.493026
3H1964-1.657635-1.575331
4H1965-1.712505-1.657635
5H1966-1.794810-1.712505
...............
4027H41310043.0627662.425012
4028H41310052.5231283.062766
4029H41310060.5117512.523128
4030H41310070.2174030.511751
4031H4131008-0.1260030.217403
+ +

4028 rows × 4 columns

+
+
+
+

We can define a naive model to test this

+
+
from sklearn.base import BaseEstimator
+
+class Naive(BaseEstimator):
+    def fit(self, X, y):
+        return self
+
+    def predict(self, X):
+        return X['lag1']
+
+
+
fcst = MLForecast(
+    models=[Naive()],
+    freq=1,
+    lags=[1],
+    target_transforms=[LocalStandardScaler()]
+)
+fcst.fit(df)
+preds = fcst.predict(1)
+preds
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
unique_iddsNaive
0H196100916.8
1H256100913.4
2H3811009207.0
3H413100934.0
+ +
+
+
+

We compare this with the last values of our serie

+
+
last_vals = df.groupby('unique_id').tail(1)
+last_vals
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
unique_iddsy
1007H196100816.8
2015H256100813.4
3023H3811008207.0
4031H413100834.0
+ +
+
+
+
+
import numpy as np
+
+
+
np.testing.assert_allclose(preds['Naive'], last_vals['y'])
+
+
+
+
+

Training

+

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.

+
+
import lightgbm as lgb
+
+
+
lgb_params = {
+    'verbosity': -1,
+    'num_leaves': 512,
+}
+
+fcst = MLForecast(
+    models={
+        'avg': lgb.LGBMRegressor(**lgb_params),
+        'q75': lgb.LGBMRegressor(**lgb_params, objective='quantile', alpha=0.75),
+        'q25': lgb.LGBMRegressor(**lgb_params, objective='quantile', alpha=0.25),
+    },
+    freq=1,
+    target_transforms=[Differences([24])],
+    lags=[1, 24],
+    lag_transforms={
+        1: [expanding_mean],
+        24: [(rolling_mean, 48)],
+    },
+    date_features=[hour_index],
+)
+fcst.fit(df)
+
+
MLForecast(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)
+
+
+

This computed the features and trained three different models using them. We can now compute our forecasts.

+
+
+

Forecasting

+
+
preds = fcst.predict(48)
+preds
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
unique_iddsavgq75q25
0H196100916.29525716.38585916.320666
1H196101015.91028216.01272815.856905
2H196101115.72836715.78486715.656658
3H196101215.46841415.50322315.401462
4H196101315.08127915.16360615.048576
..................
187H4131052100.450617116.46189852.276952
188H413105388.426800114.25715850.866960
189H413105459.67573789.67252616.440738
190H413105557.58035684.68094314.248400
191H413105642.66987952.00055912.440984
+ +

192 rows × 5 columns

+
+
+
+
+
fig = plot_series(df, preds, max_insample_length=24 * 7)
+
+

+
+
+

Updating series’ values

+

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.
  • +
+
+
fcst = MLForecast(
+    models=[Naive()],
+    freq=1,
+    lags=[1, 2, 3],
+)
+fcst.fit(df)
+fcst.predict(1)
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
unique_iddsNaive
0H196100916.8
1H256100913.4
2H3811009207.0
3H413100934.0
+ +
+
+
+
+
new_values = pd.DataFrame({
+    'unique_id': ['H196', 'H256'],
+    'ds': [1009, 1009],
+    'y': [17.0, 14.0],
+})
+fcst.ts.update(new_values)
+preds = fcst.predict(1)
+preds
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
unique_iddsNaive
0H196101017.0
1H256101014.0
2H3811009207.0
3H413100934.0
+ +
+
+
+
+
+

Estimating model performance

+
+

Cross validation

+

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
+    window_size=48,  # length of the validation set in each window
+)
+cv_result
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
unique_iddscutoffyLGBMRegressor
0H19681781615.315.383165
1H19681881614.914.923219
2H19681981614.614.667834
3H19682081614.214.275964
4H19682181613.913.973491
..................
187H413100496099.065.644823
188H413100596088.071.717097
189H413100696047.076.704377
190H413100796041.053.446638
191H413100896034.054.902634
+ +

768 rows × 5 columns

+
+
+
+
+
fig = plot_series(cv_result, cv_result.drop(columns='cutoff'), max_insample_length=0)
+
+

+

We can compute the RMSE on each split.

+
+
from utilsforecast.losses import rmse
+
+
+
def evaluate_cv(df):
+    return rmse(df, models=['LGBMRegressor'], id_col='cutoff').set_index('cutoff')
+
+split_rmse = evaluate_cv(cv_result)
+split_rmse
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
LGBMRegressor
cutoff
81629.418172
86434.257598
91213.145763
96035.066261
+ +
+
+
+

And the average RMSE across splits.

+
+
split_rmse.mean()
+
+
LGBMRegressor    27.971949
+dtype: float64
+
+
+

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.

+
+
from window_ops.ewm import ewm_mean
+
+
+
fcst = MLForecast(
+    models=lgb.LGBMRegressor(**lgb_params),
+    freq=1,
+    lags=[1, 24],
+    lag_transforms={
+        1: [(ewm_mean, 0.5)],
+        24: [(rolling_mean, 48)],      
+    },
+    date_features=[hour_index],    
+)
+cv_result2 = fcst.cross_validation(
+    df,
+    n_windows=4,
+    window_size=48,
+)
+evaluate_cv(cv_result2).mean()
+
+
LGBMRegressor    25.874446
+dtype: float64
+
+
+
+
+

LightGBMCV

+

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.
  • +
+
+
from mlforecast.lgb_cv import LightGBMCV
+
+
+
cv = LightGBMCV(
+    freq=1,
+    target_transforms=[Differences([24])],
+    lags=[1, 24],
+    lag_transforms={
+        1: [expanding_mean],
+        24: [(rolling_mean, 48)],
+    },
+    date_features=[hour_index],
+    num_threads=2,
+)
+cv_hist = cv.fit(
+    df,
+    n_windows=4,
+    window_size=48,
+    params=lgb_params,
+    eval_every=5,
+    early_stopping_evals=5,    
+    compute_cv_preds=True,
+)
+
+
[5] mape: 0.158639
+[10] mape: 0.163739
+[15] mape: 0.161535
+[20] mape: 0.169491
+[25] mape: 0.163690
+[30] mape: 0.164198
+Early stopping at round 30
+Using best iteration: 5
+
+
+

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.

+
+
cv.cv_preds_
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
unique_iddsyBoosterwindow
0H19681715.315.4731820
1H19681814.915.0385710
2H19681914.614.8494090
3H19682014.214.4483790
4H19682113.914.1483790
..................
187H413100499.061.4253963
188H413100588.062.8868903
189H413100647.057.8868903
190H413100741.038.8490093
191H413100834.044.7205623
+ +

768 rows × 5 columns

+
+
+
+
+
fig = plot_series(cv.cv_preds_, cv.cv_preds_.drop(columns='window'), max_insample_length=0)
+
+

+

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:

+
+
final_fcst = MLForecast.from_cv(cv)
+final_fcst.fit(df)
+preds = final_fcst.predict(48)
+fig = plot_series(df, preds, max_insample_length=24 * 14)
+
+

+ + +
+
+ +

Give us a ⭐ on Github

+ +
+ + + + \ No newline at end of file diff --git a/docs/getting-started/install.html b/docs/getting-started/install.html new file mode 100644 index 00000000..6855b564 --- /dev/null +++ b/docs/getting-started/install.html @@ -0,0 +1,744 @@ + + + + + + + + + + +mlforecast - Install + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+ + +
+ +
+ + +
+ + + +
+ +
+
+

Install

+
+ +
+
+ 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.

+ + +
+ +

Give us a ⭐ on Github

+ +
+ + + + \ No newline at end of file diff --git a/docs/getting-started/quick_start_distributed.html b/docs/getting-started/quick_start_distributed.html new file mode 100644 index 00000000..bd8dbb93 --- /dev/null +++ b/docs/getting-started/quick_start_distributed.html @@ -0,0 +1,994 @@ + + + + + + + + + + +mlforecast - Quick start (distributed) + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+ + +
+ +
+ + +
+ + + +
+ +
+
+

Quick start (distributed)

+
+ +
+
+ Minimal example of distributed training with MLForecast +
+
+ + +
+ + + + +
+ + +
+ + +
+

Main concepts

+

The main component for distributed training with mlforecast is the DistributedMLForecast class, which abstracts away:

+
    +
  • Feature engineering and model training through DistributedMLForecast.fit
  • +
  • Feature updates and multi step ahead predictions through DistributedMLForecast.predict
  • +
+
+
+

Setup

+

In order to perform distributed training you need a dask cluster. In this example we’ll use a local cluster but you can replace it with any other type of remote cluster and the processing will take place there.

+
+
from dask.distributed import Client, LocalCluster
+
+
+
cluster = LocalCluster(n_workers=2, threads_per_worker=1)  # change this to use a remote cluster
+client = Client(cluster)
+
+
+
+

Data format

+

The data is expected to be a dask 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.
  • +
+

You need to make sure that each serie is only in a single partition. You can do so by setting the id_col as the index in dask or with repartitionByRange in spark.

+

Here we present an example with synthetic data.

+
+
import dask.dataframe as dd
+from mlforecast.utils import generate_daily_series
+
+
+
series = generate_daily_series(100, with_trend=True)
+series
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
unique_iddsy
0id_002000-01-010.497650
1id_002000-01-021.554489
2id_002000-01-032.734311
3id_002000-01-044.028039
4id_002000-01-055.366009
............
26998id_992000-06-2534.165302
26999id_992000-06-2628.277320
27000id_992000-06-2729.450129
27001id_992000-06-2830.241885
27002id_992000-06-2931.576907
+ +

27003 rows × 3 columns

+
+
+
+

Here we can see that the index goes from id_00 to id_99, which means we have 100 different series stacked together.

+

We also have the ds column that contains the timestamps, in this case with a daily frequency, and the y column that contains the series values in each timestamp.

+

In order to perform distributed processing and training we need to have these in a dask dataframe, this is typically done loading them directly in a distributed way, for example with dd.read_parquet.

+
+
series_ddf = dd.from_pandas(series.set_index('unique_id'), npartitions=2)  # make sure we split by id
+series_ddf = series_ddf.map_partitions(lambda part: part.reset_index())  # we can't have an index
+series_ddf['unique_id'] = series_ddf['unique_id'].astype('str') # categoricals aren't supported at the moment
+series_ddf
+
+
Dask DataFrame Structure:
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
unique_iddsy
npartitions=2
id_00objectdatetime64[ns]float64
id_49.........
id_99.........
+ +
+
Dask Name: assign, 5 graph layers
+
+
+

We now have a dask dataframe with two partitions which will be processed independently in each machine and their outputs will be combined to perform distributed training.

+
+
+

Modeling

+
+
from utilsforecast.plotting import plot_series
+
+
+
fig = plot_series(series, max_insample_length=70, max_ids=4)
+
+

+

We can see that the series have 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 LightGBM model using the value from the same day of the week at the previous week (lag 7) as a feature, this is done by passing lags=[7].

+
+
from mlforecast.distributed import DistributedMLForecast
+from mlforecast.distributed.models.dask.lgb import DaskLGBMForecast
+from mlforecast.target_transforms import Differences
+
+
+
fcst = DistributedMLForecast(
+    models=DaskLGBMForecast(verbosity=-1),
+    freq='D',
+    lags=[7],
+    target_transforms=[Differences([1])],
+)
+fcst.fit(series_ddf)
+
+

The previous line computed the features and trained the model, so now we’re ready to compute our forecasts.

+
+
+

Forecasting

+

Compute the forecast for the next 14 days.

+
+
preds = fcst.predict(14)
+preds
+
+
Dask DataFrame Structure:
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
unique_iddsDaskLGBMForecast
npartitions=2
id_00objectdatetime64[ns]float64
id_49.........
id_99.........
+ +
+
Dask Name: map, 17 graph layers
+
+
+

These are returned as a dask dataframe as well. If it’s safe (memory-wise) we can bring them to the main process.

+
+
local_preds = preds.compute()
+
+
+
+

Visualize results

+

We can visualize what our prediction looks like.

+
+
fig = plot_series(series, local_preds, max_ids=4, max_insample_length=70)
+
+

+

And that’s it! You’ve trained a distributed LightGBM model and computed predictions for the next 14 days.

+ + +
+ +

Give us a ⭐ on Github

+ +
+ + + + \ No newline at end of file diff --git a/docs/getting-started/quick_start_local.html b/docs/getting-started/quick_start_local.html new file mode 100644 index 00000000..5e858446 --- /dev/null +++ b/docs/getting-started/quick_start_local.html @@ -0,0 +1,940 @@ + + + + + + + + + + +mlforecast - Quick start (local) + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+ + +
+ +
+ + +
+ + + +
+ +
+
+

Quick start (local)

+
+ +
+
+ Minimal example of MLForecast +
+
+ + +
+ + + + +
+ + +
+ + +
+

Main concepts

+

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
+
+
+
df = pd.read_csv('https://datasets-nixtla.s3.amazonaws.com/air-passengers.csv', parse_dates=['ds'])
+df.head()
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
unique_iddsy
0AirPassengers1949-01-01112
1AirPassengers1949-02-01118
2AirPassengers1949-03-01132
3AirPassengers1949-04-01129
4AirPassengers1949-05-01121
+ +
+
+
+
+
df['unique_id'].value_counts()
+
+
AirPassengers    144
+Name: unique_id, dtype: int64
+
+
+

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].

+
+
from mlforecast import MLForecast
+from mlforecast.target_transforms import Differences
+from sklearn.linear_model import LinearRegression
+
+
+
fcst = MLForecast(
+    models=LinearRegression(),
+    freq='MS',  # our serie has a monthly frequency
+    lags=[12],
+    target_transforms=[Differences([1])],
+)
+fcst.fit(df)
+
+
MLForecast(models=[LinearRegression], freq=<MonthBegin>, lag_features=['lag12'], date_features=[], num_threads=1)
+
+
+

The previous line computed the features and trained the model, so now we’re ready to compute our forecasts.

+
+
+

Forecasting

+

Compute the forecast for the next 12 months

+
+
preds = fcst.predict(12)
+preds
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
unique_iddsLinearRegression
0AirPassengers1961-01-01444.656555
1AirPassengers1961-02-01417.470734
2AirPassengers1961-03-01446.903046
3AirPassengers1961-04-01491.014130
4AirPassengers1961-05-01502.622223
5AirPassengers1961-06-01568.751465
6AirPassengers1961-07-01660.044312
7AirPassengers1961-08-01643.343323
8AirPassengers1961-09-01540.666687
9AirPassengers1961-10-01491.462708
10AirPassengers1961-11-01417.095154
11AirPassengers1961-12-01461.206238
+ +
+
+
+
+
+

Visualize results

+

We can visualize what our prediction looks like.

+
+
fig = plot_series(df, preds)
+
+

+

And that’s it! You’ve trained a linear regression to predict the air passengers for 1961.

+ + +
+ +

Give us a ⭐ on Github

+ +
+ + + + \ No newline at end of file diff --git a/docs/how-to-guides/cross_validation.html b/docs/how-to-guides/cross_validation.html new file mode 100644 index 00000000..d1025274 --- /dev/null +++ b/docs/how-to-guides/cross_validation.html @@ -0,0 +1,1009 @@ + + + + + + + + + + +mlforecast - Cross validation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+ + +
+ +
+ + +
+ + + +
+ +
+
+

Cross validation

+
+ +
+
+ In this example, we’ll implement time series cross-validation to evaluate model’s performance. +
+
+ + +
+ + + + +
+ + +
+ + +
+ +
+
+

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:

+
    +
  1. Install libraries
  2. +
  3. Load and explore data
  4. +
  5. Train model
  6. +
  7. Perform time series cross-validation
  8. +
  9. Evaluate results
  10. +
+
+
+
+ +
+
+Tip +
+
+
+

You can use Colab to run this Notebook interactively Open In Colab

+
+
+
+
+

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_iddsy
0H11605.0
1H12586.0
2H13586.0
3H14559.0
4H15511.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.

+
+
fig = plot_series(Y_df, max_ids=4, plot_random=False, max_insample_length=24 * 14)
+
+

+
+
+

Define forecast object

+

For this example, we’ll use LightGBM. We first need to import it and then we need to instantiate a new MLForecast object.

+

The MLForecast object has the following parameters:

+
    +
  • models: a list of sklearn-like (fit and predict) models.
  • +
  • freq: a string indicating the frequency of the data. See panda’s available frequencies.
  • +
  • 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.
  • +
+

In this example, we are only using differences and lags to produce features. See the full documentation to see all available features.

+

Any settings are passed into the constructor. Then you call its fit method and pass in the historical data frame df.

+
+
import lightgbm as lgb
+from mlforecast.target_transforms import Differences
+
+
+
models = [lgb.LGBMRegressor(verbosity=-1)]
+
+mlf = MLForecast(
+    models = models, 
+    freq = 1,# our series have integer timestamps, so we'll just add 1 in every timeste, 
+    target_transforms=[Differences([24])],
+    lags=range(1, 25, 1)
+)
+
+
+
+

Perform time series cross-validation

+

Once the MLForecast object has been instantiated, we can use the cross_validation method, which takes the following arguments:

+
    +
  • df: training data frame with MLForecast format
  • +
  • h (int): represents the h steps into the future that will be forecasted
  • +
  • n_windows (int): number of windows used for cross-validation, meaning the number of forecasting processes in the past you want to evaluate.
  • +
  • id_col: identifies each time series.
  • +
  • time_col: indetifies the temporal column of the time series.
  • +
  • target_col: identifies the column to model.
  • +
+

For this particular example, we’ll use 3 windows of 24 hours.

+
+
crossvalidation_df = mlf.cross_validation(
+    df=Y_df,
+    h=24,
+    n_windows=3,
+)
+
+

The crossvaldation_df object is a new data frame that includes the following columns:

+
    +
  • unique_id: identifies each time series.
  • +
  • ds: datestamp or temporal index.
  • +
  • cutoff: the last datestamp or temporal index for the n_windows.
  • +
  • y: true value
  • +
  • "model": columns with the model’s name and fitted value.
  • +
+
+
crossvalidation_df.head()
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
unique_iddscutoffyLGBMRegressor
0H1677676691.0673.703191
1H1678676618.0552.306270
2H1679676563.0541.778027
3H1680676529.0502.778027
4H1681676504.0480.778027
+ +
+
+
+

We’ll now plot the forecast for each cutoff period.

+
+
import matplotlib.pyplot as plt
+
+
+
def plot_cv(df, df_cv, uid, fname, last_n=24 * 14):
+    cutoffs = df_cv.query('unique_id == @uid')['cutoff'].unique()
+    fig, ax = plt.subplots(nrows=len(cutoffs), ncols=1, figsize=(14, 6), gridspec_kw=dict(hspace=0.8))
+    for cutoff, axi in zip(cutoffs, ax.flat):
+        df.query('unique_id == @uid').tail(last_n).set_index('ds').plot(ax=axi, title=uid, y='y')
+        df_cv.query('unique_id == @uid & cutoff == @cutoff').set_index('ds').plot(ax=axi, title=uid, y='LGBMRegressor')
+    fig.savefig(fname, bbox_inches='tight')
+    plt.close()
+
+
+
plot_cv(Y_df, crossvalidation_df, 'H1', '../../figs/cross_validation__predictions.png')
+
+

+

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.

+
+
crossvalidation_df['id_cutoff'] = crossvalidation_df['unique_id'] + '_' + crossvalidation_df['cutoff'].astype(str)
+cv_rmse = rmse(crossvalidation_df, models=['LGBMRegressor'], id_col='id_cutoff')['LGBMRegressor'].mean()
+print("RMSE using cross-validation: ", cv_rmse)
+
+
RMSE using cross-validation:  249.90517171185527
+
+
+

This measure should better reflect the predictive abilities of our model, since it used different time periods to test its accuracy.

+
+
+

References

+

Rob J. Hyndman and George Athanasopoulos (2018). “Forecasting principles and practice, Time series cross-validation”.

+ + +
+ +

Give us a ⭐ on Github

+ +
+ + + + \ No newline at end of file diff --git a/docs/how-to-guides/prediction_intervals.html b/docs/how-to-guides/prediction_intervals.html new file mode 100644 index 00000000..8e08bde1 --- /dev/null +++ b/docs/how-to-guides/prediction_intervals.html @@ -0,0 +1,1249 @@ + + + + + + + + + + +mlforecast - Probabilistic forecasting + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+ + +
+ +
+ + +
+ + + +
+ +
+
+

Probabilistic forecasting

+
+ +
+
+ In this example, we’ll implement prediction intervals +
+
+ + +
+ + + + +
+ + +
+ + +
+ +
+
+

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:

+
    +
  1. Install libraries
  2. +
  3. Load and explore the data
  4. +
  5. Train models
  6. +
  7. Plot prediction intervals
  8. +
+
+
+
+ +
+
+Tip +
+
+
+

You can use Colab to run this Notebook interactively Open In Colab

+
+
+
+
+

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
+
+
+
train = pd.read_csv('https://auto-arima-results.s3.amazonaws.com/M4-Hourly.csv')
+test = pd.read_csv('https://auto-arima-results.s3.amazonaws.com/M4-Hourly-test.csv')
+
+
+
train.head()
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
unique_iddsy
0H11605.0
1H12586.0
2H13586.0
3H14559.0
4H15511.0
+ +
+
+
+
+
test.head()
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
unique_iddsy
0H1701619.0
1H1702565.0
2H1703532.0
3H1704495.0
4H1705481.0
+ +
+
+
+

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.
  • +
+
+
fig = plot_series(train, test.rename(columns={'y': 'y_test'}), models=['y_test'], plot_random=False)
+
+

+
+
+

Train models

+

MLForecast can train multiple models that follow the sklearn syntax (fit and predict) on different time series efficiently.

+

For this example, we’ll use the following sklearn baseline models:

+ +

To use these models, we first need to import them from sklearn and then we need to instantiate them.

+
+
from mlforecast import MLForecast
+from mlforecast.target_transforms import Differences
+from mlforecast.utils import PredictionIntervals
+from sklearn.linear_model import Lasso, LinearRegression, Ridge
+from sklearn.neighbors import KNeighborsRegressor
+from sklearn.neural_network import MLPRegressor
+
+
+
# 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 in range(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.
  • +
+
+
mlf.fit(
+    train,
+    prediction_intervals=PredictionIntervals(n_windows=10, h=48),
+);
+
+

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%.
  • +
+
+
levels = [50, 80, 95]
+forecasts = mlf.predict(48, level=levels)
+forecasts.head()
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
unique_iddsRidgeLassoLinearRegressionKNeighborsRegressorMLPRegressorRidge-lo-95Ridge-lo-80Ridge-lo-50...KNeighborsRegressor-lo-50KNeighborsRegressor-hi-50KNeighborsRegressor-hi-80KNeighborsRegressor-hi-95MLPRegressor-lo-95MLPRegressor-lo-80MLPRegressor-lo-50MLPRegressor-hi-50MLPRegressor-hi-80MLPRegressor-hi-95
0H1701612.418170612.418079612.418170615.2612.651532590.473256594.326570603.409944...609.45620.95627.20631.310584.736193591.084898597.462107627.840957634.218166640.566870
1H1702552.309298552.308073552.309298551.6548.791801498.721501518.433843532.710850...535.85567.35569.16597.525497.308756500.417799515.452396582.131207597.165804600.274847
2H1703494.943384494.943367494.943384509.6490.226796448.253304463.266064475.006125...492.70526.50530.92544.180424.587658436.042788448.682502531.771091544.410804555.865935
3H1704462.815779462.815363462.815779474.6459.619069409.975219422.243593436.128272...451.80497.40510.26525.500379.291083392.580306413.353178505.884959526.657832539.947054
4H1705440.141034440.140586440.141034451.6438.091712377.999588392.523016413.474795...427.40475.80488.96503.945348.618034362.503767386.303325489.880099513.679657527.565389
+ +

5 rows × 37 columns

+
+
+
+
+
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.

+
+

KNeighborsRegressor

+
+
fig = plot_series(
+    train, 
+    test, 
+    plot_random=False, 
+    models=['KNeighborsRegressor'], 
+    level=levels, 
+    max_insample_length=48
+)
+
+

+
+
+

Lasso

+
+
fig = plot_series(
+    train, 
+    test, 
+    plot_random=False, 
+    models=['Lasso'],
+    level=levels, 
+    max_insample_length=48
+)
+
+

+
+
+

LineaRegression

+
+
fig = plot_series(
+    train, 
+    test, 
+    plot_random=False, 
+    models=['LinearRegression'],
+    level=levels, 
+    max_insample_length=48
+)
+
+

+
+
+

MLPRegressor

+
+
fig = plot_series(
+    train, 
+    test, 
+    plot_random=False, 
+    models=['MLPRegressor'],
+    level=levels, 
+    max_insample_length=48
+)
+
+

+
+
+

Ridge

+
+
fig = plot_series(
+    train, 
+    test, 
+    plot_random=False, 
+    models=['Ridge'],
+    level=levels, 
+    max_insample_length=48
+)
+
+

+

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.

+
+
+
+

References

+ + + +
+ +

Give us a ⭐ on Github

+ +
+ + + + \ No newline at end of file diff --git a/docs/how-to-guides/target_transforms_guide.html b/docs/how-to-guides/target_transforms_guide.html new file mode 100644 index 00000000..e185a105 --- /dev/null +++ b/docs/how-to-guides/target_transforms_guide.html @@ -0,0 +1,907 @@ + + + + + + + + + + +mlforecast - Target transformations + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+ + +
+ +
+ + +
+ + + +
+ +
+
+

Target transformations

+
+ +
+
+ Seamlessly transform target values +
+
+ + +
+ + + + +
+ + +
+ + +

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
+
+
+
data_path = 'data'
+await M4.async_download(data_path, group='Hourly')
+df, *_ = M4.load(data_path, 'Hourly')
+df['ds'] = df['ds'].astype('int32')
+serie = df[df['unique_id'].eq('H196')]
+
+
+
+

Local transformations

+
+

Transformations applied per serie

+
+
+

Differences

+

We’ll take a look at our serie to see possible differences that would help our models.

+
+
def plot(series, fname):
+    n_series = len(series)
+    fig, ax = plt.subplots(ncols=n_series, figsize=(7 * n_series, 6), squeeze=False)
+    for (title, serie), axi in zip(series.items(), ax.flat):
+        serie.set_index('ds')['y'].plot(title=title, ax=axi)
+    fig.savefig(f'../../figs/{fname}', bbox_inches='tight')
+    plt.close()
+
+
+
plot({'original': serie}, 'target_transforms__eda.png')
+
+

+

We can see that our data has a trend as well as a clear seasonality. We can try removing the trend first.

+
+
fcst = MLForecast(
+    models=[],
+    freq=1,
+    target_transforms=[Differences([1])],
+)
+without_trend = fcst.preprocess(serie)
+plot({'original': serie, 'without trend': without_trend}, 'target_transforms__diff1.png')
+
+

+

The trend is gone, we can now try taking the 24 difference (subtract the value at the same hour in the previous day).

+
+
fcst = MLForecast(
+    models=[],
+    freq=1,
+    target_transforms=[Differences([1, 24])],
+)
+without_trend_and_seasonality = fcst.preprocess(serie)
+plot({'original': serie, 'without trend and seasonality': without_trend_and_seasonality}, 'target_transforms__diff2.png')
+
+

+
+
+

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.

+
+
fcst = MLForecast(
+    models=[],
+    freq=1,
+    target_transforms=[Differences([1, 24]), LocalStandardScaler()],
+)
+standardized = fcst.preprocess(serie)
+plot({'original': serie, 'standardized': standardized}, 'target_transforms__standardized.png')
+standardized['y'].agg(['mean', 'var']).round(2)
+
+
mean   -0.0
+var     1.0
+Name: y, dtype: float64
+
+
+

+

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.

+
+
class Zeros(BaseEstimator):
+    def fit(self, X, y=None):
+        return self
+
+    def predict(self, X, y=None):
+        return np.zeros(X.shape[0])
+
+fcst = MLForecast(
+    models={'zeros_model': Zeros()},
+    freq=1,
+    target_transforms=[Differences([1, 24]), LocalStandardScaler()],
+)
+preds = fcst.fit(serie).predict(48)
+fig, ax = plt.subplots()
+pd.concat([serie.tail(24 * 10), preds]).set_index('ds').plot(ax=ax)
+plt.close()
+fig.savefig('target_transforms__zeros.png')
+
+

+
+
+
+

Global transformations

+
+

Transformations applied to all series

+
+
+

GlobalSklearnTransformer

+

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.

+
+
import numpy as np
+from sklearn.preprocessing import FunctionTransformer
+
+from mlforecast.target_transforms import GlobalSklearnTransformer
+
+sk_log1p = FunctionTransformer(func=np.log1p, inverse_func=np.expm1)
+fcst = MLForecast(
+    models={'zeros_model': Zeros()},
+    freq=1,
+    target_transforms=[GlobalSklearnTransformer(sk_log1p)],
+)
+log1p_transformed = fcst.preprocess(serie)
+plot({'original': serie, 'Log transformed': log1p_transformed}, 'target_transforms__log.png')
+
+

+

We can also combine this with local transformations. For example we can apply log first and then differencing.

+
+
fcst = MLForecast(
+    models=[],
+    freq=1,
+    target_transforms=[GlobalSklearnTransformer(sk_log1p), Differences([1, 24])],
+)
+log_diffs = fcst.preprocess(serie)
+plot({'original': serie, 'Log + Differences': log_diffs}, 'target_transforms__log_diffs.png')
+
+

+
+
+
+

Custom transformations

+
+

Implementing your own target transformations

+
+

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.

+
+
fcst = MLForecast(
+    models=[],
+    freq=1,
+    target_transforms=[LocalMinMaxScaler()],
+)
+minmax_scaled = fcst.preprocess(serie)
+plot({'original': serie, 'min-max scaled': minmax_scaled}, 'target_transforms__minmax.png')
+
+

+ + +
+ +

Give us a ⭐ on Github

+ +
+ + + + \ No newline at end of file diff --git a/docs/how-to-guides/transfer_learning.html b/docs/how-to-guides/transfer_learning.html new file mode 100644 index 00000000..31988311 --- /dev/null +++ b/docs/how-to-guides/transfer_learning.html @@ -0,0 +1,882 @@ + + + + + + + + + +mlforecast - Transfer Learning + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+ + +
+ +
+ + +
+ + + +
+ +
+
+

Transfer Learning

+
+ + + +
+ + + + +
+ + +
+ + +

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

+ +

You can run these experiments with Google Colab.

+

Open In Colab

+
+

Installing Libraries

+
+
# !pip install mlforecast datasetsforecast utilsforecast s3fs
+
+
+
import lightgbm as lgb
+import numpy as np
+import pandas as pd
+from datasetsforecast.m3 import M3
+from sklearn.metrics import mean_absolute_error
+from utilsforecast.plotting import plot_series
+
+from mlforecast import MLForecast
+from mlforecast.target_transforms import Differences
+
+
+
+

Load M3 Data

+

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.

+
+
Y_df_M3, _, _ = M3.load(directory='./', group='Monthly')
+
+
100%|████████████████████████████████████████████████████████████████| 1.76M/1.76M [00:00<00:00, 15.3MiB/s]
+INFO:datasetsforecast.utils:Successfully downloaded M3C.xls, 1757696, bytes.
+
+
+

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.
  • +
  • freq: a string indicating the frequency of the data. See panda’s available frequencies.
  • +
  • differences: Differences to take of the target before computing the features. These are restored at the forecasting step.
  • +
  • lags: Lags of the target to use as features.
  • +
+

In this example, we are only using differences and lags to produce features. See the full documentation to see all available features.

+

Any settings are passed into the constructor. Then you call its fit method and pass in the historical data frame Y_df_M3.

+
+
fcst = MLForecast(
+    models=models, 
+    lags=range(1, 13),
+    freq='MS',
+    target_transforms=[Differences([1, 12])],
+)
+fcst.fit(Y_df_M3);
+
+
+
+

Transfer M3 to AirPassengers

+

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
+
+
+
Y_hat_df = fcst.predict(horizon=12, new_data=Y_train_df)
+Y_hat_df.head()
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
unique_iddsLGBMRegressor
0AirPassengers1960-01-01422.740096
1AirPassengers1960-02-01399.480193
2AirPassengers1960-03-01458.220289
3AirPassengers1960-04-01442.960385
4AirPassengers1960-05-01461.700482
+ +
+
+
+
+
Y_hat_df = Y_test_df.merge(Y_hat_df, how='left', on=['unique_id', 'ds'])
+
+
+
fig = plot_series(Y_train_df, Y_hat_df)
+
+

+
+
+

Evaluate Results

+

We evaluate the forecasts of the pre-trained model with the Mean Absolute Error (mae).

+

\[ +\qquad MAE = \frac{1}{Horizon} \sum_{\tau} |y_{\tau} - \hat{y}_{\tau}|\qquad +\]

+
+
y_true = Y_test_df.y.values
+y_hat = Y_hat_df['LGBMRegressor'].values
+
+
+
print(f'LGBMRegressor     MAE: {mean_absolute_error(y_hat, y_true):.3f}')
+print('ETS               MAE: 16.222')
+print('AutoARIMA         MAE: 18.551')
+
+
LGBMRegressor     MAE: 13.560
+ETS               MAE: 16.222
+AutoARIMA         MAE: 18.551
+
+
+ + +
+ +

Give us a ⭐ on Github

+ +
+ + + + \ No newline at end of file diff --git a/docs/tutorials/electricity_load_forecasting.html b/docs/tutorials/electricity_load_forecasting.html new file mode 100644 index 00000000..5844c513 --- /dev/null +++ b/docs/tutorials/electricity_load_forecasting.html @@ -0,0 +1,2015 @@ + + + + + + + + + + +mlforecast - Electricity Load Forecast + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+ + +
+ +
+ + +
+ + + +
+ +
+
+

Electricity Load Forecast

+
+ +
+
+ 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.
  • +
  • prophet. Benchmark model developed by Facebook.
  • +
  • utilsforecast. Library with different functions for forecasting evaluation.
  • +
+

If you have already installed the libraries you can skip the next cell, if not be sure to run it.

+
+
# %%capture
+# !pip install prophet
+# !pip install -U mlforecast
+# !pip install -U utilsforecast
+
+
+
+

Forecast using Multiple Seasonalities

+
+

Electricity Load Data

+

According to the dataset’s page,

+
+

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
+
+
+
pd.plotting.register_matplotlib_converters()
+plt.rc("figure", figsize=(10, 8))
+plt.rc("font", size=10)
+
+
+
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_iddsy
32891PJM_Load_hourly2001-12-31 20:00:0036392.0
32892PJM_Load_hourly2001-12-31 21:00:0035082.0
32893PJM_Load_hourly2001-12-31 22:00:0033890.0
32894PJM_Load_hourly2001-12-31 23:00:0032590.0
32895PJM_Load_hourly2002-01-01 00:00:0031569.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.

+
+
threshold_time = df['ds'].max() - pd.Timedelta(hours=24)
+
+# Split the dataframe
+df_train = df[df['ds'] <= threshold_time]
+df_last_24_hours = df[df['ds'] > threshold_time]
+
+
+
+

Analizing Seasonalities

+

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.

+
+
def plot_differences(df, differences,fname):
+    prep = [df]
+    # Plot individual Differences
+    for d in differences:
+        fcst = MLForecast(
+        models=[],  # we're not interested in modeling yet
+        freq='H',  # our series have hourly frequency 
+        target_transforms=[Differences([d])],
+        )
+        df_ = fcst.preprocess(df)
+        df_['unique_id'] = df_['unique_id'] + f'_{d}'
+        prep.append(df_)
+        
+    # Plot combined Differences
+    fcst = MLForecast(
+    models=[],  # we're not interested in modeling yet
+    freq='H',  # our series have hourly frequency 
+    target_transforms=[Differences([24, 24*7])],
+    )
+    df_ = fcst.preprocess(df)
+    df_['unique_id'] = df_['unique_id'] + f'_all_diff'
+    prep.append(df_)
+    prep = pd.concat(prep, ignore_index=True)
+    #return prep
+    n_series = len(prep['unique_id'].unique())
+    fig, ax = plt.subplots(nrows=n_series, figsize=(7 * n_series, 10*n_series), squeeze=False)
+    for title, axi in zip(prep['unique_id'].unique(), ax.flat):
+        df_ = prep[prep['unique_id'] == title]
+        df_.set_index('ds')['y'].plot(title=title, ax=axi)
+    fig.savefig(f'../../figs/{fname}', bbox_inches='tight')
+    plt.close()
+
+

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.

+
+
plot_differences(df=df_train, differences=[24, 24*7], fname='load_forecasting__differences.png')
+
+

+

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_iddsy
192PJM_Load_hourly1998-04-09 02:00:00831.0
193PJM_Load_hourly1998-04-09 03:00:00918.0
194PJM_Load_hourly1998-04-09 04:00:00760.0
195PJM_Load_hourly1998-04-09 05:00:00849.0
196PJM_Load_hourly1998-04-09 06:00:00710.0
............
32867PJM_Load_hourly2001-12-30 20:00:003417.0
32868PJM_Load_hourly2001-12-30 21:00:003596.0
32869PJM_Load_hourly2001-12-30 22:00:003501.0
32870PJM_Load_hourly2001-12-30 23:00:003939.0
32871PJM_Load_hourly2001-12-31 00:00:004235.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 MLForecast Cross Validation methods here

+
+
import lightgbm as lgb
+from mlforecast.target_transforms import Differences
+from window_ops.expanding import expanding_mean
+from window_ops.rolling import rolling_mean
+
+from sklearn.base import BaseEstimator
+from sklearn.linear_model import Lasso, LinearRegression, Ridge
+from sklearn.neighbors import KNeighborsRegressor
+from sklearn.neural_network import MLPRegressor
+from sklearn.ensemble import RandomForestRegressor
+
+

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.

+
+
class Naive(BaseEstimator):
+    def fit(self, X, y):
+        return self
+
+    def predict(self, X):
+        return X['lag1']
+
+

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.

+
+
# Model dictionary
+models ={
+        'naive': Naive(),
+        'lgbm': lgb.LGBMRegressor(verbosity=-1),
+        'lasso': Lasso(),
+        'lin_reg': LinearRegression(),
+        'ridge': Ridge(),
+        'knn': KNeighborsRegressor(),
+        'mlp': MLPRegressor(), 
+        'rf': RandomForestRegressor()
+    }
+
+

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.

+
    lag_transforms={  
+            1: [expanding_mean],
+            24: [(rolling_mean, 48)],
+        }
+

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 )

+
+
crossvalidation_df = mlf.cross_validation(
+    df=df_train,
+    h=24,
+    n_windows=4,
+    refit=False,
+)
+crossvalidation_df.head()
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
unique_iddscutoffynaivelgbmlassolin_regridgeknnmlprf
0PJM_Load_hourly2001-12-27 01:00:002001-12-2728332.028837.028526.50557228703.18571228702.62594928702.62595628479.028799.81812028341.60
1PJM_Load_hourly2001-12-27 02:00:002001-12-2727329.027969.027467.86084727693.50231827692.39595427692.39596927521.627863.72848427266.37
2PJM_Load_hourly2001-12-27 03:00:002001-12-2726986.027435.026605.71061526991.79512426990.15756726990.15758926451.627192.19724126478.41
3PJM_Load_hourly2001-12-27 04:00:002001-12-2727009.027401.026284.06513826789.41839926787.26226226787.26229126388.427006.22823126264.58
4PJM_Load_hourly2001-12-27 05:00:002001-12-2727555.028169.026823.61707827369.64378927366.98307527366.98311126779.627617.68968527074.15
+ +
+
+
+

Now we can plot each model and window (fold) to see how it behaves

+
+
def plot_cv(df, df_cv, uid, fname, last_n=24 * 14, models={}):
+    cutoffs = df_cv.query('unique_id == @uid')['cutoff'].unique()
+    fig, ax = plt.subplots(nrows=len(cutoffs), ncols=1, figsize=(14, 14), gridspec_kw=dict(hspace=0.8))
+    for cutoff, axi in zip(cutoffs, ax.flat):
+        max_date = df_cv.query('unique_id == @uid & cutoff == @cutoff')['ds'].max()
+        df[df['ds'] < max_date].query('unique_id == @uid').tail(last_n).set_index('ds').plot(ax=axi, title=uid, y='y')
+        for m in models.keys():
+            df_cv.query('unique_id == @uid & cutoff == @cutoff').set_index('ds').plot(ax=axi, title=uid, y=m)          
+    fig.savefig(f'../../figs/{fname}', bbox_inches='tight')
+    plt.close()
+
+
+
plot_cv(df_train, crossvalidation_df, 'PJM_Load_hourly', 'load_forecasting__predictions.png', models=models)
+
+

+

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
+    ]
+
+
+
# Function to evaluate the crossvalidation
+def evaluate_crossvalidation(crossvalidation_df, metrics, models):
+    evaluations = []
+    for c in crossvalidation_df['cutoff'].unique():
+        df_cv = crossvalidation_df.query('cutoff == @c')
+        evaluation = evaluate(
+            df = df_cv,
+            metrics=metrics,
+            models=list(models.keys())
+            )
+        evaluations.append(evaluation)
+    evaluations = pd.concat(evaluations, ignore_index=True).drop(columns='unique_id')
+    evaluations = evaluations.groupby('metric').mean()
+    return evaluations.style.background_gradient(cmap='RdYlGn_r', axis=1)
+
+
+
evaluate_crossvalidation(crossvalidation_df, metrics, models)
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
 naivelgbmlassolin_regridgeknnmlprf
metric        
mae1631.395833971.5362001003.7964331007.9985971007.9985471248.1458331074.4649441159.437604
mape0.0497590.0309660.0317600.0318880.0318880.0387210.0337070.036373
rmse1871.3989191129.7132561148.6161561153.2627191153.2626641451.9643901209.1640531324.512394
smape0.0247860.0158860.0162690.0163380.0163380.0195490.0171020.018514
+
+
+

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

+
+
from mlforecast.utils import PredictionIntervals
+
+
+
models_evaluation ={
+        'lgbm': lgb.LGBMRegressor(verbosity=-1),
+        'lasso': Lasso(),
+    }
+
+mlf_evaluation = MLForecast(
+    models = models_evaluation, 
+    freq='H',  # our series have hourly frequency 
+    target_transforms=[Differences([24, 24*7])],
+    lags=[1,12,24], 
+    lag_transforms={  
+        1: [expanding_mean],
+        24: [(rolling_mean, 48)],
+    },
+    date_features=['month', 'hour', 'dayofweek']
+)
+
+

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

+
+
mlf_evaluation.fit(
+    df = df_train,
+    prediction_intervals=PredictionIntervals(n_windows=4, h=24)
+)
+
+
MLForecast(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)
+
+
+

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].

+
+
levels = [90, 95] # Levels for prediction intervals
+forecasts = mlf_evaluation.predict(24, level=levels)
+forecasts.head()
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
unique_iddslgbmlassolgbm-lo-95lgbm-lo-90lgbm-hi-90lgbm-hi-95lasso-lo-95lasso-lo-90lasso-hi-90lasso-hi-95
0PJM_Load_hourly2001-12-31 01:00:0028847.57317629124.08597628544.59346428567.60313029127.54322229150.55288828762.75226928772.60427529475.56767729485.419682
1PJM_Load_hourly2001-12-31 02:00:0027862.58919528365.33074927042.31141427128.83988828596.33850328682.86697727528.54895927619.06522429111.59627529202.112539
2PJM_Load_hourly2001-12-31 03:00:0027044.41896027712.16167625596.65989625688.23042628400.60749328492.17802326236.95536926338.08710229086.23625129187.367984
3PJM_Load_hourly2001-12-31 04:00:0026976.10412527661.57273325249.96152725286.02472228666.18352928702.24672425911.13352125959.81571529363.32975029412.011944
4PJM_Load_hourly2001-12-31 05:00:0026694.24623827393.92237025044.22084525051.54883228336.94364428344.27163125751.54789725762.52481529025.31992429036.296843
+ +
+
+
+

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_iddsylgbmlassolgbm-lo-95lgbm-lo-90lgbm-hi-90lgbm-hi-95lasso-lo-95lasso-lo-90lasso-hi-90lasso-hi-95
0PJM_Load_hourly2001-12-31 01:00:0029001.028847.57317629124.08597628544.59346428567.60313029127.54322229150.55288828762.75226928772.60427529475.56767729485.419682
1PJM_Load_hourly2001-12-31 02:00:0028138.027862.58919528365.33074927042.31141427128.83988828596.33850328682.86697727528.54895927619.06522429111.59627529202.112539
2PJM_Load_hourly2001-12-31 03:00:0027830.027044.41896027712.16167625596.65989625688.23042628400.60749328492.17802326236.95536926338.08710229086.23625129187.367984
3PJM_Load_hourly2001-12-31 04:00:0027874.026976.10412527661.57273325249.96152725286.02472228666.18352928702.24672425911.13352125959.81571529363.32975029412.011944
4PJM_Load_hourly2001-12-31 05:00:0028427.026694.24623827393.92237025044.22084525051.54883228336.94364428344.27163125751.54789725762.52481529025.31992429036.296843
+ +
+
+
+

Now we can evaluate the metrics and performance in the test set.

+
+
evaluate(
+            df = test,
+            metrics=metrics,
+            models=list(models_evaluation.keys())
+            )
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
unique_idmetriclgbmlasso
0PJM_Load_hourlymae1092.050817899.979743
1PJM_Load_hourlyrmse1340.4227621163.695525
2PJM_Load_hourlymape0.0336000.027688
3PJM_Load_hourlysmape0.0171370.013812
+ +
+
+
+

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.

+
+
fig = plot_series(
+    df_train, 
+    test, 
+    models=['lasso', 'lgbm'],
+    plot_random=False, 
+    level=levels, 
+    max_insample_length=24
+)
+
+

+
+
+

Comparison with Prophet

+

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
+
+
+
# create prophet model
+prophet = Prophet(interval_width=0.9)
+init = time()
+prophet.fit(df_train)
+# produce forecasts
+future = prophet.make_future_dataframe(periods=len(df_last_24_hours), freq='H', include_history=False)
+forecast_prophet = prophet.predict(future)
+end = time()
+# data wrangling
+forecast_prophet = forecast_prophet[['ds', 'yhat', 'yhat_lower', 'yhat_upper']]
+forecast_prophet.columns = ['ds', 'Prophet', 'Prophet-lo-90', 'Prophet-hi-90']
+forecast_prophet.insert(0, 'unique_id', 'PJM_Load_hourly')
+forecast_prophet.head()
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
unique_iddsProphetProphet-lo-90Prophet-hi-90
0PJM_Load_hourly2001-12-31 01:00:0025294.24696020448.35484629940.516875
1PJM_Load_hourly2001-12-31 02:00:0024000.72542319006.95937828729.851990
2PJM_Load_hourly2001-12-31 03:00:0023324.77196618598.12070527956.224882
3PJM_Load_hourly2001-12-31 04:00:0023332.51987118240.00671328438.714923
4PJM_Load_hourly2001-12-31 05:00:0024107.12682719107.46604929238.143993
+ +
+
+
+
+
time_prophet = (end - init) 
+print(f'Prophet Time: {time_prophet:.2f} seconds')
+
+
Prophet Time: 27.44 seconds
+
+
+
+
models_comparison ={
+        'lgbm': lgb.LGBMRegressor(verbosity=-1)
+    }
+
+mlf_comparison = MLForecast(
+    models = models_comparison, 
+    freq='H',  # our series have hourly frequency 
+    target_transforms=[Differences([24, 24*7])],
+    lags=[1,12,24],
+    lag_transforms={  
+        1: [expanding_mean],
+        24: [(rolling_mean, 48)],
+    },
+    date_features=['month', 'hour', 'dayofweek']
+)
+
+init = time()
+mlf_comparison.fit(
+    df = df_train,
+    prediction_intervals=PredictionIntervals(n_windows=4, window_size=24)
+)
+
+levels = [90]
+forecasts_comparison = mlf_comparison.predict(24, level=levels)
+end = time()
+forecasts_comparison.head()
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
unique_iddslgbmlgbm-lo-90lgbm-hi-90
0PJM_Load_hourly2001-12-31 01:00:0028847.57317628567.60313029127.543222
1PJM_Load_hourly2001-12-31 02:00:0027862.58919527128.83988828596.338503
2PJM_Load_hourly2001-12-31 03:00:0027044.41896025688.23042628400.607493
3PJM_Load_hourly2001-12-31 04:00:0026976.10412525286.02472228666.183529
4PJM_Load_hourly2001-12-31 05:00:0026694.24623825051.54883228336.943644
+ +
+
+
+
+
time_lgbm = (end - init)
+print(f'LGBM Time: {time_lgbm:.2f} seconds')
+
+
LGBM Time: 1.24 seconds
+
+
+
+
metrics_comparison = df_last_24_hours.merge(forecasts_comparison, how='left', on=['unique_id', 'ds']).merge(
+    forecast_prophet, how='left', on=['unique_id', 'ds'])
+metrics_comparison = evaluate(
+            df = metrics_comparison,
+            metrics=metrics,
+            models=['Prophet', 'lgbm']
+            )
+metrics_comparison.reset_index(drop=True).style.background_gradient(cmap='RdYlGn_r', axis=1)
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
 unique_idmetricProphetlgbm
0PJM_Load_hourlymae2282.9669771092.050817
1PJM_Load_hourlyrmse2721.8172031340.422762
2PJM_Load_hourlymape0.0737500.033600
3PJM_Load_hourlysmape0.0386330.017137
+
+
+

As we can see lgbm had consistently better metrics than prophet.

+
+
metrics_comparison['improvement'] = metrics_comparison['Prophet'] /  metrics_comparison['lgbm']
+metrics_comparison['improvement'] = metrics_comparison['improvement'].apply(lambda x: f'{x:.2f}')
+metrics_comparison.set_index('metric')[['improvement']]
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
improvement
metric
mae2.09
rmse2.03
mape2.19
smape2.25
+ +
+
+
+
+
print(f'lgbm with MLForecast has a speedup of {time_prophet/time_lgbm:.2f} compared with prophet')
+
+
lgbm with MLForecast has a speedup of 22.21 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.

+ + +
+
+ +

Give us a ⭐ on Github

+ +
+ + + + \ No newline at end of file diff --git a/docs/tutorials/electricity_peak_forecasting.html b/docs/tutorials/electricity_peak_forecasting.html new file mode 100644 index 00000000..0dc52b18 --- /dev/null +++ b/docs/tutorials/electricity_peak_forecasting.html @@ -0,0 +1,979 @@ + + + + + + + + + + +mlforecast - Detect Demand Peaks + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+ + +
+ +
+ + +
+ + + +
+ +
+
+

Detect Demand Peaks

+
+ +
+
+ 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

+
    +
  1. Install libraries
  2. +
  3. Load and explore the data
  4. +
  5. Fit LightGBM model and forecast
  6. +
  7. Peak detection
  8. +
+
+
+
+ +
+
+Tip +
+
+
+

You can use Colab to run this Notebook interactively Open In Colab

+
+
+
+
+

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
+
+
+
# Load data
+Y_df = pd.read_csv('https://datasets-nixtla.s3.amazonaws.com/ERCOT-clean.csv', parse_dates=['ds'])
+Y_df = Y_df.query("ds >= '2022-01-01' & ds <= '2022-10-01'")
+
+
+
fig = plot_series(Y_df)
+
+

+

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.

+
+
import lightgbm as lgb
+
+from mlforecast import MLForecast
+from mlforecast.target_transforms import Differences
+
+

First, instantiate the model and define the parameters.

+
+
+
+ +
+
+Tip +
+
+
+

In this example we are using the default parameters of the lgb.LGBMRegressor model, but you can change them to improve the forecasting performance.

+
+
+
+
models = [
+    lgb.LGBMRegressor(verbosity=-1) # you can include more models here
+]
+
+

We fit the model by instantiating a MLForecast object with the following required parameters:

+
    +
  • models: a list of sklearn-like (fit and predict) models.

  • +
  • freq: a string indicating the frequency of the data. (See panda’s available frequencies.)

  • +
  • 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.
  • +
  • target_col: identifies the column to model.
  • +
+
+
crossvalidation_df = mlf.cross_validation(
+    data=Y_df,
+    window_size=24,
+    n_windows=30,
+)
+
+
+
crossvalidation_df.head()
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
unique_iddscutoffyLGBMRegressor
0ERCOT2022-09-01 00:00:002022-08-31 23:00:0045482.47175745685.265537
1ERCOT2022-09-01 01:00:002022-08-31 23:00:0043602.65804343779.819515
2ERCOT2022-09-01 02:00:002022-08-31 23:00:0042284.81734242672.470923
3ERCOT2022-09-01 03:00:002022-08-31 23:00:0041663.15677142091.768192
4ERCOT2022-09-01 04:00:002022-08-31 23:00:0041710.62190442481.403168
+ +
+
+
+
+
+
+ +
+
+Important +
+
+
+

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.

+
+
import matplotlib.pyplot as plt
+
+
+
fig, ax = plt.subplots(figsize=(10, 5))
+ax.axvline(cv_df_day.iloc[max_hour]['ds'], color='black', label='True Peak')
+ax.scatter(cv_df_day.iloc[peaks]['ds'], cv_df_day.iloc[peaks]['LGBMRegressor'], color='green', label=f'Predicted Top-{npeaks}')
+ax.plot(cv_df_day['ds'], cv_df_day['y'], label='y', color='blue')
+ax.plot(cv_df_day['ds'], cv_df_day['LGBMRegressor'], label='Forecast', color='red')
+ax.set(xlabel='Time', ylabel='Load (MW)')
+ax.grid()
+ax.legend()
+fig.savefig('../../figs/electricity_peak_forecasting__predicted_peak.png', bbox_inches='tight')
+plt.close()
+
+

+
+
+
+ +
+
+Important +
+
+
+

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.

+ + +
+ +

Give us a ⭐ on Github

+ +
+ + + + \ No newline at end of file diff --git a/docs/tutorials/prediction_intervals_in_forecasting_models.html b/docs/tutorials/prediction_intervals_in_forecasting_models.html new file mode 100644 index 00000000..fb695b01 --- /dev/null +++ b/docs/tutorials/prediction_intervals_in_forecasting_models.html @@ -0,0 +1,2151 @@ + + + + + + + + + +mlforecast - Prediction intervals + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+ + +
+ +
+ + +
+ + + +
+ +
+
+

Prediction intervals

+
+ + + +
+ + + + +
+ + +
+ + +

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.

+

Let’s start!!!

+
+

Table of contents

+
    +
  1. Introduction
  2. +
  3. Forecasts and prediction intervals
  4. +
  5. Installing mlforecast
  6. +
  7. Loading libraries and data
  8. +
  9. Explore Data with the plot method
  10. +
  11. Split the data into training and testing
  12. +
  13. Modeling with mlforecast
  14. +
  15. References
  16. +
+
+
+

Introduction

+

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:

+
    +
  1. The random error term;
  2. +
  3. The parameter estimates;
  4. +
  5. The choice of model for the historical data;
  6. +
  7. The continuation of the historical data generating process into the future.
  8. +
+

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:

+
    +
  1. 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.

  2. +
  3. 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.

  4. +
  5. 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.

  6. +
  7. 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.

  8. +
+

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.

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
PercentageMultiplier
500.67
550.76
600.84
650.93
701.04
751.15
801.28
851.44
901.64
951.96
962.05
972.17
982.33
992.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

+

\[\begin{equation} + \hat{\sigma} = \sqrt{\frac{1}{T-K-M}\sum_{t=1}^T e_t^2}, \tag{1} +\end{equation}\]

+

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}\).

+ + + + + + + + + + + + + + + + + + + + + + + + + +
Methodh-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
+
+
+
from mlforecast import MLForecast
+import xgboost as xgb
+
+# 
+# ==============================================================================
+from numba import njit
+from window_ops.expanding import expanding_mean
+from window_ops.rolling import rolling_mean
+from window_ops.ewm import ewm_mean
+from mlforecast.target_transforms import Differences
+
+from mlforecast.utils import PredictionIntervals
+
+
+
# Plot
+# ==============================================================================
+import matplotlib.pyplot as plt
+import matplotlib.ticker as ticker
+from statsmodels.graphics.tsaplots import plot_acf, plot_pacf
+
+
+

Read Data

+
+
data_url = "https://raw.githubusercontent.com/Naren8520/Serie-de-tiempo-con-Machine-Learning/main/Data/nyc_taxi.csv"
+df = pd.read_csv(data_url, parse_dates=["timestamp"])
+df.head()
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
timestampvalue
02014-07-01 00:00:0010844
12014-07-01 00:30:008127
22014-07-01 01:00:006210
32014-07-01 01:30:004656
42014-07-01 02:00:003820
+ +
+
+
+

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

  • +
  • The y (numeric) represents the measurement we wish to forecast.

  • +
+
+
df["unique_id"] = "1"
+df.columns=["ds", "y", "unique_id"]
+df.head()
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
dsyunique_id
02014-07-01 00:00:00108441
12014-07-01 00:30:0081271
22014-07-01 01:00:0062101
32014-07-01 01:30:0046561
42014-07-01 02:00:0038201
+ +
+
+
+
+
df.info()
+
+
<class 'pandas.core.frame.DataFrame'>
+RangeIndex: 10320 entries, 0 to 10319
+Data columns (total 3 columns):
+ #   Column     Non-Null Count  Dtype         
+---  ------     --------------  -----         
+ 0   ds         10320 non-null  datetime64[ns]
+ 1   y          10320 non-null  int64         
+ 2   unique_id  10320 non-null  object        
+dtypes: datetime64[ns](1), int64(1), object(1)
+memory usage: 242.0+ KB
+
+
+
+
+
+

Explore Data with the plot method

+

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.

+
+
fig, axs = plt.subplots(nrows=1, ncols=2)
+
+plot_acf(df["y"],  lags=30, ax=axs[0],color="fuchsia")
+axs[0].set_title("Autocorrelation");
+
+# Grafico
+plot_pacf(df["y"],  lags=30, ax=axs[1],color="lime")
+axs[1].set_title('Partial Autocorrelation')
+plt.savefig("../../figs/prediction_intervals_in_forecasting_models__autocorrelation.png", bbox_inches='tight')
+plt.close();
+
+

+
+
+
+

Decomposition of the time series

+

How to decompose a time series and why?

+

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.

+
+
train = df[df.ds<='2015-01-21 13:30:00'] 
+test = df[df.ds>'2015-01-21 13:30:00']
+
+
+
train.shape, test.shape
+
+
((9820, 3), (500, 3))
+
+
+

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

+
+
mlf = MLForecast(models=model1,
+                 freq='30min', 
+                 target_transforms=[Differences([1])],
+                 )
+
+

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
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
dsyunique_id
12014-07-01 00:30:00-2717.01
22014-07-01 01:00:00-1917.01
32014-07-01 01:30:00-1554.01
42014-07-01 02:00:00-836.01
52014-07-01 02:30:00-947.01
............
103152015-01-31 21:30:00951.01
103162015-01-31 22:00:001051.01
103172015-01-31 22:30:001588.01
103182015-01-31 23:00:00-718.01
103192015-01-31 23:30:00-303.01
+ +

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.

+
+
mlf = MLForecast(models=model1,
+                 freq='30min',  
+                 lags=[1,24],
+                 target_transforms=[Differences([1])],
+                 )
+
+
+
prep = mlf.preprocess(df)
+prep
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
dsyunique_idlag1lag24
252014-07-01 12:30:00-22.01445.0-2717.0
262014-07-01 13:00:00-708.01-22.0-1917.0
272014-07-01 13:30:001281.01-708.0-1554.0
282014-07-01 14:00:0087.011281.0-836.0
292014-07-01 14:30:001045.0187.0-947.0
..................
103152015-01-31 21:30:00951.01428.04642.0
103162015-01-31 22:00:001051.01951.0-519.0
103172015-01-31 22:30:001588.011051.02411.0
103182015-01-31 23:00:00-718.011588.0214.0
103192015-01-31 23:30:00-303.01-718.02595.0
+ +

10295 rows × 5 columns

+
+
+
+
+
prep.drop(columns=['unique_id', 'ds']).corr()['y']
+
+
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:

+
    +
  • supply a tuple (tfm_func, arg1, arg2, …)
  • +
  • define a new function fixing the arguments
  • +
+
+
from numba import njit
+from window_ops.expanding import expanding_mean
+from window_ops.rolling import rolling_mean
+
+
+
mlf = MLForecast(models=model1,
+                 freq='30min',  
+                 lags=[1,24],
+                 lag_transforms={1: [expanding_mean],24: [(rolling_mean, 7)] },
+                 target_transforms=[Differences([1])],
+                 )
+
+
+
prep = mlf.preprocess(df)
+prep
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
dsyunique_idlag1lag24expanding_mean_lag1rolling_mean_lag24_window_size7
312014-07-01 15:30:00-836.01-1211.0-305.0284.533325-1254.285767
322014-07-01 16:00:00-2316.01-836.0157.0248.387100-843.714294
332014-07-01 16:30:00-1215.01-2316.0-63.0168.250000-578.857117
342014-07-01 17:00:002190.01-1215.0357.0126.333336-305.857147
352014-07-01 17:30:002322.012190.01849.0187.02941977.714287
........................
103152015-01-31 21:30:00951.01428.04642.01.2483032064.285645
103162015-01-31 22:00:001051.01951.0-519.01.3403781873.428589
103172015-01-31 22:30:001588.011051.02411.01.4421292179.000000
103182015-01-31 23:00:00-718.011588.0214.01.5959101888.714233
103192015-01-31 23:30:00-303.01-718.02595.01.5261682071.714355
+ +

10289 rows × 7 columns

+
+
+
+

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.

+
+
mlf = MLForecast(models=model1,
+                 freq='30min', 
+                 lags=[1,24],
+                 lag_transforms={1: [expanding_mean],24: [(rolling_mean, 7)] },
+                 target_transforms=[Differences([1])],
+                 date_features=["year", "month", "day", "hour"]) # Seasonal data
+
+
+
prep = mlf.preprocess(df)
+prep
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
dsyunique_idlag1lag24expanding_mean_lag1rolling_mean_lag24_window_size7yearmonthdayhour
312014-07-01 15:30:00-836.01-1211.0-305.0284.533325-1254.28576720147115
322014-07-01 16:00:00-2316.01-836.0157.0248.387100-843.71429420147116
332014-07-01 16:30:00-1215.01-2316.0-63.0168.250000-578.85711720147116
342014-07-01 17:00:002190.01-1215.0357.0126.333336-305.85714720147117
352014-07-01 17:30:002322.012190.01849.0187.02941977.71428720147117
....................................
103152015-01-31 21:30:00951.01428.04642.01.2483032064.285645201513121
103162015-01-31 22:00:001051.01951.0-519.01.3403781873.428589201513122
103172015-01-31 22:30:001588.011051.02411.01.4421292179.000000201513122
103182015-01-31 23:00:00-718.011588.0214.01.5959101888.714233201513123
103192015-01-31 23:30:00-303.01-718.02595.01.5261682071.714355201513123
+ +

10289 rows × 11 columns

+
+
+
+
+
+

Fit the Model

+
+
# fit the models
+mlf.fit(df,  
+ fitted=True, 
+prediction_intervals=PredictionIntervals(n_windows=5, window_size=30, method="conformal_distribution" )  )
+
+
MLForecast(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)
+
+
+

Let’s see the results of our model in this case the XGBoost model. We can observe it with the following instruction:

+

Let us now visualize the fitted values of our models.

+
+
result=mlf.forecast_fitted_values()
+result=result.set_index("unique_id")
+result
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
dsyXGBRegressor
unique_id
12014-07-01 15:30:0018544.018243.291016
12014-07-01 16:00:0016228.016489.828125
12014-07-01 16:30:0015013.015105.728516
12014-07-01 17:00:0017203.017362.349609
12014-07-01 17:30:0019525.019678.052734
............
12015-01-31 21:30:0024670.024801.906250
12015-01-31 22:00:0025721.025812.089844
12015-01-31 22:30:0027309.027192.630859
12015-01-31 23:00:0026591.027066.931641
12015-01-31 23:30:0026288.025945.341797
+ +

10289 rows × 3 columns

+
+
+
+
+
from statsmodels.stats.diagnostic import normal_ad
+from scipy import stats
+
+
+
sw_result = stats.shapiro(result["XGBRegressor"])
+ad_result = normal_ad(np.array(result["XGBRegressor"]), axis=0)
+dag_result = stats.normaltest(result["XGBRegressor"], axis=0, nan_policy='propagate')
+
+

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.

+
+
result=mlf.forecast_fitted_values()
+fig, axs = plt.subplots(nrows=2, ncols=2)
+
+# plot[1,1]
+result["XGBRegressor"].plot(ax=axs[0,0])
+axs[0,0].set_title("Residuals model");
+
+# plot
+axs[0,1].hist(result["XGBRegressor"], density=True,bins=50, alpha=0.5 )
+axs[0,1].set_title("Density plot - Residual");
+
+# plot
+stats.probplot(result["XGBRegressor"], dist="norm", plot=axs[1,0])
+axs[1,0].set_title('Plot Q-Q')
+axs[1,0].annotate("SW p-val: {:.4f}".format(sw_result[1]), xy=(0.05,0.9), xycoords='axes fraction', fontsize=15,
+            bbox=dict(boxstyle="round", fc="none", ec="gray", pad=0.6))
+
+axs[1,0].annotate("AD p-val: {:.4f}".format(ad_result[1]), xy=(0.05,0.8), xycoords='axes fraction', fontsize=15,
+            bbox=dict(boxstyle="round", fc="none", ec="gray", pad=0.6))
+
+axs[1,0].annotate("DAG p-val: {:.4f}".format(dag_result[1]), xy=(0.05,0.7), xycoords='axes fraction', fontsize=15,
+            bbox=dict(boxstyle="round", fc="none", ec="gray", pad=0.6))
+# plot
+plot_acf(result["XGBRegressor"],  lags=35, ax=axs[1,1],color="fuchsia")
+axs[1,1].set_title("Autocorrelation");
+
+plt.savefig("../../figs/prediction_intervals_in_forecasting_models__plot_residual_model.png", bbox_inches='tight')
+plt.close();
+
+

+
+
+

Predict method with prediction intervals

+

To generate forecasts use the predict method.

+
+
forecast_df = mlf.predict(h=30, level=[80,95])
+forecast_df.head()
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
unique_iddsXGBRegressorXGBRegressor-lo-95XGBRegressor-lo-80XGBRegressor-hi-80XGBRegressor-hi-95
012015-02-01 00:00:0024608.86523424016.47587324085.58806225132.14240725201.254596
112015-02-01 00:30:0023323.09765620511.10561521901.00839824745.18691426135.089697
212015-02-01 01:00:0022223.43554720161.90200220995.97128923450.89980524284.969092
312015-02-01 01:30:0020405.22851617227.14794917822.29492222988.16210923583.309082
412015-02-01 02:00:0020014.32421917422.15551817923.69238322104.95605522606.492920
+ +
+
+
+
+
+

Plot prediction intervals

+

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.

+
+
fig = plot_series(df, forecast_df, level=[80,95], max_insample_length=200,engine="matplotlib")
+fig.get_axes()[0].set_title("Prediction intervals")
+fig.savefig('../../figs/prediction_intervals_in_forecasting_models__plot_forecasting_intervals.png', bbox_inches='tight')
+
+

+

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

+
    +
  1. Changquan Huang • Alla Petukhina. Springer series (2022). Applied Time Series Analysis and Forecasting with Python.
  2. +
  3. Ivan Svetunkov. Forecasting and Analytics with the Augmented Dynamic Adaptive Model (ADAM)
  4. +
  5. James D. Hamilton. Time Series Analysis Princeton University Press, Princeton, New Jersey, 1st Edition, 1994.
  6. +
  7. Nixtla Parameters for Mlforecast.
  8. +
  9. Pandas available frequencies.
  10. +
  11. Rob J. Hyndman and George Athanasopoulos (2018). “Forecasting principles and practice, Time series cross-validation”..
  12. +
  13. Seasonal periods- Rob J Hyndman.
  14. +
+ + +
+ +

Give us a ⭐ on Github

+ +
+ + + + \ No newline at end of file diff --git a/figs/cross_validation__predictions.png b/figs/cross_validation__predictions.png new file mode 100644 index 00000000..e014e084 Binary files /dev/null and b/figs/cross_validation__predictions.png differ diff --git a/figs/cross_validation__series.png b/figs/cross_validation__series.png new file mode 100644 index 00000000..8b572526 Binary files /dev/null and b/figs/cross_validation__series.png differ diff --git a/figs/electricity_peak_forecasting__eda.png b/figs/electricity_peak_forecasting__eda.png new file mode 100644 index 00000000..bce02f59 Binary files /dev/null and b/figs/electricity_peak_forecasting__eda.png differ diff --git a/figs/electricity_peak_forecasting__predicted_peak.png b/figs/electricity_peak_forecasting__predicted_peak.png new file mode 100644 index 00000000..c5ae03b8 Binary files /dev/null and b/figs/electricity_peak_forecasting__predicted_peak.png differ diff --git a/figs/end_to_end_walkthrough__cv.png b/figs/end_to_end_walkthrough__cv.png new file mode 100644 index 00000000..876eb387 Binary files /dev/null and b/figs/end_to_end_walkthrough__cv.png differ diff --git a/figs/end_to_end_walkthrough__differences.png b/figs/end_to_end_walkthrough__differences.png new file mode 100644 index 00000000..ef18eeac Binary files /dev/null and b/figs/end_to_end_walkthrough__differences.png differ diff --git a/figs/end_to_end_walkthrough__eda.png b/figs/end_to_end_walkthrough__eda.png new file mode 100644 index 00000000..b4d06d0c Binary files /dev/null and b/figs/end_to_end_walkthrough__eda.png differ diff --git a/figs/end_to_end_walkthrough__final_forecast.png b/figs/end_to_end_walkthrough__final_forecast.png new file mode 100644 index 00000000..8b3e467e Binary files /dev/null and b/figs/end_to_end_walkthrough__final_forecast.png differ diff --git a/figs/end_to_end_walkthrough__lgbcv.png b/figs/end_to_end_walkthrough__lgbcv.png new file mode 100644 index 00000000..b955e7e4 Binary files /dev/null and b/figs/end_to_end_walkthrough__lgbcv.png differ diff --git a/figs/end_to_end_walkthrough__predictions.png b/figs/end_to_end_walkthrough__predictions.png new file mode 100644 index 00000000..ab3c2856 Binary files /dev/null and b/figs/end_to_end_walkthrough__predictions.png differ diff --git a/figs/forecast__cross_validation.png b/figs/forecast__cross_validation.png new file mode 100644 index 00000000..e4fd1385 Binary files /dev/null and b/figs/forecast__cross_validation.png differ diff --git a/figs/forecast__cross_validation_intervals.png b/figs/forecast__cross_validation_intervals.png new file mode 100644 index 00000000..f9335b1c Binary files /dev/null and b/figs/forecast__cross_validation_intervals.png differ diff --git a/figs/forecast__ercot.png b/figs/forecast__ercot.png new file mode 100644 index 00000000..5f7d3f8d Binary files /dev/null and b/figs/forecast__ercot.png differ diff --git a/figs/forecast__predict.png b/figs/forecast__predict.png new file mode 100644 index 00000000..489cae7c Binary files /dev/null and b/figs/forecast__predict.png differ diff --git a/figs/forecast__predict_intervals.png b/figs/forecast__predict_intervals.png new file mode 100644 index 00000000..bd0882d3 Binary files /dev/null and b/figs/forecast__predict_intervals.png differ diff --git a/figs/forecast__predict_intervals_window_size_1.png b/figs/forecast__predict_intervals_window_size_1.png new file mode 100644 index 00000000..e7274b10 Binary files /dev/null and b/figs/forecast__predict_intervals_window_size_1.png differ diff --git a/figs/load_forecasting__differences.png b/figs/load_forecasting__differences.png new file mode 100644 index 00000000..f52ab593 Binary files /dev/null and b/figs/load_forecasting__differences.png differ diff --git a/figs/load_forecasting__prediction_intervals.png b/figs/load_forecasting__prediction_intervals.png new file mode 100644 index 00000000..32903322 Binary files /dev/null and b/figs/load_forecasting__prediction_intervals.png differ diff --git a/figs/load_forecasting__predictions.png b/figs/load_forecasting__predictions.png new file mode 100644 index 00000000..d96bd858 Binary files /dev/null and b/figs/load_forecasting__predictions.png differ diff --git a/figs/load_forecasting__raw.png b/figs/load_forecasting__raw.png new file mode 100644 index 00000000..d35d9ded Binary files /dev/null and b/figs/load_forecasting__raw.png differ diff --git a/figs/load_forecasting__transformed.png b/figs/load_forecasting__transformed.png new file mode 100644 index 00000000..1544c457 Binary files /dev/null and b/figs/load_forecasting__transformed.png differ diff --git a/figs/prediction_intervals__eda.png b/figs/prediction_intervals__eda.png new file mode 100644 index 00000000..2f4cde6d Binary files /dev/null and b/figs/prediction_intervals__eda.png differ diff --git a/figs/prediction_intervals__knn.png b/figs/prediction_intervals__knn.png new file mode 100644 index 00000000..616da6ea Binary files /dev/null and b/figs/prediction_intervals__knn.png differ diff --git a/figs/prediction_intervals__lasso.png b/figs/prediction_intervals__lasso.png new file mode 100644 index 00000000..150e08f1 Binary files /dev/null and b/figs/prediction_intervals__lasso.png differ diff --git a/figs/prediction_intervals__lr.png b/figs/prediction_intervals__lr.png new file mode 100644 index 00000000..de72ad92 Binary files /dev/null and b/figs/prediction_intervals__lr.png differ diff --git a/figs/prediction_intervals__ridge.png b/figs/prediction_intervals__ridge.png new file mode 100644 index 00000000..439f20af Binary files /dev/null and b/figs/prediction_intervals__ridge.png differ diff --git a/figs/prediction_intervals_in_forecasting_models__autocorrelation.png b/figs/prediction_intervals_in_forecasting_models__autocorrelation.png new file mode 100644 index 00000000..773ad90c Binary files /dev/null and b/figs/prediction_intervals_in_forecasting_models__autocorrelation.png differ diff --git a/figs/prediction_intervals_in_forecasting_models__eda.png b/figs/prediction_intervals_in_forecasting_models__eda.png new file mode 100644 index 00000000..88cc40d7 Binary files /dev/null and b/figs/prediction_intervals_in_forecasting_models__eda.png differ diff --git a/figs/prediction_intervals_in_forecasting_models__plot_forecasting_intervals.png b/figs/prediction_intervals_in_forecasting_models__plot_forecasting_intervals.png new file mode 100644 index 00000000..58f777f7 Binary files /dev/null and b/figs/prediction_intervals_in_forecasting_models__plot_forecasting_intervals.png differ diff --git a/figs/prediction_intervals_in_forecasting_models__plot_residual_model.png b/figs/prediction_intervals_in_forecasting_models__plot_residual_model.png new file mode 100644 index 00000000..fc7518a6 Binary files /dev/null and b/figs/prediction_intervals_in_forecasting_models__plot_residual_model.png differ diff --git a/figs/prediction_intervals_in_forecasting_models__plot_values.png b/figs/prediction_intervals_in_forecasting_models__plot_values.png new file mode 100644 index 00000000..40edf8bf Binary files /dev/null and b/figs/prediction_intervals_in_forecasting_models__plot_values.png differ diff --git a/figs/prediction_intervals_in_forecasting_models__seasonal_decompose_aditive.png b/figs/prediction_intervals_in_forecasting_models__seasonal_decompose_aditive.png new file mode 100644 index 00000000..7afdcb5a Binary files /dev/null and b/figs/prediction_intervals_in_forecasting_models__seasonal_decompose_aditive.png differ diff --git a/figs/prediction_intervals_in_forecasting_models__seasonal_decompose_multiplicative.png b/figs/prediction_intervals_in_forecasting_models__seasonal_decompose_multiplicative.png new file mode 100644 index 00000000..7afdcb5a Binary files /dev/null and b/figs/prediction_intervals_in_forecasting_models__seasonal_decompose_multiplicative.png differ diff --git a/figs/prediction_intervals_in_forecasting_models__train_test.png b/figs/prediction_intervals_in_forecasting_models__train_test.png new file mode 100644 index 00000000..09464e3d Binary files /dev/null and b/figs/prediction_intervals_in_forecasting_models__train_test.png differ diff --git a/figs/quick_start_distributed__sample.png b/figs/quick_start_distributed__sample.png new file mode 100644 index 00000000..8778b560 Binary files /dev/null and b/figs/quick_start_distributed__sample.png differ diff --git a/figs/quick_start_distributed__sample_prediction.png b/figs/quick_start_distributed__sample_prediction.png new file mode 100644 index 00000000..e752ef93 Binary files /dev/null and b/figs/quick_start_distributed__sample_prediction.png differ diff --git a/figs/quick_start_local__eda.png b/figs/quick_start_local__eda.png new file mode 100644 index 00000000..c29ff25c Binary files /dev/null and b/figs/quick_start_local__eda.png differ diff --git a/figs/quick_start_local__predictions.png b/figs/quick_start_local__predictions.png new file mode 100644 index 00000000..56149acf Binary files /dev/null and b/figs/quick_start_local__predictions.png differ diff --git a/figs/target_transforms__diff1.png b/figs/target_transforms__diff1.png new file mode 100644 index 00000000..120e9b0c Binary files /dev/null and b/figs/target_transforms__diff1.png differ diff --git a/figs/target_transforms__diff2.png b/figs/target_transforms__diff2.png new file mode 100644 index 00000000..2fbf954d Binary files /dev/null and b/figs/target_transforms__diff2.png differ diff --git a/figs/target_transforms__eda.png b/figs/target_transforms__eda.png new file mode 100644 index 00000000..c5b1a470 Binary files /dev/null and b/figs/target_transforms__eda.png differ diff --git a/figs/target_transforms__log.png b/figs/target_transforms__log.png new file mode 100644 index 00000000..0a138b26 Binary files /dev/null and b/figs/target_transforms__log.png differ diff --git a/figs/target_transforms__log_diffs.png b/figs/target_transforms__log_diffs.png new file mode 100644 index 00000000..9973580b Binary files /dev/null and b/figs/target_transforms__log_diffs.png differ diff --git a/figs/target_transforms__minmax.png b/figs/target_transforms__minmax.png new file mode 100644 index 00000000..9c6c09ab Binary files /dev/null and b/figs/target_transforms__minmax.png differ diff --git a/figs/target_transforms__standardized.png b/figs/target_transforms__standardized.png new file mode 100644 index 00000000..a76be888 Binary files /dev/null and b/figs/target_transforms__standardized.png differ diff --git a/figs/target_transforms__zeros.png b/figs/target_transforms__zeros.png new file mode 100644 index 00000000..c02c201b Binary files /dev/null and b/figs/target_transforms__zeros.png differ diff --git a/figs/transfer_learning__eda.png b/figs/transfer_learning__eda.png new file mode 100644 index 00000000..c77b05d0 Binary files /dev/null and b/figs/transfer_learning__eda.png differ diff --git a/figs/transfer_learning__forecast.png b/figs/transfer_learning__forecast.png new file mode 100644 index 00000000..5274dd03 Binary files /dev/null and b/figs/transfer_learning__forecast.png differ diff --git a/forecast.html b/forecast.html new file mode 100644 index 00000000..c04d19fc --- /dev/null +++ b/forecast.html @@ -0,0 +1,3333 @@ + + + + + + + + + + +mlforecast - MLForecast + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+ + +
+ +
+ + +
+ + + +
+ +
+
+

MLForecast

+
+ +
+
+ Full pipeline encapsulation +
+
+ + +
+ + + + +
+ + +
+ + +
+
+

MLForecast

+
+
 MLForecast (models:Union[sklearn.base.BaseEstimator,List[sklearn.base.Bas
+             eEstimator],Dict[str,sklearn.base.BaseEstimator]], freq:Union
+             [int,str,pandas._libs.tslibs.offsets.BaseOffset,NoneType]=Non
+             e, lags:Optional[Iterable[int]]=None, lag_transforms:Optional
+             [Dict[int,List[Union[Callable,Tuple[Callable,Any]]]]]=None,
+             date_features:Optional[Iterable[Union[str,Callable]]]=None,
+             num_threads:int=1, target_transforms:Optional[List[mlforecast
+             .target_transforms.BaseTargetTransform]]=None)
+
+

Forecasting pipeline

+ ++++++ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
TypeDefaultDetails
modelstyping.Union[sklearn.base.BaseEstimator, typing.List[sklearn.base.BaseEstimator], typing.Dict[str, sklearn.base.BaseEstimator]]Models that will be trained and used to compute the forecasts.
freqtyping.Union[int, str, pandas._libs.tslibs.offsets.BaseOffset, NoneType]NonePandas offset, pandas offset alias, e.g. ‘D’, ‘W-THU’ or integer denoting the frequency of the series.
lagstyping.Optional[typing.Iterable[int]]NoneLags of the target to use as features.
lag_transformstyping.Optional[typing.Dict[int, typing.List[typing.Union[typing.Callable, typing.Tuple[typing.Callable, typing.Any]]]]]NoneMapping of target lags to their transformations.
date_featurestyping.Optional[typing.Iterable[typing.Union[str, typing.Callable]]]NoneFeatures computed from the dates. Can be pandas date attributes or functions that will take the dates as input.
num_threadsint1Number of threads to use when computing the features.
target_transformstyping.Optional[typing.List[mlforecast.target_transforms.BaseTargetTransform]]NoneTransformations that will be applied to the target before computing the features and restored after the forecasting step.
+

The MLForecast class is a high level abstraction that encapsulates all the steps in the pipeline (preprocessing, fitting the model and computing the predictions). It tries to mimic the scikit-learn API.

+
+
+

Data

+

This 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.

+
+
import random
+
+import lightgbm as lgb
+import matplotlib.pyplot as plt
+import numpy as np
+import xgboost as xgb
+from datasetsforecast.m4 import M4, M4Info
+from sklearn.linear_model import LinearRegression
+from sklearn.metrics import mean_squared_error
+from utilsforecast.evaluation import evaluate
+from utilsforecast.losses import mape
+from utilsforecast.plotting import plot_series
+from window_ops.ewm import ewm_mean
+from window_ops.expanding import expanding_mean
+from window_ops.rolling import rolling_mean
+
+from mlforecast.lgb_cv import LightGBMCV
+from mlforecast.target_transforms import Differences, LocalStandardScaler
+from mlforecast.utils import generate_daily_series, generate_prices_for_series
+
+
+
group = 'Hourly'
+await M4.async_download('data', group=group)
+df, *_ = M4.load(directory='data', group=group)
+df['ds'] = df['ds'].astype('int')
+ids = df['unique_id'].unique()
+random.seed(0)
+sample_ids = random.choices(ids, k=4)
+sample_df = df[df['unique_id'].isin(sample_ids)]
+sample_df
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
unique_iddsy
86796H196111.8
86797H196211.4
86798H196311.1
86799H196410.8
86800H196510.6
............
325235H413100499.0
325236H413100588.0
325237H413100647.0
325238H413100741.0
325239H413100834.0
+ +

4032 rows × 3 columns

+
+
+
+

We now split this data into train and validation.

+
+
info = M4Info[group]
+horizon = info.horizon
+valid = sample_df.groupby('unique_id').tail(horizon)
+train = sample_df.drop(valid.index)
+train.shape, valid.shape
+
+
((3840, 3), (192, 3))
+
+
+
+
+

Creating the Forecast object

+

The forecast 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.
  • +
+
+
fcst = MLForecast(
+    models=lgb.LGBMRegressor(random_state=0, verbosity=-1),
+    lags=[24 * (i+1) for i in range(7)],
+    lag_transforms={
+        48: [(ewm_mean, 0.3)],
+    },
+    num_threads=1,
+    target_transforms=[Differences([24])],
+)
+fcst
+
+
MLForecast(models=[LGBMRegressor], freq=1, lag_features=['lag24', 'lag48', 'lag72', 'lag96', 'lag120', 'lag144', 'lag168', 'ewm_mean_lag48_alpha0.3'], date_features=[], num_threads=1)
+
+
+

Once we have this setup we can compute the features and fit the model.

+
+
+
+

MLForecast.fit

+
+
 MLForecast.fit (df:pandas.core.frame.DataFrame, id_col:str='unique_id',
+                 time_col:str='ds', target_col:str='y',
+                 static_features:Optional[List[str]]=None,
+                 dropna:bool=True, keep_last_n:Optional[int]=None,
+                 max_horizon:Optional[int]=None, prediction_intervals:Opti
+                 onal[mlforecast.utils.PredictionIntervals]=None,
+                 fitted:bool=False,
+                 data:Optional[pandas.core.frame.DataFrame]=None)
+
+

Apply the feature engineering and train the models.

+ ++++++ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
TypeDefaultDetails
dfDataFrameSeries data in long format.
id_colstrunique_idColumn that identifies each serie.
time_colstrdsColumn that identifies each timestep, its values can be timestamps or integers.
target_colstryColumn that contains the target.
static_featurestyping.Optional[typing.List[str]]NoneNames 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.
dropnaboolTrueDrop rows with missing values produced by the transformations.
keep_last_ntyping.Optional[int]NoneKeep only these many records from each serie for the forecasting step. Can save time and memory if your features allow it.
max_horizontyping.Optional[int]None
prediction_intervalstyping.Optional[mlforecast.utils.PredictionIntervals]NoneConfiguration to calibrate prediction intervals (Conformal Prediction).
fittedboolFalseSave in-sample predictions.
datatyping.Optional[pandas.core.frame.DataFrame]NoneSeries data in long format. This argument has been replaced by df and will be removed in a later release.
ReturnsMLForecastnoqa: ARG002
+
+
fcst.fit(train, fitted=True);
+
+
+
+
+

MLForecast.forecast_fitted_values

+
+
 MLForecast.forecast_fitted_values ()
+
+

Access in-sample predictions.

+
+
fcst.forecast_fitted_values()
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
unique_iddsyLGBMRegressor
0H19619312.712.671271
1H19619412.312.271271
2H19619511.911.871271
3H19619611.711.671271
4H19619711.411.471271
...............
3067H41395659.068.280574
3068H41395758.070.427570
3069H41395853.044.767965
3070H41395938.048.691257
3071H41396046.046.652238
+ +

3072 rows × 4 columns

+
+
+
+

Once we’ve run this we’re ready to compute our predictions.

+
+
+
+

MLForecast.predict

+
+
 MLForecast.predict (h:int,
+                     dynamic_dfs:Optional[List[pandas.core.frame.DataFrame
+                     ]]=None,
+                     before_predict_callback:Optional[Callable]=None,
+                     after_predict_callback:Optional[Callable]=None,
+                     new_df:Optional[pandas.core.frame.DataFrame]=None,
+                     level:Optional[List[Union[int,float]]]=None,
+                     X_df:Optional[pandas.core.frame.DataFrame]=None,
+                     ids:Optional[List[str]]=None,
+                     horizon:Optional[int]=None,
+                     new_data:Optional[pandas.core.frame.DataFrame]=None)
+
+

Compute the predictions for the next h steps.

+ ++++++ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
TypeDefaultDetails
hintNumber of periods to predict.
dynamic_dfstyping.Optional[typing.List[pandas.core.frame.DataFrame]]NoneFuture values of the dynamic features, e.g. prices.
before_predict_callbacktyping.Optional[typing.Callable]NoneFunction 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_callbacktyping.Optional[typing.Callable]NoneFunction 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_dftyping.Optional[pandas.core.frame.DataFrame]NoneSeries 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.
leveltyping.Optional[typing.List[typing.Union[int, float]]]NoneConfidence levels between 0 and 100 for prediction intervals.
X_dftyping.Optional[pandas.core.frame.DataFrame]NoneDataframe with the future exogenous features. Should have the id column and the time column.
idstyping.Optional[typing.List[str]]NoneList with subset of ids seen during training for which the forecasts should be computed.
horizontyping.Optional[int]NoneNumber of periods to predict. This argument has been replaced by h and will be removed in a later release.
new_datatyping.Optional[pandas.core.frame.DataFrame]NoneSeries 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_data is not None, the method will generate forecasts for the new observations.
ReturnsDataFramenoqa: ARG002
noqa: ARG002
+
+
predictions = fcst.predict(horizon)
+
+

We can see at a couple of results.

+
+
results = valid.merge(predictions, on=['unique_id', 'ds'])
+fig = plot_series(train, results, max_insample_length=0)
+fig.savefig('figs/forecast__predict.png', bbox_inches='tight')
+
+

+
+

Predicting a subset of the training series

+

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=sample_ids[:2])
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + +
unique_iddsLGBMRegressor
0H38196153.462100
1H41396125.206026
+ +
+
+
+
+
+

Prediction intervals

+

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.
  • +
+
+
fcst.fit(
+    train, 
+    prediction_intervals=PredictionIntervals(n_windows=3, h=48)
+);
+
+

After 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.

+
+
predictions_w_intervals = fcst.predict(48, level=[50, 80, 95])
+
+
+
predictions_w_intervals.head()
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
unique_iddsLGBMRegressorLGBMRegressor-lo-95LGBMRegressor-lo-80LGBMRegressor-lo-50LGBMRegressor-hi-50LGBMRegressor-hi-80LGBMRegressor-hi-95
0H19696116.07127115.95804215.97127116.00509116.13745216.17127116.184501
1H19696215.67127115.55363215.55363215.57863215.76391115.78891115.788911
2H19696315.27127115.15363215.15363215.16245215.38009115.38891115.388911
3H19696414.97127114.85804214.87127114.90509115.03745215.07127115.084501
4H19696514.67127114.55363214.55363214.56245214.78009114.78891114.788911
+ +
+
+
+
+
# test we can forecast horizon lower than h 
+# with prediction intervals
+for method in ['conformal_distribution', 'conformal_errors']:
+    fcst.fit(
+        train, 
+        prediction_intervals=PredictionIntervals(n_windows=3, h=48)
+    )
+
+    preds_h_lower_h = fcst.predict(1, level=[50, 80, 95])
+    preds_h_lower_h = fcst.predict(30, level=[50, 80, 95])
+
+    # test monotonicity of intervals
+    test_eq(
+        preds_h_lower_h.filter(regex='lo|hi').apply(
+            lambda x: x.is_monotonic_increasing,
+            axis=1
+        ).sum(),
+        len(preds_h_lower_h)
+    )
+
+

Let’s explore the generated intervals.

+
+
results = valid.merge(predictions_w_intervals, on=['unique_id', 'ds'])
+fig = plot_series(train, results, max_insample_length=0, level=[50, 80, 95])
+fig.savefig('figs/forecast__predict_intervals.png', bbox_inches='tight')
+
+

+

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.

+
+
fcst.fit(
+    train,  
+    prediction_intervals=PredictionIntervals(n_windows=3, h=1)
+);
+
+
+
predictions_w_intervals_ws_1 = fcst.predict(48, level=[80, 90, 95])
+
+

Let’s explore the generated intervals.

+
+
results = valid.merge(predictions_w_intervals_ws_1, on=['unique_id', 'ds'])
+fig = plot_series(train, results, max_insample_length=0, level=[90])
+fig.savefig('figs/forecast__predict_intervals_window_size_1.png', bbox_inches='tight')
+
+

+
+
+

Forecast using a pretrained model

+

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_data 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_data` 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.

+
+
+
+
+

MLForecast.preprocess

+
+
 MLForecast.preprocess (df:pandas.core.frame.DataFrame,
+                        id_col:str='unique_id', time_col:str='ds',
+                        target_col:str='y',
+                        static_features:Optional[List[str]]=None,
+                        dropna:bool=True, keep_last_n:Optional[int]=None,
+                        max_horizon:Optional[int]=None,
+                        return_X_y:bool=False,
+                        data:Optional[pandas.core.frame.DataFrame]=None)
+
+

Add the features to data.

+ ++++++ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
TypeDefaultDetails
dfDataFrameSeries data in long format.
id_colstrunique_idColumn that identifies each serie.
time_colstrdsColumn that identifies each timestep, its values can be timestamps or integers.
target_colstryColumn that contains the target.
static_featurestyping.Optional[typing.List[str]]NoneNames of the features that are static and will be repeated when forecasting.
dropnaboolTrueDrop rows with missing values produced by the transformations.
keep_last_ntyping.Optional[int]NoneKeep only these many records from each serie for the forecasting step. Can save time and memory if your features allow it.
max_horizontyping.Optional[int]None
return_X_yboolFalse
datatyping.Optional[pandas.core.frame.DataFrame]NoneSeries data in long format. This argument has been replaced by df and will be removed in a later release.
Returnstyping.Union[pandas.core.frame.DataFrame, typing.Tuple[pandas.core.frame.DataFrame, typing.Union[pandas.core.series.Series, pandas.core.frame.DataFrame]]]noqa: ARG002
+
+
prep_df = fcst.preprocess(train)
+prep_df
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
unique_iddsylag24lag48lag72lag96lag120lag144lag168ewm_mean_lag48_alpha0.3
86988H1961930.10.00.00.00.30.10.10.30.002810
86989H1961940.1-0.10.10.00.30.10.10.30.031967
86990H1961950.1-0.10.10.00.30.10.20.10.052377
86991H1961960.10.00.00.00.30.20.10.20.036664
86992H1961970.00.00.00.10.20.20.10.20.025665
....................................
325187H4139560.010.01.06.0-53.044.0-21.021.07.963225
325188H4139579.010.010.0-7.0-46.027.0-19.024.08.574257
325189H41395816.08.05.0-9.0-36.032.0-13.08.07.501980
325190H413959-3.017.0-7.02.0-31.022.05.0-2.03.151386
325191H41396015.011.0-6.0-5.0-17.022.0-18.010.00.405970
+ +

3072 rows × 11 columns

+
+
+
+

If we do this we then have to call Forecast.fit_models, since this only stores the series information.

+
+
+
+

MLForecast.fit_models

+
+
 MLForecast.fit_models (X:pandas.core.frame.DataFrame,
+                        y:Union[pandas.core.series.Series,pandas.core.fram
+                        e.DataFrame])
+
+

Manually train models. Use this if you called Forecast.preprocess beforehand.

+ +++++ + + + + + + + + + + + + + + + + + + + + + + + + +
TypeDetails
XDataFrameFeatures.
ytyping.Union[pandas.core.series.Series, pandas.core.frame.DataFrame]Target.
ReturnsMLForecastForecast object with trained models.
+
+
X, y = prep_df.drop(columns=['unique_id', 'ds', 'y']), prep_df['y']
+fcst.fit_models(X, y)
+
+
MLForecast(models=[LGBMRegressor], freq=1, lag_features=['lag24', 'lag48', 'lag72', 'lag96', 'lag120', 'lag144', 'lag168', 'ewm_mean_lag48_alpha0.3'], date_features=[], num_threads=1)
+
+
+
+
predictions2 = fcst.predict(horizon)
+pd.testing.assert_frame_equal(predictions, predictions2)
+
+
+
+

Multi-output model

+

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_mape(df):
+    full = df.merge(valid)
+    return (
+        evaluate(full, metrics=[mape])
+        .drop(columns='metric')
+        .set_index('unique_id')
+        .squeeze()
+    )
+
+
+
fcst = MLForecast(
+    models=lgb.LGBMRegressor(random_state=0, verbosity=-1),
+    lags=[24 * (i+1) for i in range(7)],
+    lag_transforms={
+        1: [(rolling_mean, 24)],
+        24: [(rolling_mean, 24)],
+        48: [(ewm_mean, 0.3)],
+    },
+    num_threads=1,
+    target_transforms=[Differences([24])],
+)
+
+
+
max_horizon = 24
+# the following will train 24 models, one for each horizon
+individual_fcst = fcst.fit(train, max_horizon=max_horizon)
+individual_preds = individual_fcst.predict(max_horizon)
+avg_mape_individual = avg_mape(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(max_horizon)
+avg_mape_recursive = avg_mape(recursive_preds).rename('recursive')
+# results
+print('Average MAPE per method and serie')
+avg_mape_individual.to_frame().join(avg_mape_recursive).applymap('{:.1%}'.format)
+
+
Average MAPE per method and serie
+
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
individualrecursive
unique_id
H1960.5%0.6%
H2560.7%0.6%
H38148.9%20.3%
H41326.9%35.1%
+ +
+
+
+
+
+

Cross validation

+

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:

+
    +
  1. Train: 1 to 72. Validation: 73 to 86.
  2. +
  3. Train: 1 to 86. Validation: 87 to 100.
  4. +
+

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:

+
    +
  1. Train: 1 to 85. Validation: 86 to 99.
  2. +
  3. Train: 1 to 86. Validation: 87 to 100.
  4. +
+

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.

+
+
+
+

MLForecast.cross_validation

+
+
 MLForecast.cross_validation (df:pandas.core.frame.DataFrame,
+                              n_windows:int, h:int,
+                              id_col:str='unique_id', time_col:str='ds',
+                              target_col:str='y',
+                              step_size:Optional[int]=None,
+                              static_features:Optional[List[str]]=None,
+                              dropna:bool=True,
+                              keep_last_n:Optional[int]=None,
+                              refit:Union[bool,int]=True,
+                              max_horizon:Optional[int]=None, before_predi
+                              ct_callback:Optional[Callable]=None, after_p
+                              redict_callback:Optional[Callable]=None, pre
+                              diction_intervals:Optional[mlforecast.utils.
+                              PredictionIntervals]=None,
+                              level:Optional[List[Union[int,float]]]=None,
+                              input_size:Optional[int]=None,
+                              fitted:bool=False, data:Optional[pandas.core
+                              .frame.DataFrame]=None,
+                              window_size:Optional[int]=None)
+
+

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.

+ ++++++ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
TypeDefaultDetails
dfDataFrameSeries data in long format.
n_windowsintNumber of windows to evaluate.
hintForecast horizon.
id_colstrunique_idColumn that identifies each serie.
time_colstrdsColumn that identifies each timestep, its values can be timestamps or integers.
target_colstryColumn that contains the target.
step_sizetyping.Optional[int]NoneStep size between each cross validation window. If None it will be equal to h.
static_featurestyping.Optional[typing.List[str]]NoneNames of the features that are static and will be repeated when forecasting.
dropnaboolTrueDrop rows with missing values produced by the transformations.
keep_last_ntyping.Optional[int]NoneKeep only these many records from each serie for the forecasting step. Can save time and memory if your features allow it.
refittyping.Union[bool, int]TrueRetrain 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_horizontyping.Optional[int]None
before_predict_callbacktyping.Optional[typing.Callable]NoneFunction 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_callbacktyping.Optional[typing.Callable]NoneFunction 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.
prediction_intervalstyping.Optional[mlforecast.utils.PredictionIntervals]NoneConfiguration to calibrate prediction intervals (Conformal Prediction).
leveltyping.Optional[typing.List[typing.Union[int, float]]]NoneConfidence levels between 0 and 100 for prediction intervals.
input_sizetyping.Optional[int]NoneMaximum training samples per serie in each window. If None, will use an expanding window.
fittedboolFalseStore the in-sample predictions.
datatyping.Optional[pandas.core.frame.DataFrame]NoneSeries data in long format. This argument has been replaced by df and will be removed in a later release.
window_sizetyping.Optional[int]NoneForecast horizon. This argument has been replaced by h and will be removed in a later release.
Returnspandas DataFramePredictions for each window with the series id, timestamp, last train date, target value and predictions from each model.
+
+
fcst = MLForecast(
+    models=lgb.LGBMRegressor(random_state=0, verbosity=-1),
+    lags=[24 * (i+1) for i in range(7)],
+    lag_transforms={
+        1: [(rolling_mean, 24)],
+        24: [(rolling_mean, 24)],
+        48: [(ewm_mean, 0.3)],
+    },
+    num_threads=1,
+    target_transforms=[Differences([24])],
+)
+cv_results = fcst.cross_validation(
+    train,
+    n_windows=4,
+    h=horizon,
+    step_size=horizon,
+    fitted=True,
+)
+cv_results
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
unique_iddscutoffyLGBMRegressor
0H19676976815.215.167163
1H19677076814.814.767163
2H19677176814.414.467163
3H19677276814.114.167163
4H19677376813.813.867163
..................
187H41395691259.064.284167
188H41395791258.064.830429
189H41395891253.040.726851
190H41395991238.042.739657
191H41396091246.052.802769
+ +

768 rows × 5 columns

+
+
+
+

Since we set fitted=True we can access the predictions for the training sets as well with the cross_validation_fitted_values method.

+
+
fcst.cross_validation_fitted_values()
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
unique_iddsfoldyLGBMRegressor
0H196193012.712.667163
1H196194012.312.267163
2H196195011.911.867163
3H196196011.711.667163
4H196197011.411.467163
..................
10363H413908349.050.620196
10364H413909339.035.972331
10365H413910329.029.359678
10366H413911324.025.784563
10367H413912320.023.168413
+ +

10368 rows × 5 columns

+
+
+
+

We can also compute prediction intervals by passing a configuration to prediction_intervals as well as values for the width through levels.

+
+
cv_results_intervals = fcst.cross_validation(
+    train,
+    n_windows=4,
+    h=horizon,
+    step_size=horizon,
+    prediction_intervals=PredictionIntervals(h=horizon),
+    level=[80, 90]
+)
+cv_results_intervals
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
unique_iddscutoffyLGBMRegressorLGBMRegressor-lo-90LGBMRegressor-lo-80LGBMRegressor-hi-80LGBMRegressor-hi-90
0H19676976815.215.16716315.14175115.14175115.19257515.192575
1H19677076814.814.76716314.74175114.74175114.79257514.792575
2H19677176814.414.46716314.39995114.40732814.52699814.534374
3H19677276814.114.16716314.09257514.09257514.24175114.241751
4H19677376813.813.86716313.79257513.79257513.94175113.941751
..............................
187H41395691259.064.28416729.89009934.37154594.19678898.678234
188H41395791258.064.83042956.87457257.82768971.83316972.786285
189H41395891253.040.72685135.29619535.84620645.60749546.157506
190H41395991238.042.73965735.29215335.80764049.67167450.187161
191H41396091246.052.80276942.46559743.89567061.70986963.139941
+ +

768 rows × 9 columns

+
+
+
+

The refit argument allows us to control if we want to retrain the models in every window. It can either be:

+
    +
  • A boolean: True will retrain on every window and False only on the first one.
  • +
  • A positive integer: The models will be trained on the first window and then every refit windows.
  • +
+
+
fcst = MLForecast(
+    models=LinearRegression(),
+    lags=[1, 24],
+)
+for refit, expected_models in zip([True, False, 2], [4, 1, 2]):
+    fcst.cross_validation(
+        train,
+        n_windows=4,
+        h=horizon,
+        refit=refit,
+    )
+    test_eq(len(fcst.cv_models_), expected_models)
+
+
+
fig = plot_series(cv_results, cv_results.drop(columns='cutoff'), max_insample_length=0)
+fig.savefig('figs/forecast__cross_validation.png', bbox_inches='tight')
+
+

+
+
fig = plot_series(cv_results_intervals, cv_results_intervals.drop(columns='cutoff'), level=[90], max_insample_length=0)
+fig.savefig('figs/forecast__cross_validation_intervals.png', bbox_inches='tight')
+
+

+
+
+

Create MLForecast from LightGBMCV

+

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.

+
+
cv = LightGBMCV(
+    freq=1,
+    lags=[24 * (i+1) for i in range(7)],
+    lag_transforms={
+        48: [(ewm_mean, 0.3)],
+    },
+    num_threads=1,
+    target_transforms=[Differences([24])]
+)
+hist = cv.fit(
+    train,
+    n_windows=2,
+    h=horizon,
+    params={'verbosity': -1},
+)
+
+
[LightGBM] [Info] Start training from score 0.084340
+[10] mape: 0.118569
+[20] mape: 0.111506
+[30] mape: 0.107314
+[40] mape: 0.106089
+[50] mape: 0.106630
+Early stopping at round 50
+Using best iteration: 40
+
+
+
+
fcst = MLForecast.from_cv(cv)
+assert cv.best_iteration_ == fcst.models['LGBMRegressor'].n_estimators
+
+
+
fcst.fit(train)
+fcst.predict(horizon)
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
unique_iddsLGBMRegressor
0H19696116.111079
1H19696215.711079
2H19696315.311079
3H19696415.011079
4H19696514.711079
............
187H413100492.722032
188H413100569.153603
189H413100668.811675
190H413100753.693346
191H413100846.055481
+ +

192 rows × 3 columns

+
+
+
+
+
+

Dynamic features

+

We’re going to use a synthetic dataset from this point onwards to demonstrate some other functionalities regarding external regressors.

+
+
series = generate_daily_series(100, equal_ends=True, n_static_features=2, static_as_categorical=False)
+series
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
unique_iddsystatic_0static_1
0id_002000-10-0539.8119837945
1id_002000-10-06103.2740137945
2id_002000-10-07176.5747447945
3id_002000-10-08258.9879007945
4id_002000-10-09344.9404047945
..................
26998id_992001-05-10453.4005096935
26999id_992001-05-1130.2294786935
27000id_992001-05-12101.3137136935
27001id_992001-05-13145.7243356935
27002id_992001-05-14228.1635716935
+ +

27003 rows × 5 columns

+
+
+
+

As we saw in the previous example, the required columns are the series identifier, time and target. Whatever extra columns you have, like static_0 and static_1 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 they will still be used for training, so you’ll have to provide them to MLForecast.predict through the X_df argument.

+

By default the predict method repeats the static features and updates the transformations and the date features. If you have dynamic features like prices or a calendar with holidays you can pass them as a dataframe to the X_df argument of MLForecast.predict, which will call pd.DataFrame.merge on it in each timestep.

+

Here’s an example:

+

Suppose that we have a prices catalog for each id and date.

+
+
dynamic_series = series.rename(columns={'static_1': 'product_id'})
+prices_catalog = generate_prices_for_series(dynamic_series)
+prices_catalog
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
dsunique_idprice
02000-10-05id_000.548814
12000-10-06id_000.715189
22000-10-07id_000.602763
32000-10-08id_000.544883
42000-10-09id_000.423655
............
276982001-05-17id_990.682296
276992001-05-18id_990.123657
277002001-05-19id_990.068762
277012001-05-20id_990.324157
277022001-05-21id_990.605791
+ +

27703 rows × 3 columns

+
+
+
+

And you have already merged these prices into your series dataframe.

+
+
series_with_prices = dynamic_series.merge(prices_catalog, how='left')
+series_with_prices
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
unique_iddsystatic_0product_idprice
0id_002000-10-0539.81198379450.548814
1id_002000-10-06103.27401379450.715189
2id_002000-10-07176.57474479450.602763
3id_002000-10-08258.98790079450.544883
4id_002000-10-09344.94040479450.423655
.....................
26998id_992001-05-10453.40050969350.112841
26999id_992001-05-1130.22947869350.883449
27000id_992001-05-12101.31371369350.762250
27001id_992001-05-13145.72433569350.025932
27002id_992001-05-14228.16357169350.651356
+ +

27003 rows × 6 columns

+
+
+
+

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 and we’ll have to update price in every timestep, which basically involves merging the updated features with the prices catalog.

+
+
def even_day(dates):
+    return dates.day % 2 == 0
+
+fcst = MLForecast(
+    models=lgb.LGBMRegressor(n_jobs=1, random_state=0, verbosity=-1),
+    freq='D',
+    lags=[7],
+    lag_transforms={
+        1: [expanding_mean],
+        7: [(rolling_mean, 14)]
+    },
+    date_features=['dayofweek', 'month', even_day],
+    num_threads=2,
+)
+fcst.fit(series_with_prices, static_features=['static_0', 'product_id'])
+
+
MLForecast(models=[LGBMRegressor], freq=<Day>, lag_features=['lag7', 'expanding_mean_lag1', 'rolling_mean_lag7_window_size14'], date_features=['dayofweek', 'month', <function even_day>], num_threads=2)
+
+
+

The features used for training are stored in MLForecast.ts.features_order_, as you can see price was used for training.

+
+
fcst.ts.features_order_
+
+
['static_0',
+ 'product_id',
+ 'price',
+ 'lag7',
+ 'expanding_mean_lag1',
+ 'rolling_mean_lag7_window_size14',
+ 'dayofweek',
+ 'month',
+ 'even_day']
+
+
+

So in order to update the price in each timestep we just call MLForecast.predict with our forecast horizon and pass the prices catalog as a dynamic dataframe.

+
+
preds = fcst.predict(7, X_df=prices_catalog)
+preds
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
unique_iddsLGBMRegressor
0id_002001-05-15423.827509
1id_002001-05-16500.693693
2id_002001-05-1719.287859
3id_002001-05-18103.152614
4id_002001-05-19188.337877
............
695id_992001-05-17444.845741
696id_992001-05-1818.827125
697id_992001-05-1990.571682
698id_992001-05-20151.555404
699id_992001-05-21229.120406
+ +

700 rows × 3 columns

+
+
+
+
+
+

Custom predictions

+

As you may have noticed MLForecast.predict can take a before_predict_callback and after_predict_callback. By default the predict method repeats the static features and updates the transformations and the date features. If you have dynamic features you can pass them as a dataframe to MLForecast.predict through the X_df argument. However, if you want to do something to the input before predicting or do 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.

+

Suppose that we want to look at our inputs and scale our predictions so that our series are updated with these scaled values. We can achieve that with the following:

+
+
from IPython.display import display
+
+
+
def inspect_input(new_x):
+    """Displays the first row of our input to inspect it"""
+    print('Inputs:')
+    display(new_x.head(1))
+    return new_x
+
+def increase_predictions(predictions):
+    """Prints the last prediction and increases all of them by 10%."""
+    print(f'Prediction:\n{predictions.tail(1)}\n')
+    return 1.1 * predictions
+
+

And now we just pass these functions to MLForecast.predict.

+
+
fcst = MLForecast(lgb.LGBMRegressor(verbosity=-1), freq='D', lags=[1])
+fcst.fit(series)
+
+preds = fcst.predict(2, before_predict_callback=inspect_input, after_predict_callback=increase_predictions)
+preds
+
+
Inputs:
+Prediction:
+unique_id
+id_99    306.432532
+dtype: float64
+
+Inputs:
+Prediction:
+unique_id
+id_99    410.240636
+dtype: float64
+
+
+
+
+ + + + + + + + + + + + + + + + + + + +
static_0static_1lag1
07945348.622446
+ +
+
+
+
+ + + + + + + + + + + + + + + + + + + +
static_0static_1lag1
07945463.963464
+ +
+
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
unique_iddsLGBMRegressor
0id_002001-05-15463.963464
1id_002001-05-16236.519441
2id_012001-05-15143.889544
3id_012001-05-16177.969900
4id_022001-05-15156.405278
............
195id_972001-05-16398.496930
196id_982001-05-1584.086265
197id_982001-05-1642.909331
198id_992001-05-15337.075785
199id_992001-05-16451.264699
+ +

200 rows × 3 columns

+
+
+
+ + +
+ +

Give us a ⭐ on Github

+ +
+ + + + \ No newline at end of file diff --git a/grouped_array.html b/grouped_array.html new file mode 100644 index 00000000..ddfa56df --- /dev/null +++ b/grouped_array.html @@ -0,0 +1,758 @@ + + + + + + + + + +mlforecast – grouped_array + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+ + +
+ +
+ + +
+ + + +
+ + + + +
+
+

GroupedArray

+
+
 GroupedArray (data:numpy.ndarray, indptr:numpy.ndarray)
+
+

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')
+
+
+
# __setitem__
+new_vals = np.array([10, 11])
+ga[0] = new_vals
+np.testing.assert_equal(ga.data, np.append(new_vals, np.arange(2, 10)))
+
+ + +
+ +

Give us a ⭐ on Github

+ +
+ + + + \ No newline at end of file diff --git a/index.html b/index.html new file mode 100644 index 00000000..1e100823 --- /dev/null +++ b/index.html @@ -0,0 +1,1068 @@ + + + + + + + + + +mlforecast - Nixtla     + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+ + +
+ +
+ + +
+ + + +
+ +
+
+

Nixtla   Tweet  Slack

+
+ + + +
+ + + + +
+ + +
+ + +
+
+ +
+

+Machine Learning 🤖 Forecast +

+

+Scalable machine learning for time series forecasting +

+

CI Python PyPi conda-forge License

+

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

+

If you want to perform distributed training, you can instead use pip install "mlforecast[distributed]", which will also install dask. Note that you’ll also need to install either LightGBM or XGBoost.

+
+
+

conda-forge

+

conda install -c conda-forge mlforecast

+

Note that this installation comes with the required dependencies for the local interface. If you want to perform distributed training, you must install dask (conda install -c conda-forge dask) and either LightGBM or XGBoost.

+
+
+
+

Quick Start

+

Minimal Example

+
import lightgbm as lgb
+
+from mlforecast import MLForecast
+from sklearn.linear_model import LinearRegression
+
+mlf = MLForecast(
+    models = [LinearRegression(), lgb.LGBMRegressor()],
+    lags=[1, 12],
+    freq = 'M'
+)
+mlf.fit(df)
+mlf.predict(12)
+

Get Started with this quick guide.

+

Follow this end-to-end walkthrough for best practices.

+
+
+

Why?

+

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 Slack

+
+
+

Examples and Guides

+

📚 End to End Walkthrough: model training, evaluation and selection for multiple time series.

+

🔎 Probabilistic Forecasting: use Conformal Prediction to produce prediciton intervals.

+

👩‍🔬 Cross Validation: robust model’s performance evaluation.

+

🔌 Predict Demand Peaks: electricity load forecasting for detecting daily peaks and reducing electric bills.

+

📈 Transfer Learning: pretrain a model using a set of time series and then predict another one using that pretrained model.

+

🌡️ Distributed Training: use a Dask cluster to train models at scale.

+
+
+

How to use

+

The following provides a very basic overview, for a more detailed description see the documentation.

+
+

Data setup

+

Store your time series in a pandas dataframe in long format, that is, each row represents an observation for a specific serie and timestamp.

+
+
from mlforecast.utils import generate_daily_series
+
+series = generate_daily_series(
+    n_series=20,
+    max_length=100,
+    n_static_features=1,
+    static_as_categorical=False,
+    with_trend=True
+)
+series.head()
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
unique_iddsystatic_0
0id_002000-01-0117.51916772
1id_002000-01-0287.79969572
2id_002000-01-03177.44297572
3id_002000-01-04232.70411072
4id_002000-01-05317.51047472
+ +
+
+
+
+
+

Models

+

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.

+
+
import lightgbm as lgb
+import xgboost as xgb
+from sklearn.ensemble import RandomForestRegressor
+
+models = [
+    lgb.LGBMRegressor(verbosity=-1),
+    xgb.XGBRegressor(),
+    RandomForestRegressor(random_state=0),
+]
+
+
+
+

Forecast object

+

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 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.

+
+
from mlforecast import MLForecast
+from mlforecast.target_transforms import Differences
+from numba import njit
+from window_ops.expanding import expanding_mean
+from window_ops.rolling import rolling_mean
+
+
+@njit
+def rolling_mean_28(x):
+    return rolling_mean(x, window_size=28)
+
+
+fcst = MLForecast(
+    models=models,
+    freq='D',
+    lags=[7, 14],
+    lag_transforms={
+        1: [expanding_mean],
+        7: [rolling_mean_28]
+    },
+    date_features=['dayofweek'],
+    target_transforms=[Differences([1])],
+)
+
+
+
+

Training

+

To compute the features and train the models call fit on your Forecast object.

+
+
fcst.fit(series)
+
+
MLForecast(models=[LGBMRegressor, XGBRegressor, RandomForestRegressor], freq=<Day>, lag_features=['lag7', 'lag14', 'expanding_mean_lag1', 'rolling_mean_28_lag7'], date_features=['dayofweek'], num_threads=1)
+
+
+
+
+

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.

+
+
predictions = fcst.predict(14)
+predictions
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
unique_iddsLGBMRegressorXGBRegressorRandomForestRegressor
0id_002000-04-04299.923771309.664124298.424164
1id_002000-04-05365.424147382.150085365.816014
2id_002000-04-06432.562441453.373779436.360620
3id_002000-04-07495.628000527.965149503.670100
4id_002000-04-0860.78622375.76229962.176080
..................
275id_192000-03-2336.26678029.88912034.799780
276id_192000-03-2444.37098434.96888439.920982
277id_192000-03-2550.74622239.97023846.196266
278id_192000-03-2658.90652445.12530551.653060
279id_192000-03-2763.07394950.68271656.845384
+ +

280 rows × 5 columns

+
+
+
+
+
+

Visualize results

+
+
from utilsforecast.plotting import plot_series
+
+
+
fig = plot_series(series, predictions, max_ids=4, plot_random=False)
+fig.savefig('figs/index.png', bbox_inches='tight')
+
+

+
+
+
+

Sample notebooks

+ +
+
+

How to contribute

+

See CONTRIBUTING.md.

+ + +
+ +

Give us a ⭐ on Github

+ +
+ + + + \ No newline at end of file diff --git a/lgb_cv.html b/lgb_cv.html new file mode 100644 index 00000000..58130d4a --- /dev/null +++ b/lgb_cv.html @@ -0,0 +1,1790 @@ + + + + + + + + + + +mlforecast - LightGBMCV + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+ + +
+ +
+ + +
+ + + +
+ +
+
+

LightGBMCV

+
+ +
+
+ Time series cross validation with LightGBM. +
+
+ + +
+ + + + +
+ + +
+ + +
+
+

LightGBMCV

+
+
 LightGBMCV (freq:Union[int,str,pandas._libs.tslibs.offsets.BaseOffset,Non
+             eType]=None, lags:Optional[Iterable[int]]=None, lag_transform
+             s:Optional[Dict[int,List[Union[Callable,Tuple[Callable,Any]]]
+             ]]=None,
+             date_features:Optional[Iterable[Union[str,Callable]]]=None,
+             num_threads:int=1, target_transforms:Optional[List[mlforecast
+             .target_transforms.BaseTargetTransform]]=None)
+
+

Create LightGBM CV object.

+ ++++++ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
TypeDefaultDetails
freqtyping.Union[int, str, pandas._libs.tslibs.offsets.BaseOffset, NoneType]NonePandas offset alias, e.g. ‘D’, ‘W-THU’ or integer denoting the frequency of the series.
lagstyping.Optional[typing.Iterable[int]]NoneLags of the target to use as features.
lag_transformstyping.Optional[typing.Dict[int, typing.List[typing.Union[typing.Callable, typing.Tuple[typing.Callable, typing.Any]]]]]NoneMapping of target lags to their transformations.
date_featurestyping.Optional[typing.Iterable[typing.Union[str, typing.Callable]]]NoneFeatures computed from the dates. Can be pandas date attributes or functions that will take the dates as input.
num_threadsint1Number of threads to use when computing the features.
target_transformstyping.Optional[typing.List[mlforecast.target_transforms.BaseTargetTransform]]NoneTransformations that will be applied to the target before computing the features and restored after the forecasting step.
+
+
+

Example

+

This 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.

+
+
import random
+
+from datasetsforecast.m4 import M4, M4Info
+from fastcore.test import test_eq, test_fail
+from mlforecast.target_transforms import Differences
+from nbdev import show_doc
+from window_ops.ewm import ewm_mean
+from window_ops.rolling import rolling_mean, seasonal_rolling_mean
+
+
+
group = 'Hourly'
+await M4.async_download('data', group=group)
+df, *_ = M4.load(directory='data', group=group)
+df['ds'] = df['ds'].astype('int')
+ids = df['unique_id'].unique()
+random.seed(0)
+sample_ids = random.choices(ids, k=4)
+sample_df = df[df['unique_id'].isin(sample_ids)]
+sample_df
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
unique_iddsy
86796H196111.8
86797H196211.4
86798H196311.1
86799H196410.8
86800H196510.6
............
325235H413100499.0
325236H413100588.0
325237H413100647.0
325238H413100741.0
325239H413100834.0
+ +

4032 rows × 3 columns

+
+
+
+
+
info = M4Info[group]
+horizon = info.horizon
+valid = sample_df.groupby('unique_id').tail(horizon)
+train = sample_df.drop(valid.index)
+train.shape, valid.shape
+
+
((3840, 3), (192, 3))
+
+
+

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 in range(7)],  # one week of lags
+)
+
+
+
+

LightGBMCV.fit

+
+
 LightGBMCV.fit (df:pandas.core.frame.DataFrame, n_windows:int, h:int,
+                 id_col:str='unique_id', time_col:str='ds',
+                 target_col:str='y', step_size:Optional[int]=None,
+                 num_iterations:int=100,
+                 params:Optional[Dict[str,Any]]=None,
+                 static_features:Optional[List[str]]=None,
+                 dropna:bool=True, keep_last_n:Optional[int]=None,
+                 eval_every:int=10,
+                 weights:Optional[Sequence[float]]=None,
+                 metric:Union[str,Callable]='mape',
+                 verbose_eval:bool=True, early_stopping_evals:int=2,
+                 early_stopping_pct:float=0.01,
+                 compute_cv_preds:bool=False,
+                 before_predict_callback:Optional[Callable]=None,
+                 after_predict_callback:Optional[Callable]=None,
+                 input_size:Optional[int]=None,
+                 data:Optional[pandas.core.frame.DataFrame]=None,
+                 window_size:Optional[int]=None)
+
+

Train boosters simultaneously and assess their performance on the complete forecasting window.

+ ++++++ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
TypeDefaultDetails
dfDataFrameSeries data in long format.
n_windowsintNumber of windows to evaluate.
hintForecast horizon.
id_colstrunique_idColumn that identifies each serie.
time_colstrdsColumn that identifies each timestep, its values can be timestamps or integers.
target_colstryColumn that contains the target.
step_sizetyping.Optional[int]NoneStep size between each cross validation window. If None it will be equal to h.
num_iterationsint100Maximum number of boosting iterations to run.
paramstyping.Optional[typing.Dict[str, typing.Any]]NoneParameters to be passed to the LightGBM Boosters.
static_featurestyping.Optional[typing.List[str]]NoneNames of the features that are static and will be repeated when forecasting.
dropnaboolTrueDrop rows with missing values produced by the transformations.
keep_last_ntyping.Optional[int]NoneKeep only these many records from each serie for the forecasting step. Can save time and memory if your features allow it.
eval_everyint10Number of boosting iterations to train before evaluating on the whole forecast window.
weightstyping.Optional[typing.Sequence[float]]NoneWeights to multiply the metric of each window. If None, all windows have the same weight.
metrictyping.Union[str, typing.Callable]mapeMetric used to assess the performance of the models and perform early stopping.
verbose_evalboolTruePrint the metrics of each evaluation.
early_stopping_evalsint2Maximum number of evaluations to run without improvement.
early_stopping_pctfloat0.01Minimum percentage improvement in metric value in early_stopping_evals evaluations.
compute_cv_predsboolFalseCompute predictions for each window after finding the best iteration.
before_predict_callbacktyping.Optional[typing.Callable]NoneFunction 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_callbacktyping.Optional[typing.Callable]NoneFunction 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_sizetyping.Optional[int]NoneMaximum training samples per serie in each window. If None, will use an expanding window.
datatyping.Optional[pandas.core.frame.DataFrame]NoneSeries data in long format. This argument has been replaced by df and will be removed in a later release.
window_sizetyping.Optional[int]NoneForecast horizon. This argument has been replaced by h and will be removed in a later release.
Returnstyping.List[typing.Tuple[int, float]]noqa: ARG002
noqa: ARG002
+
+
hist = cv.fit(train, **static_fit_config)
+
+
[LightGBM] [Info] Start training from score 51.745632
+[10] mape: 0.590690
+[20] mape: 0.251093
+[30] mape: 0.143643
+[40] mape: 0.109723
+[50] mape: 0.102099
+[60] mape: 0.099448
+[70] mape: 0.098349
+[80] mape: 0.098006
+[90] mape: 0.098718
+Early stopping at round 90
+Using best iteration: 80
+
+
+

By setting compute_cv_preds we get the predictions from each model on their corresponding validation fold.

+
+
cv.cv_preds_
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
unique_iddsyBoosterwindow
0H19686515.515.5229240
1H19686615.114.9858320
2H19686714.814.6679010
3H19686814.414.5145920
4H19686914.214.0357930
..................
187H41395659.077.2279051
188H41395758.080.5896411
189H41395853.053.9868341
190H41395938.036.7497861
191H41396046.036.2812251
+ +

384 rows × 5 columns

+
+
+
+

The individual models we trained are saved, so calling predict returns the predictions from every model trained.

+
+
+
+

LightGBMCV.predict

+
+
 LightGBMCV.predict (h:int,
+                     dynamic_dfs:Optional[List[pandas.core.frame.DataFrame
+                     ]]=None,
+                     before_predict_callback:Optional[Callable]=None,
+                     after_predict_callback:Optional[Callable]=None,
+                     X_df:Optional[pandas.core.frame.DataFrame]=None,
+                     horizon:Optional[int]=None)
+
+

Compute predictions with each of the trained boosters.

+ ++++++ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
TypeDefaultDetails
hintForecast horizon.
dynamic_dfstyping.Optional[typing.List[pandas.core.frame.DataFrame]]NoneFuture values of the dynamic features, e.g. prices.
before_predict_callbacktyping.Optional[typing.Callable]NoneFunction 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_callbacktyping.Optional[typing.Callable]NoneFunction 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_dftyping.Optional[pandas.core.frame.DataFrame]NoneDataframe with the future exogenous features. Should have the id column and the time column.
horizontyping.Optional[int]NoneForecast horizon. This argument has been replaced by h and will be removed in a later release.
ReturnsDataFramenoqa: ARG002
+
+
preds = cv.predict(horizon)
+preds
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
unique_iddsBooster0Booster1
0H19696115.67025215.848888
1H19696215.52292415.697399
2H19696314.98583215.166213
3H19696414.98583214.723238
4H19696514.56215214.451092
...............
187H413100470.69524265.917620
188H413100566.21658062.615788
189H413100663.89657367.848598
190H413100746.92279750.981950
191H413100845.00654142.752819
+ +

192 rows × 4 columns

+
+
+
+

We can average these predictions and evaluate them.

+
+
def evaluate_on_valid(preds):
+    preds = preds.copy()
+    preds['final_prediction'] = preds.drop(columns=['unique_id', 'ds']).mean(1)
+    merged = preds.merge(valid, on=['unique_id', 'ds'])
+    merged['abs_err'] = abs(merged['final_prediction'] - merged['y']) / merged['y']
+    return merged.groupby('unique_id')['abs_err'].mean().mean()
+
+
+
eval1 = evaluate_on_valid(preds)
+eval1
+
+
0.11036194712311806
+
+
+

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 in range(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.

+
+
preds2 = cv2.predict(horizon)
+eval2 = evaluate_on_valid(preds2)
+eval2
+
+
0.08956665504570135
+
+
+
+
assert eval2 < eval1
+
+

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.

+
+
cv3 = LightGBMCV(
+    freq=1,
+    target_transforms=[Differences([24 * 7])],
+    lags=[24 * (i+1) for i in range(7)],
+    lag_transforms={
+        48: [(seasonal_rolling_mean, 24, 7)],
+    },
+)
+hist3 = cv3.fit(train, **static_fit_config)
+
+
[LightGBM] [Info] Start training from score 0.273641
+[10] mape: 0.086724
+[20] mape: 0.088466
+[30] mape: 0.090536
+Early stopping at round 30
+Using best iteration: 10
+
+
+

Seems like this is helping as well!

+
+
assert hist3[-1][1] < hist2[-1][1]
+
+

Does this reflect on the validation set?

+
+
preds3 = cv3.predict(horizon)
+eval3 = evaluate_on_valid(preds3)
+eval3
+
+
0.08961279023129345
+
+
+

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.

+
+
+
+

LightGBMCV.setup

+
+
 LightGBMCV.setup (df:pandas.core.frame.DataFrame, n_windows:int, h:int,
+                   id_col:str='unique_id', time_col:str='ds',
+                   target_col:str='y', step_size:Optional[int]=None,
+                   params:Optional[Dict[str,Any]]=None,
+                   static_features:Optional[List[str]]=None,
+                   dropna:bool=True, keep_last_n:Optional[int]=None,
+                   weights:Optional[Sequence[float]]=None,
+                   metric:Union[str,Callable]='mape',
+                   input_size:Optional[int]=None,
+                   data:Optional[pandas.core.frame.DataFrame]=None,
+                   window_size:Optional[int]=None)
+
+

Initialize internal data structures to iteratively train the boosters. Use this before calling partial_fit.

+ ++++++ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
TypeDefaultDetails
dfDataFrameSeries data in long format.
n_windowsintNumber of windows to evaluate.
hintForecast horizon.
id_colstrunique_idColumn that identifies each serie.
time_colstrdsColumn that identifies each timestep, its values can be timestamps or integers.
target_colstryColumn that contains the target.
step_sizetyping.Optional[int]NoneStep size between each cross validation window. If None it will be equal to h.
paramstyping.Optional[typing.Dict[str, typing.Any]]NoneParameters to be passed to the LightGBM Boosters.
static_featurestyping.Optional[typing.List[str]]NoneNames of the features that are static and will be repeated when forecasting.
dropnaboolTrueDrop rows with missing values produced by the transformations.
keep_last_ntyping.Optional[int]NoneKeep only these many records from each serie for the forecasting step. Can save time and memory if your features allow it.
weightstyping.Optional[typing.Sequence[float]]NoneWeights to multiply the metric of each window. If None, all windows have the same weight.
metrictyping.Union[str, typing.Callable]mapeMetric used to assess the performance of the models and perform early stopping.
input_sizetyping.Optional[int]NoneMaximum training samples per serie in each window. If None, will use an expanding window.
datatyping.Optional[pandas.core.frame.DataFrame]NoneSeries data in long format. This argument has been replaced by df and will be removed in a later release.
window_sizetyping.Optional[int]NoneForecast horizon. This argument has been replaced by h and will be removed in a later release.
ReturnsLightGBMCVCV object with internal data structures for partial_fit.
+
+
cv4 = LightGBMCV(
+    freq=1,
+    lags=[24 * (i+1) for i in range(7)],
+)
+cv4.setup(
+    train,
+    n_windows=2,
+    h=horizon,
+    params={'verbose': -1},
+)
+
+
LightGBMCV(freq=1, lag_features=['lag24', 'lag48', 'lag72', 'lag96', 'lag120', 'lag144', 'lag168'], date_features=[], num_threads=1, bst_threads=8)
+
+
+

Once we have this we can call partial_fit to only train for some iterations and return the score of the forecast window.

+
+
+
+

LightGBMCV.partial_fit

+
+
 LightGBMCV.partial_fit (num_iterations:int,
+                         before_predict_callback:Optional[Callable]=None,
+                         after_predict_callback:Optional[Callable]=None)
+
+

Train the boosters for some iterations.

+ ++++++ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
TypeDefaultDetails
num_iterationsintNumber of boosting iterations to run
before_predict_callbacktyping.Optional[typing.Callable]NoneFunction 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_callbacktyping.Optional[typing.Callable]NoneFunction 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.
ReturnsfloatWeighted 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()
+
+
+
_ = LightGBMCV(
+    freq=1,
+    lags=[24 * (i+1) for i in range(7)],
+).fit(
+    train,
+    n_windows=2,
+    h=horizon,
+    params={'verbose': -1},
+    metric=weighted_mape,
+)
+
+
[LightGBM] [Info] Start training from score 51.745632
+[10] weighted_mape: 0.480353
+[20] weighted_mape: 0.218670
+[30] weighted_mape: 0.161706
+[40] weighted_mape: 0.149992
+[50] weighted_mape: 0.149024
+[60] weighted_mape: 0.148496
+Early stopping at round 60
+Using best iteration: 60
+
+
+ + +
+
+ +

Give us a ⭐ on Github

+ +
+ + + + \ 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..4682d8f3 --- /dev/null +++ b/search.json @@ -0,0 +1,807 @@ +[ + { + "objectID": "forecast.html", + "href": "forecast.html", + "title": "MLForecast", + "section": "", + "text": "MLForecast\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 class is a high level abstraction that encapsulates all the steps in the pipeline (preprocessing, fitting the model and computing the predictions). It tries to mimic the scikit-learn API.\n\n\nData\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\nCreating the Forecast object\nThe forecast 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 data:Optional[pandas.core.frame.DataFrame]=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\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\ndata\ntyping.Optional[pandas.core.frame.DataFrame]\nNone\nSeries data in long format. This argument has been replaced by df and will be removed in a later release.\n\n\nReturns\nMLForecast\n\nnoqa: ARG002\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 dynamic_dfs:Optional[List[pandas.core.frame.DataFrame\n ]]=None,\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 horizon:Optional[int]=None,\n new_data:Optional[pandas.core.frame.DataFrame]=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\ndynamic_dfs\ntyping.Optional[typing.List[pandas.core.frame.DataFrame]]\nNone\nFuture values of the dynamic features, e.g. prices.\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\nhorizon\ntyping.Optional[int]\nNone\nNumber of periods to predict. This argument has been replaced by h and will be removed in a later release.\n\n\nnew_data\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_data is not None, the method will generate forecasts for the new observations.\n\n\nReturns\nDataFrame\n\nnoqa: ARG002noqa: ARG002\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\nPredicting a subset of the training series\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=sample_ids[:2])\n\n\n\n\n\n\n\n\nunique_id\nds\nLGBMRegressor\n\n\n\n\n0\nH381\n961\n53.462100\n\n\n1\nH413\n961\n25.206026\n\n\n\n\n\n\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_data 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_data` 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 data:Optional[pandas.core.frame.DataFrame]=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\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\ndata\ntyping.Optional[pandas.core.frame.DataFrame]\nNone\nSeries data in long format. This argument has been replaced by df and will be removed in a later release.\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\nnoqa: ARG002\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\nMulti-output model\nBy 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.\n\ndef avg_mape(df):\n full = df.merge(valid)\n return (\n evaluate(full, metrics=[mape])\n .drop(columns='metric')\n .set_index('unique_id')\n .squeeze()\n )\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 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\nmax_horizon = 24\n# the following will train 24 models, one for each horizon\nindividual_fcst = fcst.fit(train, max_horizon=max_horizon)\nindividual_preds = individual_fcst.predict(max_horizon)\navg_mape_individual = avg_mape(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(max_horizon)\navg_mape_recursive = avg_mape(recursive_preds).rename('recursive')\n# results\nprint('Average MAPE per method and serie')\navg_mape_individual.to_frame().join(avg_mape_recursive).applymap('{:.1%}'.format)\n\nAverage MAPE 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.5%\n0.6%\n\n\nH256\n0.7%\n0.6%\n\n\nH381\n48.9%\n20.3%\n\n\nH413\n26.9%\n35.1%\n\n\n\n\n\n\n\n\n\nCross validation\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\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, data:Optional[pandas.core\n .frame.DataFrame]=None,\n window_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\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\ndata\ntyping.Optional[pandas.core.frame.DataFrame]\nNone\nSeries data in long format. This argument has been replaced by df and will be removed in a later release.\n\n\nwindow_size\ntyping.Optional[int]\nNone\nForecast horizon. This argument has been replaced by h and will be removed in a later release.\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\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\nCreate MLForecast from LightGBMCV\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\nDynamic features\nWe’re going to use a synthetic dataset from this point onwards to demonstrate some other functionalities regarding external regressors.\n\nseries = generate_daily_series(100, equal_ends=True, n_static_features=2, static_as_categorical=False)\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-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...\n...\n\n\n26998\nid_99\n2001-05-10\n453.400509\n69\n35\n\n\n26999\nid_99\n2001-05-11\n30.229478\n69\n35\n\n\n27000\nid_99\n2001-05-12\n101.313713\n69\n35\n\n\n27001\nid_99\n2001-05-13\n145.724335\n69\n35\n\n\n27002\nid_99\n2001-05-14\n228.163571\n69\n35\n\n\n\n\n27003 rows × 5 columns\n\n\n\nAs we saw in the previous example, the required columns are the series identifier, time and target. Whatever extra columns you have, like static_0 and static_1 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 they will still be used for training, so you’ll have to provide them to MLForecast.predict through the X_df argument.\nBy default the predict method repeats the static features and updates the transformations and the date features. If you have dynamic features like prices or a calendar with holidays you can pass them as a dataframe to the X_df argument of MLForecast.predict, which will call pd.DataFrame.merge on it in each timestep.\nHere’s an example:\nSuppose that we have a prices catalog for each id and date.\n\ndynamic_series = series.rename(columns={'static_1': 'product_id'})\nprices_catalog = generate_prices_for_series(dynamic_series)\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\n27698\n2001-05-17\nid_99\n0.682296\n\n\n27699\n2001-05-18\nid_99\n0.123657\n\n\n27700\n2001-05-19\nid_99\n0.068762\n\n\n27701\n2001-05-20\nid_99\n0.324157\n\n\n27702\n2001-05-21\nid_99\n0.605791\n\n\n\n\n27703 rows × 3 columns\n\n\n\nAnd you have already merged these prices into your series dataframe.\n\nseries_with_prices = dynamic_series.merge(prices_catalog, how='left')\nseries_with_prices\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...\n...\n...\n\n\n26998\nid_99\n2001-05-10\n453.400509\n69\n35\n0.112841\n\n\n26999\nid_99\n2001-05-11\n30.229478\n69\n35\n0.883449\n\n\n27000\nid_99\n2001-05-12\n101.313713\n69\n35\n0.762250\n\n\n27001\nid_99\n2001-05-13\n145.724335\n69\n35\n0.025932\n\n\n27002\nid_99\n2001-05-14\n228.163571\n69\n35\n0.651356\n\n\n\n\n27003 rows × 6 columns\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 and we’ll have to update price in every timestep, which basically involves merging the updated features with the prices catalog.\n\ndef even_day(dates):\n return dates.day % 2 == 0\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', even_day],\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', <function even_day>], 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 'even_day']\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 as a dynamic dataframe.\n\npreds = fcst.predict(7, X_df=prices_catalog)\npreds\n\n\n\n\n\n\n\n\nunique_id\nds\nLGBMRegressor\n\n\n\n\n0\nid_00\n2001-05-15\n423.827509\n\n\n1\nid_00\n2001-05-16\n500.693693\n\n\n2\nid_00\n2001-05-17\n19.287859\n\n\n3\nid_00\n2001-05-18\n103.152614\n\n\n4\nid_00\n2001-05-19\n188.337877\n\n\n...\n...\n...\n...\n\n\n695\nid_99\n2001-05-17\n444.845741\n\n\n696\nid_99\n2001-05-18\n18.827125\n\n\n697\nid_99\n2001-05-19\n90.571682\n\n\n698\nid_99\n2001-05-20\n151.555404\n\n\n699\nid_99\n2001-05-21\n229.120406\n\n\n\n\n700 rows × 3 columns\n\n\n\n\n\nCustom predictions\nAs you may have noticed MLForecast.predict can take a before_predict_callback and after_predict_callback. By default the predict method repeats the static features and updates the transformations and the date features. If you have dynamic features you can pass them as a dataframe to MLForecast.predict through the X_df argument. However, if you want to do something to the input before predicting or do 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.\nSuppose that we want to look at our inputs and scale our predictions so that our series are updated with these scaled values. We can achieve that with the following:\n\nfrom IPython.display import display\n\n\ndef inspect_input(new_x):\n \"\"\"Displays the first row of our input to inspect it\"\"\"\n print('Inputs:')\n display(new_x.head(1))\n return new_x\n\ndef increase_predictions(predictions):\n \"\"\"Prints the last prediction and increases all of them by 10%.\"\"\"\n print(f'Prediction:\\n{predictions.tail(1)}\\n')\n return 1.1 * predictions\n\nAnd now we just pass these functions to MLForecast.predict.\n\nfcst = MLForecast(lgb.LGBMRegressor(verbosity=-1), freq='D', lags=[1])\nfcst.fit(series)\n\npreds = fcst.predict(2, before_predict_callback=inspect_input, after_predict_callback=increase_predictions)\npreds\n\nInputs:\nPrediction:\nunique_id\nid_99 306.432532\ndtype: float64\n\nInputs:\nPrediction:\nunique_id\nid_99 410.240636\ndtype: float64\n\n\n\n\n\n\n\n\n\n\nstatic_0\nstatic_1\nlag1\n\n\n\n\n0\n79\n45\n348.622446\n\n\n\n\n\n\n\n\n\n\n\n\n\n\nstatic_0\nstatic_1\nlag1\n\n\n\n\n0\n79\n45\n463.963464\n\n\n\n\n\n\n\n\n\n\n\n\n\n\nunique_id\nds\nLGBMRegressor\n\n\n\n\n0\nid_00\n2001-05-15\n463.963464\n\n\n1\nid_00\n2001-05-16\n236.519441\n\n\n2\nid_01\n2001-05-15\n143.889544\n\n\n3\nid_01\n2001-05-16\n177.969900\n\n\n4\nid_02\n2001-05-15\n156.405278\n\n\n...\n...\n...\n...\n\n\n195\nid_97\n2001-05-16\n398.496930\n\n\n196\nid_98\n2001-05-15\n84.086265\n\n\n197\nid_98\n2001-05-16\n42.909331\n\n\n198\nid_99\n2001-05-15\n337.075785\n\n\n199\nid_99\n2001-05-16\n451.264699\n\n\n\n\n200 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 window_size=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 window_size=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 window_size=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, window_size=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/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\n100%|████████████████████████████████████████████████████████████████| 1.76M/1.76M [00:00<00:00, 15.3MiB/s]\nINFO:datasetsforecast.utils:Successfully downloaded M3C.xls, 1757696, bytes.\n\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(horizon=12, new_data=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/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": "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": "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 window_size:Optional[int]=None)\n\nClass for storing prediction intervals metadata information.\n\n\n\n\nType\nDefault\nDetails\n\n\n\n\nn_windows\nint\n2\n\n\n\nh\nint\n1\n\n\n\nmethod\nstr\nconformal_distribution\n\n\n\nwindow_size\ntyping.Optional[int]\nNone\nnoqa: ARG002\n\n\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, dynamic_\n dfs:Optional[List[pandas.core.frame.DataFrame]]=None,\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": "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": "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": "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": "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.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": "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": "index.html", + "href": "index.html", + "title": "Nixtla    ", + "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": "Nixtla    ", + "section": "Install", + "text": "Install\n\nPyPI\npip install mlforecast\nIf you want to perform distributed training, you can instead use pip install \"mlforecast[distributed]\", which will also install dask. Note that you’ll also need to install either LightGBM or XGBoost.\n\n\nconda-forge\nconda install -c conda-forge mlforecast\nNote that this installation comes with the required dependencies for the local interface. If you want to perform distributed training, you must install dask (conda install -c conda-forge dask) and either LightGBM or XGBoost." + }, + { + "objectID": "index.html#quick-start", + "href": "index.html#quick-start", + "title": "Nixtla    ", + "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." + }, + { + "objectID": "index.html#why", + "href": "index.html#why", + "title": "Nixtla    ", + "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": "Nixtla    ", + "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": "Nixtla    ", + "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 cluster to train models at scale." + }, + { + "objectID": "index.html#how-to-use", + "href": "index.html#how-to-use", + "title": "Nixtla    ", + "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#sample-notebooks", + "href": "index.html#sample-notebooks", + "title": "Nixtla    ", + "section": "Sample notebooks", + "text": "Sample notebooks\n\nm5\nm4\nm4-cv" + }, + { + "objectID": "index.html#how-to-contribute", + "href": "index.html#how-to-contribute", + "title": "Nixtla    ", + "section": "How to contribute", + "text": "How to contribute\nSee CONTRIBUTING.md." + }, + { + "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": "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 data:Optional[pandas.core.frame.DataFrame]=None,\n window_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\ndata\ntyping.Optional[pandas.core.frame.DataFrame]\nNone\nSeries data in long format. This argument has been replaced by df and will be removed in a later release.\n\n\nwindow_size\ntyping.Optional[int]\nNone\nForecast horizon. This argument has been replaced by h and will be removed in a later release.\n\n\nReturns\ntyping.List[typing.Tuple[int, float]]\n\nnoqa: ARG002noqa: ARG002\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 dynamic_dfs:Optional[List[pandas.core.frame.DataFrame\n ]]=None,\n before_predict_callback:Optional[Callable]=None,\n after_predict_callback:Optional[Callable]=None,\n X_df:Optional[pandas.core.frame.DataFrame]=None,\n horizon:Optional[int]=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\ndynamic_dfs\ntyping.Optional[typing.List[pandas.core.frame.DataFrame]]\nNone\nFuture values of the dynamic features, e.g. prices.\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\nhorizon\ntyping.Optional[int]\nNone\nForecast horizon. This argument has been replaced by h and will be removed in a later release.\n\n\nReturns\nDataFrame\n\nnoqa: ARG002\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 data:Optional[pandas.core.frame.DataFrame]=None,\n window_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\ndata\ntyping.Optional[pandas.core.frame.DataFrame]\nNone\nSeries data in long format. This argument has been replaced by df and will be removed in a later release.\n\n\nwindow_size\ntyping.Optional[int]\nNone\nForecast horizon. This argument has been replaced by h and will be removed in a later release.\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.forecast.html", + "href": "distributed.forecast.html", + "title": "DistributedMLForecast", + "section": "", + "text": "Give us a ⭐ on Github" + }, + { + "objectID": "distributed.forecast.html#dask", + "href": "distributed.forecast.html#dask", + "title": "DistributedMLForecast", + "section": "Dask", + "text": "Dask\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\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.\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\n\nDistributedMLForecast(models=[DaskXGBForecast, DaskLGBMForecast], freq=<Day>, lag_features=['lag7', 'expanding_mean_lag1', 'rolling_mean_lag7_window_size14'], date_features=['dayofweek', 'month'], num_threads=1, engine=<Client: 'tcp://127.0.0.1:42319' processes=2 threads=2, memory=15.48 GiB>)\n\n\nHere where we say that:\n\nOur series have daily frequency.\nWe want to use lag 7 as a feature\nWe want the lag transformations to be:\n\nexpanding mean of the lag 1\nrolling mean of the lag 7 over a window of size 14\n\nWe want to use dayofweek and month as date features.\nWe want to perform the preprocessing and the forecasting steps using 1 thread, because we have 10 partitions and 2 workers.\n\nFrom this point we have two options:\n\nCompute the features and fit our models.\nCompute the features and get them back as a dataframe to do some custom splitting or adding additional features, then training the models.\n\n\n\n1. Using all the data" + }, + { + "objectID": "distributed.forecast.html#distributedmlforecast.fit", + "href": "distributed.forecast.html#distributedmlforecast.fit", + "title": "DistributedMLForecast", + "section": "DistributedMLForecast.fit", + "text": "DistributedMLForecast.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 data:Optional[~AnyDataFrame]=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\ndata\ntyping.Optional[~AnyDataFrame]\nNone\n\n\n\nReturns\nDistributedMLForecast\n\nnoqa: ARG002\n\n\n\nCalling fit on our data computes the features independently for each partition and performs distributed training.\n\nfcst.fit(partitioned_series)\n\n\nForecasting" + }, + { + "objectID": "distributed.forecast.html#distributedmlforecast.predict", + "href": "distributed.forecast.html#distributedmlforecast.predict", + "title": "DistributedMLForecast", + "section": "DistributedMLForecast.predict", + "text": "DistributedMLForecast.predict\n\n DistributedMLForecast.predict (h:int,\n dynamic_dfs:Optional[List[pandas.core.fram\n e.DataFrame]]=None, before_predict_callbac\n k:Optional[Callable]=None, after_predict_c\n allback:Optional[Callable]=None,\n new_df:Optional[~AnyDataFrame]=None,\n horizon:Optional[int]=None,\n new_data: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\ndynamic_dfs\ntyping.Optional[typing.List[pandas.core.frame.DataFrame]]\nNone\nFuture values of the dynamic features, e.g. prices.\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\nhorizon\ntyping.Optional[int]\nNone\n\n\n\nnew_data\ntyping.Optional[~AnyDataFrame]\nNone\n\n\n\nReturns\nAnyDataFrame\n\nPredictions for each serie and timestep, with one column per model.\n\n\n\nOnce we have our fitted models we can compute the predictions for the next 7 timesteps.\n\npreds = fcst.predict(7)\npreds\n\nDask DataFrame Structure:\n\n\n\n\n\n\n\nunique_id\nds\nDaskXGBForecast\nDaskLGBMForecast\n\n\nnpartitions=10\n\n\n\n\n\n\n\n\nid_00\nobject\ndatetime64[ns]\nfloat64\nfloat64\n\n\nid_10\n...\n...\n...\n...\n\n\n...\n...\n...\n...\n...\n\n\nid_89\n...\n...\n...\n...\n\n\nid_99\n...\n...\n...\n...\n\n\n\n\n\nDask Name: map, 17 graph layers\n\n\n\n2. Preprocess and train\nIf we only want to perform the preprocessing step we call preprocess with our data." + }, + { + "objectID": "distributed.forecast.html#distributedmlforecast.preprocess", + "href": "distributed.forecast.html#distributedmlforecast.preprocess", + "title": "DistributedMLForecast", + "section": "DistributedMLForecast.preprocess", + "text": "DistributedMLForecast.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 data:Optional[~AnyDataFrame]=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\ndata\ntyping.Optional[~AnyDataFrame]\nNone\n\n\n\nReturns\nAnyDataFrame\n\nnoqa: ARG002\n\n\n\n\nfeatures_ddf = fcst.preprocess(partitioned_series)\nfeatures_ddf.head()\n\n\n\n\n\n\n\n\nunique_id\nds\ny\nstatic_0\nstatic_1\nlag7\nexpanding_mean_lag1\nrolling_mean_lag7_window_size14\n\n\n\n\n20\nid_00\n2000-10-25\n49.766844\n79\n45\n50.694639\n25.001367\n26.320060\n\n\n21\nid_00\n2000-10-26\n3.918347\n79\n45\n3.887780\n26.180675\n26.313387\n\n\n22\nid_00\n2000-10-27\n9.437778\n79\n45\n11.512774\n25.168751\n26.398056\n\n\n23\nid_00\n2000-10-28\n17.923574\n79\n45\n18.038498\n24.484796\n26.425272\n\n\n24\nid_00\n2000-10-29\n26.754645\n79\n45\n24.222859\n24.211411\n26.305563\n\n\n\n\n\n\n\nThis is useful if we want to inspect the data the model will be trained. If we do this we must manually train our models and add a local version of them to the models_ attribute.\n\nX, y = features_ddf.drop(columns=['unique_id', 'ds', 'y']), features_ddf['y']\nmodel = DaskXGBForecast(random_state=0).fit(X, y)\nfcst.models_ = {'DaskXGBForecast': model.model_}\nfcst.predict(7)\n\n\nDynamic features\nBy default the predict method repeats the static features and updates the transformations and the date features. If you have dynamic features like prices or a calendar with holidays you can pass them as a list to the dynamic_dfs argument of DistributedMLForecast.predict, which will call pd.DataFrame.merge on each of them in order.\nHere’s an example:\nSuppose that we have a product_id column and we have a catalog for prices based on that product_id and the date.\n\ndynamic_series = series.rename(columns={'static_1': 'product_id'})\nprices_catalog = generate_prices_for_series(dynamic_series)\nprices_catalog\n\n\n\n\n\n\n\n\nds\nproduct_id\nprice\n\n\n\n\n0\n2000-06-09\n1\n0.548814\n\n\n1\n2000-06-10\n1\n0.715189\n\n\n2\n2000-06-11\n1\n0.602763\n\n\n3\n2000-06-12\n1\n0.544883\n\n\n4\n2000-06-13\n1\n0.423655\n\n\n...\n...\n...\n...\n\n\n20180\n2001-05-17\n99\n0.223520\n\n\n20181\n2001-05-18\n99\n0.446104\n\n\n20182\n2001-05-19\n99\n0.044783\n\n\n20183\n2001-05-20\n99\n0.483216\n\n\n20184\n2001-05-21\n99\n0.799660\n\n\n\n\n20185 rows × 3 columns\n\n\n\nAnd you have already merged these prices into your series dataframe.\n\ndynamic_series = partitioned_series.rename(columns={'static_1': 'product_id'})\ndynamic_series = dynamic_series\nseries_with_prices = dynamic_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\n3.981198\n79\n45\n0.570826\n\n\n1\nid_00\n2000-10-06\n10.327401\n79\n45\n0.260562\n\n\n2\nid_00\n2000-10-07\n17.657474\n79\n45\n0.274048\n\n\n3\nid_00\n2000-10-08\n25.898790\n79\n45\n0.433878\n\n\n4\nid_00\n2000-10-09\n34.494040\n79\n45\n0.653738\n\n\n\n\n\n\n\nThis dataframe will be passed to DistributedMLForecast.fit (or DistributedMLForecast.preprocess), however since the price is dynamic we have to tell that method that only static_0 and product_id are static and we’ll have to update price in every timestep, which basically involves merging the updated features with the prices catalog.\n\nfcst = DistributedMLForecast(\n 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)\nseries_with_prices = series_with_prices\nfcst.fit(\n series_with_prices,\n static_features=['static_0', 'product_id'],\n)\n\nSo in order to update the price in each timestep we just call DistributedMLForecast.predict with our forecast horizon and pass the prices catalog as a dynamic dataframe.\n\npreds = fcst.predict(7, dynamic_dfs=[prices_catalog])\npreds.compute()\n\n\n\n\n\n\n\n\nunique_id\nds\nDaskXGBForecast\nDaskLGBMForecast\n\n\n\n\n0\nid_00\n2001-05-15\n42.404003\n43.094384\n\n\n1\nid_00\n2001-05-16\n50.457447\n49.880064\n\n\n2\nid_00\n2001-05-17\n2.044398\n1.938665\n\n\n3\nid_00\n2001-05-18\n10.102403\n10.250496\n\n\n4\nid_00\n2001-05-19\n18.245543\n18.473560\n\n\n...\n...\n...\n...\n...\n\n\n72\nid_99\n2001-05-17\n43.536346\n44.494822\n\n\n73\nid_99\n2001-05-18\n2.063584\n2.093080\n\n\n74\nid_99\n2001-05-19\n9.016212\n9.148367\n\n\n75\nid_99\n2001-05-20\n15.630316\n15.048958\n\n\n76\nid_99\n2001-05-21\n22.420233\n23.037681\n\n\n\n\n700 rows × 4 columns\n\n\n\n\n\nCustom predictions\nIf you want to do something like scaling the predictions you can define a function and pass it to DistributedMLForecast.predict as described in Custom predictions.\n\nCross validation\nRefer to MLForecast.cross_validation." + }, + { + "objectID": "distributed.forecast.html#distributedmlforecast.cross_validation", + "href": "distributed.forecast.html#distributedmlforecast.cross_validation", + "title": "DistributedMLForecast", + "section": "DistributedMLForecast.cross_validation", + "text": "DistributedMLForecast.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, da\n ta:Optional[~AnyDataFrame]=None,\n window_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\ndata\ntyping.Optional[~AnyDataFrame]\nNone\n\n\n\nwindow_size\ntyping.Optional[int]\nNone\n\n\n\nReturns\nAnyDataFrame\n\nnoqa: ARG002noqa: ARG002\n\n\n\n\nfcst = DistributedMLForecast(\n models=[DaskLGBMForecast(), DaskXGBForecast()],\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)\n\n\nn_windows = 2\nwindow_size = 14\n\ncv_results = fcst.cross_validation(\n partitioned_series,\n n_windows,\n window_size,\n)\ncv_results\n\nWe can aggregate these by date to get a rough estimate of how our model is doing.\n\nagg_results = cv_results_df.drop(columns='cutoff').groupby('ds').mean()\nagg_results.head()\n\n\n\n\n\n\n\n\nDaskLGBMForecast\nDaskXGBForecast\ny\n\n\nds\n\n\n\n\n\n\n\n2001-04-17\n16.195230\n16.168709\n16.123231\n\n\n2001-04-18\n15.145318\n15.135734\n15.213920\n\n\n2001-04-19\n17.149119\n17.087150\n16.985699\n\n\n2001-04-20\n18.002781\n18.045092\n18.068340\n\n\n2001-04-21\n18.136612\n18.142144\n18.200609\n\n\n\n\n\n\n\nWe can also compute the error for each model.\n\ndef mse_from_dask_dataframe(ddf):\n mses = {}\n for model_name in ddf.columns.drop(['unique_id', 'ds', 'y', 'cutoff']):\n mses[model_name] = (ddf['y'] - ddf[model_name]).pow(2).mean()\n return client.gather(client.compute(mses))\n\n{k: round(v, 2) for k, v in mse_from_dask_dataframe(cv_results).items()}\n\n{'DaskLGBMForecast': 0.92, 'DaskXGBForecast': 0.86}\n\n\n\nclient.close()" + }, + { + "objectID": "distributed.forecast.html#spark", + "href": "distributed.forecast.html#spark", + "title": "DistributedMLForecast", + "section": "Spark", + "text": "Spark\n\nSession setup\n\nfrom pyspark.sql import SparkSession\n\n\nspark = (\n SparkSession.builder.appName(\"MyApp\")\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()\n\n \n\n\n\n\n\n\n\n\n\nunique_id\nds\nSparkLGBMForecast\nSparkXGBForecast\n\n\n\n\n0\nid_00\n2001-05-15\n42.213984\n42.305004\n\n\n1\nid_00\n2001-05-16\n49.718021\n50.262386\n\n\n2\nid_00\n2001-05-17\n1.306248\n1.912686\n\n\n3\nid_00\n2001-05-18\n10.060104\n10.240939\n\n\n4\nid_00\n2001-05-19\n18.070785\n18.265749\n\n\n...\n...\n...\n...\n...\n\n\n1395\nid_99\n2001-05-24\n43.426901\n43.780163\n\n\n1396\nid_99\n2001-05-25\n1.361680\n2.097803\n\n\n1397\nid_99\n2001-05-26\n8.787283\n8.593580\n\n\n1398\nid_99\n2001-05-27\n15.551965\n15.622238\n\n\n1399\nid_99\n2001-05-28\n22.518518\n22.943216\n\n\n\n\n1400 rows × 4 columns\n\n\n\n\n\nCross validation\n\ncv_res = fcst.cross_validation(\n spark_series,\n n_windows=2,\n window_size=14,\n).toPandas()\n\n\ncv_res\n\n\n\n\n\n\n\n\nunique_id\nds\nSparkLGBMForecast\nSparkXGBForecast\ncutoff\ny\n\n\n\n\n0\nid_17\n2001-04-30\n31.467849\n31.676336\n2001-04-16\n30.832464\n\n\n1\nid_07\n2001-04-17\n1.015429\n1.039312\n2001-04-16\n1.034871\n\n\n2\nid_06\n2001-04-29\n21.133919\n1.368022\n2001-04-16\n0.944155\n\n\n3\nid_11\n2001-04-17\n57.069013\n57.591526\n2001-04-16\n57.406090\n\n\n4\nid_12\n2001-04-27\n7.965585\n7.741258\n2001-04-16\n8.498222\n\n\n...\n...\n...\n...\n...\n...\n...\n\n\n2795\nid_96\n2001-05-12\n9.069598\n8.925149\n2001-04-30\n7.983343\n\n\n2796\nid_84\n2001-05-04\n10.474623\n9.959846\n2001-04-30\n10.683266\n\n\n2797\nid_87\n2001-05-07\n2.162316\n2.065432\n2001-04-30\n1.277810\n\n\n2798\nid_80\n2001-05-11\n22.679552\n20.547785\n2001-04-30\n19.823192\n\n\n2799\nid_90\n2001-05-08\n40.225448\n40.293419\n2001-04-30\n39.215204\n\n\n\n\n2800 rows × 6 columns\n\n\n\n\nspark.stop()" + }, + { + "objectID": "distributed.forecast.html#ray", + "href": "distributed.forecast.html#ray", + "title": "DistributedMLForecast", + "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\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)\n\n\npreds.to_pandas()\n\n\n\n\n\n\n\n\nunique_id\nds\nRayLGBMForecast\nRayXGBForecast\n\n\n\n\n0\nid_00\n2001-05-15\n42.213984\n41.992321\n\n\n1\nid_00\n2001-05-16\n49.718021\n50.999878\n\n\n2\nid_00\n2001-05-17\n1.306248\n1.712625\n\n\n3\nid_00\n2001-05-18\n10.060104\n10.157331\n\n\n4\nid_00\n2001-05-19\n18.070785\n18.163649\n\n\n...\n...\n...\n...\n...\n\n\n1395\nid_99\n2001-05-24\n43.426901\n42.060478\n\n\n1396\nid_99\n2001-05-25\n1.361680\n2.587303\n\n\n1397\nid_99\n2001-05-26\n8.787283\n8.652343\n\n\n1398\nid_99\n2001-05-27\n15.551965\n15.278493\n\n\n1399\nid_99\n2001-05-28\n22.518518\n22.898369\n\n\n\n\n1400 rows × 4 columns\n\n\n\n\n\nCross validation\n\ncv_res = fcst.cross_validation(\n ray_series,\n n_windows=2,\n window_size=14,\n).to_pandas()\n\n\ncv_res\n\n\n\n\n\n\n\n\nunique_id\nds\nRayLGBMForecast\nRayXGBForecast\ncutoff\ny\n\n\n\n\n0\nid_00\n2001-04-17\n41.395948\n41.968201\n2001-04-16\n40.499332\n\n\n1\nid_00\n2001-04-18\n50.004670\n50.191704\n2001-04-16\n50.888323\n\n\n2\nid_00\n2001-04-19\n1.821105\n1.978645\n2001-04-16\n0.121812\n\n\n3\nid_00\n2001-04-20\n10.266459\n10.211697\n2001-04-16\n10.987977\n\n\n4\nid_00\n2001-04-21\n18.285400\n17.944368\n2001-04-16\n16.370385\n\n\n...\n...\n...\n...\n...\n...\n...\n\n\n2795\nid_69\n2001-05-07\n2.151752\n1.971745\n2001-04-30\n0.768383\n\n\n2796\nid_82\n2001-05-09\n29.733492\n29.844685\n2001-04-30\n29.584063\n\n\n2797\nid_80\n2001-05-03\n14.978611\n15.564403\n2001-04-30\n14.888339\n\n\n2798\nid_28\n2001-05-10\n18.204017\n16.644882\n2001-04-30\n16.512652\n\n\n2799\nid_93\n2001-05-01\n29.756173\n29.035599\n2001-04-30\n29.340027\n\n\n\n\n2800 rows × 6 columns\n\n\n\n\nray.shutdown()" + }, + { + "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/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.\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.\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\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, which takes the following arguments:\n\ndf: training data frame with MLForecast format\nh (int): represents the h steps into the future that will be forecasted\nn_windows (int): number of windows used for cross-validation, meaning the number of forecasting processes in the past you want to evaluate.\nid_col: identifies each time series.\ntime_col: indetifies the temporal column of the time series.\ntarget_col: identifies the column to model.\n\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/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 data=Y_df,\n window_size=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\n28799.818120\n28341.60\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\n27863.728484\n27266.37\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\n27192.197241\n26478.41\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\n27006.228231\n26264.58\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\n27617.689685\n27074.15\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\n1074.464944\n1159.437604\n\n\nmape\n0.049759\n0.030966\n0.031760\n0.031888\n0.031888\n0.038721\n0.033707\n0.036373\n\n\nrmse\n1871.398919\n1129.713256\n1148.616156\n1153.262719\n1153.262664\n1451.964390\n1209.164053\n1324.512394\n\n\nsmape\n0.024786\n0.015886\n0.016269\n0.016338\n0.016338\n0.019549\n0.017102\n0.018514\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\n20448.354846\n29940.516875\n\n\n1\nPJM_Load_hourly\n2001-12-31 02:00:00\n24000.725423\n19006.959378\n28729.851990\n\n\n2\nPJM_Load_hourly\n2001-12-31 03:00:00\n23324.771966\n18598.120705\n27956.224882\n\n\n3\nPJM_Load_hourly\n2001-12-31 04:00:00\n23332.519871\n18240.006713\n28438.714923\n\n\n4\nPJM_Load_hourly\n2001-12-31 05:00:00\n24107.126827\n19107.466049\n29238.143993\n\n\n\n\n\n\n\n\ntime_prophet = (end - init) \nprint(f'Prophet Time: {time_prophet:.2f} seconds')\n\nProphet Time: 27.44 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, window_size=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.24 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 22.21 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 main component for distributed training with mlforecast is the DistributedMLForecast class, which abstracts away:\n\nFeature engineering and model training through DistributedMLForecast.fit\nFeature updates and multi step ahead predictions through DistributedMLForecast.predict\nGive us a ⭐ on Github" + }, + { + "objectID": "docs/getting-started/quick_start_distributed.html#main-concepts", + "href": "docs/getting-started/quick_start_distributed.html#main-concepts", + "title": "Quick start (distributed)", + "section": "", + "text": "The main component for distributed training with mlforecast is the DistributedMLForecast class, which abstracts away:\n\nFeature engineering and model training through DistributedMLForecast.fit\nFeature updates and multi step ahead predictions through DistributedMLForecast.predict" + }, + { + "objectID": "docs/getting-started/quick_start_distributed.html#setup", + "href": "docs/getting-started/quick_start_distributed.html#setup", + "title": "Quick start (distributed)", + "section": "Setup", + "text": "Setup\nIn order to perform distributed training you need a dask cluster. In this example we’ll use a local cluster but you can replace it with any other type of remote cluster and the processing will take place there.\n\nfrom dask.distributed import Client, LocalCluster\n\n\ncluster = LocalCluster(n_workers=2, threads_per_worker=1) # change this to use a remote cluster\nclient = Client(cluster)" + }, + { + "objectID": "docs/getting-started/quick_start_distributed.html#data-format", + "href": "docs/getting-started/quick_start_distributed.html#data-format", + "title": "Quick start (distributed)", + "section": "Data format", + "text": "Data format\nThe data is expected to be a dask 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\nYou need to make sure that each serie is only in a single partition. You can do so by setting the id_col as the index in dask or with repartitionByRange in spark.\nHere we present an example with synthetic data.\n\nimport dask.dataframe as dd\nfrom mlforecast.utils import generate_daily_series\n\n\nseries = generate_daily_series(100, with_trend=True)\nseries\n\n\n\n\n\n\n\n\nunique_id\nds\ny\n\n\n\n\n0\nid_00\n2000-01-01\n0.497650\n\n\n1\nid_00\n2000-01-02\n1.554489\n\n\n2\nid_00\n2000-01-03\n2.734311\n\n\n3\nid_00\n2000-01-04\n4.028039\n\n\n4\nid_00\n2000-01-05\n5.366009\n\n\n...\n...\n...\n...\n\n\n26998\nid_99\n2000-06-25\n34.165302\n\n\n26999\nid_99\n2000-06-26\n28.277320\n\n\n27000\nid_99\n2000-06-27\n29.450129\n\n\n27001\nid_99\n2000-06-28\n30.241885\n\n\n27002\nid_99\n2000-06-29\n31.576907\n\n\n\n\n27003 rows × 3 columns\n\n\n\nHere we can see that the index goes from id_00 to id_99, which means we have 100 different series stacked together.\nWe also have the ds column that contains the timestamps, in this case with a daily frequency, and the y column that contains the series values in each timestamp.\nIn order to perform distributed processing and training we need to have these in a dask dataframe, this is typically done loading them directly in a distributed way, for example with dd.read_parquet.\n\nseries_ddf = dd.from_pandas(series.set_index('unique_id'), npartitions=2) # make sure we split by id\nseries_ddf = series_ddf.map_partitions(lambda part: part.reset_index()) # we can't have an index\nseries_ddf['unique_id'] = series_ddf['unique_id'].astype('str') # categoricals aren't supported at the moment\nseries_ddf\n\nDask DataFrame Structure:\n\n\n\n\n\n\n\nunique_id\nds\ny\n\n\nnpartitions=2\n\n\n\n\n\n\n\nid_00\nobject\ndatetime64[ns]\nfloat64\n\n\nid_49\n...\n...\n...\n\n\nid_99\n...\n...\n...\n\n\n\n\n\nDask Name: assign, 5 graph layers\n\n\nWe now have a dask dataframe with two partitions which will be processed independently in each machine and their outputs will be combined to perform distributed training." + }, + { + "objectID": "docs/getting-started/quick_start_distributed.html#modeling", + "href": "docs/getting-started/quick_start_distributed.html#modeling", + "title": "Quick start (distributed)", + "section": "Modeling", + "text": "Modeling\n\nfrom utilsforecast.plotting import plot_series\n\n\nfig = plot_series(series, max_insample_length=70, max_ids=4)\n\n\nWe can see that the series have 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 LightGBM model using the value from the same day of the week at the previous week (lag 7) as a feature, this is done by passing lags=[7].\n\nfrom mlforecast.distributed import DistributedMLForecast\nfrom mlforecast.distributed.models.dask.lgb import DaskLGBMForecast\nfrom mlforecast.target_transforms import Differences\n\n\nfcst = DistributedMLForecast(\n models=DaskLGBMForecast(verbosity=-1),\n freq='D',\n lags=[7],\n target_transforms=[Differences([1])],\n)\nfcst.fit(series_ddf)\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_distributed.html#forecasting", + "href": "docs/getting-started/quick_start_distributed.html#forecasting", + "title": "Quick start (distributed)", + "section": "Forecasting", + "text": "Forecasting\nCompute the forecast for the next 14 days.\n\npreds = fcst.predict(14)\npreds\n\nDask DataFrame Structure:\n\n\n\n\n\n\n\nunique_id\nds\nDaskLGBMForecast\n\n\nnpartitions=2\n\n\n\n\n\n\n\nid_00\nobject\ndatetime64[ns]\nfloat64\n\n\nid_49\n...\n...\n...\n\n\nid_99\n...\n...\n...\n\n\n\n\n\nDask Name: map, 17 graph layers\n\n\nThese are returned as a dask dataframe as well. If it’s safe (memory-wise) we can bring them to the main process.\n\nlocal_preds = preds.compute()" + }, + { + "objectID": "docs/getting-started/quick_start_distributed.html#visualize-results", + "href": "docs/getting-started/quick_start_distributed.html#visualize-results", + "title": "Quick start (distributed)", + "section": "Visualize results", + "text": "Visualize results\nWe can visualize what our prediction looks like.\n\nfig = plot_series(series, local_preds, max_ids=4, max_insample_length=70)\n\n\nAnd that’s it! You’ve trained a distributed LightGBM model and computed predictions for the next 14 days." + } +] \ 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:''},pn={...un.DefaultType,content:"(string|element|function)"},mn={HIDE:"hide.bs.popover",HIDDEN:"hidden.bs.popover",SHOW:"show.bs.popover",SHOWN:"shown.bs.popover",INSERTED:"inserted.bs.popover",CLICK:"click.bs.popover",FOCUSIN:"focusin.bs.popover",FOCUSOUT:"focusout.bs.popover",MOUSEENTER:"mouseenter.bs.popover",MOUSELEAVE:"mouseleave.bs.popover"};class gn extends un{static get Default(){return fn}static get NAME(){return"popover"}static get Event(){return mn}static get DefaultType(){return pn}isWithContent(){return this.getTitle()||this._getContent()}setContent(t){this._sanitizeAndSetContent(t,this.getTitle(),".popover-header"),this._sanitizeAndSetContent(t,this._getContent(),".popover-body")}_getContent(){return this._resolvePossibleFunction(this._config.content)}_getBasicClassPrefix(){return"bs-popover"}static jQueryInterface(t){return this.each((function(){const e=gn.getOrCreateInstance(this,t);if("string"==typeof t){if(void 0===e[t])throw new TypeError(`No method named "${t}"`);e[t]()}}))}}g(gn);const _n="scrollspy",bn={offset:10,method:"auto",target:""},vn={offset:"number",method:"string",target:"(string|element)"},yn="active",wn=".nav-link, .list-group-item, .dropdown-item",En="position";class An extends B{constructor(t,e){super(t),this._scrollElement="BODY"===this._element.tagName?window:this._element,this._config=this._getConfig(e),this._offsets=[],this._targets=[],this._activeTarget=null,this._scrollHeight=0,j.on(this._scrollElement,"scroll.bs.scrollspy",(()=>this._process())),this.refresh(),this._process()}static get Default(){return bn}static get NAME(){return _n}refresh(){const t=this._scrollElement===this._scrollElement.window?"offset":En,e="auto"===this._config.method?t:this._config.method,n=e===En?this._getScrollTop():0;this._offsets=[],this._targets=[],this._scrollHeight=this._getScrollHeight(),V.find(wn,this._config.target).map((t=>{const s=i(t),o=s?V.findOne(s):null;if(o){const t=o.getBoundingClientRect();if(t.width||t.height)return[U[e](o).top+n,s]}return null})).filter((t=>t)).sort(((t,e)=>t[0]-e[0])).forEach((t=>{this._offsets.push(t[0]),this._targets.push(t[1])}))}dispose(){j.off(this._scrollElement,".bs.scrollspy"),super.dispose()}_getConfig(t){return(t={...bn,...U.getDataAttributes(this._element),..."object"==typeof t&&t?t:{}}).target=r(t.target)||document.documentElement,a(_n,t,vn),t}_getScrollTop(){return this._scrollElement===window?this._scrollElement.pageYOffset:this._scrollElement.scrollTop}_getScrollHeight(){return this._scrollElement.scrollHeight||Math.max(document.body.scrollHeight,document.documentElement.scrollHeight)}_getOffsetHeight(){return this._scrollElement===window?window.innerHeight:this._scrollElement.getBoundingClientRect().height}_process(){const t=this._getScrollTop()+this._config.offset,e=this._getScrollHeight(),i=this._config.offset+e-this._getOffsetHeight();if(this._scrollHeight!==e&&this.refresh(),t>=i){const t=this._targets[this._targets.length-1];this._activeTarget!==t&&this._activate(t)}else{if(this._activeTarget&&t0)return this._activeTarget=null,void this._clear();for(let e=this._offsets.length;e--;)this._activeTarget!==this._targets[e]&&t>=this._offsets[e]&&(void 0===this._offsets[e+1]||t`${e}[data-bs-target="${t}"],${e}[href="${t}"]`)),i=V.findOne(e.join(","),this._config.target);i.classList.add(yn),i.classList.contains("dropdown-item")?V.findOne(".dropdown-toggle",i.closest(".dropdown")).classList.add(yn):V.parents(i,".nav, .list-group").forEach((t=>{V.prev(t,".nav-link, .list-group-item").forEach((t=>t.classList.add(yn))),V.prev(t,".nav-item").forEach((t=>{V.children(t,".nav-link").forEach((t=>t.classList.add(yn)))}))})),j.trigger(this._scrollElement,"activate.bs.scrollspy",{relatedTarget:t})}_clear(){V.find(wn,this._config.target).filter((t=>t.classList.contains(yn))).forEach((t=>t.classList.remove(yn)))}static jQueryInterface(t){return this.each((function(){const e=An.getOrCreateInstance(this,t);if("string"==typeof t){if(void 0===e[t])throw new TypeError(`No method named "${t}"`);e[t]()}}))}}j.on(window,"load.bs.scrollspy.data-api",(()=>{V.find('[data-bs-spy="scroll"]').forEach((t=>new An(t)))})),g(An);const Tn="active",On="fade",Cn="show",kn=".active",Ln=":scope > li > .active";class xn extends B{static get NAME(){return"tab"}show(){if(this._element.parentNode&&this._element.parentNode.nodeType===Node.ELEMENT_NODE&&this._element.classList.contains(Tn))return;let t;const e=n(this._element),i=this._element.closest(".nav, .list-group");if(i){const e="UL"===i.nodeName||"OL"===i.nodeName?Ln:kn;t=V.find(e,i),t=t[t.length-1]}const s=t?j.trigger(t,"hide.bs.tab",{relatedTarget:this._element}):null;if(j.trigger(this._element,"show.bs.tab",{relatedTarget:t}).defaultPrevented||null!==s&&s.defaultPrevented)return;this._activate(this._element,i);const o=()=>{j.trigger(t,"hidden.bs.tab",{relatedTarget:this._element}),j.trigger(this._element,"shown.bs.tab",{relatedTarget:t})};e?this._activate(e,e.parentNode,o):o()}_activate(t,e,i){const n=(!e||"UL"!==e.nodeName&&"OL"!==e.nodeName?V.children(e,kn):V.find(Ln,e))[0],s=i&&n&&n.classList.contains(On),o=()=>this._transitionComplete(t,n,i);n&&s?(n.classList.remove(Cn),this._queueCallback(o,t,!0)):o()}_transitionComplete(t,e,i){if(e){e.classList.remove(Tn);const t=V.findOne(":scope > .dropdown-menu .active",e.parentNode);t&&t.classList.remove(Tn),"tab"===e.getAttribute("role")&&e.setAttribute("aria-selected",!1)}t.classList.add(Tn),"tab"===t.getAttribute("role")&&t.setAttribute("aria-selected",!0),u(t),t.classList.contains(On)&&t.classList.add(Cn);let n=t.parentNode;if(n&&"LI"===n.nodeName&&(n=n.parentNode),n&&n.classList.contains("dropdown-menu")){const e=t.closest(".dropdown");e&&V.find(".dropdown-toggle",e).forEach((t=>t.classList.add(Tn))),t.setAttribute("aria-expanded",!0)}i&&i()}static jQueryInterface(t){return this.each((function(){const e=xn.getOrCreateInstance(this);if("string"==typeof t){if(void 0===e[t])throw new TypeError(`No method named "${t}"`);e[t]()}}))}}j.on(document,"click.bs.tab.data-api",'[data-bs-toggle="tab"], [data-bs-toggle="pill"], [data-bs-toggle="list"]',(function(t){["A","AREA"].includes(this.tagName)&&t.preventDefault(),c(this)||xn.getOrCreateInstance(this).show()})),g(xn);const Dn="toast",Sn="hide",Nn="show",In="showing",Pn={animation:"boolean",autohide:"boolean",delay:"number"},jn={animation:!0,autohide:!0,delay:5e3};class Mn extends B{constructor(t,e){super(t),this._config=this._getConfig(e),this._timeout=null,this._hasMouseInteraction=!1,this._hasKeyboardInteraction=!1,this._setListeners()}static get DefaultType(){return Pn}static get Default(){return jn}static get NAME(){return Dn}show(){j.trigger(this._element,"show.bs.toast").defaultPrevented||(this._clearTimeout(),this._config.animation&&this._element.classList.add("fade"),this._element.classList.remove(Sn),u(this._element),this._element.classList.add(Nn),this._element.classList.add(In),this._queueCallback((()=>{this._element.classList.remove(In),j.trigger(this._element,"shown.bs.toast"),this._maybeScheduleHide()}),this._element,this._config.animation))}hide(){this._element.classList.contains(Nn)&&(j.trigger(this._element,"hide.bs.toast").defaultPrevented||(this._element.classList.add(In),this._queueCallback((()=>{this._element.classList.add(Sn),this._element.classList.remove(In),this._element.classList.remove(Nn),j.trigger(this._element,"hidden.bs.toast")}),this._element,this._config.animation)))}dispose(){this._clearTimeout(),this._element.classList.contains(Nn)&&this._element.classList.remove(Nn),super.dispose()}_getConfig(t){return t={...jn,...U.getDataAttributes(this._element),..."object"==typeof t&&t?t:{}},a(Dn,t,this.constructor.DefaultType),t}_maybeScheduleHide(){this._config.autohide&&(this._hasMouseInteraction||this._hasKeyboardInteraction||(this._timeout=setTimeout((()=>{this.hide()}),this._config.delay)))}_onInteraction(t,e){switch(t.type){case"mouseover":case"mouseout":this._hasMouseInteraction=e;break;case"focusin":case"focusout":this._hasKeyboardInteraction=e}if(e)return void this._clearTimeout();const i=t.relatedTarget;this._element===i||this._element.contains(i)||this._maybeScheduleHide()}_setListeners(){j.on(this._element,"mouseover.bs.toast",(t=>this._onInteraction(t,!0))),j.on(this._element,"mouseout.bs.toast",(t=>this._onInteraction(t,!1))),j.on(this._element,"focusin.bs.toast",(t=>this._onInteraction(t,!0))),j.on(this._element,"focusout.bs.toast",(t=>this._onInteraction(t,!1)))}_clearTimeout(){clearTimeout(this._timeout),this._timeout=null}static jQueryInterface(t){return this.each((function(){const e=Mn.getOrCreateInstance(this,t);if("string"==typeof t){if(void 0===e[t])throw new TypeError(`No method named "${t}"`);e[t](this)}}))}}return R(Mn),g(Mn),{Alert:W,Button:z,Carousel:st,Collapse:pt,Dropdown:hi,Modal:Hi,Offcanvas:Fi,Popover:gn,ScrollSpy:An,Tab:xn,Toast:Mn,Tooltip:un}})); +//# sourceMappingURL=bootstrap.bundle.min.js.map \ No newline at end of file diff --git a/site_libs/clipboard/clipboard.min.js b/site_libs/clipboard/clipboard.min.js new file mode 100644 index 00000000..1103f811 --- /dev/null +++ b/site_libs/clipboard/clipboard.min.js @@ -0,0 +1,7 @@ +/*! + * clipboard.js v2.0.11 + * https://clipboardjs.com/ + * + * Licensed MIT © Zeno Rocha + */ +!function(t,e){"object"==typeof exports&&"object"==typeof module?module.exports=e():"function"==typeof define&&define.amd?define([],e):"object"==typeof exports?exports.ClipboardJS=e():t.ClipboardJS=e()}(this,function(){return n={686:function(t,e,n){"use strict";n.d(e,{default:function(){return b}});var e=n(279),i=n.n(e),e=n(370),u=n.n(e),e=n(817),r=n.n(e);function c(t){try{return document.execCommand(t)}catch(t){return}}var a=function(t){t=r()(t);return c("cut"),t};function o(t,e){var n,o,t=(n=t,o="rtl"===document.documentElement.getAttribute("dir"),(t=document.createElement("textarea")).style.fontSize="12pt",t.style.border="0",t.style.padding="0",t.style.margin="0",t.style.position="absolute",t.style[o?"right":"left"]="-9999px",o=window.pageYOffset||document.documentElement.scrollTop,t.style.top="".concat(o,"px"),t.setAttribute("readonly",""),t.value=n,t);return e.container.appendChild(t),e=r()(t),c("copy"),t.remove(),e}var f=function(t){var e=1.anchorjs-link,.anchorjs-link:focus{opacity:1}",u.sheet.cssRules.length),u.sheet.insertRule("[data-anchorjs-icon]::after{content:attr(data-anchorjs-icon)}",u.sheet.cssRules.length),u.sheet.insertRule('@font-face{font-family:anchorjs-icons;src:url(data:n/a;base64,AAEAAAALAIAAAwAwT1MvMg8yG2cAAAE4AAAAYGNtYXDp3gC3AAABpAAAAExnYXNwAAAAEAAAA9wAAAAIZ2x5ZlQCcfwAAAH4AAABCGhlYWQHFvHyAAAAvAAAADZoaGVhBnACFwAAAPQAAAAkaG10eASAADEAAAGYAAAADGxvY2EACACEAAAB8AAAAAhtYXhwAAYAVwAAARgAAAAgbmFtZQGOH9cAAAMAAAAAunBvc3QAAwAAAAADvAAAACAAAQAAAAEAAHzE2p9fDzz1AAkEAAAAAADRecUWAAAAANQA6R8AAAAAAoACwAAAAAgAAgAAAAAAAAABAAADwP/AAAACgAAA/9MCrQABAAAAAAAAAAAAAAAAAAAAAwABAAAAAwBVAAIAAAAAAAIAAAAAAAAAAAAAAAAAAAAAAAMCQAGQAAUAAAKZAswAAACPApkCzAAAAesAMwEJAAAAAAAAAAAAAAAAAAAAARAAAAAAAAAAAAAAAAAAAAAAQAAg//0DwP/AAEADwABAAAAAAQAAAAAAAAAAAAAAIAAAAAAAAAIAAAACgAAxAAAAAwAAAAMAAAAcAAEAAwAAABwAAwABAAAAHAAEADAAAAAIAAgAAgAAACDpy//9//8AAAAg6cv//f///+EWNwADAAEAAAAAAAAAAAAAAAAACACEAAEAAAAAAAAAAAAAAAAxAAACAAQARAKAAsAAKwBUAAABIiYnJjQ3NzY2MzIWFxYUBwcGIicmNDc3NjQnJiYjIgYHBwYUFxYUBwYGIwciJicmNDc3NjIXFhQHBwYUFxYWMzI2Nzc2NCcmNDc2MhcWFAcHBgYjARQGDAUtLXoWOR8fORYtLTgKGwoKCjgaGg0gEhIgDXoaGgkJBQwHdR85Fi0tOAobCgoKOBoaDSASEiANehoaCQkKGwotLXoWOR8BMwUFLYEuehYXFxYugC44CQkKGwo4GkoaDQ0NDXoaShoKGwoFBe8XFi6ALjgJCQobCjgaShoNDQ0NehpKGgobCgoKLYEuehYXAAAADACWAAEAAAAAAAEACAAAAAEAAAAAAAIAAwAIAAEAAAAAAAMACAAAAAEAAAAAAAQACAAAAAEAAAAAAAUAAQALAAEAAAAAAAYACAAAAAMAAQQJAAEAEAAMAAMAAQQJAAIABgAcAAMAAQQJAAMAEAAMAAMAAQQJAAQAEAAMAAMAAQQJAAUAAgAiAAMAAQQJAAYAEAAMYW5jaG9yanM0MDBAAGEAbgBjAGgAbwByAGoAcwA0ADAAMABAAAAAAwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAAH//wAP) format("truetype")}',u.sheet.cssRules.length)),u=document.querySelectorAll("[id]"),t=[].map.call(u,function(A){return A.id}),i=0;i\]./()*\\\n\t\b\v\u00A0]/g,"-").replace(/-{2,}/g,"-").substring(0,this.options.truncate).replace(/^-+|-+$/gm,"").toLowerCase()},this.hasAnchorJSLink=function(A){var e=A.firstChild&&-1<(" "+A.firstChild.className+" ").indexOf(" anchorjs-link "),A=A.lastChild&&-1<(" "+A.lastChild.className+" ").indexOf(" anchorjs-link ");return e||A||!1}}}); +// @license-end \ No newline at end of file diff --git a/site_libs/quarto-html/popper.min.js b/site_libs/quarto-html/popper.min.js new file mode 100644 index 00000000..2269d669 --- /dev/null +++ b/site_libs/quarto-html/popper.min.js @@ -0,0 +1,6 @@ +/** + * @popperjs/core v2.11.4 - MIT License + */ + +!function(e,t){"object"==typeof exports&&"undefined"!=typeof module?t(exports):"function"==typeof define&&define.amd?define(["exports"],t):t((e="undefined"!=typeof globalThis?globalThis:e||self).Popper={})}(this,(function(e){"use strict";function t(e){if(null==e)return window;if("[object Window]"!==e.toString()){var t=e.ownerDocument;return t&&t.defaultView||window}return e}function n(e){return e instanceof t(e).Element||e instanceof Element}function r(e){return e instanceof t(e).HTMLElement||e instanceof HTMLElement}function o(e){return"undefined"!=typeof ShadowRoot&&(e instanceof t(e).ShadowRoot||e instanceof ShadowRoot)}var i=Math.max,a=Math.min,s=Math.round;function f(e,t){void 0===t&&(t=!1);var n=e.getBoundingClientRect(),o=1,i=1;if(r(e)&&t){var a=e.offsetHeight,f=e.offsetWidth;f>0&&(o=s(n.width)/f||1),a>0&&(i=s(n.height)/a||1)}return{width:n.width/o,height:n.height/i,top:n.top/i,right:n.right/o,bottom:n.bottom/i,left:n.left/o,x:n.left/o,y:n.top/i}}function c(e){var n=t(e);return{scrollLeft:n.pageXOffset,scrollTop:n.pageYOffset}}function p(e){return e?(e.nodeName||"").toLowerCase():null}function u(e){return((n(e)?e.ownerDocument:e.document)||window.document).documentElement}function l(e){return f(u(e)).left+c(e).scrollLeft}function d(e){return t(e).getComputedStyle(e)}function h(e){var t=d(e),n=t.overflow,r=t.overflowX,o=t.overflowY;return/auto|scroll|overlay|hidden/.test(n+o+r)}function m(e,n,o){void 0===o&&(o=!1);var i,a,d=r(n),m=r(n)&&function(e){var t=e.getBoundingClientRect(),n=s(t.width)/e.offsetWidth||1,r=s(t.height)/e.offsetHeight||1;return 1!==n||1!==r}(n),v=u(n),g=f(e,m),y={scrollLeft:0,scrollTop:0},b={x:0,y:0};return(d||!d&&!o)&&(("body"!==p(n)||h(v))&&(y=(i=n)!==t(i)&&r(i)?{scrollLeft:(a=i).scrollLeft,scrollTop:a.scrollTop}:c(i)),r(n)?((b=f(n,!0)).x+=n.clientLeft,b.y+=n.clientTop):v&&(b.x=l(v))),{x:g.left+y.scrollLeft-b.x,y:g.top+y.scrollTop-b.y,width:g.width,height:g.height}}function v(e){var t=f(e),n=e.offsetWidth,r=e.offsetHeight;return Math.abs(t.width-n)<=1&&(n=t.width),Math.abs(t.height-r)<=1&&(r=t.height),{x:e.offsetLeft,y:e.offsetTop,width:n,height:r}}function g(e){return"html"===p(e)?e:e.assignedSlot||e.parentNode||(o(e)?e.host:null)||u(e)}function y(e){return["html","body","#document"].indexOf(p(e))>=0?e.ownerDocument.body:r(e)&&h(e)?e:y(g(e))}function b(e,n){var r;void 0===n&&(n=[]);var o=y(e),i=o===(null==(r=e.ownerDocument)?void 0:r.body),a=t(o),s=i?[a].concat(a.visualViewport||[],h(o)?o:[]):o,f=n.concat(s);return i?f:f.concat(b(g(s)))}function x(e){return["table","td","th"].indexOf(p(e))>=0}function w(e){return r(e)&&"fixed"!==d(e).position?e.offsetParent:null}function O(e){for(var n=t(e),i=w(e);i&&x(i)&&"static"===d(i).position;)i=w(i);return i&&("html"===p(i)||"body"===p(i)&&"static"===d(i).position)?n:i||function(e){var t=-1!==navigator.userAgent.toLowerCase().indexOf("firefox");if(-1!==navigator.userAgent.indexOf("Trident")&&r(e)&&"fixed"===d(e).position)return null;var n=g(e);for(o(n)&&(n=n.host);r(n)&&["html","body"].indexOf(p(n))<0;){var i=d(n);if("none"!==i.transform||"none"!==i.perspective||"paint"===i.contain||-1!==["transform","perspective"].indexOf(i.willChange)||t&&"filter"===i.willChange||t&&i.filter&&"none"!==i.filter)return n;n=n.parentNode}return null}(e)||n}var j="top",E="bottom",D="right",A="left",L="auto",P=[j,E,D,A],M="start",k="end",W="viewport",B="popper",H=P.reduce((function(e,t){return e.concat([t+"-"+M,t+"-"+k])}),[]),T=[].concat(P,[L]).reduce((function(e,t){return e.concat([t,t+"-"+M,t+"-"+k])}),[]),R=["beforeRead","read","afterRead","beforeMain","main","afterMain","beforeWrite","write","afterWrite"];function S(e){var t=new Map,n=new Set,r=[];function o(e){n.add(e.name),[].concat(e.requires||[],e.requiresIfExists||[]).forEach((function(e){if(!n.has(e)){var r=t.get(e);r&&o(r)}})),r.push(e)}return e.forEach((function(e){t.set(e.name,e)})),e.forEach((function(e){n.has(e.name)||o(e)})),r}function C(e){return e.split("-")[0]}function q(e,t){var n=t.getRootNode&&t.getRootNode();if(e.contains(t))return!0;if(n&&o(n)){var r=t;do{if(r&&e.isSameNode(r))return!0;r=r.parentNode||r.host}while(r)}return!1}function V(e){return Object.assign({},e,{left:e.x,top:e.y,right:e.x+e.width,bottom:e.y+e.height})}function N(e,r){return r===W?V(function(e){var n=t(e),r=u(e),o=n.visualViewport,i=r.clientWidth,a=r.clientHeight,s=0,f=0;return o&&(i=o.width,a=o.height,/^((?!chrome|android).)*safari/i.test(navigator.userAgent)||(s=o.offsetLeft,f=o.offsetTop)),{width:i,height:a,x:s+l(e),y:f}}(e)):n(r)?function(e){var t=f(e);return t.top=t.top+e.clientTop,t.left=t.left+e.clientLeft,t.bottom=t.top+e.clientHeight,t.right=t.left+e.clientWidth,t.width=e.clientWidth,t.height=e.clientHeight,t.x=t.left,t.y=t.top,t}(r):V(function(e){var t,n=u(e),r=c(e),o=null==(t=e.ownerDocument)?void 0:t.body,a=i(n.scrollWidth,n.clientWidth,o?o.scrollWidth:0,o?o.clientWidth:0),s=i(n.scrollHeight,n.clientHeight,o?o.scrollHeight:0,o?o.clientHeight:0),f=-r.scrollLeft+l(e),p=-r.scrollTop;return"rtl"===d(o||n).direction&&(f+=i(n.clientWidth,o?o.clientWidth:0)-a),{width:a,height:s,x:f,y:p}}(u(e)))}function I(e,t,o){var s="clippingParents"===t?function(e){var t=b(g(e)),o=["absolute","fixed"].indexOf(d(e).position)>=0&&r(e)?O(e):e;return n(o)?t.filter((function(e){return n(e)&&q(e,o)&&"body"!==p(e)})):[]}(e):[].concat(t),f=[].concat(s,[o]),c=f[0],u=f.reduce((function(t,n){var r=N(e,n);return t.top=i(r.top,t.top),t.right=a(r.right,t.right),t.bottom=a(r.bottom,t.bottom),t.left=i(r.left,t.left),t}),N(e,c));return u.width=u.right-u.left,u.height=u.bottom-u.top,u.x=u.left,u.y=u.top,u}function _(e){return e.split("-")[1]}function F(e){return["top","bottom"].indexOf(e)>=0?"x":"y"}function U(e){var t,n=e.reference,r=e.element,o=e.placement,i=o?C(o):null,a=o?_(o):null,s=n.x+n.width/2-r.width/2,f=n.y+n.height/2-r.height/2;switch(i){case j:t={x:s,y:n.y-r.height};break;case E:t={x:s,y:n.y+n.height};break;case D:t={x:n.x+n.width,y:f};break;case A:t={x:n.x-r.width,y:f};break;default:t={x:n.x,y:n.y}}var c=i?F(i):null;if(null!=c){var p="y"===c?"height":"width";switch(a){case M:t[c]=t[c]-(n[p]/2-r[p]/2);break;case k:t[c]=t[c]+(n[p]/2-r[p]/2)}}return t}function z(e){return Object.assign({},{top:0,right:0,bottom:0,left:0},e)}function X(e,t){return t.reduce((function(t,n){return t[n]=e,t}),{})}function Y(e,t){void 0===t&&(t={});var r=t,o=r.placement,i=void 0===o?e.placement:o,a=r.boundary,s=void 0===a?"clippingParents":a,c=r.rootBoundary,p=void 0===c?W:c,l=r.elementContext,d=void 0===l?B:l,h=r.altBoundary,m=void 0!==h&&h,v=r.padding,g=void 0===v?0:v,y=z("number"!=typeof g?g:X(g,P)),b=d===B?"reference":B,x=e.rects.popper,w=e.elements[m?b:d],O=I(n(w)?w:w.contextElement||u(e.elements.popper),s,p),A=f(e.elements.reference),L=U({reference:A,element:x,strategy:"absolute",placement:i}),M=V(Object.assign({},x,L)),k=d===B?M:A,H={top:O.top-k.top+y.top,bottom:k.bottom-O.bottom+y.bottom,left:O.left-k.left+y.left,right:k.right-O.right+y.right},T=e.modifiersData.offset;if(d===B&&T){var R=T[i];Object.keys(H).forEach((function(e){var t=[D,E].indexOf(e)>=0?1:-1,n=[j,E].indexOf(e)>=0?"y":"x";H[e]+=R[n]*t}))}return H}var G={placement:"bottom",modifiers:[],strategy:"absolute"};function J(){for(var e=arguments.length,t=new Array(e),n=0;n=0?-1:1,i="function"==typeof n?n(Object.assign({},t,{placement:e})):n,a=i[0],s=i[1];return a=a||0,s=(s||0)*o,[A,D].indexOf(r)>=0?{x:s,y:a}:{x:a,y:s}}(n,t.rects,i),e}),{}),s=a[t.placement],f=s.x,c=s.y;null!=t.modifiersData.popperOffsets&&(t.modifiersData.popperOffsets.x+=f,t.modifiersData.popperOffsets.y+=c),t.modifiersData[r]=a}},ie={left:"right",right:"left",bottom:"top",top:"bottom"};function ae(e){return e.replace(/left|right|bottom|top/g,(function(e){return ie[e]}))}var se={start:"end",end:"start"};function fe(e){return e.replace(/start|end/g,(function(e){return se[e]}))}function ce(e,t){void 0===t&&(t={});var n=t,r=n.placement,o=n.boundary,i=n.rootBoundary,a=n.padding,s=n.flipVariations,f=n.allowedAutoPlacements,c=void 0===f?T:f,p=_(r),u=p?s?H:H.filter((function(e){return _(e)===p})):P,l=u.filter((function(e){return c.indexOf(e)>=0}));0===l.length&&(l=u);var d=l.reduce((function(t,n){return t[n]=Y(e,{placement:n,boundary:o,rootBoundary:i,padding:a})[C(n)],t}),{});return Object.keys(d).sort((function(e,t){return d[e]-d[t]}))}var pe={name:"flip",enabled:!0,phase:"main",fn:function(e){var t=e.state,n=e.options,r=e.name;if(!t.modifiersData[r]._skip){for(var o=n.mainAxis,i=void 0===o||o,a=n.altAxis,s=void 0===a||a,f=n.fallbackPlacements,c=n.padding,p=n.boundary,u=n.rootBoundary,l=n.altBoundary,d=n.flipVariations,h=void 0===d||d,m=n.allowedAutoPlacements,v=t.options.placement,g=C(v),y=f||(g===v||!h?[ae(v)]:function(e){if(C(e)===L)return[];var t=ae(e);return[fe(e),t,fe(t)]}(v)),b=[v].concat(y).reduce((function(e,n){return e.concat(C(n)===L?ce(t,{placement:n,boundary:p,rootBoundary:u,padding:c,flipVariations:h,allowedAutoPlacements:m}):n)}),[]),x=t.rects.reference,w=t.rects.popper,O=new Map,P=!0,k=b[0],W=0;W=0,S=R?"width":"height",q=Y(t,{placement:B,boundary:p,rootBoundary:u,altBoundary:l,padding:c}),V=R?T?D:A:T?E:j;x[S]>w[S]&&(V=ae(V));var N=ae(V),I=[];if(i&&I.push(q[H]<=0),s&&I.push(q[V]<=0,q[N]<=0),I.every((function(e){return e}))){k=B,P=!1;break}O.set(B,I)}if(P)for(var F=function(e){var t=b.find((function(t){var n=O.get(t);if(n)return n.slice(0,e).every((function(e){return e}))}));if(t)return k=t,"break"},U=h?3:1;U>0;U--){if("break"===F(U))break}t.placement!==k&&(t.modifiersData[r]._skip=!0,t.placement=k,t.reset=!0)}},requiresIfExists:["offset"],data:{_skip:!1}};function ue(e,t,n){return i(e,a(t,n))}var le={name:"preventOverflow",enabled:!0,phase:"main",fn:function(e){var t=e.state,n=e.options,r=e.name,o=n.mainAxis,s=void 0===o||o,f=n.altAxis,c=void 0!==f&&f,p=n.boundary,u=n.rootBoundary,l=n.altBoundary,d=n.padding,h=n.tether,m=void 0===h||h,g=n.tetherOffset,y=void 0===g?0:g,b=Y(t,{boundary:p,rootBoundary:u,padding:d,altBoundary:l}),x=C(t.placement),w=_(t.placement),L=!w,P=F(x),k="x"===P?"y":"x",W=t.modifiersData.popperOffsets,B=t.rects.reference,H=t.rects.popper,T="function"==typeof y?y(Object.assign({},t.rects,{placement:t.placement})):y,R="number"==typeof T?{mainAxis:T,altAxis:T}:Object.assign({mainAxis:0,altAxis:0},T),S=t.modifiersData.offset?t.modifiersData.offset[t.placement]:null,q={x:0,y:0};if(W){if(s){var V,N="y"===P?j:A,I="y"===P?E:D,U="y"===P?"height":"width",z=W[P],X=z+b[N],G=z-b[I],J=m?-H[U]/2:0,K=w===M?B[U]:H[U],Q=w===M?-H[U]:-B[U],Z=t.elements.arrow,$=m&&Z?v(Z):{width:0,height:0},ee=t.modifiersData["arrow#persistent"]?t.modifiersData["arrow#persistent"].padding:{top:0,right:0,bottom:0,left:0},te=ee[N],ne=ee[I],re=ue(0,B[U],$[U]),oe=L?B[U]/2-J-re-te-R.mainAxis:K-re-te-R.mainAxis,ie=L?-B[U]/2+J+re+ne+R.mainAxis:Q+re+ne+R.mainAxis,ae=t.elements.arrow&&O(t.elements.arrow),se=ae?"y"===P?ae.clientTop||0:ae.clientLeft||0:0,fe=null!=(V=null==S?void 0:S[P])?V:0,ce=z+ie-fe,pe=ue(m?a(X,z+oe-fe-se):X,z,m?i(G,ce):G);W[P]=pe,q[P]=pe-z}if(c){var le,de="x"===P?j:A,he="x"===P?E:D,me=W[k],ve="y"===k?"height":"width",ge=me+b[de],ye=me-b[he],be=-1!==[j,A].indexOf(x),xe=null!=(le=null==S?void 0:S[k])?le:0,we=be?ge:me-B[ve]-H[ve]-xe+R.altAxis,Oe=be?me+B[ve]+H[ve]-xe-R.altAxis:ye,je=m&&be?function(e,t,n){var r=ue(e,t,n);return r>n?n:r}(we,me,Oe):ue(m?we:ge,me,m?Oe:ye);W[k]=je,q[k]=je-me}t.modifiersData[r]=q}},requiresIfExists:["offset"]};var de={name:"arrow",enabled:!0,phase:"main",fn:function(e){var t,n=e.state,r=e.name,o=e.options,i=n.elements.arrow,a=n.modifiersData.popperOffsets,s=C(n.placement),f=F(s),c=[A,D].indexOf(s)>=0?"height":"width";if(i&&a){var p=function(e,t){return z("number"!=typeof(e="function"==typeof e?e(Object.assign({},t.rects,{placement:t.placement})):e)?e:X(e,P))}(o.padding,n),u=v(i),l="y"===f?j:A,d="y"===f?E:D,h=n.rects.reference[c]+n.rects.reference[f]-a[f]-n.rects.popper[c],m=a[f]-n.rects.reference[f],g=O(i),y=g?"y"===f?g.clientHeight||0:g.clientWidth||0:0,b=h/2-m/2,x=p[l],w=y-u[c]-p[d],L=y/2-u[c]/2+b,M=ue(x,L,w),k=f;n.modifiersData[r]=((t={})[k]=M,t.centerOffset=M-L,t)}},effect:function(e){var t=e.state,n=e.options.element,r=void 0===n?"[data-popper-arrow]":n;null!=r&&("string"!=typeof r||(r=t.elements.popper.querySelector(r)))&&q(t.elements.popper,r)&&(t.elements.arrow=r)},requires:["popperOffsets"],requiresIfExists:["preventOverflow"]};function he(e,t,n){return void 0===n&&(n={x:0,y:0}),{top:e.top-t.height-n.y,right:e.right-t.width+n.x,bottom:e.bottom-t.height+n.y,left:e.left-t.width-n.x}}function me(e){return[j,D,E,A].some((function(t){return e[t]>=0}))}var ve={name:"hide",enabled:!0,phase:"main",requiresIfExists:["preventOverflow"],fn:function(e){var t=e.state,n=e.name,r=t.rects.reference,o=t.rects.popper,i=t.modifiersData.preventOverflow,a=Y(t,{elementContext:"reference"}),s=Y(t,{altBoundary:!0}),f=he(a,r),c=he(s,o,i),p=me(f),u=me(c);t.modifiersData[n]={referenceClippingOffsets:f,popperEscapeOffsets:c,isReferenceHidden:p,hasPopperEscaped:u},t.attributes.popper=Object.assign({},t.attributes.popper,{"data-popper-reference-hidden":p,"data-popper-escaped":u})}},ge=K({defaultModifiers:[Z,$,ne,re]}),ye=[Z,$,ne,re,oe,pe,le,de,ve],be=K({defaultModifiers:ye});e.applyStyles=re,e.arrow=de,e.computeStyles=ne,e.createPopper=be,e.createPopperLite=ge,e.defaultModifiers=ye,e.detectOverflow=Y,e.eventListeners=Z,e.flip=pe,e.hide=ve,e.offset=oe,e.popperGenerator=K,e.popperOffsets=$,e.preventOverflow=le,Object.defineProperty(e,"__esModule",{value:!0})})); + diff --git a/site_libs/quarto-html/quarto-syntax-highlighting.css b/site_libs/quarto-html/quarto-syntax-highlighting.css new file mode 100644 index 00000000..d9fd98f0 --- /dev/null +++ b/site_libs/quarto-html/quarto-syntax-highlighting.css @@ -0,0 +1,203 @@ +/* quarto syntax highlight colors */ +:root { + --quarto-hl-ot-color: #003B4F; + --quarto-hl-at-color: #657422; + --quarto-hl-ss-color: #20794D; + --quarto-hl-an-color: #5E5E5E; + --quarto-hl-fu-color: #4758AB; + --quarto-hl-st-color: #20794D; + --quarto-hl-cf-color: #003B4F; + --quarto-hl-op-color: #5E5E5E; + --quarto-hl-er-color: #AD0000; + --quarto-hl-bn-color: #AD0000; + --quarto-hl-al-color: #AD0000; + --quarto-hl-va-color: #111111; + --quarto-hl-bu-color: inherit; + --quarto-hl-ex-color: inherit; + --quarto-hl-pp-color: #AD0000; + --quarto-hl-in-color: #5E5E5E; + --quarto-hl-vs-color: #20794D; + --quarto-hl-wa-color: #5E5E5E; + --quarto-hl-do-color: #5E5E5E; + --quarto-hl-im-color: #00769E; + --quarto-hl-ch-color: #20794D; + --quarto-hl-dt-color: #AD0000; + --quarto-hl-fl-color: #AD0000; + --quarto-hl-co-color: #5E5E5E; + --quarto-hl-cv-color: #5E5E5E; + --quarto-hl-cn-color: #8f5902; + --quarto-hl-sc-color: #5E5E5E; + --quarto-hl-dv-color: #AD0000; + --quarto-hl-kw-color: #003B4F; +} + +/* other quarto variables */ +:root { + --quarto-font-monospace: SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace; +} + +pre > code.sourceCode > span { + color: #003B4F; +} + +code span { + color: #003B4F; +} + +code.sourceCode > span { + color: #003B4F; +} + +div.sourceCode, +div.sourceCode pre.sourceCode { + color: #003B4F; +} + +code span.ot { + color: #003B4F; + font-style: inherit; +} + +code span.at { + color: #657422; + font-style: inherit; +} + +code span.ss { + color: #20794D; + font-style: inherit; +} + +code span.an { + color: #5E5E5E; + font-style: inherit; +} + +code span.fu { + color: #4758AB; + font-style: inherit; +} + +code span.st { + color: #20794D; + font-style: inherit; +} + +code span.cf { + color: #003B4F; + font-style: inherit; +} + +code span.op { + color: #5E5E5E; + font-style: inherit; +} + +code span.er { + color: #AD0000; + font-style: inherit; +} + +code span.bn { + color: #AD0000; + font-style: inherit; +} + +code span.al { + color: #AD0000; + font-style: inherit; +} + +code span.va { + color: #111111; + font-style: inherit; +} + +code span.bu { + font-style: inherit; +} + +code span.ex { + font-style: inherit; +} + +code span.pp { + color: #AD0000; + font-style: inherit; +} + +code span.in { + color: #5E5E5E; + font-style: inherit; +} + +code span.vs { + color: #20794D; + font-style: inherit; +} + +code span.wa { + color: #5E5E5E; + font-style: italic; +} + +code span.do { + color: #5E5E5E; + font-style: italic; +} + +code span.im { + color: #00769E; + font-style: inherit; +} + +code span.ch { + color: #20794D; + font-style: inherit; +} + +code span.dt { + color: #AD0000; + font-style: inherit; +} + +code span.fl { + color: #AD0000; + font-style: inherit; +} + +code span.co { + color: #5E5E5E; + font-style: inherit; +} + +code span.cv { + color: #5E5E5E; + font-style: italic; +} + +code span.cn { + color: #8f5902; + font-style: inherit; +} + +code span.sc { + color: #5E5E5E; + font-style: inherit; +} + +code span.dv { + color: #AD0000; + font-style: inherit; +} + +code span.kw { + color: #003B4F; + font-style: inherit; +} + +.prevent-inlining { + content: " { + // Find any conflicting margin elements and add margins to the + // top to prevent overlap + const marginChildren = window.document.querySelectorAll( + ".column-margin.column-container > * " + ); + + let lastBottom = 0; + for (const marginChild of marginChildren) { + if (marginChild.offsetParent !== null) { + // clear the top margin so we recompute it + marginChild.style.marginTop = null; + const top = marginChild.getBoundingClientRect().top + window.scrollY; + console.log({ + childtop: marginChild.getBoundingClientRect().top, + scroll: window.scrollY, + top, + lastBottom, + }); + if (top < lastBottom) { + const margin = lastBottom - top; + marginChild.style.marginTop = `${margin}px`; + } + const styles = window.getComputedStyle(marginChild); + const marginTop = parseFloat(styles["marginTop"]); + + console.log({ + top, + height: marginChild.getBoundingClientRect().height, + marginTop, + total: top + marginChild.getBoundingClientRect().height + marginTop, + }); + lastBottom = top + marginChild.getBoundingClientRect().height + marginTop; + } + } +}; + +window.document.addEventListener("DOMContentLoaded", function (_event) { + // Recompute the position of margin elements anytime the body size changes + if (window.ResizeObserver) { + const resizeObserver = new window.ResizeObserver( + throttle(layoutMarginEls, 50) + ); + resizeObserver.observe(window.document.body); + } + + const tocEl = window.document.querySelector('nav.toc-active[role="doc-toc"]'); + const sidebarEl = window.document.getElementById("quarto-sidebar"); + const leftTocEl = window.document.getElementById("quarto-sidebar-toc-left"); + const marginSidebarEl = window.document.getElementById( + "quarto-margin-sidebar" + ); + // function to determine whether the element has a previous sibling that is active + const prevSiblingIsActiveLink = (el) => { + const sibling = el.previousElementSibling; + if (sibling && sibling.tagName === "A") { + return sibling.classList.contains("active"); + } else { + return false; + } + }; + + // fire slideEnter for bootstrap tab activations (for htmlwidget resize behavior) + function fireSlideEnter(e) { + const event = window.document.createEvent("Event"); + event.initEvent("slideenter", true, true); + window.document.dispatchEvent(event); + } + const tabs = window.document.querySelectorAll('a[data-bs-toggle="tab"]'); + tabs.forEach((tab) => { + tab.addEventListener("shown.bs.tab", fireSlideEnter); + }); + + // fire slideEnter for tabby tab activations (for htmlwidget resize behavior) + document.addEventListener("tabby", fireSlideEnter, false); + + // Track scrolling and mark TOC links as active + // get table of contents and sidebar (bail if we don't have at least one) + const tocLinks = tocEl + ? [...tocEl.querySelectorAll("a[data-scroll-target]")] + : []; + const makeActive = (link) => tocLinks[link].classList.add("active"); + const removeActive = (link) => tocLinks[link].classList.remove("active"); + const removeAllActive = () => + [...Array(tocLinks.length).keys()].forEach((link) => removeActive(link)); + + // activate the anchor for a section associated with this TOC entry + tocLinks.forEach((link) => { + link.addEventListener("click", () => { + if (link.href.indexOf("#") !== -1) { + const anchor = link.href.split("#")[1]; + const heading = window.document.querySelector( + `[data-anchor-id=${anchor}]` + ); + if (heading) { + // Add the class + heading.classList.add("reveal-anchorjs-link"); + + // function to show the anchor + const handleMouseout = () => { + heading.classList.remove("reveal-anchorjs-link"); + heading.removeEventListener("mouseout", handleMouseout); + }; + + // add a function to clear the anchor when the user mouses out of it + heading.addEventListener("mouseout", handleMouseout); + } + } + }); + }); + + const sections = tocLinks.map((link) => { + const target = link.getAttribute("data-scroll-target"); + if (target.startsWith("#")) { + return window.document.getElementById(decodeURI(`${target.slice(1)}`)); + } else { + return window.document.querySelector(decodeURI(`${target}`)); + } + }); + + const sectionMargin = 200; + let currentActive = 0; + // track whether we've initialized state the first time + let init = false; + + const updateActiveLink = () => { + // The index from bottom to top (e.g. reversed list) + let sectionIndex = -1; + if ( + window.innerHeight + window.pageYOffset >= + window.document.body.offsetHeight + ) { + sectionIndex = 0; + } else { + sectionIndex = [...sections].reverse().findIndex((section) => { + if (section) { + return window.pageYOffset >= section.offsetTop - sectionMargin; + } else { + return false; + } + }); + } + if (sectionIndex > -1) { + const current = sections.length - sectionIndex - 1; + if (current !== currentActive) { + removeAllActive(); + currentActive = current; + makeActive(current); + if (init) { + window.dispatchEvent(sectionChanged); + } + init = true; + } + } + }; + + const inHiddenRegion = (top, bottom, hiddenRegions) => { + for (const region of hiddenRegions) { + if (top <= region.bottom && bottom >= region.top) { + return true; + } + } + return false; + }; + + const categorySelector = "header.quarto-title-block .quarto-category"; + const activateCategories = (href) => { + // Find any categories + // Surround them with a link pointing back to: + // #category=Authoring + try { + const categoryEls = window.document.querySelectorAll(categorySelector); + for (const categoryEl of categoryEls) { + const categoryText = categoryEl.textContent; + if (categoryText) { + const link = `${href}#category=${encodeURIComponent(categoryText)}`; + const linkEl = window.document.createElement("a"); + linkEl.setAttribute("href", link); + for (const child of categoryEl.childNodes) { + linkEl.append(child); + } + categoryEl.appendChild(linkEl); + } + } + } catch { + // Ignore errors + } + }; + function hasTitleCategories() { + return window.document.querySelector(categorySelector) !== null; + } + + function offsetRelativeUrl(url) { + const offset = getMeta("quarto:offset"); + return offset ? offset + url : url; + } + + function offsetAbsoluteUrl(url) { + const offset = getMeta("quarto:offset"); + const baseUrl = new URL(offset, window.location); + + const projRelativeUrl = url.replace(baseUrl, ""); + if (projRelativeUrl.startsWith("/")) { + return projRelativeUrl; + } else { + return "/" + projRelativeUrl; + } + } + + // read a meta tag value + function getMeta(metaName) { + const metas = window.document.getElementsByTagName("meta"); + for (let i = 0; i < metas.length; i++) { + if (metas[i].getAttribute("name") === metaName) { + return metas[i].getAttribute("content"); + } + } + return ""; + } + + async function findAndActivateCategories() { + const currentPagePath = offsetAbsoluteUrl(window.location.href); + const response = await fetch(offsetRelativeUrl("listings.json")); + if (response.status == 200) { + return response.json().then(function (listingPaths) { + const listingHrefs = []; + for (const listingPath of listingPaths) { + const pathWithoutLeadingSlash = listingPath.listing.substring(1); + for (const item of listingPath.items) { + if ( + item === currentPagePath || + item === currentPagePath + "index.html" + ) { + // Resolve this path against the offset to be sure + // we already are using the correct path to the listing + // (this adjusts the listing urls to be rooted against + // whatever root the page is actually running against) + const relative = offsetRelativeUrl(pathWithoutLeadingSlash); + const baseUrl = window.location; + const resolvedPath = new URL(relative, baseUrl); + listingHrefs.push(resolvedPath.pathname); + break; + } + } + } + + // Look up the tree for a nearby linting and use that if we find one + const nearestListing = findNearestParentListing( + offsetAbsoluteUrl(window.location.pathname), + listingHrefs + ); + if (nearestListing) { + activateCategories(nearestListing); + } else { + // See if the referrer is a listing page for this item + const referredRelativePath = offsetAbsoluteUrl(document.referrer); + const referrerListing = listingHrefs.find((listingHref) => { + const isListingReferrer = + listingHref === referredRelativePath || + listingHref === referredRelativePath + "index.html"; + return isListingReferrer; + }); + + if (referrerListing) { + // Try to use the referrer if possible + activateCategories(referrerListing); + } else if (listingHrefs.length > 0) { + // Otherwise, just fall back to the first listing + activateCategories(listingHrefs[0]); + } + } + }); + } + } + if (hasTitleCategories()) { + findAndActivateCategories(); + } + + const findNearestParentListing = (href, listingHrefs) => { + if (!href || !listingHrefs) { + return undefined; + } + // Look up the tree for a nearby linting and use that if we find one + const relativeParts = href.substring(1).split("/"); + while (relativeParts.length > 0) { + const path = relativeParts.join("/"); + for (const listingHref of listingHrefs) { + if (listingHref.startsWith(path)) { + return listingHref; + } + } + relativeParts.pop(); + } + + return undefined; + }; + + const manageSidebarVisiblity = (el, placeholderDescriptor) => { + let isVisible = true; + let elRect; + + return (hiddenRegions) => { + if (el === null) { + return; + } + + // Find the last element of the TOC + const lastChildEl = el.lastElementChild; + + if (lastChildEl) { + // Converts the sidebar to a menu + const convertToMenu = () => { + for (const child of el.children) { + child.style.opacity = 0; + child.style.overflow = "hidden"; + } + + nexttick(() => { + const toggleContainer = window.document.createElement("div"); + toggleContainer.style.width = "100%"; + toggleContainer.classList.add("zindex-over-content"); + toggleContainer.classList.add("quarto-sidebar-toggle"); + toggleContainer.classList.add("headroom-target"); // Marks this to be managed by headeroom + toggleContainer.id = placeholderDescriptor.id; + toggleContainer.style.position = "fixed"; + + const toggleIcon = window.document.createElement("i"); + toggleIcon.classList.add("quarto-sidebar-toggle-icon"); + toggleIcon.classList.add("bi"); + toggleIcon.classList.add("bi-caret-down-fill"); + + const toggleTitle = window.document.createElement("div"); + const titleEl = window.document.body.querySelector( + placeholderDescriptor.titleSelector + ); + if (titleEl) { + toggleTitle.append( + titleEl.textContent || titleEl.innerText, + toggleIcon + ); + } + toggleTitle.classList.add("zindex-over-content"); + toggleTitle.classList.add("quarto-sidebar-toggle-title"); + toggleContainer.append(toggleTitle); + + const toggleContents = window.document.createElement("div"); + toggleContents.classList = el.classList; + toggleContents.classList.add("zindex-over-content"); + toggleContents.classList.add("quarto-sidebar-toggle-contents"); + for (const child of el.children) { + if (child.id === "toc-title") { + continue; + } + + const clone = child.cloneNode(true); + clone.style.opacity = 1; + clone.style.display = null; + toggleContents.append(clone); + } + toggleContents.style.height = "0px"; + const positionToggle = () => { + // position the element (top left of parent, same width as parent) + if (!elRect) { + elRect = el.getBoundingClientRect(); + } + toggleContainer.style.left = `${elRect.left}px`; + toggleContainer.style.top = `${elRect.top}px`; + toggleContainer.style.width = `${elRect.width}px`; + }; + positionToggle(); + + toggleContainer.append(toggleContents); + el.parentElement.prepend(toggleContainer); + + // Process clicks + let tocShowing = false; + // Allow the caller to control whether this is dismissed + // when it is clicked (e.g. sidebar navigation supports + // opening and closing the nav tree, so don't dismiss on click) + const clickEl = placeholderDescriptor.dismissOnClick + ? toggleContainer + : toggleTitle; + + const closeToggle = () => { + if (tocShowing) { + toggleContainer.classList.remove("expanded"); + toggleContents.style.height = "0px"; + tocShowing = false; + } + }; + + // Get rid of any expanded toggle if the user scrolls + window.document.addEventListener( + "scroll", + throttle(() => { + closeToggle(); + }, 50) + ); + + // Handle positioning of the toggle + window.addEventListener( + "resize", + throttle(() => { + elRect = undefined; + positionToggle(); + }, 50) + ); + + window.addEventListener("quarto-hrChanged", () => { + elRect = undefined; + }); + + // Process the click + clickEl.onclick = () => { + if (!tocShowing) { + toggleContainer.classList.add("expanded"); + toggleContents.style.height = null; + tocShowing = true; + } else { + closeToggle(); + } + }; + }); + }; + + // Converts a sidebar from a menu back to a sidebar + const convertToSidebar = () => { + for (const child of el.children) { + child.style.opacity = 1; + child.style.overflow = null; + } + + const placeholderEl = window.document.getElementById( + placeholderDescriptor.id + ); + if (placeholderEl) { + placeholderEl.remove(); + } + + el.classList.remove("rollup"); + }; + + if (isReaderMode()) { + convertToMenu(); + isVisible = false; + } else { + // Find the top and bottom o the element that is being managed + const elTop = el.offsetTop; + const elBottom = + elTop + lastChildEl.offsetTop + lastChildEl.offsetHeight; + + if (!isVisible) { + // If the element is current not visible reveal if there are + // no conflicts with overlay regions + if (!inHiddenRegion(elTop, elBottom, hiddenRegions)) { + convertToSidebar(); + isVisible = true; + } + } else { + // If the element is visible, hide it if it conflicts with overlay regions + // and insert a placeholder toggle (or if we're in reader mode) + if (inHiddenRegion(elTop, elBottom, hiddenRegions)) { + convertToMenu(); + isVisible = false; + } + } + } + } + }; + }; + + const tabEls = document.querySelectorAll('a[data-bs-toggle="tab"]'); + for (const tabEl of tabEls) { + const id = tabEl.getAttribute("data-bs-target"); + if (id) { + const columnEl = document.querySelector( + `${id} .column-margin, .tabset-margin-content` + ); + if (columnEl) + tabEl.addEventListener("shown.bs.tab", function (event) { + const el = event.srcElement; + if (el) { + const visibleCls = `${el.id}-margin-content`; + // walk up until we find a parent tabset + let panelTabsetEl = el.parentElement; + while (panelTabsetEl) { + if (panelTabsetEl.classList.contains("panel-tabset")) { + break; + } + panelTabsetEl = panelTabsetEl.parentElement; + } + + if (panelTabsetEl) { + const prevSib = panelTabsetEl.previousElementSibling; + if ( + prevSib && + prevSib.classList.contains("tabset-margin-container") + ) { + const childNodes = prevSib.querySelectorAll( + ".tabset-margin-content" + ); + for (const childEl of childNodes) { + if (childEl.classList.contains(visibleCls)) { + childEl.classList.remove("collapse"); + } else { + childEl.classList.add("collapse"); + } + } + } + } + } + + layoutMarginEls(); + }); + } + } + + // Manage the visibility of the toc and the sidebar + const marginScrollVisibility = manageSidebarVisiblity(marginSidebarEl, { + id: "quarto-toc-toggle", + titleSelector: "#toc-title", + dismissOnClick: true, + }); + const sidebarScrollVisiblity = manageSidebarVisiblity(sidebarEl, { + id: "quarto-sidebarnav-toggle", + titleSelector: ".title", + dismissOnClick: false, + }); + let tocLeftScrollVisibility; + if (leftTocEl) { + tocLeftScrollVisibility = manageSidebarVisiblity(leftTocEl, { + id: "quarto-lefttoc-toggle", + titleSelector: "#toc-title", + dismissOnClick: true, + }); + } + + // Find the first element that uses formatting in special columns + const conflictingEls = window.document.body.querySelectorAll( + '[class^="column-"], [class*=" column-"], aside, [class*="margin-caption"], [class*=" margin-caption"], [class*="margin-ref"], [class*=" margin-ref"]' + ); + + // Filter all the possibly conflicting elements into ones + // the do conflict on the left or ride side + const arrConflictingEls = Array.from(conflictingEls); + const leftSideConflictEls = arrConflictingEls.filter((el) => { + if (el.tagName === "ASIDE") { + return false; + } + return Array.from(el.classList).find((className) => { + return ( + className !== "column-body" && + className.startsWith("column-") && + !className.endsWith("right") && + !className.endsWith("container") && + className !== "column-margin" + ); + }); + }); + const rightSideConflictEls = arrConflictingEls.filter((el) => { + if (el.tagName === "ASIDE") { + return true; + } + + const hasMarginCaption = Array.from(el.classList).find((className) => { + return className == "margin-caption"; + }); + if (hasMarginCaption) { + return true; + } + + return Array.from(el.classList).find((className) => { + return ( + className !== "column-body" && + !className.endsWith("container") && + className.startsWith("column-") && + !className.endsWith("left") + ); + }); + }); + + const kOverlapPaddingSize = 10; + function toRegions(els) { + return els.map((el) => { + const boundRect = el.getBoundingClientRect(); + const top = + boundRect.top + + document.documentElement.scrollTop - + kOverlapPaddingSize; + return { + top, + bottom: top + el.scrollHeight + 2 * kOverlapPaddingSize, + }; + }); + } + + let hasObserved = false; + const visibleItemObserver = (els) => { + let visibleElements = [...els]; + const intersectionObserver = new IntersectionObserver( + (entries, _observer) => { + entries.forEach((entry) => { + if (entry.isIntersecting) { + if (visibleElements.indexOf(entry.target) === -1) { + visibleElements.push(entry.target); + } + } else { + visibleElements = visibleElements.filter((visibleEntry) => { + return visibleEntry !== entry; + }); + } + }); + + if (!hasObserved) { + hideOverlappedSidebars(); + } + hasObserved = true; + }, + {} + ); + els.forEach((el) => { + intersectionObserver.observe(el); + }); + + return { + getVisibleEntries: () => { + return visibleElements; + }, + }; + }; + + const rightElementObserver = visibleItemObserver(rightSideConflictEls); + const leftElementObserver = visibleItemObserver(leftSideConflictEls); + + const hideOverlappedSidebars = () => { + marginScrollVisibility(toRegions(rightElementObserver.getVisibleEntries())); + sidebarScrollVisiblity(toRegions(leftElementObserver.getVisibleEntries())); + if (tocLeftScrollVisibility) { + tocLeftScrollVisibility( + toRegions(leftElementObserver.getVisibleEntries()) + ); + } + }; + + window.quartoToggleReader = () => { + // Applies a slow class (or removes it) + // to update the transition speed + const slowTransition = (slow) => { + const manageTransition = (id, slow) => { + const el = document.getElementById(id); + if (el) { + if (slow) { + el.classList.add("slow"); + } else { + el.classList.remove("slow"); + } + } + }; + + manageTransition("TOC", slow); + manageTransition("quarto-sidebar", slow); + }; + const readerMode = !isReaderMode(); + setReaderModeValue(readerMode); + + // If we're entering reader mode, slow the transition + if (readerMode) { + slowTransition(readerMode); + } + highlightReaderToggle(readerMode); + hideOverlappedSidebars(); + + // If we're exiting reader mode, restore the non-slow transition + if (!readerMode) { + slowTransition(!readerMode); + } + }; + + const highlightReaderToggle = (readerMode) => { + const els = document.querySelectorAll(".quarto-reader-toggle"); + if (els) { + els.forEach((el) => { + if (readerMode) { + el.classList.add("reader"); + } else { + el.classList.remove("reader"); + } + }); + } + }; + + const setReaderModeValue = (val) => { + if (window.location.protocol !== "file:") { + window.localStorage.setItem("quarto-reader-mode", val); + } else { + localReaderMode = val; + } + }; + + const isReaderMode = () => { + if (window.location.protocol !== "file:") { + return window.localStorage.getItem("quarto-reader-mode") === "true"; + } else { + return localReaderMode; + } + }; + let localReaderMode = null; + + const tocOpenDepthStr = tocEl?.getAttribute("data-toc-expanded"); + const tocOpenDepth = tocOpenDepthStr ? Number(tocOpenDepthStr) : 1; + + // Walk the TOC and collapse/expand nodes + // Nodes are expanded if: + // - they are top level + // - they have children that are 'active' links + // - they are directly below an link that is 'active' + const walk = (el, depth) => { + // Tick depth when we enter a UL + if (el.tagName === "UL") { + depth = depth + 1; + } + + // It this is active link + let isActiveNode = false; + if (el.tagName === "A" && el.classList.contains("active")) { + isActiveNode = true; + } + + // See if there is an active child to this element + let hasActiveChild = false; + for (child of el.children) { + hasActiveChild = walk(child, depth) || hasActiveChild; + } + + // Process the collapse state if this is an UL + if (el.tagName === "UL") { + if (tocOpenDepth === -1 && depth > 1) { + el.classList.add("collapse"); + } else if ( + depth <= tocOpenDepth || + hasActiveChild || + prevSiblingIsActiveLink(el) + ) { + el.classList.remove("collapse"); + } else { + el.classList.add("collapse"); + } + + // untick depth when we leave a UL + depth = depth - 1; + } + return hasActiveChild || isActiveNode; + }; + + // walk the TOC and expand / collapse any items that should be shown + + if (tocEl) { + walk(tocEl, 0); + updateActiveLink(); + } + + // Throttle the scroll event and walk peridiocally + window.document.addEventListener( + "scroll", + throttle(() => { + if (tocEl) { + updateActiveLink(); + walk(tocEl, 0); + } + if (!isReaderMode()) { + hideOverlappedSidebars(); + } + }, 5) + ); + window.addEventListener( + "resize", + throttle(() => { + if (!isReaderMode()) { + hideOverlappedSidebars(); + } + }, 10) + ); + hideOverlappedSidebars(); + highlightReaderToggle(isReaderMode()); +}); + +// grouped tabsets +window.addEventListener("pageshow", (_event) => { + function getTabSettings() { + const data = localStorage.getItem("quarto-persistent-tabsets-data"); + if (!data) { + localStorage.setItem("quarto-persistent-tabsets-data", "{}"); + return {}; + } + if (data) { + return JSON.parse(data); + } + } + + function setTabSettings(data) { + localStorage.setItem( + "quarto-persistent-tabsets-data", + JSON.stringify(data) + ); + } + + function setTabState(groupName, groupValue) { + const data = getTabSettings(); + data[groupName] = groupValue; + setTabSettings(data); + } + + function toggleTab(tab, active) { + const tabPanelId = tab.getAttribute("aria-controls"); + const tabPanel = document.getElementById(tabPanelId); + if (active) { + tab.classList.add("active"); + tabPanel.classList.add("active"); + } else { + tab.classList.remove("active"); + tabPanel.classList.remove("active"); + } + } + + function toggleAll(selectedGroup, selectorsToSync) { + for (const [thisGroup, tabs] of Object.entries(selectorsToSync)) { + const active = selectedGroup === thisGroup; + for (const tab of tabs) { + toggleTab(tab, active); + } + } + } + + function findSelectorsToSyncByLanguage() { + const result = {}; + const tabs = Array.from( + document.querySelectorAll(`div[data-group] a[id^='tabset-']`) + ); + for (const item of tabs) { + const div = item.parentElement.parentElement.parentElement; + const group = div.getAttribute("data-group"); + if (!result[group]) { + result[group] = {}; + } + const selectorsToSync = result[group]; + const value = item.innerHTML; + if (!selectorsToSync[value]) { + selectorsToSync[value] = []; + } + selectorsToSync[value].push(item); + } + return result; + } + + function setupSelectorSync() { + const selectorsToSync = findSelectorsToSyncByLanguage(); + Object.entries(selectorsToSync).forEach(([group, tabSetsByValue]) => { + Object.entries(tabSetsByValue).forEach(([value, items]) => { + items.forEach((item) => { + item.addEventListener("click", (_event) => { + setTabState(group, value); + toggleAll(value, selectorsToSync[group]); + }); + }); + }); + }); + return selectorsToSync; + } + + const selectorsToSync = setupSelectorSync(); + for (const [group, selectedName] of Object.entries(getTabSettings())) { + const selectors = selectorsToSync[group]; + // it's possible that stale state gives us empty selections, so we explicitly check here. + if (selectors) { + toggleAll(selectedName, selectors); + } + } +}); + +function throttle(func, wait) { + let waiting = false; + return function () { + if (!waiting) { + func.apply(this, arguments); + waiting = true; + setTimeout(function () { + waiting = false; + }, wait); + } + }; +} + +function nexttick(func) { + return setTimeout(func, 0); +} diff --git a/site_libs/quarto-html/tippy.css b/site_libs/quarto-html/tippy.css new file mode 100644 index 00000000..e6ae635c --- /dev/null +++ b/site_libs/quarto-html/tippy.css @@ -0,0 +1 @@ +.tippy-box[data-animation=fade][data-state=hidden]{opacity:0}[data-tippy-root]{max-width:calc(100vw - 10px)}.tippy-box{position:relative;background-color:#333;color:#fff;border-radius:4px;font-size:14px;line-height:1.4;white-space:normal;outline:0;transition-property:transform,visibility,opacity}.tippy-box[data-placement^=top]>.tippy-arrow{bottom:0}.tippy-box[data-placement^=top]>.tippy-arrow:before{bottom:-7px;left:0;border-width:8px 8px 0;border-top-color:initial;transform-origin:center top}.tippy-box[data-placement^=bottom]>.tippy-arrow{top:0}.tippy-box[data-placement^=bottom]>.tippy-arrow:before{top:-7px;left:0;border-width:0 8px 8px;border-bottom-color:initial;transform-origin:center bottom}.tippy-box[data-placement^=left]>.tippy-arrow{right:0}.tippy-box[data-placement^=left]>.tippy-arrow:before{border-width:8px 0 8px 8px;border-left-color:initial;right:-7px;transform-origin:center left}.tippy-box[data-placement^=right]>.tippy-arrow{left:0}.tippy-box[data-placement^=right]>.tippy-arrow:before{left:-7px;border-width:8px 8px 8px 0;border-right-color:initial;transform-origin:center right}.tippy-box[data-inertia][data-state=visible]{transition-timing-function:cubic-bezier(.54,1.5,.38,1.11)}.tippy-arrow{width:16px;height:16px;color:#333}.tippy-arrow:before{content:"";position:absolute;border-color:transparent;border-style:solid}.tippy-content{position:relative;padding:5px 9px;z-index:1} \ No newline at end of file diff --git a/site_libs/quarto-html/tippy.umd.min.js b/site_libs/quarto-html/tippy.umd.min.js new file mode 100644 index 00000000..ca292be3 --- /dev/null +++ b/site_libs/quarto-html/tippy.umd.min.js @@ -0,0 +1,2 @@ +!function(e,t){"object"==typeof exports&&"undefined"!=typeof module?module.exports=t(require("@popperjs/core")):"function"==typeof define&&define.amd?define(["@popperjs/core"],t):(e=e||self).tippy=t(e.Popper)}(this,(function(e){"use strict";var t={passive:!0,capture:!0},n=function(){return document.body};function r(e,t,n){if(Array.isArray(e)){var r=e[t];return null==r?Array.isArray(n)?n[t]:n:r}return e}function o(e,t){var n={}.toString.call(e);return 0===n.indexOf("[object")&&n.indexOf(t+"]")>-1}function i(e,t){return"function"==typeof e?e.apply(void 0,t):e}function a(e,t){return 0===t?e:function(r){clearTimeout(n),n=setTimeout((function(){e(r)}),t)};var n}function s(e,t){var n=Object.assign({},e);return t.forEach((function(e){delete n[e]})),n}function u(e){return[].concat(e)}function c(e,t){-1===e.indexOf(t)&&e.push(t)}function p(e){return e.split("-")[0]}function f(e){return[].slice.call(e)}function l(e){return Object.keys(e).reduce((function(t,n){return void 0!==e[n]&&(t[n]=e[n]),t}),{})}function d(){return document.createElement("div")}function v(e){return["Element","Fragment"].some((function(t){return o(e,t)}))}function m(e){return o(e,"MouseEvent")}function g(e){return!(!e||!e._tippy||e._tippy.reference!==e)}function h(e){return v(e)?[e]:function(e){return o(e,"NodeList")}(e)?f(e):Array.isArray(e)?e:f(document.querySelectorAll(e))}function b(e,t){e.forEach((function(e){e&&(e.style.transitionDuration=t+"ms")}))}function y(e,t){e.forEach((function(e){e&&e.setAttribute("data-state",t)}))}function w(e){var t,n=u(e)[0];return null!=n&&null!=(t=n.ownerDocument)&&t.body?n.ownerDocument:document}function E(e,t,n){var r=t+"EventListener";["transitionend","webkitTransitionEnd"].forEach((function(t){e[r](t,n)}))}function O(e,t){for(var n=t;n;){var r;if(e.contains(n))return!0;n=null==n.getRootNode||null==(r=n.getRootNode())?void 0:r.host}return!1}var x={isTouch:!1},C=0;function T(){x.isTouch||(x.isTouch=!0,window.performance&&document.addEventListener("mousemove",A))}function A(){var e=performance.now();e-C<20&&(x.isTouch=!1,document.removeEventListener("mousemove",A)),C=e}function L(){var e=document.activeElement;if(g(e)){var t=e._tippy;e.blur&&!t.state.isVisible&&e.blur()}}var D=!!("undefined"!=typeof window&&"undefined"!=typeof document)&&!!window.msCrypto,R=Object.assign({appendTo:n,aria:{content:"auto",expanded:"auto"},delay:0,duration:[300,250],getReferenceClientRect:null,hideOnClick:!0,ignoreAttributes:!1,interactive:!1,interactiveBorder:2,interactiveDebounce:0,moveTransition:"",offset:[0,10],onAfterUpdate:function(){},onBeforeUpdate:function(){},onCreate:function(){},onDestroy:function(){},onHidden:function(){},onHide:function(){},onMount:function(){},onShow:function(){},onShown:function(){},onTrigger:function(){},onUntrigger:function(){},onClickOutside:function(){},placement:"top",plugins:[],popperOptions:{},render:null,showOnCreate:!1,touch:!0,trigger:"mouseenter focus",triggerTarget:null},{animateFill:!1,followCursor:!1,inlinePositioning:!1,sticky:!1},{allowHTML:!1,animation:"fade",arrow:!0,content:"",inertia:!1,maxWidth:350,role:"tooltip",theme:"",zIndex:9999}),k=Object.keys(R);function P(e){var t=(e.plugins||[]).reduce((function(t,n){var r,o=n.name,i=n.defaultValue;o&&(t[o]=void 0!==e[o]?e[o]:null!=(r=R[o])?r:i);return t}),{});return Object.assign({},e,t)}function j(e,t){var n=Object.assign({},t,{content:i(t.content,[e])},t.ignoreAttributes?{}:function(e,t){return(t?Object.keys(P(Object.assign({},R,{plugins:t}))):k).reduce((function(t,n){var r=(e.getAttribute("data-tippy-"+n)||"").trim();if(!r)return t;if("content"===n)t[n]=r;else try{t[n]=JSON.parse(r)}catch(e){t[n]=r}return t}),{})}(e,t.plugins));return n.aria=Object.assign({},R.aria,n.aria),n.aria={expanded:"auto"===n.aria.expanded?t.interactive:n.aria.expanded,content:"auto"===n.aria.content?t.interactive?null:"describedby":n.aria.content},n}function M(e,t){e.innerHTML=t}function V(e){var t=d();return!0===e?t.className="tippy-arrow":(t.className="tippy-svg-arrow",v(e)?t.appendChild(e):M(t,e)),t}function I(e,t){v(t.content)?(M(e,""),e.appendChild(t.content)):"function"!=typeof t.content&&(t.allowHTML?M(e,t.content):e.textContent=t.content)}function S(e){var t=e.firstElementChild,n=f(t.children);return{box:t,content:n.find((function(e){return e.classList.contains("tippy-content")})),arrow:n.find((function(e){return e.classList.contains("tippy-arrow")||e.classList.contains("tippy-svg-arrow")})),backdrop:n.find((function(e){return e.classList.contains("tippy-backdrop")}))}}function N(e){var t=d(),n=d();n.className="tippy-box",n.setAttribute("data-state","hidden"),n.setAttribute("tabindex","-1");var r=d();function o(n,r){var o=S(t),i=o.box,a=o.content,s=o.arrow;r.theme?i.setAttribute("data-theme",r.theme):i.removeAttribute("data-theme"),"string"==typeof r.animation?i.setAttribute("data-animation",r.animation):i.removeAttribute("data-animation"),r.inertia?i.setAttribute("data-inertia",""):i.removeAttribute("data-inertia"),i.style.maxWidth="number"==typeof r.maxWidth?r.maxWidth+"px":r.maxWidth,r.role?i.setAttribute("role",r.role):i.removeAttribute("role"),n.content===r.content&&n.allowHTML===r.allowHTML||I(a,e.props),r.arrow?s?n.arrow!==r.arrow&&(i.removeChild(s),i.appendChild(V(r.arrow))):i.appendChild(V(r.arrow)):s&&i.removeChild(s)}return r.className="tippy-content",r.setAttribute("data-state","hidden"),I(r,e.props),t.appendChild(n),n.appendChild(r),o(e.props,e.props),{popper:t,onUpdate:o}}N.$$tippy=!0;var B=1,H=[],U=[];function _(o,s){var v,g,h,C,T,A,L,k,M=j(o,Object.assign({},R,P(l(s)))),V=!1,I=!1,N=!1,_=!1,F=[],W=a(we,M.interactiveDebounce),X=B++,Y=(k=M.plugins).filter((function(e,t){return k.indexOf(e)===t})),$={id:X,reference:o,popper:d(),popperInstance:null,props:M,state:{isEnabled:!0,isVisible:!1,isDestroyed:!1,isMounted:!1,isShown:!1},plugins:Y,clearDelayTimeouts:function(){clearTimeout(v),clearTimeout(g),cancelAnimationFrame(h)},setProps:function(e){if($.state.isDestroyed)return;ae("onBeforeUpdate",[$,e]),be();var t=$.props,n=j(o,Object.assign({},t,l(e),{ignoreAttributes:!0}));$.props=n,he(),t.interactiveDebounce!==n.interactiveDebounce&&(ce(),W=a(we,n.interactiveDebounce));t.triggerTarget&&!n.triggerTarget?u(t.triggerTarget).forEach((function(e){e.removeAttribute("aria-expanded")})):n.triggerTarget&&o.removeAttribute("aria-expanded");ue(),ie(),J&&J(t,n);$.popperInstance&&(Ce(),Ae().forEach((function(e){requestAnimationFrame(e._tippy.popperInstance.forceUpdate)})));ae("onAfterUpdate",[$,e])},setContent:function(e){$.setProps({content:e})},show:function(){var e=$.state.isVisible,t=$.state.isDestroyed,o=!$.state.isEnabled,a=x.isTouch&&!$.props.touch,s=r($.props.duration,0,R.duration);if(e||t||o||a)return;if(te().hasAttribute("disabled"))return;if(ae("onShow",[$],!1),!1===$.props.onShow($))return;$.state.isVisible=!0,ee()&&(z.style.visibility="visible");ie(),de(),$.state.isMounted||(z.style.transition="none");if(ee()){var u=re(),p=u.box,f=u.content;b([p,f],0)}A=function(){var e;if($.state.isVisible&&!_){if(_=!0,z.offsetHeight,z.style.transition=$.props.moveTransition,ee()&&$.props.animation){var t=re(),n=t.box,r=t.content;b([n,r],s),y([n,r],"visible")}se(),ue(),c(U,$),null==(e=$.popperInstance)||e.forceUpdate(),ae("onMount",[$]),$.props.animation&&ee()&&function(e,t){me(e,t)}(s,(function(){$.state.isShown=!0,ae("onShown",[$])}))}},function(){var e,t=$.props.appendTo,r=te();e=$.props.interactive&&t===n||"parent"===t?r.parentNode:i(t,[r]);e.contains(z)||e.appendChild(z);$.state.isMounted=!0,Ce()}()},hide:function(){var e=!$.state.isVisible,t=$.state.isDestroyed,n=!$.state.isEnabled,o=r($.props.duration,1,R.duration);if(e||t||n)return;if(ae("onHide",[$],!1),!1===$.props.onHide($))return;$.state.isVisible=!1,$.state.isShown=!1,_=!1,V=!1,ee()&&(z.style.visibility="hidden");if(ce(),ve(),ie(!0),ee()){var i=re(),a=i.box,s=i.content;$.props.animation&&(b([a,s],o),y([a,s],"hidden"))}se(),ue(),$.props.animation?ee()&&function(e,t){me(e,(function(){!$.state.isVisible&&z.parentNode&&z.parentNode.contains(z)&&t()}))}(o,$.unmount):$.unmount()},hideWithInteractivity:function(e){ne().addEventListener("mousemove",W),c(H,W),W(e)},enable:function(){$.state.isEnabled=!0},disable:function(){$.hide(),$.state.isEnabled=!1},unmount:function(){$.state.isVisible&&$.hide();if(!$.state.isMounted)return;Te(),Ae().forEach((function(e){e._tippy.unmount()})),z.parentNode&&z.parentNode.removeChild(z);U=U.filter((function(e){return e!==$})),$.state.isMounted=!1,ae("onHidden",[$])},destroy:function(){if($.state.isDestroyed)return;$.clearDelayTimeouts(),$.unmount(),be(),delete o._tippy,$.state.isDestroyed=!0,ae("onDestroy",[$])}};if(!M.render)return $;var q=M.render($),z=q.popper,J=q.onUpdate;z.setAttribute("data-tippy-root",""),z.id="tippy-"+$.id,$.popper=z,o._tippy=$,z._tippy=$;var G=Y.map((function(e){return e.fn($)})),K=o.hasAttribute("aria-expanded");return he(),ue(),ie(),ae("onCreate",[$]),M.showOnCreate&&Le(),z.addEventListener("mouseenter",(function(){$.props.interactive&&$.state.isVisible&&$.clearDelayTimeouts()})),z.addEventListener("mouseleave",(function(){$.props.interactive&&$.props.trigger.indexOf("mouseenter")>=0&&ne().addEventListener("mousemove",W)})),$;function Q(){var e=$.props.touch;return Array.isArray(e)?e:[e,0]}function Z(){return"hold"===Q()[0]}function ee(){var e;return!(null==(e=$.props.render)||!e.$$tippy)}function te(){return L||o}function ne(){var e=te().parentNode;return e?w(e):document}function re(){return S(z)}function oe(e){return $.state.isMounted&&!$.state.isVisible||x.isTouch||C&&"focus"===C.type?0:r($.props.delay,e?0:1,R.delay)}function ie(e){void 0===e&&(e=!1),z.style.pointerEvents=$.props.interactive&&!e?"":"none",z.style.zIndex=""+$.props.zIndex}function ae(e,t,n){var r;(void 0===n&&(n=!0),G.forEach((function(n){n[e]&&n[e].apply(n,t)})),n)&&(r=$.props)[e].apply(r,t)}function se(){var e=$.props.aria;if(e.content){var t="aria-"+e.content,n=z.id;u($.props.triggerTarget||o).forEach((function(e){var r=e.getAttribute(t);if($.state.isVisible)e.setAttribute(t,r?r+" "+n:n);else{var o=r&&r.replace(n,"").trim();o?e.setAttribute(t,o):e.removeAttribute(t)}}))}}function ue(){!K&&$.props.aria.expanded&&u($.props.triggerTarget||o).forEach((function(e){$.props.interactive?e.setAttribute("aria-expanded",$.state.isVisible&&e===te()?"true":"false"):e.removeAttribute("aria-expanded")}))}function ce(){ne().removeEventListener("mousemove",W),H=H.filter((function(e){return e!==W}))}function pe(e){if(!x.isTouch||!N&&"mousedown"!==e.type){var t=e.composedPath&&e.composedPath()[0]||e.target;if(!$.props.interactive||!O(z,t)){if(u($.props.triggerTarget||o).some((function(e){return O(e,t)}))){if(x.isTouch)return;if($.state.isVisible&&$.props.trigger.indexOf("click")>=0)return}else ae("onClickOutside",[$,e]);!0===$.props.hideOnClick&&($.clearDelayTimeouts(),$.hide(),I=!0,setTimeout((function(){I=!1})),$.state.isMounted||ve())}}}function fe(){N=!0}function le(){N=!1}function de(){var e=ne();e.addEventListener("mousedown",pe,!0),e.addEventListener("touchend",pe,t),e.addEventListener("touchstart",le,t),e.addEventListener("touchmove",fe,t)}function ve(){var e=ne();e.removeEventListener("mousedown",pe,!0),e.removeEventListener("touchend",pe,t),e.removeEventListener("touchstart",le,t),e.removeEventListener("touchmove",fe,t)}function me(e,t){var n=re().box;function r(e){e.target===n&&(E(n,"remove",r),t())}if(0===e)return t();E(n,"remove",T),E(n,"add",r),T=r}function ge(e,t,n){void 0===n&&(n=!1),u($.props.triggerTarget||o).forEach((function(r){r.addEventListener(e,t,n),F.push({node:r,eventType:e,handler:t,options:n})}))}function he(){var e;Z()&&(ge("touchstart",ye,{passive:!0}),ge("touchend",Ee,{passive:!0})),(e=$.props.trigger,e.split(/\s+/).filter(Boolean)).forEach((function(e){if("manual"!==e)switch(ge(e,ye),e){case"mouseenter":ge("mouseleave",Ee);break;case"focus":ge(D?"focusout":"blur",Oe);break;case"focusin":ge("focusout",Oe)}}))}function be(){F.forEach((function(e){var t=e.node,n=e.eventType,r=e.handler,o=e.options;t.removeEventListener(n,r,o)})),F=[]}function ye(e){var t,n=!1;if($.state.isEnabled&&!xe(e)&&!I){var r="focus"===(null==(t=C)?void 0:t.type);C=e,L=e.currentTarget,ue(),!$.state.isVisible&&m(e)&&H.forEach((function(t){return t(e)})),"click"===e.type&&($.props.trigger.indexOf("mouseenter")<0||V)&&!1!==$.props.hideOnClick&&$.state.isVisible?n=!0:Le(e),"click"===e.type&&(V=!n),n&&!r&&De(e)}}function we(e){var t=e.target,n=te().contains(t)||z.contains(t);"mousemove"===e.type&&n||function(e,t){var n=t.clientX,r=t.clientY;return e.every((function(e){var t=e.popperRect,o=e.popperState,i=e.props.interactiveBorder,a=p(o.placement),s=o.modifiersData.offset;if(!s)return!0;var u="bottom"===a?s.top.y:0,c="top"===a?s.bottom.y:0,f="right"===a?s.left.x:0,l="left"===a?s.right.x:0,d=t.top-r+u>i,v=r-t.bottom-c>i,m=t.left-n+f>i,g=n-t.right-l>i;return d||v||m||g}))}(Ae().concat(z).map((function(e){var t,n=null==(t=e._tippy.popperInstance)?void 0:t.state;return n?{popperRect:e.getBoundingClientRect(),popperState:n,props:M}:null})).filter(Boolean),e)&&(ce(),De(e))}function Ee(e){xe(e)||$.props.trigger.indexOf("click")>=0&&V||($.props.interactive?$.hideWithInteractivity(e):De(e))}function Oe(e){$.props.trigger.indexOf("focusin")<0&&e.target!==te()||$.props.interactive&&e.relatedTarget&&z.contains(e.relatedTarget)||De(e)}function xe(e){return!!x.isTouch&&Z()!==e.type.indexOf("touch")>=0}function Ce(){Te();var t=$.props,n=t.popperOptions,r=t.placement,i=t.offset,a=t.getReferenceClientRect,s=t.moveTransition,u=ee()?S(z).arrow:null,c=a?{getBoundingClientRect:a,contextElement:a.contextElement||te()}:o,p=[{name:"offset",options:{offset:i}},{name:"preventOverflow",options:{padding:{top:2,bottom:2,left:5,right:5}}},{name:"flip",options:{padding:5}},{name:"computeStyles",options:{adaptive:!s}},{name:"$$tippy",enabled:!0,phase:"beforeWrite",requires:["computeStyles"],fn:function(e){var t=e.state;if(ee()){var n=re().box;["placement","reference-hidden","escaped"].forEach((function(e){"placement"===e?n.setAttribute("data-placement",t.placement):t.attributes.popper["data-popper-"+e]?n.setAttribute("data-"+e,""):n.removeAttribute("data-"+e)})),t.attributes.popper={}}}}];ee()&&u&&p.push({name:"arrow",options:{element:u,padding:3}}),p.push.apply(p,(null==n?void 0:n.modifiers)||[]),$.popperInstance=e.createPopper(c,z,Object.assign({},n,{placement:r,onFirstUpdate:A,modifiers:p}))}function Te(){$.popperInstance&&($.popperInstance.destroy(),$.popperInstance=null)}function Ae(){return f(z.querySelectorAll("[data-tippy-root]"))}function Le(e){$.clearDelayTimeouts(),e&&ae("onTrigger",[$,e]),de();var t=oe(!0),n=Q(),r=n[0],o=n[1];x.isTouch&&"hold"===r&&o&&(t=o),t?v=setTimeout((function(){$.show()}),t):$.show()}function De(e){if($.clearDelayTimeouts(),ae("onUntrigger",[$,e]),$.state.isVisible){if(!($.props.trigger.indexOf("mouseenter")>=0&&$.props.trigger.indexOf("click")>=0&&["mouseleave","mousemove"].indexOf(e.type)>=0&&V)){var t=oe(!1);t?g=setTimeout((function(){$.state.isVisible&&$.hide()}),t):h=requestAnimationFrame((function(){$.hide()}))}}else ve()}}function F(e,n){void 0===n&&(n={});var r=R.plugins.concat(n.plugins||[]);document.addEventListener("touchstart",T,t),window.addEventListener("blur",L);var o=Object.assign({},n,{plugins:r}),i=h(e).reduce((function(e,t){var n=t&&_(t,o);return n&&e.push(n),e}),[]);return v(e)?i[0]:i}F.defaultProps=R,F.setDefaultProps=function(e){Object.keys(e).forEach((function(t){R[t]=e[t]}))},F.currentInput=x;var W=Object.assign({},e.applyStyles,{effect:function(e){var t=e.state,n={popper:{position:t.options.strategy,left:"0",top:"0",margin:"0"},arrow:{position:"absolute"},reference:{}};Object.assign(t.elements.popper.style,n.popper),t.styles=n,t.elements.arrow&&Object.assign(t.elements.arrow.style,n.arrow)}}),X={mouseover:"mouseenter",focusin:"focus",click:"click"};var Y={name:"animateFill",defaultValue:!1,fn:function(e){var t;if(null==(t=e.props.render)||!t.$$tippy)return{};var n=S(e.popper),r=n.box,o=n.content,i=e.props.animateFill?function(){var e=d();return e.className="tippy-backdrop",y([e],"hidden"),e}():null;return{onCreate:function(){i&&(r.insertBefore(i,r.firstElementChild),r.setAttribute("data-animatefill",""),r.style.overflow="hidden",e.setProps({arrow:!1,animation:"shift-away"}))},onMount:function(){if(i){var e=r.style.transitionDuration,t=Number(e.replace("ms",""));o.style.transitionDelay=Math.round(t/10)+"ms",i.style.transitionDuration=e,y([i],"visible")}},onShow:function(){i&&(i.style.transitionDuration="0ms")},onHide:function(){i&&y([i],"hidden")}}}};var $={clientX:0,clientY:0},q=[];function z(e){var t=e.clientX,n=e.clientY;$={clientX:t,clientY:n}}var J={name:"followCursor",defaultValue:!1,fn:function(e){var t=e.reference,n=w(e.props.triggerTarget||t),r=!1,o=!1,i=!0,a=e.props;function s(){return"initial"===e.props.followCursor&&e.state.isVisible}function u(){n.addEventListener("mousemove",f)}function c(){n.removeEventListener("mousemove",f)}function p(){r=!0,e.setProps({getReferenceClientRect:null}),r=!1}function f(n){var r=!n.target||t.contains(n.target),o=e.props.followCursor,i=n.clientX,a=n.clientY,s=t.getBoundingClientRect(),u=i-s.left,c=a-s.top;!r&&e.props.interactive||e.setProps({getReferenceClientRect:function(){var e=t.getBoundingClientRect(),n=i,r=a;"initial"===o&&(n=e.left+u,r=e.top+c);var s="horizontal"===o?e.top:r,p="vertical"===o?e.right:n,f="horizontal"===o?e.bottom:r,l="vertical"===o?e.left:n;return{width:p-l,height:f-s,top:s,right:p,bottom:f,left:l}}})}function l(){e.props.followCursor&&(q.push({instance:e,doc:n}),function(e){e.addEventListener("mousemove",z)}(n))}function d(){0===(q=q.filter((function(t){return t.instance!==e}))).filter((function(e){return e.doc===n})).length&&function(e){e.removeEventListener("mousemove",z)}(n)}return{onCreate:l,onDestroy:d,onBeforeUpdate:function(){a=e.props},onAfterUpdate:function(t,n){var i=n.followCursor;r||void 0!==i&&a.followCursor!==i&&(d(),i?(l(),!e.state.isMounted||o||s()||u()):(c(),p()))},onMount:function(){e.props.followCursor&&!o&&(i&&(f($),i=!1),s()||u())},onTrigger:function(e,t){m(t)&&($={clientX:t.clientX,clientY:t.clientY}),o="focus"===t.type},onHidden:function(){e.props.followCursor&&(p(),c(),i=!0)}}}};var G={name:"inlinePositioning",defaultValue:!1,fn:function(e){var t,n=e.reference;var r=-1,o=!1,i=[],a={name:"tippyInlinePositioning",enabled:!0,phase:"afterWrite",fn:function(o){var a=o.state;e.props.inlinePositioning&&(-1!==i.indexOf(a.placement)&&(i=[]),t!==a.placement&&-1===i.indexOf(a.placement)&&(i.push(a.placement),e.setProps({getReferenceClientRect:function(){return function(e){return function(e,t,n,r){if(n.length<2||null===e)return t;if(2===n.length&&r>=0&&n[0].left>n[1].right)return n[r]||t;switch(e){case"top":case"bottom":var o=n[0],i=n[n.length-1],a="top"===e,s=o.top,u=i.bottom,c=a?o.left:i.left,p=a?o.right:i.right;return{top:s,bottom:u,left:c,right:p,width:p-c,height:u-s};case"left":case"right":var f=Math.min.apply(Math,n.map((function(e){return e.left}))),l=Math.max.apply(Math,n.map((function(e){return e.right}))),d=n.filter((function(t){return"left"===e?t.left===f:t.right===l})),v=d[0].top,m=d[d.length-1].bottom;return{top:v,bottom:m,left:f,right:l,width:l-f,height:m-v};default:return t}}(p(e),n.getBoundingClientRect(),f(n.getClientRects()),r)}(a.placement)}})),t=a.placement)}};function s(){var t;o||(t=function(e,t){var n;return{popperOptions:Object.assign({},e.popperOptions,{modifiers:[].concat(((null==(n=e.popperOptions)?void 0:n.modifiers)||[]).filter((function(e){return e.name!==t.name})),[t])})}}(e.props,a),o=!0,e.setProps(t),o=!1)}return{onCreate:s,onAfterUpdate:s,onTrigger:function(t,n){if(m(n)){var o=f(e.reference.getClientRects()),i=o.find((function(e){return e.left-2<=n.clientX&&e.right+2>=n.clientX&&e.top-2<=n.clientY&&e.bottom+2>=n.clientY})),a=o.indexOf(i);r=a>-1?a:r}},onHidden:function(){r=-1}}}};var K={name:"sticky",defaultValue:!1,fn:function(e){var t=e.reference,n=e.popper;function r(t){return!0===e.props.sticky||e.props.sticky===t}var o=null,i=null;function a(){var s=r("reference")?(e.popperInstance?e.popperInstance.state.elements.reference:t).getBoundingClientRect():null,u=r("popper")?n.getBoundingClientRect():null;(s&&Q(o,s)||u&&Q(i,u))&&e.popperInstance&&e.popperInstance.update(),o=s,i=u,e.state.isMounted&&requestAnimationFrame(a)}return{onMount:function(){e.props.sticky&&a()}}}};function Q(e,t){return!e||!t||(e.top!==t.top||e.right!==t.right||e.bottom!==t.bottom||e.left!==t.left)}return F.setDefaultProps({plugins:[Y,J,G,K],render:N}),F.createSingleton=function(e,t){var n;void 0===t&&(t={});var r,o=e,i=[],a=[],c=t.overrides,p=[],f=!1;function l(){a=o.map((function(e){return u(e.props.triggerTarget||e.reference)})).reduce((function(e,t){return e.concat(t)}),[])}function v(){i=o.map((function(e){return e.reference}))}function m(e){o.forEach((function(t){e?t.enable():t.disable()}))}function g(e){return o.map((function(t){var n=t.setProps;return t.setProps=function(o){n(o),t.reference===r&&e.setProps(o)},function(){t.setProps=n}}))}function h(e,t){var n=a.indexOf(t);if(t!==r){r=t;var s=(c||[]).concat("content").reduce((function(e,t){return e[t]=o[n].props[t],e}),{});e.setProps(Object.assign({},s,{getReferenceClientRect:"function"==typeof s.getReferenceClientRect?s.getReferenceClientRect:function(){var e;return null==(e=i[n])?void 0:e.getBoundingClientRect()}}))}}m(!1),v(),l();var b={fn:function(){return{onDestroy:function(){m(!0)},onHidden:function(){r=null},onClickOutside:function(e){e.props.showOnCreate&&!f&&(f=!0,r=null)},onShow:function(e){e.props.showOnCreate&&!f&&(f=!0,h(e,i[0]))},onTrigger:function(e,t){h(e,t.currentTarget)}}}},y=F(d(),Object.assign({},s(t,["overrides"]),{plugins:[b].concat(t.plugins||[]),triggerTarget:a,popperOptions:Object.assign({},t.popperOptions,{modifiers:[].concat((null==(n=t.popperOptions)?void 0:n.modifiers)||[],[W])})})),w=y.show;y.show=function(e){if(w(),!r&&null==e)return h(y,i[0]);if(!r||null!=e){if("number"==typeof e)return i[e]&&h(y,i[e]);if(o.indexOf(e)>=0){var t=e.reference;return h(y,t)}return i.indexOf(e)>=0?h(y,e):void 0}},y.showNext=function(){var e=i[0];if(!r)return y.show(0);var t=i.indexOf(r);y.show(i[t+1]||e)},y.showPrevious=function(){var e=i[i.length-1];if(!r)return y.show(e);var t=i.indexOf(r),n=i[t-1]||e;y.show(n)};var E=y.setProps;return y.setProps=function(e){c=e.overrides||c,E(e)},y.setInstances=function(e){m(!0),p.forEach((function(e){return e()})),o=e,m(!1),v(),l(),p=g(y),y.setProps({triggerTarget:a})},p=g(y),y},F.delegate=function(e,n){var r=[],o=[],i=!1,a=n.target,c=s(n,["target"]),p=Object.assign({},c,{trigger:"manual",touch:!1}),f=Object.assign({touch:R.touch},c,{showOnCreate:!0}),l=F(e,p);function d(e){if(e.target&&!i){var t=e.target.closest(a);if(t){var r=t.getAttribute("data-tippy-trigger")||n.trigger||R.trigger;if(!t._tippy&&!("touchstart"===e.type&&"boolean"==typeof f.touch||"touchstart"!==e.type&&r.indexOf(X[e.type])<0)){var s=F(t,f);s&&(o=o.concat(s))}}}}function v(e,t,n,o){void 0===o&&(o=!1),e.addEventListener(t,n,o),r.push({node:e,eventType:t,handler:n,options:o})}return u(l).forEach((function(e){var n=e.destroy,a=e.enable,s=e.disable;e.destroy=function(e){void 0===e&&(e=!0),e&&o.forEach((function(e){e.destroy()})),o=[],r.forEach((function(e){var t=e.node,n=e.eventType,r=e.handler,o=e.options;t.removeEventListener(n,r,o)})),r=[],n()},e.enable=function(){a(),o.forEach((function(e){return e.enable()})),i=!1},e.disable=function(){s(),o.forEach((function(e){return e.disable()})),i=!0},function(e){var n=e.reference;v(n,"touchstart",d,t),v(n,"mouseover",d),v(n,"focusin",d),v(n,"click",d)}(e)})),l},F.hideAll=function(e){var t=void 0===e?{}:e,n=t.exclude,r=t.duration;U.forEach((function(e){var t=!1;if(n&&(t=g(n)?e.reference===n:e.popper===n.popper),!t){var o=e.props.duration;e.setProps({duration:r}),e.hide(),e.state.isDestroyed||e.setProps({duration:o})}}))},F.roundArrow='',F})); + diff --git a/site_libs/quarto-nav/headroom.min.js b/site_libs/quarto-nav/headroom.min.js new file mode 100644 index 00000000..b08f1dff --- /dev/null +++ b/site_libs/quarto-nav/headroom.min.js @@ -0,0 +1,7 @@ +/*! + * headroom.js v0.12.0 - Give your page some headroom. Hide your header until you need it + * Copyright (c) 2020 Nick Williams - http://wicky.nillia.ms/headroom.js + * License: MIT + */ + +!function(t,n){"object"==typeof exports&&"undefined"!=typeof module?module.exports=n():"function"==typeof define&&define.amd?define(n):(t=t||self).Headroom=n()}(this,function(){"use strict";function t(){return"undefined"!=typeof window}function d(t){return function(t){return t&&t.document&&function(t){return 9===t.nodeType}(t.document)}(t)?function(t){var n=t.document,o=n.body,s=n.documentElement;return{scrollHeight:function(){return Math.max(o.scrollHeight,s.scrollHeight,o.offsetHeight,s.offsetHeight,o.clientHeight,s.clientHeight)},height:function(){return t.innerHeight||s.clientHeight||o.clientHeight},scrollY:function(){return void 0!==t.pageYOffset?t.pageYOffset:(s||o.parentNode||o).scrollTop}}}(t):function(t){return{scrollHeight:function(){return Math.max(t.scrollHeight,t.offsetHeight,t.clientHeight)},height:function(){return Math.max(t.offsetHeight,t.clientHeight)},scrollY:function(){return t.scrollTop}}}(t)}function n(t,s,e){var n,o=function(){var n=!1;try{var t={get passive(){n=!0}};window.addEventListener("test",t,t),window.removeEventListener("test",t,t)}catch(t){n=!1}return n}(),i=!1,r=d(t),l=r.scrollY(),a={};function c(){var t=Math.round(r.scrollY()),n=r.height(),o=r.scrollHeight();a.scrollY=t,a.lastScrollY=l,a.direction=ls.tolerance[a.direction],e(a),l=t,i=!1}function h(){i||(i=!0,n=requestAnimationFrame(c))}var u=!!o&&{passive:!0,capture:!1};return t.addEventListener("scroll",h,u),c(),{destroy:function(){cancelAnimationFrame(n),t.removeEventListener("scroll",h,u)}}}function o(t){return t===Object(t)?t:{down:t,up:t}}function s(t,n){n=n||{},Object.assign(this,s.options,n),this.classes=Object.assign({},s.options.classes,n.classes),this.elem=t,this.tolerance=o(this.tolerance),this.offset=o(this.offset),this.initialised=!1,this.frozen=!1}return s.prototype={constructor:s,init:function(){return s.cutsTheMustard&&!this.initialised&&(this.addClass("initial"),this.initialised=!0,setTimeout(function(t){t.scrollTracker=n(t.scroller,{offset:t.offset,tolerance:t.tolerance},t.update.bind(t))},100,this)),this},destroy:function(){this.initialised=!1,Object.keys(this.classes).forEach(this.removeClass,this),this.scrollTracker.destroy()},unpin:function(){!this.hasClass("pinned")&&this.hasClass("unpinned")||(this.addClass("unpinned"),this.removeClass("pinned"),this.onUnpin&&this.onUnpin.call(this))},pin:function(){this.hasClass("unpinned")&&(this.addClass("pinned"),this.removeClass("unpinned"),this.onPin&&this.onPin.call(this))},freeze:function(){this.frozen=!0,this.addClass("frozen")},unfreeze:function(){this.frozen=!1,this.removeClass("frozen")},top:function(){this.hasClass("top")||(this.addClass("top"),this.removeClass("notTop"),this.onTop&&this.onTop.call(this))},notTop:function(){this.hasClass("notTop")||(this.addClass("notTop"),this.removeClass("top"),this.onNotTop&&this.onNotTop.call(this))},bottom:function(){this.hasClass("bottom")||(this.addClass("bottom"),this.removeClass("notBottom"),this.onBottom&&this.onBottom.call(this))},notBottom:function(){this.hasClass("notBottom")||(this.addClass("notBottom"),this.removeClass("bottom"),this.onNotBottom&&this.onNotBottom.call(this))},shouldUnpin:function(t){return"down"===t.direction&&!t.top&&t.toleranceExceeded},shouldPin:function(t){return"up"===t.direction&&t.toleranceExceeded||t.top},addClass:function(t){this.elem.classList.add.apply(this.elem.classList,this.classes[t].split(" "))},removeClass:function(t){this.elem.classList.remove.apply(this.elem.classList,this.classes[t].split(" "))},hasClass:function(t){return this.classes[t].split(" ").every(function(t){return this.classList.contains(t)},this.elem)},update:function(t){t.isOutOfBounds||!0!==this.frozen&&(t.top?this.top():this.notTop(),t.bottom?this.bottom():this.notBottom(),this.shouldUnpin(t)?this.unpin():this.shouldPin(t)&&this.pin())}},s.options={tolerance:{up:0,down:0},offset:0,scroller:t()?window:null,classes:{frozen:"headroom--frozen",pinned:"headroom--pinned",unpinned:"headroom--unpinned",top:"headroom--top",notTop:"headroom--not-top",bottom:"headroom--bottom",notBottom:"headroom--not-bottom",initial:"headroom"}},s.cutsTheMustard=!!(t()&&function(){}.bind&&"classList"in document.documentElement&&Object.assign&&Object.keys&&requestAnimationFrame),s}); diff --git a/site_libs/quarto-nav/quarto-nav.js b/site_libs/quarto-nav/quarto-nav.js new file mode 100644 index 00000000..3b21201f --- /dev/null +++ b/site_libs/quarto-nav/quarto-nav.js @@ -0,0 +1,277 @@ +const headroomChanged = new CustomEvent("quarto-hrChanged", { + detail: {}, + bubbles: true, + cancelable: false, + composed: false, +}); + +window.document.addEventListener("DOMContentLoaded", function () { + let init = false; + + // Manage the back to top button, if one is present. + let lastScrollTop = window.pageYOffset || document.documentElement.scrollTop; + const scrollDownBuffer = 5; + const scrollUpBuffer = 35; + const btn = document.getElementById("quarto-back-to-top"); + const hideBackToTop = () => { + btn.style.display = "none"; + }; + const showBackToTop = () => { + btn.style.display = "inline-block"; + }; + if (btn) { + window.document.addEventListener( + "scroll", + function () { + const currentScrollTop = + window.pageYOffset || document.documentElement.scrollTop; + + // Shows and hides the button 'intelligently' as the user scrolls + if (currentScrollTop - scrollDownBuffer > lastScrollTop) { + hideBackToTop(); + lastScrollTop = currentScrollTop <= 0 ? 0 : currentScrollTop; + } else if (currentScrollTop < lastScrollTop - scrollUpBuffer) { + showBackToTop(); + lastScrollTop = currentScrollTop <= 0 ? 0 : currentScrollTop; + } + + // Show the button at the bottom, hides it at the top + if (currentScrollTop <= 0) { + hideBackToTop(); + } else if ( + window.innerHeight + currentScrollTop >= + document.body.offsetHeight + ) { + showBackToTop(); + } + }, + false + ); + } + + function throttle(func, wait) { + var timeout; + return function () { + const context = this; + const args = arguments; + const later = function () { + clearTimeout(timeout); + timeout = null; + func.apply(context, args); + }; + + if (!timeout) { + timeout = setTimeout(later, wait); + } + }; + } + + function headerOffset() { + // Set an offset if there is are fixed top navbar + const headerEl = window.document.querySelector("header.fixed-top"); + if (headerEl) { + return headerEl.clientHeight; + } else { + return 0; + } + } + + function footerOffset() { + const footerEl = window.document.querySelector("footer.footer"); + if (footerEl) { + return footerEl.clientHeight; + } else { + return 0; + } + } + + function updateDocumentOffsetWithoutAnimation() { + updateDocumentOffset(false); + } + + function updateDocumentOffset(animated) { + // set body offset + const topOffset = headerOffset(); + const bodyOffset = topOffset + footerOffset(); + const bodyEl = window.document.body; + bodyEl.setAttribute("data-bs-offset", topOffset); + bodyEl.style.paddingTop = topOffset + "px"; + + // deal with sidebar offsets + const sidebars = window.document.querySelectorAll( + ".sidebar, .headroom-target" + ); + sidebars.forEach((sidebar) => { + if (!animated) { + sidebar.classList.add("notransition"); + // Remove the no transition class after the animation has time to complete + setTimeout(function () { + sidebar.classList.remove("notransition"); + }, 201); + } + + if (window.Headroom && sidebar.classList.contains("sidebar-unpinned")) { + sidebar.style.top = "0"; + sidebar.style.maxHeight = "100vh"; + } else { + sidebar.style.top = topOffset + "px"; + sidebar.style.maxHeight = "calc(100vh - " + topOffset + "px)"; + } + }); + + // allow space for footer + const mainContainer = window.document.querySelector(".quarto-container"); + if (mainContainer) { + mainContainer.style.minHeight = "calc(100vh - " + bodyOffset + "px)"; + } + + // link offset + let linkStyle = window.document.querySelector("#quarto-target-style"); + if (!linkStyle) { + linkStyle = window.document.createElement("style"); + linkStyle.setAttribute("id", "quarto-target-style"); + window.document.head.appendChild(linkStyle); + } + while (linkStyle.firstChild) { + linkStyle.removeChild(linkStyle.firstChild); + } + if (topOffset > 0) { + linkStyle.appendChild( + window.document.createTextNode(` + section:target::before { + content: ""; + display: block; + height: ${topOffset}px; + margin: -${topOffset}px 0 0; + }`) + ); + } + if (init) { + window.dispatchEvent(headroomChanged); + } + init = true; + } + + // initialize headroom + var header = window.document.querySelector("#quarto-header"); + if (header && window.Headroom) { + const headroom = new window.Headroom(header, { + tolerance: 5, + onPin: function () { + const sidebars = window.document.querySelectorAll( + ".sidebar, .headroom-target" + ); + sidebars.forEach((sidebar) => { + sidebar.classList.remove("sidebar-unpinned"); + }); + updateDocumentOffset(); + }, + onUnpin: function () { + const sidebars = window.document.querySelectorAll( + ".sidebar, .headroom-target" + ); + sidebars.forEach((sidebar) => { + sidebar.classList.add("sidebar-unpinned"); + }); + updateDocumentOffset(); + }, + }); + headroom.init(); + + let frozen = false; + window.quartoToggleHeadroom = function () { + if (frozen) { + headroom.unfreeze(); + frozen = false; + } else { + headroom.freeze(); + frozen = true; + } + }; + } + + window.addEventListener( + "hashchange", + function (e) { + if ( + getComputedStyle(document.documentElement).scrollBehavior !== "smooth" + ) { + window.scrollTo(0, window.pageYOffset - headerOffset()); + } + }, + false + ); + + // Observe size changed for the header + const headerEl = window.document.querySelector("header.fixed-top"); + if (headerEl && window.ResizeObserver) { + const observer = new window.ResizeObserver( + updateDocumentOffsetWithoutAnimation + ); + observer.observe(headerEl, { + attributes: true, + childList: true, + characterData: true, + }); + } else { + window.addEventListener( + "resize", + throttle(updateDocumentOffsetWithoutAnimation, 50) + ); + } + setTimeout(updateDocumentOffsetWithoutAnimation, 250); + + // fixup index.html links if we aren't on the filesystem + if (window.location.protocol !== "file:") { + const links = window.document.querySelectorAll("a"); + for (let i = 0; i < links.length; i++) { + if (links[i].href) { + links[i].href = links[i].href.replace(/\/index\.html/, "/"); + } + } + + // Fixup any sharing links that require urls + // Append url to any sharing urls + const sharingLinks = window.document.querySelectorAll( + "a.sidebar-tools-main-item" + ); + for (let i = 0; i < sharingLinks.length; i++) { + const sharingLink = sharingLinks[i]; + const href = sharingLink.getAttribute("href"); + if (href) { + sharingLink.setAttribute( + "href", + href.replace("|url|", window.location.href) + ); + } + } + + // Scroll the active navigation item into view, if necessary + const navSidebar = window.document.querySelector("nav#quarto-sidebar"); + if (navSidebar) { + // Find the active item + const activeItem = navSidebar.querySelector("li.sidebar-item a.active"); + if (activeItem) { + // Wait for the scroll height and height to resolve by observing size changes on the + // nav element that is scrollable + const resizeObserver = new ResizeObserver((_entries) => { + // The bottom of the element + const elBottom = activeItem.offsetTop; + const viewBottom = navSidebar.scrollTop + navSidebar.clientHeight; + + // The element height and scroll height are the same, then we are still loading + if (viewBottom !== navSidebar.scrollHeight) { + // Determine if the item isn't visible and scroll to it + if (elBottom >= viewBottom) { + navSidebar.scrollTop = elBottom; + } + + // stop observing now since we've completed the scroll + resizeObserver.unobserve(navSidebar); + } + }); + resizeObserver.observe(navSidebar); + } + } + } +}); diff --git a/site_libs/quarto-search/autocomplete.umd.js b/site_libs/quarto-search/autocomplete.umd.js new file mode 100644 index 00000000..619c57cc --- /dev/null +++ b/site_libs/quarto-search/autocomplete.umd.js @@ -0,0 +1,3 @@ +/*! @algolia/autocomplete-js 1.7.3 | MIT License | © Algolia, Inc. and contributors | https://github.com/algolia/autocomplete */ +!function(e,t){"object"==typeof exports&&"undefined"!=typeof module?t(exports):"function"==typeof define&&define.amd?define(["exports"],t):t((e="undefined"!=typeof globalThis?globalThis:e||self)["@algolia/autocomplete-js"]={})}(this,(function(e){"use strict";function t(e,t){var n=Object.keys(e);if(Object.getOwnPropertySymbols){var r=Object.getOwnPropertySymbols(e);t&&(r=r.filter((function(t){return Object.getOwnPropertyDescriptor(e,t).enumerable}))),n.push.apply(n,r)}return n}function n(e){for(var n=1;n=0||(o[n]=e[n]);return o}(e,t);if(Object.getOwnPropertySymbols){var i=Object.getOwnPropertySymbols(e);for(r=0;r=0||Object.prototype.propertyIsEnumerable.call(e,n)&&(o[n]=e[n])}return o}function a(e,t){return function(e){if(Array.isArray(e))return e}(e)||function(e,t){var n=null==e?null:"undefined"!=typeof Symbol&&e[Symbol.iterator]||e["@@iterator"];if(null==n)return;var r,o,i=[],u=!0,a=!1;try{for(n=n.call(e);!(u=(r=n.next()).done)&&(i.push(r.value),!t||i.length!==t);u=!0);}catch(e){a=!0,o=e}finally{try{u||null==n.return||n.return()}finally{if(a)throw o}}return i}(e,t)||l(e,t)||function(){throw new TypeError("Invalid attempt to destructure non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.")}()}function c(e){return function(e){if(Array.isArray(e))return s(e)}(e)||function(e){if("undefined"!=typeof Symbol&&null!=e[Symbol.iterator]||null!=e["@@iterator"])return Array.from(e)}(e)||l(e)||function(){throw new TypeError("Invalid attempt to spread non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.")}()}function l(e,t){if(e){if("string"==typeof e)return s(e,t);var n=Object.prototype.toString.call(e).slice(8,-1);return"Object"===n&&e.constructor&&(n=e.constructor.name),"Map"===n||"Set"===n?Array.from(e):"Arguments"===n||/^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(n)?s(e,t):void 0}}function s(e,t){(null==t||t>e.length)&&(t=e.length);for(var n=0,r=new Array(t);n=n?null===r?null:0:o}function S(e,t){var n=Object.keys(e);if(Object.getOwnPropertySymbols){var r=Object.getOwnPropertySymbols(e);t&&(r=r.filter((function(t){return Object.getOwnPropertyDescriptor(e,t).enumerable}))),n.push.apply(n,r)}return n}function I(e,t,n){return t in e?Object.defineProperty(e,t,{value:n,enumerable:!0,configurable:!0,writable:!0}):e[t]=n,e}function E(e,t){var n=[];return Promise.resolve(e(t)).then((function(e){return Promise.all(e.filter((function(e){return Boolean(e)})).map((function(e){if(e.sourceId,n.includes(e.sourceId))throw new Error("[Autocomplete] The `sourceId` ".concat(JSON.stringify(e.sourceId)," is not unique."));n.push(e.sourceId);var t=function(e){for(var t=1;te.length)&&(t=e.length);for(var n=0,r=new Array(t);ne.length)&&(t=e.length);for(var n=0,r=new Array(t);n=0||(o[n]=e[n]);return o}(e,t);if(Object.getOwnPropertySymbols){var i=Object.getOwnPropertySymbols(e);for(r=0;r=0||Object.prototype.propertyIsEnumerable.call(e,n)&&(o[n]=e[n])}return o}var ae,ce,le,se=null,pe=(ae=-1,ce=-1,le=void 0,function(e){var t=++ae;return Promise.resolve(e).then((function(e){return le&&t=0||(o[n]=e[n]);return o}(e,t);if(Object.getOwnPropertySymbols){var i=Object.getOwnPropertySymbols(e);for(r=0;r=0||Object.prototype.propertyIsEnumerable.call(e,n)&&(o[n]=e[n])}return o}var ye=["props","refresh","store"],be=["inputElement","formElement","panelElement"],Oe=["inputElement"],_e=["inputElement","maxLength"],Pe=["item","source"];function je(e,t){var n=Object.keys(e);if(Object.getOwnPropertySymbols){var r=Object.getOwnPropertySymbols(e);t&&(r=r.filter((function(t){return Object.getOwnPropertyDescriptor(e,t).enumerable}))),n.push.apply(n,r)}return n}function we(e){for(var t=1;t=0||(o[n]=e[n]);return o}(e,t);if(Object.getOwnPropertySymbols){var i=Object.getOwnPropertySymbols(e);for(r=0;r=0||Object.prototype.propertyIsEnumerable.call(e,n)&&(o[n]=e[n])}return o}function Ee(e){var t=e.props,n=e.refresh,r=e.store,o=Ie(e,ye);return{getEnvironmentProps:function(e){var n=e.inputElement,o=e.formElement,i=e.panelElement;function u(e){!r.getState().isOpen&&r.pendingRequests.isEmpty()||e.target===n||!1===[o,i].some((function(t){return n=t,r=e.target,n===r||n.contains(r);var n,r}))&&(r.dispatch("blur",null),t.debug||r.pendingRequests.cancelAll())}return we({onTouchStart:u,onMouseDown:u,onTouchMove:function(e){!1!==r.getState().isOpen&&n===t.environment.document.activeElement&&e.target!==n&&n.blur()}},Ie(e,be))},getRootProps:function(e){return we({role:"combobox","aria-expanded":r.getState().isOpen,"aria-haspopup":"listbox","aria-owns":r.getState().isOpen?"".concat(t.id,"-list"):void 0,"aria-labelledby":"".concat(t.id,"-label")},e)},getFormProps:function(e){return e.inputElement,we({action:"",noValidate:!0,role:"search",onSubmit:function(i){var u;i.preventDefault(),t.onSubmit(we({event:i,refresh:n,state:r.getState()},o)),r.dispatch("submit",null),null===(u=e.inputElement)||void 0===u||u.blur()},onReset:function(i){var u;i.preventDefault(),t.onReset(we({event:i,refresh:n,state:r.getState()},o)),r.dispatch("reset",null),null===(u=e.inputElement)||void 0===u||u.focus()}},Ie(e,Oe))},getLabelProps:function(e){return we({htmlFor:"".concat(t.id,"-input"),id:"".concat(t.id,"-label")},e)},getInputProps:function(e){var i;function u(e){(t.openOnFocus||Boolean(r.getState().query))&&fe(we({event:e,props:t,query:r.getState().completion||r.getState().query,refresh:n,store:r},o)),r.dispatch("focus",null)}var a=e||{};a.inputElement;var c=a.maxLength,l=void 0===c?512:c,s=Ie(a,_e),p=A(r.getState()),f=function(e){return Boolean(e&&e.match(C))}((null===(i=t.environment.navigator)||void 0===i?void 0:i.userAgent)||""),d=null!=p&&p.itemUrl&&!f?"go":"search";return we({"aria-autocomplete":"both","aria-activedescendant":r.getState().isOpen&&null!==r.getState().activeItemId?"".concat(t.id,"-item-").concat(r.getState().activeItemId):void 0,"aria-controls":r.getState().isOpen?"".concat(t.id,"-list"):void 0,"aria-labelledby":"".concat(t.id,"-label"),value:r.getState().completion||r.getState().query,id:"".concat(t.id,"-input"),autoComplete:"off",autoCorrect:"off",autoCapitalize:"off",enterKeyHint:d,spellCheck:"false",autoFocus:t.autoFocus,placeholder:t.placeholder,maxLength:l,type:"search",onChange:function(e){fe(we({event:e,props:t,query:e.currentTarget.value.slice(0,l),refresh:n,store:r},o))},onKeyDown:function(e){!function(e){var t=e.event,n=e.props,r=e.refresh,o=e.store,i=ge(e,de);if("ArrowUp"===t.key||"ArrowDown"===t.key){var u=function(){var e=n.environment.document.getElementById("".concat(n.id,"-item-").concat(o.getState().activeItemId));e&&(e.scrollIntoViewIfNeeded?e.scrollIntoViewIfNeeded(!1):e.scrollIntoView(!1))},a=function(){var e=A(o.getState());if(null!==o.getState().activeItemId&&e){var n=e.item,u=e.itemInputValue,a=e.itemUrl,c=e.source;c.onActive(ve({event:t,item:n,itemInputValue:u,itemUrl:a,refresh:r,source:c,state:o.getState()},i))}};t.preventDefault(),!1===o.getState().isOpen&&(n.openOnFocus||Boolean(o.getState().query))?fe(ve({event:t,props:n,query:o.getState().query,refresh:r,store:o},i)).then((function(){o.dispatch(t.key,{nextActiveItemId:n.defaultActiveItemId}),a(),setTimeout(u,0)})):(o.dispatch(t.key,{}),a(),u())}else if("Escape"===t.key)t.preventDefault(),o.dispatch(t.key,null),o.pendingRequests.cancelAll();else if("Tab"===t.key)o.dispatch("blur",null),o.pendingRequests.cancelAll();else if("Enter"===t.key){if(null===o.getState().activeItemId||o.getState().collections.every((function(e){return 0===e.items.length})))return void(n.debug||o.pendingRequests.cancelAll());t.preventDefault();var c=A(o.getState()),l=c.item,s=c.itemInputValue,p=c.itemUrl,f=c.source;if(t.metaKey||t.ctrlKey)void 0!==p&&(f.onSelect(ve({event:t,item:l,itemInputValue:s,itemUrl:p,refresh:r,source:f,state:o.getState()},i)),n.navigator.navigateNewTab({itemUrl:p,item:l,state:o.getState()}));else if(t.shiftKey)void 0!==p&&(f.onSelect(ve({event:t,item:l,itemInputValue:s,itemUrl:p,refresh:r,source:f,state:o.getState()},i)),n.navigator.navigateNewWindow({itemUrl:p,item:l,state:o.getState()}));else if(t.altKey);else{if(void 0!==p)return f.onSelect(ve({event:t,item:l,itemInputValue:s,itemUrl:p,refresh:r,source:f,state:o.getState()},i)),void n.navigator.navigate({itemUrl:p,item:l,state:o.getState()});fe(ve({event:t,nextState:{isOpen:!1},props:n,query:s,refresh:r,store:o},i)).then((function(){f.onSelect(ve({event:t,item:l,itemInputValue:s,itemUrl:p,refresh:r,source:f,state:o.getState()},i))}))}}}(we({event:e,props:t,refresh:n,store:r},o))},onFocus:u,onBlur:y,onClick:function(n){e.inputElement!==t.environment.document.activeElement||r.getState().isOpen||u(n)}},s)},getPanelProps:function(e){return we({onMouseDown:function(e){e.preventDefault()},onMouseLeave:function(){r.dispatch("mouseleave",null)}},e)},getListProps:function(e){return we({role:"listbox","aria-labelledby":"".concat(t.id,"-label"),id:"".concat(t.id,"-list")},e)},getItemProps:function(e){var i=e.item,u=e.source,a=Ie(e,Pe);return we({id:"".concat(t.id,"-item-").concat(i.__autocomplete_id),role:"option","aria-selected":r.getState().activeItemId===i.__autocomplete_id,onMouseMove:function(e){if(i.__autocomplete_id!==r.getState().activeItemId){r.dispatch("mousemove",i.__autocomplete_id);var t=A(r.getState());if(null!==r.getState().activeItemId&&t){var u=t.item,a=t.itemInputValue,c=t.itemUrl,l=t.source;l.onActive(we({event:e,item:u,itemInputValue:a,itemUrl:c,refresh:n,source:l,state:r.getState()},o))}}},onMouseDown:function(e){e.preventDefault()},onClick:function(e){var a=u.getItemInputValue({item:i,state:r.getState()}),c=u.getItemUrl({item:i,state:r.getState()});(c?Promise.resolve():fe(we({event:e,nextState:{isOpen:!1},props:t,query:a,refresh:n,store:r},o))).then((function(){u.onSelect(we({event:e,item:i,itemInputValue:a,itemUrl:c,refresh:n,source:u,state:r.getState()},o))}))}},a)}}}function Ae(e,t){var n=Object.keys(e);if(Object.getOwnPropertySymbols){var r=Object.getOwnPropertySymbols(e);t&&(r=r.filter((function(t){return Object.getOwnPropertyDescriptor(e,t).enumerable}))),n.push.apply(n,r)}return n}function Ce(e){for(var t=1;t0},reshape:function(e){return e.sources}},e),{},{id:null!==(n=e.id)&&void 0!==n?n:v(),plugins:o,initialState:H({activeItemId:null,query:"",completion:null,collections:[],isOpen:!1,status:"idle",context:{}},e.initialState),onStateChange:function(t){var n;null===(n=e.onStateChange)||void 0===n||n.call(e,t),o.forEach((function(e){var n;return null===(n=e.onStateChange)||void 0===n?void 0:n.call(e,t)}))},onSubmit:function(t){var n;null===(n=e.onSubmit)||void 0===n||n.call(e,t),o.forEach((function(e){var n;return null===(n=e.onSubmit)||void 0===n?void 0:n.call(e,t)}))},onReset:function(t){var n;null===(n=e.onReset)||void 0===n||n.call(e,t),o.forEach((function(e){var n;return null===(n=e.onReset)||void 0===n?void 0:n.call(e,t)}))},getSources:function(n){return Promise.all([].concat(F(o.map((function(e){return e.getSources}))),[e.getSources]).filter(Boolean).map((function(e){return E(e,n)}))).then((function(e){return d(e)})).then((function(e){return e.map((function(e){return H(H({},e),{},{onSelect:function(n){e.onSelect(n),t.forEach((function(e){var t;return null===(t=e.onSelect)||void 0===t?void 0:t.call(e,n)}))},onActive:function(n){e.onActive(n),t.forEach((function(e){var t;return null===(t=e.onActive)||void 0===t?void 0:t.call(e,n)}))}})}))}))},navigator:H({navigate:function(e){var t=e.itemUrl;r.location.assign(t)},navigateNewTab:function(e){var t=e.itemUrl,n=r.open(t,"_blank","noopener");null==n||n.focus()},navigateNewWindow:function(e){var t=e.itemUrl;r.open(t,"_blank","noopener")}},e.navigator)})}(e,t),r=R(Te,n,(function(e){var t=e.prevState,r=e.state;n.onStateChange(Be({prevState:t,state:r,refresh:u},o))})),o=function(e){var t=e.store;return{setActiveItemId:function(e){t.dispatch("setActiveItemId",e)},setQuery:function(e){t.dispatch("setQuery",e)},setCollections:function(e){var n=0,r=e.map((function(e){return L(L({},e),{},{items:d(e.items).map((function(e){return L(L({},e),{},{__autocomplete_id:n++})}))})}));t.dispatch("setCollections",r)},setIsOpen:function(e){t.dispatch("setIsOpen",e)},setStatus:function(e){t.dispatch("setStatus",e)},setContext:function(e){t.dispatch("setContext",e)}}}({store:r}),i=Ee(Be({props:n,refresh:u,store:r},o));function u(){return fe(Be({event:new Event("input"),nextState:{isOpen:r.getState().isOpen},props:n,query:r.getState().query,refresh:u,store:r},o))}return n.plugins.forEach((function(e){var n;return null===(n=e.subscribe)||void 0===n?void 0:n.call(e,Be(Be({},o),{},{refresh:u,onSelect:function(e){t.push({onSelect:e})},onActive:function(e){t.push({onActive:e})}}))})),function(e){var t,n,r=e.metadata,o=e.environment;if(null===(t=o.navigator)||void 0===t||null===(n=t.userAgent)||void 0===n?void 0:n.includes("Algolia Crawler")){var i=o.document.createElement("meta"),u=o.document.querySelector("head");i.name="algolia:metadata",setTimeout((function(){i.content=JSON.stringify(r),u.appendChild(i)}),0)}}({metadata:ke({plugins:n.plugins,options:e}),environment:n.environment}),Be(Be({refresh:u},i),o)}var Ue=function(e,t,n,r){var o;t[0]=0;for(var i=1;i=5&&((o||!e&&5===r)&&(u.push(r,0,o,n),r=6),e&&(u.push(r,e,0,n),r=6)),o=""},c=0;c"===t?(r=1,o=""):o=t+o[0]:i?t===i?i="":o+=t:'"'===t||"'"===t?i=t:">"===t?(a(),r=1):r&&("="===t?(r=5,n=o,o=""):"/"===t&&(r<5||">"===e[c][l+1])?(a(),3===r&&(u=u[0]),r=u,(u=u[0]).push(2,0,r),r=0):" "===t||"\t"===t||"\n"===t||"\r"===t?(a(),r=2):o+=t),3===r&&"!--"===o&&(r=4,u=u[0])}return a(),u}(e)),t),arguments,[])).length>1?t:t[0]}var We=function(e){var t=e.environment,n=t.document.createElementNS("http://www.w3.org/2000/svg","svg");n.setAttribute("class","aa-ClearIcon"),n.setAttribute("viewBox","0 0 24 24"),n.setAttribute("width","18"),n.setAttribute("height","18"),n.setAttribute("fill","currentColor");var r=t.document.createElementNS("http://www.w3.org/2000/svg","path");return r.setAttribute("d","M5.293 6.707l5.293 5.293-5.293 5.293c-0.391 0.391-0.391 1.024 0 1.414s1.024 0.391 1.414 0l5.293-5.293 5.293 5.293c0.391 0.391 1.024 0.391 1.414 0s0.391-1.024 0-1.414l-5.293-5.293 5.293-5.293c0.391-0.391 0.391-1.024 0-1.414s-1.024-0.391-1.414 0l-5.293 5.293-5.293-5.293c-0.391-0.391-1.024-0.391-1.414 0s-0.391 1.024 0 1.414z"),n.appendChild(r),n};function Qe(e,t){if("string"==typeof t){var n=e.document.querySelector(t);return"The element ".concat(JSON.stringify(t)," is not in the document."),n}return t}function $e(){for(var e=arguments.length,t=new Array(e),n=0;n2&&(u.children=arguments.length>3?lt.call(arguments,2):n),"function"==typeof e&&null!=e.defaultProps)for(i in e.defaultProps)void 0===u[i]&&(u[i]=e.defaultProps[i]);return _t(e,u,r,o,null)}function _t(e,t,n,r,o){var i={type:e,props:t,key:n,ref:r,__k:null,__:null,__b:0,__e:null,__d:void 0,__c:null,__h:null,constructor:void 0,__v:null==o?++pt:o};return null==o&&null!=st.vnode&&st.vnode(i),i}function Pt(e){return e.children}function jt(e,t){this.props=e,this.context=t}function wt(e,t){if(null==t)return e.__?wt(e.__,e.__.__k.indexOf(e)+1):null;for(var n;t0?_t(d.type,d.props,d.key,null,d.__v):d)){if(d.__=n,d.__b=n.__b+1,null===(f=g[s])||f&&d.key==f.key&&d.type===f.type)g[s]=void 0;else for(p=0;p0&&void 0!==arguments[0]?arguments[0]:[];return{get:function(){return e},add:function(t){var n=e[e.length-1];(null==n?void 0:n.isHighlighted)===t.isHighlighted?e[e.length-1]={value:n.value+t.value,isHighlighted:n.isHighlighted}:e.push(t)}}}(n?[{value:n,isHighlighted:!1}]:[]);return t.forEach((function(e){var t=e.split(Ht);r.add({value:t[0],isHighlighted:!0}),""!==t[1]&&r.add({value:t[1],isHighlighted:!1})})),r.get()}function Wt(e){return function(e){if(Array.isArray(e))return Qt(e)}(e)||function(e){if("undefined"!=typeof Symbol&&null!=e[Symbol.iterator]||null!=e["@@iterator"])return Array.from(e)}(e)||function(e,t){if(!e)return;if("string"==typeof e)return Qt(e,t);var n=Object.prototype.toString.call(e).slice(8,-1);"Object"===n&&e.constructor&&(n=e.constructor.name);if("Map"===n||"Set"===n)return Array.from(e);if("Arguments"===n||/^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(n))return Qt(e,t)}(e)||function(){throw new TypeError("Invalid attempt to spread non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.")}()}function Qt(e,t){(null==t||t>e.length)&&(t=e.length);for(var n=0,r=new Array(t);n",""":'"',"'":"'"},Gt=new RegExp(/\w/i),Kt=/&(amp|quot|lt|gt|#39);/g,Jt=RegExp(Kt.source);function Yt(e,t){var n,r,o,i=e[t],u=(null===(n=e[t+1])||void 0===n?void 0:n.isHighlighted)||!0,a=(null===(r=e[t-1])||void 0===r?void 0:r.isHighlighted)||!0;return Gt.test((o=i.value)&&Jt.test(o)?o.replace(Kt,(function(e){return zt[e]})):o)||a!==u?i.isHighlighted:a}function Xt(e,t){var n=Object.keys(e);if(Object.getOwnPropertySymbols){var r=Object.getOwnPropertySymbols(e);t&&(r=r.filter((function(t){return Object.getOwnPropertyDescriptor(e,t).enumerable}))),n.push.apply(n,r)}return n}function Zt(e){for(var t=1;te.length)&&(t=e.length);for(var n=0,r=new Array(t);n=0||(o[n]=e[n]);return o}(e,t);if(Object.getOwnPropertySymbols){var i=Object.getOwnPropertySymbols(e);for(r=0;r=0||Object.prototype.propertyIsEnumerable.call(e,n)&&(o[n]=e[n])}return o}function mn(e){return function(e){if(Array.isArray(e))return vn(e)}(e)||function(e){if("undefined"!=typeof Symbol&&null!=e[Symbol.iterator]||null!=e["@@iterator"])return Array.from(e)}(e)||function(e,t){if(!e)return;if("string"==typeof e)return vn(e,t);var n=Object.prototype.toString.call(e).slice(8,-1);"Object"===n&&e.constructor&&(n=e.constructor.name);if("Map"===n||"Set"===n)return Array.from(e);if("Arguments"===n||/^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(n))return vn(e,t)}(e)||function(){throw new TypeError("Invalid attempt to spread non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.")}()}function vn(e,t){(null==t||t>e.length)&&(t=e.length);for(var n=0,r=new Array(t);n0;if(!O.value.core.openOnFocus&&!t.query)return n;var r=Boolean(h.current||O.value.renderer.renderNoResults);return!n&&r||n},__autocomplete_metadata:{userAgents:Sn,options:e}}))})),j=p(n({collections:[],completion:null,context:{},isOpen:!1,query:"",activeItemId:null,status:"idle"},O.value.core.initialState)),w={getEnvironmentProps:O.value.renderer.getEnvironmentProps,getFormProps:O.value.renderer.getFormProps,getInputProps:O.value.renderer.getInputProps,getItemProps:O.value.renderer.getItemProps,getLabelProps:O.value.renderer.getLabelProps,getListProps:O.value.renderer.getListProps,getPanelProps:O.value.renderer.getPanelProps,getRootProps:O.value.renderer.getRootProps},S={setActiveItemId:P.value.setActiveItemId,setQuery:P.value.setQuery,setCollections:P.value.setCollections,setIsOpen:P.value.setIsOpen,setStatus:P.value.setStatus,setContext:P.value.setContext,refresh:P.value.refresh},I=d((function(){return Ve.bind(O.value.renderer.renderer.createElement)})),E=d((function(){return ct({autocomplete:P.value,autocompleteScopeApi:S,classNames:O.value.renderer.classNames,environment:O.value.core.environment,isDetached:_.value,placeholder:O.value.core.placeholder,propGetters:w,setIsModalOpen:k,state:j.current,translations:O.value.renderer.translations})}));function A(){tt(E.value.panel,{style:_.value?{}:wn({panelPlacement:O.value.renderer.panelPlacement,container:E.value.root,form:E.value.form,environment:O.value.core.environment})})}function C(e){j.current=e;var t={autocomplete:P.value,autocompleteScopeApi:S,classNames:O.value.renderer.classNames,components:O.value.renderer.components,container:O.value.renderer.container,html:I.value,dom:E.value,panelContainer:_.value?E.value.detachedContainer:O.value.renderer.panelContainer,propGetters:w,state:j.current,renderer:O.value.renderer.renderer},r=!g(e)&&!h.current&&O.value.renderer.renderNoResults||O.value.renderer.render;!function(e){var t=e.autocomplete,r=e.autocompleteScopeApi,o=e.dom,i=e.propGetters,u=e.state;nt(o.root,i.getRootProps(n({state:u,props:t.getRootProps({})},r))),nt(o.input,i.getInputProps(n({state:u,props:t.getInputProps({inputElement:o.input}),inputElement:o.input},r))),tt(o.label,{hidden:"stalled"===u.status}),tt(o.loadingIndicator,{hidden:"stalled"!==u.status}),tt(o.clearButton,{hidden:!u.query})}(t),function(e,t){var r=t.autocomplete,o=t.autocompleteScopeApi,u=t.classNames,a=t.html,c=t.dom,l=t.panelContainer,s=t.propGetters,p=t.state,f=t.components,d=t.renderer;if(p.isOpen){l.contains(c.panel)||"loading"===p.status||l.appendChild(c.panel),c.panel.classList.toggle("aa-Panel--stalled","stalled"===p.status);var m=p.collections.filter((function(e){var t=e.source,n=e.items;return t.templates.noResults||n.length>0})).map((function(e,t){var c=e.source,l=e.items;return d.createElement("section",{key:t,className:u.source,"data-autocomplete-source-id":c.sourceId},c.templates.header&&d.createElement("div",{className:u.sourceHeader},c.templates.header({components:f,createElement:d.createElement,Fragment:d.Fragment,items:l,source:c,state:p,html:a})),c.templates.noResults&&0===l.length?d.createElement("div",{className:u.sourceNoResults},c.templates.noResults({components:f,createElement:d.createElement,Fragment:d.Fragment,source:c,state:p,html:a})):d.createElement("ul",i({className:u.list},s.getListProps(n({state:p,props:r.getListProps({})},o))),l.map((function(e){var t=r.getItemProps({item:e,source:c});return d.createElement("li",i({key:t.id,className:u.item},s.getItemProps(n({state:p,props:t},o))),c.templates.item({components:f,createElement:d.createElement,Fragment:d.Fragment,item:e,state:p,html:a}))}))),c.templates.footer&&d.createElement("div",{className:u.sourceFooter},c.templates.footer({components:f,createElement:d.createElement,Fragment:d.Fragment,items:l,source:c,state:p,html:a})))})),v=d.createElement(d.Fragment,null,d.createElement("div",{className:u.panelLayout},m),d.createElement("div",{className:"aa-GradientBottom"})),h=m.reduce((function(e,t){return e[t.props["data-autocomplete-source-id"]]=t,e}),{});e(n(n({children:v,state:p,sections:m,elements:h},d),{},{components:f,html:a},o),c.panel)}else l.contains(c.panel)&&l.removeChild(c.panel)}(r,t)}function D(){var e=arguments.length>0&&void 0!==arguments[0]?arguments[0]:{};c();var t=O.value.renderer,n=t.components,r=u(t,In);y.current=Ge(r,O.value.core,{components:Ke(n,(function(e){return!e.value.hasOwnProperty("__autocomplete_componentName")})),initialState:j.current},e),m(),l(),P.value.refresh().then((function(){C(j.current)}))}function k(e){requestAnimationFrame((function(){var t=O.value.core.environment.document.body.contains(E.value.detachedOverlay);e!==t&&(e?(O.value.core.environment.document.body.appendChild(E.value.detachedOverlay),O.value.core.environment.document.body.classList.add("aa-Detached"),E.value.input.focus()):(O.value.core.environment.document.body.removeChild(E.value.detachedOverlay),O.value.core.environment.document.body.classList.remove("aa-Detached"),P.value.setQuery(""),P.value.refresh()))}))}return a((function(){var e=P.value.getEnvironmentProps({formElement:E.value.form,panelElement:E.value.panel,inputElement:E.value.input});return tt(O.value.core.environment,e),function(){tt(O.value.core.environment,Object.keys(e).reduce((function(e,t){return n(n({},e),{},o({},t,void 0))}),{}))}})),a((function(){var e=_.value?O.value.core.environment.document.body:O.value.renderer.panelContainer,t=_.value?E.value.detachedOverlay:E.value.panel;return _.value&&j.current.isOpen&&k(!0),C(j.current),function(){e.contains(t)&&e.removeChild(t)}})),a((function(){var e=O.value.renderer.container;return e.appendChild(E.value.root),function(){e.removeChild(E.value.root)}})),a((function(){var e=f((function(e){C(e.state)}),0);return b.current=function(t){var n=t.state,r=t.prevState;(_.value&&r.isOpen!==n.isOpen&&k(n.isOpen),_.value||!n.isOpen||r.isOpen||A(),n.query!==r.query)&&O.value.core.environment.document.querySelectorAll(".aa-Panel--scrollable").forEach((function(e){0!==e.scrollTop&&(e.scrollTop=0)}));e({state:n})},function(){b.current=void 0}})),a((function(){var e=f((function(){var e=_.value;_.value=O.value.core.environment.matchMedia(O.value.renderer.detachedMediaQuery).matches,e!==_.value?D({}):requestAnimationFrame(A)}),20);return O.value.core.environment.addEventListener("resize",e),function(){O.value.core.environment.removeEventListener("resize",e)}})),a((function(){if(!_.value)return function(){};function e(e){E.value.detachedContainer.classList.toggle("aa-DetachedContainer--modal",e)}function t(t){e(t.matches)}var n=O.value.core.environment.matchMedia(getComputedStyle(O.value.core.environment.document.documentElement).getPropertyValue("--aa-detached-modal-media-query"));e(n.matches);var r=Boolean(n.addEventListener);return r?n.addEventListener("change",t):n.addListener(t),function(){r?n.removeEventListener("change",t):n.removeListener(t)}})),a((function(){return requestAnimationFrame(A),function(){}})),n(n({},S),{},{update:D,destroy:function(){c()}})},e.getAlgoliaFacets=function(e){var t=En({transformResponse:function(e){return e.facetHits}}),r=e.queries.map((function(e){return n(n({},e),{},{type:"facet"})}));return t(n(n({},e),{},{queries:r}))},e.getAlgoliaResults=An,Object.defineProperty(e,"__esModule",{value:!0})})); + diff --git a/site_libs/quarto-search/fuse.min.js b/site_libs/quarto-search/fuse.min.js new file mode 100644 index 00000000..adc28356 --- /dev/null +++ b/site_libs/quarto-search/fuse.min.js @@ -0,0 +1,9 @@ +/** + * Fuse.js v6.6.2 - Lightweight fuzzy-search (http://fusejs.io) + * + * Copyright (c) 2022 Kiro Risk (http://kiro.me) + * All Rights Reserved. Apache Software License 2.0 + * + * http://www.apache.org/licenses/LICENSE-2.0 + */ +var e,t;e=this,t=function(){"use strict";function e(e,t){var n=Object.keys(e);if(Object.getOwnPropertySymbols){var r=Object.getOwnPropertySymbols(e);t&&(r=r.filter((function(t){return Object.getOwnPropertyDescriptor(e,t).enumerable}))),n.push.apply(n,r)}return n}function t(t){for(var n=1;ne.length)&&(t=e.length);for(var n=0,r=new Array(t);n0&&void 0!==arguments[0]?arguments[0]:1,t=arguments.length>1&&void 0!==arguments[1]?arguments[1]:3,n=new Map,r=Math.pow(10,t);return{get:function(t){var i=t.match(C).length;if(n.has(i))return n.get(i);var o=1/Math.pow(i,.5*e),c=parseFloat(Math.round(o*r)/r);return n.set(i,c),c},clear:function(){n.clear()}}}var $=function(){function e(){var t=arguments.length>0&&void 0!==arguments[0]?arguments[0]:{},n=t.getFn,i=void 0===n?I.getFn:n,o=t.fieldNormWeight,c=void 0===o?I.fieldNormWeight:o;r(this,e),this.norm=E(c,3),this.getFn=i,this.isCreated=!1,this.setIndexRecords()}return o(e,[{key:"setSources",value:function(){var e=arguments.length>0&&void 0!==arguments[0]?arguments[0]:[];this.docs=e}},{key:"setIndexRecords",value:function(){var e=arguments.length>0&&void 0!==arguments[0]?arguments[0]:[];this.records=e}},{key:"setKeys",value:function(){var e=this,t=arguments.length>0&&void 0!==arguments[0]?arguments[0]:[];this.keys=t,this._keysMap={},t.forEach((function(t,n){e._keysMap[t.id]=n}))}},{key:"create",value:function(){var e=this;!this.isCreated&&this.docs.length&&(this.isCreated=!0,g(this.docs[0])?this.docs.forEach((function(t,n){e._addString(t,n)})):this.docs.forEach((function(t,n){e._addObject(t,n)})),this.norm.clear())}},{key:"add",value:function(e){var t=this.size();g(e)?this._addString(e,t):this._addObject(e,t)}},{key:"removeAt",value:function(e){this.records.splice(e,1);for(var t=e,n=this.size();t2&&void 0!==arguments[2]?arguments[2]:{},r=n.getFn,i=void 0===r?I.getFn:r,o=n.fieldNormWeight,c=void 0===o?I.fieldNormWeight:o,a=new $({getFn:i,fieldNormWeight:c});return a.setKeys(e.map(_)),a.setSources(t),a.create(),a}function R(e){var t=arguments.length>1&&void 0!==arguments[1]?arguments[1]:{},n=t.errors,r=void 0===n?0:n,i=t.currentLocation,o=void 0===i?0:i,c=t.expectedLocation,a=void 0===c?0:c,s=t.distance,u=void 0===s?I.distance:s,h=t.ignoreLocation,l=void 0===h?I.ignoreLocation:h,f=r/e.length;if(l)return f;var d=Math.abs(a-o);return u?f+d/u:d?1:f}function N(){for(var e=arguments.length>0&&void 0!==arguments[0]?arguments[0]:[],t=arguments.length>1&&void 0!==arguments[1]?arguments[1]:I.minMatchCharLength,n=[],r=-1,i=-1,o=0,c=e.length;o=t&&n.push([r,i]),r=-1)}return e[o-1]&&o-r>=t&&n.push([r,o-1]),n}var P=32;function W(e){for(var t={},n=0,r=e.length;n1&&void 0!==arguments[1]?arguments[1]:{},o=i.location,c=void 0===o?I.location:o,a=i.threshold,s=void 0===a?I.threshold:a,u=i.distance,h=void 0===u?I.distance:u,l=i.includeMatches,f=void 0===l?I.includeMatches:l,d=i.findAllMatches,v=void 0===d?I.findAllMatches:d,g=i.minMatchCharLength,y=void 0===g?I.minMatchCharLength:g,p=i.isCaseSensitive,m=void 0===p?I.isCaseSensitive:p,k=i.ignoreLocation,M=void 0===k?I.ignoreLocation:k;if(r(this,e),this.options={location:c,threshold:s,distance:h,includeMatches:f,findAllMatches:v,minMatchCharLength:y,isCaseSensitive:m,ignoreLocation:M},this.pattern=m?t:t.toLowerCase(),this.chunks=[],this.pattern.length){var b=function(e,t){n.chunks.push({pattern:e,alphabet:W(e),startIndex:t})},x=this.pattern.length;if(x>P){for(var w=0,L=x%P,S=x-L;w3&&void 0!==arguments[3]?arguments[3]:{},i=r.location,o=void 0===i?I.location:i,c=r.distance,a=void 0===c?I.distance:c,s=r.threshold,u=void 0===s?I.threshold:s,h=r.findAllMatches,l=void 0===h?I.findAllMatches:h,f=r.minMatchCharLength,d=void 0===f?I.minMatchCharLength:f,v=r.includeMatches,g=void 0===v?I.includeMatches:v,y=r.ignoreLocation,p=void 0===y?I.ignoreLocation:y;if(t.length>P)throw new Error(w(P));for(var m,k=t.length,M=e.length,b=Math.max(0,Math.min(o,M)),x=u,L=b,S=d>1||g,_=S?Array(M):[];(m=e.indexOf(t,L))>-1;){var O=R(t,{currentLocation:m,expectedLocation:b,distance:a,ignoreLocation:p});if(x=Math.min(O,x),L=m+k,S)for(var j=0;j=z;q-=1){var B=q-1,J=n[e.charAt(B)];if(S&&(_[B]=+!!J),K[q]=(K[q+1]<<1|1)&J,F&&(K[q]|=(A[q+1]|A[q])<<1|1|A[q+1]),K[q]&$&&(C=R(t,{errors:F,currentLocation:B,expectedLocation:b,distance:a,ignoreLocation:p}))<=x){if(x=C,(L=B)<=b)break;z=Math.max(1,2*b-L)}}if(R(t,{errors:F+1,currentLocation:b,expectedLocation:b,distance:a,ignoreLocation:p})>x)break;A=K}var U={isMatch:L>=0,score:Math.max(.001,C)};if(S){var V=N(_,d);V.length?g&&(U.indices=V):U.isMatch=!1}return U}(e,n,i,{location:c+o,distance:a,threshold:s,findAllMatches:u,minMatchCharLength:h,includeMatches:r,ignoreLocation:l}),p=y.isMatch,m=y.score,k=y.indices;p&&(g=!0),v+=m,p&&k&&(d=[].concat(f(d),f(k)))}));var y={isMatch:g,score:g?v/this.chunks.length:1};return g&&r&&(y.indices=d),y}}]),e}(),z=function(){function e(t){r(this,e),this.pattern=t}return o(e,[{key:"search",value:function(){}}],[{key:"isMultiMatch",value:function(e){return D(e,this.multiRegex)}},{key:"isSingleMatch",value:function(e){return D(e,this.singleRegex)}}]),e}();function D(e,t){var n=e.match(t);return n?n[1]:null}var K=function(e){a(n,e);var t=l(n);function n(e){return r(this,n),t.call(this,e)}return o(n,[{key:"search",value:function(e){var t=e===this.pattern;return{isMatch:t,score:t?0:1,indices:[0,this.pattern.length-1]}}}],[{key:"type",get:function(){return"exact"}},{key:"multiRegex",get:function(){return/^="(.*)"$/}},{key:"singleRegex",get:function(){return/^=(.*)$/}}]),n}(z),q=function(e){a(n,e);var t=l(n);function n(e){return r(this,n),t.call(this,e)}return o(n,[{key:"search",value:function(e){var t=-1===e.indexOf(this.pattern);return{isMatch:t,score:t?0:1,indices:[0,e.length-1]}}}],[{key:"type",get:function(){return"inverse-exact"}},{key:"multiRegex",get:function(){return/^!"(.*)"$/}},{key:"singleRegex",get:function(){return/^!(.*)$/}}]),n}(z),B=function(e){a(n,e);var t=l(n);function n(e){return r(this,n),t.call(this,e)}return o(n,[{key:"search",value:function(e){var t=e.startsWith(this.pattern);return{isMatch:t,score:t?0:1,indices:[0,this.pattern.length-1]}}}],[{key:"type",get:function(){return"prefix-exact"}},{key:"multiRegex",get:function(){return/^\^"(.*)"$/}},{key:"singleRegex",get:function(){return/^\^(.*)$/}}]),n}(z),J=function(e){a(n,e);var t=l(n);function n(e){return r(this,n),t.call(this,e)}return o(n,[{key:"search",value:function(e){var t=!e.startsWith(this.pattern);return{isMatch:t,score:t?0:1,indices:[0,e.length-1]}}}],[{key:"type",get:function(){return"inverse-prefix-exact"}},{key:"multiRegex",get:function(){return/^!\^"(.*)"$/}},{key:"singleRegex",get:function(){return/^!\^(.*)$/}}]),n}(z),U=function(e){a(n,e);var t=l(n);function n(e){return r(this,n),t.call(this,e)}return o(n,[{key:"search",value:function(e){var t=e.endsWith(this.pattern);return{isMatch:t,score:t?0:1,indices:[e.length-this.pattern.length,e.length-1]}}}],[{key:"type",get:function(){return"suffix-exact"}},{key:"multiRegex",get:function(){return/^"(.*)"\$$/}},{key:"singleRegex",get:function(){return/^(.*)\$$/}}]),n}(z),V=function(e){a(n,e);var t=l(n);function n(e){return r(this,n),t.call(this,e)}return o(n,[{key:"search",value:function(e){var t=!e.endsWith(this.pattern);return{isMatch:t,score:t?0:1,indices:[0,e.length-1]}}}],[{key:"type",get:function(){return"inverse-suffix-exact"}},{key:"multiRegex",get:function(){return/^!"(.*)"\$$/}},{key:"singleRegex",get:function(){return/^!(.*)\$$/}}]),n}(z),G=function(e){a(n,e);var t=l(n);function n(e){var i,o=arguments.length>1&&void 0!==arguments[1]?arguments[1]:{},c=o.location,a=void 0===c?I.location:c,s=o.threshold,u=void 0===s?I.threshold:s,h=o.distance,l=void 0===h?I.distance:h,f=o.includeMatches,d=void 0===f?I.includeMatches:f,v=o.findAllMatches,g=void 0===v?I.findAllMatches:v,y=o.minMatchCharLength,p=void 0===y?I.minMatchCharLength:y,m=o.isCaseSensitive,k=void 0===m?I.isCaseSensitive:m,M=o.ignoreLocation,b=void 0===M?I.ignoreLocation:M;return r(this,n),(i=t.call(this,e))._bitapSearch=new T(e,{location:a,threshold:u,distance:l,includeMatches:d,findAllMatches:g,minMatchCharLength:p,isCaseSensitive:k,ignoreLocation:b}),i}return o(n,[{key:"search",value:function(e){return this._bitapSearch.searchIn(e)}}],[{key:"type",get:function(){return"fuzzy"}},{key:"multiRegex",get:function(){return/^"(.*)"$/}},{key:"singleRegex",get:function(){return/^(.*)$/}}]),n}(z),H=function(e){a(n,e);var t=l(n);function n(e){return r(this,n),t.call(this,e)}return o(n,[{key:"search",value:function(e){for(var t,n=0,r=[],i=this.pattern.length;(t=e.indexOf(this.pattern,n))>-1;)n=t+i,r.push([t,n-1]);var o=!!r.length;return{isMatch:o,score:o?0:1,indices:r}}}],[{key:"type",get:function(){return"include"}},{key:"multiRegex",get:function(){return/^'"(.*)"$/}},{key:"singleRegex",get:function(){return/^'(.*)$/}}]),n}(z),Q=[K,H,B,J,V,U,q,G],X=Q.length,Y=/ +(?=(?:[^\"]*\"[^\"]*\")*[^\"]*$)/;function Z(e){var t=arguments.length>1&&void 0!==arguments[1]?arguments[1]:{};return e.split("|").map((function(e){for(var n=e.trim().split(Y).filter((function(e){return e&&!!e.trim()})),r=[],i=0,o=n.length;i1&&void 0!==arguments[1]?arguments[1]:{},i=n.isCaseSensitive,o=void 0===i?I.isCaseSensitive:i,c=n.includeMatches,a=void 0===c?I.includeMatches:c,s=n.minMatchCharLength,u=void 0===s?I.minMatchCharLength:s,h=n.ignoreLocation,l=void 0===h?I.ignoreLocation:h,f=n.findAllMatches,d=void 0===f?I.findAllMatches:f,v=n.location,g=void 0===v?I.location:v,y=n.threshold,p=void 0===y?I.threshold:y,m=n.distance,k=void 0===m?I.distance:m;r(this,e),this.query=null,this.options={isCaseSensitive:o,includeMatches:a,minMatchCharLength:u,findAllMatches:d,ignoreLocation:l,location:g,threshold:p,distance:k},this.pattern=o?t:t.toLowerCase(),this.query=Z(this.pattern,this.options)}return o(e,[{key:"searchIn",value:function(e){var t=this.query;if(!t)return{isMatch:!1,score:1};var n=this.options,r=n.includeMatches;e=n.isCaseSensitive?e:e.toLowerCase();for(var i=0,o=[],c=0,a=0,s=t.length;a-1&&(n.refIndex=e.idx),t.matches.push(n)}}))}function ve(e,t){t.score=e.score}function ge(e,t){var n=arguments.length>2&&void 0!==arguments[2]?arguments[2]:{},r=n.includeMatches,i=void 0===r?I.includeMatches:r,o=n.includeScore,c=void 0===o?I.includeScore:o,a=[];return i&&a.push(de),c&&a.push(ve),e.map((function(e){var n=e.idx,r={item:t[n],refIndex:n};return a.length&&a.forEach((function(t){t(e,r)})),r}))}var ye=function(){function e(n){var i=arguments.length>1&&void 0!==arguments[1]?arguments[1]:{},o=arguments.length>2?arguments[2]:void 0;r(this,e),this.options=t(t({},I),i),this.options.useExtendedSearch,this._keyStore=new S(this.options.keys),this.setCollection(n,o)}return o(e,[{key:"setCollection",value:function(e,t){if(this._docs=e,t&&!(t instanceof $))throw new Error("Incorrect 'index' type");this._myIndex=t||F(this.options.keys,this._docs,{getFn:this.options.getFn,fieldNormWeight:this.options.fieldNormWeight})}},{key:"add",value:function(e){k(e)&&(this._docs.push(e),this._myIndex.add(e))}},{key:"remove",value:function(){for(var e=arguments.length>0&&void 0!==arguments[0]?arguments[0]:function(){return!1},t=[],n=0,r=this._docs.length;n1&&void 0!==arguments[1]?arguments[1]:{},n=t.limit,r=void 0===n?-1:n,i=this.options,o=i.includeMatches,c=i.includeScore,a=i.shouldSort,s=i.sortFn,u=i.ignoreFieldNorm,h=g(e)?g(this._docs[0])?this._searchStringList(e):this._searchObjectList(e):this._searchLogical(e);return fe(h,{ignoreFieldNorm:u}),a&&h.sort(s),y(r)&&r>-1&&(h=h.slice(0,r)),ge(h,this._docs,{includeMatches:o,includeScore:c})}},{key:"_searchStringList",value:function(e){var t=re(e,this.options),n=this._myIndex.records,r=[];return n.forEach((function(e){var n=e.v,i=e.i,o=e.n;if(k(n)){var c=t.searchIn(n),a=c.isMatch,s=c.score,u=c.indices;a&&r.push({item:n,idx:i,matches:[{score:s,value:n,norm:o,indices:u}]})}})),r}},{key:"_searchLogical",value:function(e){var t=this,n=function(e,t){var n=(arguments.length>2&&void 0!==arguments[2]?arguments[2]:{}).auto,r=void 0===n||n,i=function e(n){var i=Object.keys(n),o=ue(n);if(!o&&i.length>1&&!se(n))return e(le(n));if(he(n)){var c=o?n[ce]:i[0],a=o?n[ae]:n[c];if(!g(a))throw new Error(x(c));var s={keyId:j(c),pattern:a};return r&&(s.searcher=re(a,t)),s}var u={children:[],operator:i[0]};return i.forEach((function(t){var r=n[t];v(r)&&r.forEach((function(t){u.children.push(e(t))}))})),u};return se(e)||(e=le(e)),i(e)}(e,this.options),r=function e(n,r,i){if(!n.children){var o=n.keyId,c=n.searcher,a=t._findMatches({key:t._keyStore.get(o),value:t._myIndex.getValueForItemAtKeyId(r,o),searcher:c});return a&&a.length?[{idx:i,item:r,matches:a}]:[]}for(var s=[],u=0,h=n.children.length;u1&&void 0!==arguments[1]?arguments[1]:{},n=t.getFn,r=void 0===n?I.getFn:n,i=t.fieldNormWeight,o=void 0===i?I.fieldNormWeight:i,c=e.keys,a=e.records,s=new $({getFn:r,fieldNormWeight:o});return s.setKeys(c),s.setIndexRecords(a),s},ye.config=I,function(){ne.push.apply(ne,arguments)}(te),ye},"object"==typeof exports&&"undefined"!=typeof module?module.exports=t():"function"==typeof define&&define.amd?define(t):(e="undefined"!=typeof globalThis?globalThis:e||self).Fuse=t(); \ No newline at end of file diff --git a/site_libs/quarto-search/quarto-search.js b/site_libs/quarto-search/quarto-search.js new file mode 100644 index 00000000..f5d852d1 --- /dev/null +++ b/site_libs/quarto-search/quarto-search.js @@ -0,0 +1,1140 @@ +const kQueryArg = "q"; +const kResultsArg = "show-results"; + +// If items don't provide a URL, then both the navigator and the onSelect +// function aren't called (and therefore, the default implementation is used) +// +// We're using this sentinel URL to signal to those handlers that this +// item is a more item (along with the type) and can be handled appropriately +const kItemTypeMoreHref = "0767FDFD-0422-4E5A-BC8A-3BE11E5BBA05"; + +window.document.addEventListener("DOMContentLoaded", function (_event) { + // Ensure that search is available on this page. If it isn't, + // should return early and not do anything + var searchEl = window.document.getElementById("quarto-search"); + if (!searchEl) return; + + const { autocomplete } = window["@algolia/autocomplete-js"]; + + let quartoSearchOptions = {}; + let language = {}; + const searchOptionEl = window.document.getElementById( + "quarto-search-options" + ); + if (searchOptionEl) { + const jsonStr = searchOptionEl.textContent; + quartoSearchOptions = JSON.parse(jsonStr); + language = quartoSearchOptions.language; + } + + // note the search mode + if (quartoSearchOptions.type === "overlay") { + searchEl.classList.add("type-overlay"); + } else { + searchEl.classList.add("type-textbox"); + } + + // Used to determine highlighting behavior for this page + // A `q` query param is expected when the user follows a search + // to this page + const currentUrl = new URL(window.location); + const query = currentUrl.searchParams.get(kQueryArg); + const showSearchResults = currentUrl.searchParams.get(kResultsArg); + const mainEl = window.document.querySelector("main"); + + // highlight matches on the page + if (query !== null && mainEl) { + // perform any highlighting + highlight(escapeRegExp(query), mainEl); + + // fix up the URL to remove the q query param + const replacementUrl = new URL(window.location); + replacementUrl.searchParams.delete(kQueryArg); + window.history.replaceState({}, "", replacementUrl); + } + + // function to clear highlighting on the page when the search query changes + // (e.g. if the user edits the query or clears it) + let highlighting = true; + const resetHighlighting = (searchTerm) => { + if (mainEl && highlighting && query !== null && searchTerm !== query) { + clearHighlight(query, mainEl); + highlighting = false; + } + }; + + // Clear search highlighting when the user scrolls sufficiently + const resetFn = () => { + resetHighlighting(""); + window.removeEventListener("quarto-hrChanged", resetFn); + window.removeEventListener("quarto-sectionChanged", resetFn); + }; + + // Register this event after the initial scrolling and settling of events + // on the page + window.addEventListener("quarto-hrChanged", resetFn); + window.addEventListener("quarto-sectionChanged", resetFn); + + // Responsively switch to overlay mode if the search is present on the navbar + // Note that switching the sidebar to overlay mode requires more coordinate (not just + // the media query since we generate different HTML for sidebar overlays than we do + // for sidebar input UI) + const detachedMediaQuery = + quartoSearchOptions.type === "overlay" ? "all" : "(max-width: 991px)"; + + // If configured, include the analytics client to send insights + const plugins = configurePlugins(quartoSearchOptions); + + let lastState = null; + const { setIsOpen, setQuery, setCollections } = autocomplete({ + container: searchEl, + detachedMediaQuery: detachedMediaQuery, + defaultActiveItemId: 0, + panelContainer: "#quarto-search-results", + panelPlacement: quartoSearchOptions["panel-placement"], + debug: false, + openOnFocus: true, + plugins, + classNames: { + form: "d-flex", + }, + translations: { + clearButtonTitle: language["search-clear-button-title"], + detachedCancelButtonText: language["search-detached-cancel-button-title"], + submitButtonTitle: language["search-submit-button-title"], + }, + initialState: { + query, + }, + getItemUrl({ item }) { + return item.href; + }, + onStateChange({ state }) { + // Perhaps reset highlighting + resetHighlighting(state.query); + + // If the panel just opened, ensure the panel is positioned properly + if (state.isOpen) { + if (lastState && !lastState.isOpen) { + setTimeout(() => { + positionPanel(quartoSearchOptions["panel-placement"]); + }, 150); + } + } + + // Perhaps show the copy link + showCopyLink(state.query, quartoSearchOptions); + + lastState = state; + }, + reshape({ sources, state }) { + return sources.map((source) => { + try { + const items = source.getItems(); + + // Validate the items + validateItems(items); + + // group the items by document + const groupedItems = new Map(); + items.forEach((item) => { + const hrefParts = item.href.split("#"); + const baseHref = hrefParts[0]; + const isDocumentItem = hrefParts.length === 1; + + const items = groupedItems.get(baseHref); + if (!items) { + groupedItems.set(baseHref, [item]); + } else { + // If the href for this item matches the document + // exactly, place this item first as it is the item that represents + // the document itself + if (isDocumentItem) { + items.unshift(item); + } else { + items.push(item); + } + groupedItems.set(baseHref, items); + } + }); + + const reshapedItems = []; + let count = 1; + for (const [_key, value] of groupedItems) { + const firstItem = value[0]; + reshapedItems.push({ + ...firstItem, + type: kItemTypeDoc, + }); + + const collapseMatches = quartoSearchOptions["collapse-after"]; + const collapseCount = + typeof collapseMatches === "number" ? collapseMatches : 1; + + if (value.length > 1) { + const target = `search-more-${count}`; + const isExpanded = + state.context.expanded && + state.context.expanded.includes(target); + + const remainingCount = value.length - collapseCount; + + for (let i = 1; i < value.length; i++) { + if (collapseMatches && i === collapseCount) { + reshapedItems.push({ + target, + title: isExpanded + ? language["search-hide-matches-text"] + : remainingCount === 1 + ? `${remainingCount} ${language["search-more-match-text"]}` + : `${remainingCount} ${language["search-more-matches-text"]}`, + type: kItemTypeMore, + href: kItemTypeMoreHref, + }); + } + + if (isExpanded || !collapseMatches || i < collapseCount) { + reshapedItems.push({ + ...value[i], + type: kItemTypeItem, + target, + }); + } + } + } + count += 1; + } + + return { + ...source, + getItems() { + return reshapedItems; + }, + }; + } catch (error) { + // Some form of error occurred + return { + ...source, + getItems() { + return [ + { + title: error.name || "An Error Occurred While Searching", + text: + error.message || + "An unknown error occurred while attempting to perform the requested search.", + type: kItemTypeError, + }, + ]; + }, + }; + } + }); + }, + navigator: { + navigate({ itemUrl }) { + if (itemUrl !== offsetURL(kItemTypeMoreHref)) { + window.location.assign(itemUrl); + } + }, + navigateNewTab({ itemUrl }) { + if (itemUrl !== offsetURL(kItemTypeMoreHref)) { + const windowReference = window.open(itemUrl, "_blank", "noopener"); + if (windowReference) { + windowReference.focus(); + } + } + }, + navigateNewWindow({ itemUrl }) { + if (itemUrl !== offsetURL(kItemTypeMoreHref)) { + window.open(itemUrl, "_blank", "noopener"); + } + }, + }, + getSources({ state, setContext, setActiveItemId, refresh }) { + return [ + { + sourceId: "documents", + getItemUrl({ item }) { + if (item.href) { + return offsetURL(item.href); + } else { + return undefined; + } + }, + onSelect({ + item, + state, + setContext, + setIsOpen, + setActiveItemId, + refresh, + }) { + if (item.type === kItemTypeMore) { + toggleExpanded(item, state, setContext, setActiveItemId, refresh); + + // Toggle more + setIsOpen(true); + } + }, + getItems({ query }) { + if (query === null || query === "") { + return []; + } + + const limit = quartoSearchOptions.limit; + if (quartoSearchOptions.algolia) { + return algoliaSearch(query, limit, quartoSearchOptions.algolia); + } else { + // Fuse search options + const fuseSearchOptions = { + isCaseSensitive: false, + shouldSort: true, + minMatchCharLength: 2, + limit: limit, + }; + + return readSearchData().then(function (fuse) { + return fuseSearch(query, fuse, fuseSearchOptions); + }); + } + }, + templates: { + noResults({ createElement }) { + const hasQuery = lastState.query; + + return createElement( + "div", + { + class: `quarto-search-no-results${ + hasQuery ? "" : " no-query" + }`, + }, + language["search-no-results-text"] + ); + }, + header({ items, createElement }) { + // count the documents + const count = items.filter((item) => { + return item.type === kItemTypeDoc; + }).length; + + if (count > 0) { + return createElement( + "div", + { class: "search-result-header" }, + `${count} ${language["search-matching-documents-text"]}` + ); + } else { + return createElement( + "div", + { class: "search-result-header-no-results" }, + `` + ); + } + }, + footer({ _items, createElement }) { + if ( + quartoSearchOptions.algolia && + quartoSearchOptions.algolia["show-logo"] + ) { + const libDir = quartoSearchOptions.algolia["libDir"]; + const logo = createElement("img", { + src: offsetURL( + `${libDir}/quarto-search/search-by-algolia.svg` + ), + class: "algolia-search-logo", + }); + return createElement( + "a", + { href: "http://www.algolia.com/" }, + logo + ); + } + }, + + item({ item, createElement }) { + return renderItem( + item, + createElement, + state, + setActiveItemId, + setContext, + refresh + ); + }, + }, + }, + ]; + }, + }); + + window.quartoOpenSearch = () => { + setIsOpen(false); + setIsOpen(true); + focusSearchInput(); + }; + + // Remove the labeleledby attribute since it is pointing + // to a non-existent label + if (quartoSearchOptions.type === "overlay") { + const inputEl = window.document.querySelector( + "#quarto-search .aa-Autocomplete" + ); + if (inputEl) { + inputEl.removeAttribute("aria-labelledby"); + } + } + + // If the main document scrolls dismiss the search results + // (otherwise, since they're floating in the document they can scroll with the document) + window.document.body.onscroll = () => { + setIsOpen(false); + }; + + if (showSearchResults) { + setIsOpen(true); + focusSearchInput(); + } +}); + +function configurePlugins(quartoSearchOptions) { + const autocompletePlugins = []; + const algoliaOptions = quartoSearchOptions.algolia; + if ( + algoliaOptions && + algoliaOptions["analytics-events"] && + algoliaOptions["search-only-api-key"] && + algoliaOptions["application-id"] + ) { + const apiKey = algoliaOptions["search-only-api-key"]; + const appId = algoliaOptions["application-id"]; + + // Aloglia insights may not be loaded because they require cookie consent + // Use deferred loading so events will start being recorded when/if consent + // is granted. + const algoliaInsightsDeferredPlugin = deferredLoadPlugin(() => { + if ( + window.aa && + window["@algolia/autocomplete-plugin-algolia-insights"] + ) { + window.aa("init", { + appId, + apiKey, + useCookie: true, + }); + + const { createAlgoliaInsightsPlugin } = + window["@algolia/autocomplete-plugin-algolia-insights"]; + // Register the insights client + const algoliaInsightsPlugin = createAlgoliaInsightsPlugin({ + insightsClient: window.aa, + onItemsChange({ insights, insightsEvents }) { + const events = insightsEvents.map((event) => { + const maxEvents = event.objectIDs.slice(0, 20); + return { + ...event, + objectIDs: maxEvents, + }; + }); + + insights.viewedObjectIDs(...events); + }, + }); + return algoliaInsightsPlugin; + } + }); + + // Add the plugin + autocompletePlugins.push(algoliaInsightsDeferredPlugin); + return autocompletePlugins; + } +} + +// For plugins that may not load immediately, create a wrapper +// plugin and forward events and plugin data once the plugin +// is initialized. This is useful for cases like cookie consent +// which may prevent the analytics insights event plugin from initializing +// immediately. +function deferredLoadPlugin(createPlugin) { + let plugin = undefined; + let subscribeObj = undefined; + const wrappedPlugin = () => { + if (!plugin && subscribeObj) { + plugin = createPlugin(); + if (plugin && plugin.subscribe) { + plugin.subscribe(subscribeObj); + } + } + return plugin; + }; + + return { + subscribe: (obj) => { + subscribeObj = obj; + }, + onStateChange: (obj) => { + const plugin = wrappedPlugin(); + if (plugin && plugin.onStateChange) { + plugin.onStateChange(obj); + } + }, + onSubmit: (obj) => { + const plugin = wrappedPlugin(); + if (plugin && plugin.onSubmit) { + plugin.onSubmit(obj); + } + }, + onReset: (obj) => { + const plugin = wrappedPlugin(); + if (plugin && plugin.onReset) { + plugin.onReset(obj); + } + }, + getSources: (obj) => { + const plugin = wrappedPlugin(); + if (plugin && plugin.getSources) { + return plugin.getSources(obj); + } else { + return Promise.resolve([]); + } + }, + data: (obj) => { + const plugin = wrappedPlugin(); + if (plugin && plugin.data) { + plugin.data(obj); + } + }, + }; +} + +function validateItems(items) { + // Validate the first item + if (items.length > 0) { + const item = items[0]; + const missingFields = []; + if (item.href == undefined) { + missingFields.push("href"); + } + if (!item.title == undefined) { + missingFields.push("title"); + } + if (!item.text == undefined) { + missingFields.push("text"); + } + + if (missingFields.length === 1) { + throw { + name: `Error: Search index is missing the ${missingFields[0]} field.`, + message: `The items being returned for this search do not include all the required fields. Please ensure that your index items include the ${missingFields[0]} field or use index-fields in your _quarto.yml file to specify the field names.`, + }; + } else if (missingFields.length > 1) { + const missingFieldList = missingFields + .map((field) => { + return `${field}`; + }) + .join(", "); + + throw { + name: `Error: Search index is missing the following fields: ${missingFieldList}.`, + message: `The items being returned for this search do not include all the required fields. Please ensure that your index items includes the following fields: ${missingFieldList}, or use index-fields in your _quarto.yml file to specify the field names.`, + }; + } + } +} + +let lastQuery = null; +function showCopyLink(query, options) { + const language = options.language; + lastQuery = query; + // Insert share icon + const inputSuffixEl = window.document.body.querySelector( + ".aa-Form .aa-InputWrapperSuffix" + ); + + if (inputSuffixEl) { + let copyButtonEl = window.document.body.querySelector( + ".aa-Form .aa-InputWrapperSuffix .aa-CopyButton" + ); + + if (copyButtonEl === null) { + copyButtonEl = window.document.createElement("button"); + copyButtonEl.setAttribute("class", "aa-CopyButton"); + copyButtonEl.setAttribute("type", "button"); + copyButtonEl.setAttribute("title", language["search-copy-link-title"]); + copyButtonEl.onmousedown = (e) => { + e.preventDefault(); + e.stopPropagation(); + }; + + const linkIcon = "bi-clipboard"; + const checkIcon = "bi-check2"; + + const shareIconEl = window.document.createElement("i"); + shareIconEl.setAttribute("class", `bi ${linkIcon}`); + copyButtonEl.appendChild(shareIconEl); + inputSuffixEl.prepend(copyButtonEl); + + const clipboard = new window.ClipboardJS(".aa-CopyButton", { + text: function (_trigger) { + const copyUrl = new URL(window.location); + copyUrl.searchParams.set(kQueryArg, lastQuery); + copyUrl.searchParams.set(kResultsArg, "1"); + return copyUrl.toString(); + }, + }); + clipboard.on("success", function (e) { + // Focus the input + + // button target + const button = e.trigger; + const icon = button.querySelector("i.bi"); + + // flash "checked" + icon.classList.add(checkIcon); + icon.classList.remove(linkIcon); + setTimeout(function () { + icon.classList.remove(checkIcon); + icon.classList.add(linkIcon); + }, 1000); + }); + } + + // If there is a query, show the link icon + if (copyButtonEl) { + if (lastQuery && options["copy-button"]) { + copyButtonEl.style.display = "flex"; + } else { + copyButtonEl.style.display = "none"; + } + } + } +} + +/* Search Index Handling */ +// create the index +var fuseIndex = undefined; +async function readSearchData() { + // Initialize the search index on demand + if (fuseIndex === undefined) { + // create fuse index + const options = { + keys: [ + { name: "title", weight: 20 }, + { name: "section", weight: 20 }, + { name: "text", weight: 10 }, + ], + ignoreLocation: true, + threshold: 0.1, + }; + const fuse = new window.Fuse([], options); + + // fetch the main search.json + const response = await fetch(offsetURL("search.json")); + if (response.status == 200) { + return response.json().then(function (searchDocs) { + searchDocs.forEach(function (searchDoc) { + fuse.add(searchDoc); + }); + fuseIndex = fuse; + return fuseIndex; + }); + } else { + return Promise.reject( + new Error( + "Unexpected status from search index request: " + response.status + ) + ); + } + } + return fuseIndex; +} + +function inputElement() { + return window.document.body.querySelector(".aa-Form .aa-Input"); +} + +function focusSearchInput() { + setTimeout(() => { + const inputEl = inputElement(); + if (inputEl) { + inputEl.focus(); + } + }, 50); +} + +/* Panels */ +const kItemTypeDoc = "document"; +const kItemTypeMore = "document-more"; +const kItemTypeItem = "document-item"; +const kItemTypeError = "error"; + +function renderItem( + item, + createElement, + state, + setActiveItemId, + setContext, + refresh +) { + switch (item.type) { + case kItemTypeDoc: + return createDocumentCard( + createElement, + "file-richtext", + item.title, + item.section, + item.text, + item.href + ); + case kItemTypeMore: + return createMoreCard( + createElement, + item, + state, + setActiveItemId, + setContext, + refresh + ); + case kItemTypeItem: + return createSectionCard( + createElement, + item.section, + item.text, + item.href + ); + case kItemTypeError: + return createErrorCard(createElement, item.title, item.text); + default: + return undefined; + } +} + +function createDocumentCard(createElement, icon, title, section, text, href) { + const iconEl = createElement("i", { + class: `bi bi-${icon} search-result-icon`, + }); + const titleEl = createElement("p", { class: "search-result-title" }, title); + const titleContainerEl = createElement( + "div", + { class: "search-result-title-container" }, + [iconEl, titleEl] + ); + + const textEls = []; + if (section) { + const sectionEl = createElement( + "p", + { class: "search-result-section" }, + section + ); + textEls.push(sectionEl); + } + const descEl = createElement("p", { + class: "search-result-text", + dangerouslySetInnerHTML: { + __html: text, + }, + }); + textEls.push(descEl); + + const textContainerEl = createElement( + "div", + { class: "search-result-text-container" }, + textEls + ); + + const containerEl = createElement( + "div", + { + class: "search-result-container", + }, + [titleContainerEl, textContainerEl] + ); + + const linkEl = createElement( + "a", + { + href: offsetURL(href), + class: "search-result-link", + }, + containerEl + ); + + const classes = ["search-result-doc", "search-item"]; + if (!section) { + classes.push("document-selectable"); + } + + return createElement( + "div", + { + class: classes.join(" "), + }, + linkEl + ); +} + +function createMoreCard( + createElement, + item, + state, + setActiveItemId, + setContext, + refresh +) { + const moreCardEl = createElement( + "div", + { + class: "search-result-more search-item", + onClick: (e) => { + // Handle expanding the sections by adding the expanded + // section to the list of expanded sections + toggleExpanded(item, state, setContext, setActiveItemId, refresh); + e.stopPropagation(); + }, + }, + item.title + ); + + return moreCardEl; +} + +function toggleExpanded(item, state, setContext, setActiveItemId, refresh) { + const expanded = state.context.expanded || []; + if (expanded.includes(item.target)) { + setContext({ + expanded: expanded.filter((target) => target !== item.target), + }); + } else { + setContext({ expanded: [...expanded, item.target] }); + } + + refresh(); + setActiveItemId(item.__autocomplete_id); +} + +function createSectionCard(createElement, section, text, href) { + const sectionEl = createSection(createElement, section, text, href); + return createElement( + "div", + { + class: "search-result-doc-section search-item", + }, + sectionEl + ); +} + +function createSection(createElement, title, text, href) { + const descEl = createElement("p", { + class: "search-result-text", + dangerouslySetInnerHTML: { + __html: text, + }, + }); + + const titleEl = createElement("p", { class: "search-result-section" }, title); + const linkEl = createElement( + "a", + { + href: offsetURL(href), + class: "search-result-link", + }, + [titleEl, descEl] + ); + return linkEl; +} + +function createErrorCard(createElement, title, text) { + const descEl = createElement("p", { + class: "search-error-text", + dangerouslySetInnerHTML: { + __html: text, + }, + }); + + const titleEl = createElement("p", { + class: "search-error-title", + dangerouslySetInnerHTML: { + __html: ` ${title}`, + }, + }); + const errorEl = createElement("div", { class: "search-error" }, [ + titleEl, + descEl, + ]); + return errorEl; +} + +function positionPanel(pos) { + const panelEl = window.document.querySelector( + "#quarto-search-results .aa-Panel" + ); + const inputEl = window.document.querySelector( + "#quarto-search .aa-Autocomplete" + ); + + if (panelEl && inputEl) { + panelEl.style.top = `${Math.round(panelEl.offsetTop)}px`; + if (pos === "start") { + panelEl.style.left = `${Math.round(inputEl.left)}px`; + } else { + panelEl.style.right = `${Math.round(inputEl.offsetRight)}px`; + } + } +} + +/* Highlighting */ +// highlighting functions +function highlightMatch(query, text) { + if (text) { + const start = text.toLowerCase().indexOf(query.toLowerCase()); + if (start !== -1) { + const startMark = ""; + const endMark = ""; + + const end = start + query.length; + text = + text.slice(0, start) + + startMark + + text.slice(start, end) + + endMark + + text.slice(end); + const startInfo = clipStart(text, start); + const endInfo = clipEnd( + text, + startInfo.position + startMark.length + endMark.length + ); + text = + startInfo.prefix + + text.slice(startInfo.position, endInfo.position) + + endInfo.suffix; + + return text; + } else { + return text; + } + } else { + return text; + } +} + +function clipStart(text, pos) { + const clipStart = pos - 50; + if (clipStart < 0) { + // This will just return the start of the string + return { + position: 0, + prefix: "", + }; + } else { + // We're clipping before the start of the string, walk backwards to the first space. + const spacePos = findSpace(text, pos, -1); + return { + position: spacePos.position, + prefix: "", + }; + } +} + +function clipEnd(text, pos) { + const clipEnd = pos + 200; + if (clipEnd > text.length) { + return { + position: text.length, + suffix: "", + }; + } else { + const spacePos = findSpace(text, clipEnd, 1); + return { + position: spacePos.position, + suffix: spacePos.clipped ? "…" : "", + }; + } +} + +function findSpace(text, start, step) { + let stepPos = start; + while (stepPos > -1 && stepPos < text.length) { + const char = text[stepPos]; + if (char === " " || char === "," || char === ":") { + return { + position: step === 1 ? stepPos : stepPos - step, + clipped: stepPos > 1 && stepPos < text.length, + }; + } + stepPos = stepPos + step; + } + + return { + position: stepPos - step, + clipped: false, + }; +} + +// removes highlighting as implemented by the mark tag +function clearHighlight(searchterm, el) { + const childNodes = el.childNodes; + for (let i = childNodes.length - 1; i >= 0; i--) { + const node = childNodes[i]; + if (node.nodeType === Node.ELEMENT_NODE) { + if ( + node.tagName === "MARK" && + node.innerText.toLowerCase() === searchterm.toLowerCase() + ) { + el.replaceChild(document.createTextNode(node.innerText), node); + } else { + clearHighlight(searchterm, node); + } + } + } +} + +function escapeRegExp(string) { + return string.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); // $& means the whole matched string +} + +// highlight matches +function highlight(term, el) { + const termRegex = new RegExp(term, "ig"); + const childNodes = el.childNodes; + + // walk back to front avoid mutating elements in front of us + for (let i = childNodes.length - 1; i >= 0; i--) { + const node = childNodes[i]; + + if (node.nodeType === Node.TEXT_NODE) { + // Search text nodes for text to highlight + const text = node.nodeValue; + + let startIndex = 0; + let matchIndex = text.search(termRegex); + if (matchIndex > -1) { + const markFragment = document.createDocumentFragment(); + while (matchIndex > -1) { + const prefix = text.slice(startIndex, matchIndex); + markFragment.appendChild(document.createTextNode(prefix)); + + const mark = document.createElement("mark"); + mark.appendChild( + document.createTextNode( + text.slice(matchIndex, matchIndex + term.length) + ) + ); + markFragment.appendChild(mark); + + startIndex = matchIndex + term.length; + matchIndex = text.slice(startIndex).search(new RegExp(term, "ig")); + if (matchIndex > -1) { + matchIndex = startIndex + matchIndex; + } + } + if (startIndex < text.length) { + markFragment.appendChild( + document.createTextNode(text.slice(startIndex, text.length)) + ); + } + + el.replaceChild(markFragment, node); + } + } else if (node.nodeType === Node.ELEMENT_NODE) { + // recurse through elements + highlight(term, node); + } + } +} + +/* Link Handling */ +// get the offset from this page for a given site root relative url +function offsetURL(url) { + var offset = getMeta("quarto:offset"); + return offset ? offset + url : url; +} + +// read a meta tag value +function getMeta(metaName) { + var metas = window.document.getElementsByTagName("meta"); + for (let i = 0; i < metas.length; i++) { + if (metas[i].getAttribute("name") === metaName) { + return metas[i].getAttribute("content"); + } + } + return ""; +} + +function algoliaSearch(query, limit, algoliaOptions) { + const { getAlgoliaResults } = window["@algolia/autocomplete-preset-algolia"]; + + const applicationId = algoliaOptions["application-id"]; + const searchOnlyApiKey = algoliaOptions["search-only-api-key"]; + const indexName = algoliaOptions["index-name"]; + const indexFields = algoliaOptions["index-fields"]; + const searchClient = window.algoliasearch(applicationId, searchOnlyApiKey); + const searchParams = algoliaOptions["params"]; + const searchAnalytics = !!algoliaOptions["analytics-events"]; + + return getAlgoliaResults({ + searchClient, + queries: [ + { + indexName: indexName, + query, + params: { + hitsPerPage: limit, + clickAnalytics: searchAnalytics, + ...searchParams, + }, + }, + ], + transformResponse: (response) => { + if (!indexFields) { + return response.hits.map((hit) => { + return hit.map((item) => { + return { + ...item, + text: highlightMatch(query, item.text), + }; + }); + }); + } else { + const remappedHits = response.hits.map((hit) => { + return hit.map((item) => { + const newItem = { ...item }; + ["href", "section", "title", "text"].forEach((keyName) => { + const mappedName = indexFields[keyName]; + if ( + mappedName && + item[mappedName] !== undefined && + mappedName !== keyName + ) { + newItem[keyName] = item[mappedName]; + delete newItem[mappedName]; + } + }); + newItem.text = highlightMatch(query, newItem.text); + return newItem; + }); + }); + return remappedHits; + } + }, + }); +} + +function fuseSearch(query, fuse, fuseOptions) { + return fuse.search(query, fuseOptions).map((result) => { + const addParam = (url, name, value) => { + const anchorParts = url.split("#"); + const baseUrl = anchorParts[0]; + const sep = baseUrl.search("\\?") > 0 ? "&" : "?"; + anchorParts[0] = baseUrl + sep + name + "=" + value; + return anchorParts.join("#"); + }; + + return { + title: result.item.title, + section: result.item.section, + href: addParam(result.item.href, kQueryArg, query), + text: highlightMatch(query, result.item.text), + }; + }); +} diff --git a/sitemap.xml b/sitemap.xml new file mode 100644 index 00000000..f6bf60f6 --- /dev/null +++ b/sitemap.xml @@ -0,0 +1,103 @@ + + + + https://Nixtla.github.io/forecast.html + 2023-09-29T20:00:49.974Z + + + https://Nixtla.github.io/docs/getting-started/quick_start_local.html + 2023-09-29T20:00:46.258Z + + + https://Nixtla.github.io/docs/getting-started/end_to_end_walkthrough.html + 2023-09-29T20:00:45.322Z + + + https://Nixtla.github.io/docs/tutorials/prediction_intervals_in_forecasting_models.html + 2023-09-29T20:00:42.170Z + + + https://Nixtla.github.io/docs/how-to-guides/transfer_learning.html + 2023-09-29T20:00:39.438Z + + + https://Nixtla.github.io/docs/how-to-guides/prediction_intervals.html + 2023-09-29T20:00:38.274Z + + + https://Nixtla.github.io/distributed.models.dask.lgb.html + 2023-09-29T20:00:36.918Z + + + https://Nixtla.github.io/utils.html + 2023-09-29T20:00:34.642Z + + + https://Nixtla.github.io/core.html + 2023-09-29T20:00:32.482Z + + + https://Nixtla.github.io/grouped_array.html + 2023-09-29T20:00:31.418Z + + + https://Nixtla.github.io/distributed.models.spark.lgb.html + 2023-09-29T20:00:30.390Z + + + https://Nixtla.github.io/distributed.models.dask.xgb.html + 2023-09-29T20:00:29.650Z + + + https://Nixtla.github.io/target_transforms.html + 2023-09-29T20:00:26.782Z + + + https://Nixtla.github.io/distributed.models.ray.xgb.html + 2023-09-29T20:00:28.594Z + + + https://Nixtla.github.io/distributed.models.spark.xgb.html + 2023-09-29T20:00:30.062Z + + + https://Nixtla.github.io/index.html + 2023-09-29T20:00:31.050Z + + + https://Nixtla.github.io/distributed.models.ray.lgb.html + 2023-09-29T20:00:31.762Z + + + https://Nixtla.github.io/lgb_cv.html + 2023-09-29T20:00:34.014Z + + + https://Nixtla.github.io/distributed.forecast.html + 2023-09-29T20:00:36.574Z + + + https://Nixtla.github.io/docs/how-to-guides/target_transforms_guide.html + 2023-09-29T20:00:37.390Z + + + https://Nixtla.github.io/docs/how-to-guides/cross_validation.html + 2023-09-29T20:00:38.946Z + + + https://Nixtla.github.io/docs/tutorials/electricity_peak_forecasting.html + 2023-09-29T20:00:40.110Z + + + https://Nixtla.github.io/docs/tutorials/electricity_load_forecasting.html + 2023-09-29T20:00:43.722Z + + + https://Nixtla.github.io/docs/getting-started/install.html + 2023-09-29T20:00:45.734Z + + + https://Nixtla.github.io/docs/getting-started/quick_start_distributed.html + 2023-09-29T20:00:46.822Z + + diff --git a/styles.css b/styles.css new file mode 100644 index 00000000..54a9c7c9 --- /dev/null +++ b/styles.css @@ -0,0 +1,57 @@ +.cell { + margin-bottom: 1rem; +} + +.cell > .sourceCode { + margin-bottom: 0; +} + +.cell-output > pre { + margin-bottom: 0; +} + +.cell-output > pre, .cell-output > .sourceCode > pre, .cell-output-stdout > pre { + margin-left: 0.8rem; + margin-top: 0; + background: none; + border-left: 2px solid lightsalmon; + border-top-left-radius: 0; + border-top-right-radius: 0; +} + +.cell-output > .sourceCode { + border: none; +} + +.cell-output > .sourceCode { + background: none; + margin-top: 0; +} + +div.description { + padding-left: 2px; + padding-top: 5px; + font-style: italic; + font-size: 135%; + opacity: 70%; +} + +/* show_doc signature */ +blockquote > pre { + font-size: 14px; +} + +.table { + font-size: 16px; + /* disable striped tables */ + --bs-table-striped-bg: var(--bs-table-bg); +} + +.quarto-figure-center > figure > figcaption { + text-align: center; +} + +.figure-caption { + font-size: 75%; + font-style: italic; +} diff --git a/target_transforms.html b/target_transforms.html new file mode 100644 index 00000000..32152d80 --- /dev/null +++ b/target_transforms.html @@ -0,0 +1,831 @@ + + + + + + + + + +mlforecast - Target transforms + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+ + +
+ +
+ + +
+ + + +
+ +
+
+

Target transforms

+
+ + + +
+ + + + +
+ + +
+ + +
+
import pandas as pd
+from fastcore.test import test_fail
+from sklearn.linear_model import LinearRegression
+from sklearn.preprocessing import PowerTransformer
+
+from mlforecast import MLForecast
+from mlforecast.utils import generate_daily_series
+
+
+
+

BaseTargetTransform

+
+
 BaseTargetTransform ()
+
+

Base class used for target transformations.

+
+
+
+

Differences

+
+
 Differences (differences:Iterable[int])
+
+

Subtracts previous values of the serie. Can be used to remove trend or seasonalities.

+
+
series = generate_daily_series(10, min_length=50, max_length=100)
+
+
+
diffs = Differences([1, 2, 5])
+diffs.set_column_names('unique_id', 'ds', 'y')
+
+# differences are applied correctly
+transformed = diffs.fit_transform(series)
+assert diffs.fitted_ == []
+expected = series.copy()
+for d in diffs.differences:
+    expected['y'] -= expected.groupby('unique_id')['y'].shift(d)
+pd.testing.assert_frame_equal(transformed, expected)
+
+# fitted differences are restored correctly
+diffs.store_fitted = True
+transformed = diffs.fit_transform(series)
+keep_mask = transformed['y'].notnull()
+restored = diffs.inverse_transform_fitted(
+    transformed,
+    transformed.groupby('unique_id', observed=True).size().values,
+)
+pd.testing.assert_frame_equal(series[keep_mask], restored[keep_mask])
+restored_subs = diffs.inverse_transform_fitted(
+    transformed[keep_mask],
+    transformed[keep_mask].groupby('unique_id', observed=True).size().values,
+)
+pd.testing.assert_frame_equal(series[keep_mask], restored_subs)
+
+# short series
+test_fail(lambda: diffs.fit_transform(series.head(2)), contains="too short for the differences: ['id_0']")
+
+
+
+
+

LocalStandardScaler

+
+
 LocalStandardScaler ()
+
+

Standardizes each serie by subtracting its mean and dividing by its standard deviation.

+
+
sc = LocalStandardScaler()
+sc.set_column_names('unique_id', 'ds', 'y')
+sizes = series.groupby('unique_id', observed=True).size().values
+transformed = sc.fit_transform(series)
+pd.testing.assert_frame_equal(
+    sc.inverse_transform_fitted(transformed, sizes),
+    series,
+)
+
+def filter_df(df):
+    return (
+        df[df['unique_id'].isin(['id_0', 'id_7'])]
+        .groupby('unique_id', observed=True)
+        .head(10)
+    )
+
+subset = filter_df(series)
+transformed_subset = filter_df(transformed)
+sc.idxs = [0, 7]
+pd.testing.assert_frame_equal(
+    sc.inverse_transform(transformed_subset),
+    subset
+)
+
+
+
+
+

GlobalSklearnTransformer

+
+
 GlobalSklearnTransformer (transformer:sklearn.base.TransformerMixin)
+
+

Applies the same scikit-learn transformer to all series.

+
+
sk_boxcox = PowerTransformer(method='box-cox', standardize=False)
+boxcox_global = GlobalSklearnTransformer(sk_boxcox)
+single_difference = Differences([1])
+series = generate_daily_series(10)
+fcst = MLForecast(
+    models=[LinearRegression()],
+    freq='D',
+    lags=[1, 2],
+    target_transforms=[boxcox_global, single_difference]
+)
+prep = fcst.preprocess(series, dropna=False)
+expected = (
+    pd.Series(
+        sk_boxcox.fit_transform(series[['y']])[:, 0], index=series['unique_id']
+    ).groupby('unique_id')
+    .diff()
+    .values
+)
+np.testing.assert_allclose(prep['y'].values, expected)
+
+ + +
+ +

Give us a ⭐ on Github

+ +
+ + + + \ No newline at end of file diff --git a/utils.html b/utils.html new file mode 100644 index 00000000..9084ce0f --- /dev/null +++ b/utils.html @@ -0,0 +1,1117 @@ + + + + + + + + + +mlforecast - Utils + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+ + +
+ +
+ + +
+ + + +
+ +
+
+

Utils

+
+ + + +
+ + + + +
+ + +
+ + +
+
from fastcore.test import test_eq, test_fail, test_warns
+
+
+
+

generate_daily_series

+
+
 generate_daily_series (n_series:int, min_length:int=50,
+                        max_length:int=500, n_static_features:int=0,
+                        equal_ends:bool=False,
+                        static_as_categorical:bool=True,
+                        with_trend:bool=False, seed:int=0)
+
+

Generates n_series of different lengths in the interval [min_length, max_length].

+

If 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.

+

Generate 20 series with lengths between 100 and 1,000.

+
+
n_series = 20
+min_length = 100
+max_length = 1000
+
+series = generate_daily_series(n_series, min_length, max_length)
+series
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
unique_iddsy
0id_002000-01-010.395863
1id_002000-01-021.264447
2id_002000-01-032.284022
3id_002000-01-043.462798
4id_002000-01-054.035518
............
12446id_192002-03-110.309275
12447id_192002-03-121.189464
12448id_192002-03-132.325032
12449id_192002-03-143.333198
12450id_192002-03-154.306117
+ +

12451 rows × 3 columns

+
+
+
+

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.

+
+
n_static_features = 2
+
+series_with_statics = generate_daily_series(n_series, min_length, max_length, n_static_features)
+series_with_statics
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
unique_iddsystatic_0static_1
0id_002000-01-017.5213881810
1id_002000-01-0224.0245021810
2id_002000-01-0343.3964231810
3id_002000-01-0465.7931681810
4id_002000-01-0576.6748431810
..................
12446id_192002-03-1127.8347718942
12447id_192002-03-12107.0517468942
12448id_192002-03-13209.2528458942
12449id_192002-03-14299.9878018942
12450id_192002-03-15387.5505368942
+ +

12451 rows × 5 columns

+
+
+
+
+
for i in range(n_static_features):
+    assert all(series_with_statics.groupby('unique_id')[f'static_{i}'].nunique() == 1)
+
+

If equal_ends=False (the default) then every serie has a different end date.

+
+
assert series_with_statics.groupby('unique_id')['ds'].max().nunique() > 1
+
+

We can have all of them end at the same date by specifying equal_ends=True.

+
+
series_equal_ends = generate_daily_series(n_series, min_length, max_length, equal_ends=True)
+
+assert series_equal_ends.groupby('unique_id')['ds'].max().nunique() == 1
+
+
+
+
+

generate_prices_for_series

+
+
 generate_prices_for_series (series:pandas.core.frame.DataFrame,
+                             horizon:int=7, seed:int=0)
+
+
+
series_for_prices = generate_daily_series(20, n_static_features=2, equal_ends=True)
+series_for_prices.rename(columns={'static_1': 'product_id'}, inplace=True)
+prices_catalog = generate_prices_for_series(series_for_prices, horizon=7)
+prices_catalog
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
dsunique_idprice
02000-10-05id_000.548814
12000-10-06id_000.715189
22000-10-07id_000.602763
32000-10-08id_000.544883
42000-10-09id_000.423655
............
50092001-05-17id_190.288027
50102001-05-18id_190.846305
50112001-05-19id_190.791284
50122001-05-20id_190.578636
50132001-05-21id_190.288589
+ +

5014 rows × 3 columns

+
+
+
+
+
test_eq(set(prices_catalog['unique_id']), set(series_for_prices['unique_id']))
+test_fail(lambda: generate_prices_for_series(series), contains='equal ends')
+
+
+
+
+

backtest_splits

+
+
 backtest_splits (df:pandas.core.frame.DataFrame, n_windows:int, h:int,
+                  id_col:str, time_col:str,
+                  freq:Union[pandas._libs.tslibs.offsets.BaseOffset,int],
+                  step_size:Optional[int]=None,
+                  input_size:Optional[int]=None)
+
+
+
+
+

PredictionIntervals

+
+
 PredictionIntervals (n_windows:int=2, h:int=1,
+                      method:str='conformal_distribution',
+                      window_size:Optional[int]=None)
+
+

Class for storing prediction intervals metadata information.

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
TypeDefaultDetails
n_windowsint2
hint1
methodstrconformal_distribution
window_sizetyping.Optional[int]Nonenoqa: ARG002
+ + +
+ +

Give us a ⭐ on Github

+ +
+ + + + \ No newline at end of file