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..add3f45e --- /dev/null +++ b/core.html @@ -0,0 +1,1135 @@ + + + + + + + + + +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 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-010.7404532753
1id_002000-01-023.5952622753
2id_002000-01-036.8958352753
3id_002000-01-048.4994502753
4id_002000-01-0511.3219812753
..................
4869id_192000-03-2540.0606819745
4870id_192000-03-2653.8794829745
4871id_192000-03-2762.0202109745
4872id_192000-03-282.0625439745
4873id_192000-03-2914.1513179745
+ +

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-010.7404532753
1id_002000-01-023.5952622753
2id_002000-01-036.8958352753
3id_002000-01-048.4994502753
4id_002000-01-0511.3219812753
..................
217id_002000-08-051.3263192753
218id_002000-08-063.8231982753
219id_002000-08-075.9555182753
220id_002000-08-088.6986372753
221id_002000-08-0911.9254812753
+ +

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,
+             differences:Optional[Iterable[int]]=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..f6bbd5b3 --- /dev/null +++ b/distributed.forecast.html @@ -0,0 +1,2477 @@ + + + + + + + + + + +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,
+                        differences:Optional[Iterable[int]]=None,
+                        num_threads:int=1, target_transforms:Optional[List
+                        [mlforecast.target_transforms.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..836ef9db --- /dev/null +++ b/distributed.models.dask.lgb.html @@ -0,0 +1,662 @@ + + + + + + + + + + +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..028b8fa2 --- /dev/null +++ b/distributed.models.dask.xgb.html @@ -0,0 +1,945 @@ + + + + + + + + + + +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:int=100, verbosity:Optional[int]=None, obje
+                  ctive:Union[str,Callable[[numpy.ndarray,numpy.ndarray],T
+                  uple[numpy.ndarray,numpy.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[int,
+                  numpy.random.mtrand.RandomState,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,
+                  gpu_id:Optional[int]=None,
+                  validate_parameters:Optional[bool]=None,
+                  predictor:Optional[str]=None,
+                  enable_categorical:bool=False,
+                  feature_types:Sequence[str]=None,
+                  max_cat_to_onehot:Optional[int]=None,
+                  max_cat_threshold:Optional[int]=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.

+ ++++++ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
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_estimatorsint100Number 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 gpu_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[int, numpy.random.mtrand.RandomState, 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
gpu_idtyping.Optional[int]NoneDevice ordinal.
validate_parameterstyping.Optional[bool]NoneGive warnings for unknown parameter.
predictortyping.Optional[str]NoneForce XGBoost to use specific predictor, available choices are [cpu_predictor,
gpu_predictor].
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.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.
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.

The method returns the model from the last iteration (not the best one). 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.

If early stopping occurs, the model will have three additional fields:
:py:attr:best_score, :py:attr:best_iteration and
:py:attr:best_ntree_limit.

.. 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)]
xgboost.train(params, Xy, callbacks=callbacks)
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..b4e94f53 --- /dev/null +++ b/distributed.models.ray.lgb.html @@ -0,0 +1,660 @@ + + + + + + + + + + +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..55ae3e61 --- /dev/null +++ b/distributed.models.ray.xgb.html @@ -0,0 +1,681 @@ + + + + + + + + + + +mlforecast - RayXGBForecast + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+ + +
+ +
+ + +
+ + + +
+ +
+
+

RayXGBForecast

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

+ ++++++ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
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..20ba9ad3 --- /dev/null +++ b/distributed.models.spark.lgb.html @@ -0,0 +1,643 @@ + + + + + + + + + + +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..366e62d1 --- /dev/null +++ b/distributed.models.spark.xgb.html @@ -0,0 +1,653 @@ + + + + + + + + + + +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 Examples
+  else: warn(msg)
+
+
+

SparkXGBForecast

+
+
 SparkXGBForecast (**kwargs)
+
+

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 xgboost.XGBRegressor constructor and most of the parameters used in :py:class:xgboost.XGBRegressor fit and predict method.

+

SparkXGBRegressor doesn’t support setting gpu_id but support another param use_gpu, see doc below for more details.

+

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.

+

callbacks: The export and import of the callback functions are at best effort. For details, see :py:attr:xgboost.spark.SparkXGBRegressor.callbacks param doc. validation_indicator_col For params related to xgboost.XGBRegressor training with evaluation dataset’s supervision, set :py:attr:xgboost.spark.SparkXGBRegressor.validation_indicator_col parameter instead of setting the eval_set parameter in xgboost.XGBRegressor fit method. weight_col: To specify the weight of the training and validation dataset, set :py:attr:xgboost.spark.SparkXGBRegressor.weight_col parameter instead of setting sample_weight and sample_weight_eval_set parameter in xgboost.XGBRegressor fit method. xgb_model: Set the value to be the instance returned by :func:xgboost.spark.SparkXGBRegressorModel.get_booster. num_workers: Integer that specifies the number of XGBoost workers to use. Each XGBoost worker corresponds to one spark task. use_gpu: Boolean that specifies whether the executors are running on GPU instances. base_margin_col: To specify the base margins of the training and validation dataset, set :py:attr:xgboost.spark.SparkXGBRegressor.base_margin_col parameter instead of setting base_margin and base_margin_eval_set in the xgboost.XGBRegressor fit method. Note: this isn’t available for distributed training.

+

.. Note:: The Parameters chart above contains parameters that need special handling. For a full list of parameters, see entries with Param(parent=... below.

+

.. Note:: This API is experimental.

+ + +
+ +

Give us a ⭐ on Github

+ +
+ + + + \ No newline at end of file diff --git a/docs/cross_validation.html b/docs/cross_validation.html new file mode 100644 index 00000000..3b0192bb --- /dev/null +++ b/docs/cross_validation.html @@ -0,0 +1,977 @@ + + + + + + + + + + +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
+
+
+
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.

+
+
import pandas as pd 
+
+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.

+
+
import matplotlib.pyplot as plt
+
+def plot(df, fname, last_n=24 * 14):
+    fig, ax = plt.subplots(nrows=2, ncols=2, figsize=(14, 6), gridspec_kw=dict(hspace=0.5))
+    uids = df['unique_id'].unique()
+    for i, (uid, axi) in enumerate(zip(uids, ax.flat)):
+        legend = i % 2 == 0
+        df[df['unique_id'].eq(uid)].tail(last_n).set_index('ds').plot(ax=axi, title=uid, legend=legend)
+    fig.savefig(fname, bbox_inches='tight')
+    plt.close()
+
+
+
plot(Y_df, '../figs/cross_validation__series.png')
+
+

+
+
+

Train model

+

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()]
+
+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:

+
    +
  • data: training data frame with MLForecast format
  • +
  • window_size (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(
+    data=Y_df,
+    window_size=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.

+
+
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 first need to install datasetsforecast, a Python library developed by Nixtla that includes a function to compute the RMSE.

+
+
pip install datasetsforecast
+
+
+
from datasetsforecast.losses import rmse
+
+

The function to compute the RMSE takes two arguments:

+
    +
  1. The actual values.
    +
  2. +
  3. The forecasts, in this case, LGBMRegressor.
  4. +
+

In this case we will compute the rmse per time series and cutoff and then we will take the mean of the results.

+
+
cv_rmse = crossvalidation_df.groupby(['unique_id', 'cutoff']).apply(lambda df: rmse(df['y'], df['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/electricity_peak_forecasting.html b/docs/electricity_peak_forecasting.html new file mode 100644 index 00000000..1b84c732 --- /dev/null +++ b/docs/electricity_peak_forecasting.html @@ -0,0 +1,937 @@ + + + + + + + + + + +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
+
+
+
# 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'")
+
+
+
Y_df.plot(x='ds', y='y', figsize=(20, 7))
+
+
<AxesSubplot: xlabel='ds'>
+
+
+

+
+
+

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 LighGBM 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() # 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/electricity_peak_forecasting_files/figure-html/cell-4-output-2.png b/docs/electricity_peak_forecasting_files/figure-html/cell-4-output-2.png new file mode 100644 index 00000000..956d31a0 Binary files /dev/null and b/docs/electricity_peak_forecasting_files/figure-html/cell-4-output-2.png differ diff --git a/docs/end_to_end_walkthrough.html b/docs/end_to_end_walkthrough.html new file mode 100644 index 00000000..684b590e --- /dev/null +++ b/docs/end_to_end_walkthrough.html @@ -0,0 +1,2202 @@ + + + + + + + + + + +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
+
+
+
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.

+
+
import matplotlib.pyplot as plt
+
+def plot(df, fname, last_n=24 * 14):
+    fig, ax = plt.subplots(nrows=2, ncols=2, figsize=(14, 6), gridspec_kw=dict(hspace=0.5))
+    uids = df['unique_id'].unique()
+    for i, (uid, axi) in enumerate(zip(uids, ax.flat)):
+        legend = i % 2 == 0
+        df[df['unique_id'].eq(uid)].tail(last_n).set_index('ds').plot(ax=axi, title=uid, legend=legend)
+    fig.savefig(fname, bbox_inches='tight')
+    plt.close()
+
+
+
plot(df, 'figs/end_to_end_walkthrough__eda.png')
+
+

+

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.

+
+
plot(prep, 'figs/end_to_end_walkthrough__differences.png')
+
+

+
+
+

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 like the following:

+
+
from mlforecast.target_transforms import BaseTargetTransform
+
+class StandardScaler(BaseTargetTransform):
+    """Standardizes the series by subtracting their mean and dividing by their standard deviation."""
+    def fit_transform(self, df: pd.DataFrame) -> pd.DataFrame:
+        self.norm_ = df.groupby(self.id_col)[self.target_col].agg(['mean', 'std'])
+        df = df.merge(self.norm_, on=self.id_col)
+        df[self.target_col] = (df[self.target_col] - df['mean']) / df['std']
+        df = df.drop(columns=['mean', 'std'])
+        return df
+
+    def inverse_transform(self, df: pd.DataFrame) -> pd.DataFrame:
+        df = df.merge(self.norm_, on=self.id_col)
+        for col in df.columns.drop([self.id_col, self.time_col, 'mean', 'std']):
+            df[col] = df[col] * df['std'] + df['mean']
+        df = df.drop(columns=['std', 'mean'])
+        return df
+
+
+
fcst = MLForecast(
+    models=[],
+    freq=1,
+    lags=[1],
+    target_transforms=[StandardScaler()]
+)
+fcst.preprocess(df)
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
unique_iddsylag1
1H1962-1.492285-1.382600
2H1963-1.574549-1.492285
3H1964-1.656813-1.574549
4H1965-1.711656-1.656813
5H1966-1.793919-1.711656
...............
4027H41310043.0612462.423809
4028H41310052.5218763.061246
4029H41310060.5114972.521876
4030H41310070.2172950.511497
4031H4131008-0.1259410.217295
+ +

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=[StandardScaler()]
+)
+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

+
+
+
+
+
import pandas as pd
+
+
+
plot(pd.concat([df, preds]), 'figs/end_to_end_walkthrough__predictions.png', last_n=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

+
+
+
+
+
plot(cv_result.drop(columns='cutoff'), 'figs/end_to_end_walkthrough__cv.png')
+
+

+

We can compute the RMSE on each split.

+
+
def evaluate_cv(df):
+    return df['y'].sub(df['LGBMRegressor']).pow(2).groupby(df['cutoff']).mean().pow(0.5)
+
+split_rmse = evaluate_cv(cv_result)
+split_rmse
+
+
cutoff
+816    29.418172
+864    34.257598
+912    13.145763
+960    35.066261
+dtype: float64
+
+
+

And the average RMSE across splits.

+
+
split_rmse.mean()
+
+
27.971948676289266
+
+
+

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()
+
+
25.87444576840234
+
+
+
+
+

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

+
+
+
+
+
plot(cv.cv_preds_.drop(columns='window'), 'figs/end_to_end_walkthrough__lgbcv.png')
+
+

+

+

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)
+plot(pd.concat([df, preds]), 'figs/end_to_end_walkthrough__final_forecast.png')
+
+

+ + +
+
+ +

Give us a ⭐ on Github

+ +
+ + + + \ No newline at end of file diff --git a/docs/install.html b/docs/install.html new file mode 100644 index 00000000..9f0cd733 --- /dev/null +++ b/docs/install.html @@ -0,0 +1,700 @@ + + + + + + + + + + +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
  • +
+
+
+

Distributed training

+

If you want to perform distributed training you have to include the dask extra:

+

pip install "mlforecast[dask]"

+

and also either LightGBM or XGBoost.

+
+
+
+

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 also have to install dask:

+

conda install -c conda-forge dask

+

and also either LightGBM or XGBoost.

+
+
+
+
+

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/prediction_intervals.html b/docs/prediction_intervals.html new file mode 100644 index 00000000..98d7df20 --- /dev/null +++ b/docs/prediction_intervals.html @@ -0,0 +1,1209 @@ + + + + + + + + + + +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)
+fig.savefig('../figs/prediction_intervals__eda.png', bbox_inches='tight')
+
+

+
+
+

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
+)
+fig.savefig('../figs/prediction_intervals__knn.png', bbox_inches='tight')
+
+

+
+
+

Lasso

+
+
fig = plot_series(
+    train, 
+    test, 
+    plot_random=False, 
+    models=['Lasso'],
+    level=levels, 
+    max_insample_length=48
+)
+fig.savefig('../figs/prediction_intervals__lasso.png', bbox_inches='tight')
+
+

+
+
+

LineaRegression

+
+
fig = plot_series(
+    train, 
+    test, 
+    plot_random=False, 
+    models=['LinearRegression'],
+    level=levels, 
+    max_insample_length=48
+)
+fig.savefig('../figs/prediction_intervals__lr.png', bbox_inches='tight')
+
+

+
+
+

MLPRegressor

+
+
fig = plot_series(
+    train, 
+    test, 
+    plot_random=False, 
+    models=['MLPRegressor'],
+    level=levels, 
+    max_insample_length=48
+)
+fig.savefig('../figs/prediction_intervals__mlp.png', bbox_inches='tight')
+
+

+
+
+

Ridge

+
+
fig = plot_series(
+    train, 
+    test, 
+    plot_random=False, 
+    models=['Ridge'],
+    level=levels, 
+    max_insample_length=48
+)
+fig.savefig('../figs/prediction_intervals__ridge.png', bbox_inches='tight')
+
+

+

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/quick_start_distributed.html b/docs/quick_start_distributed.html new file mode 100644 index 00000000..ca4713dc --- /dev/null +++ b/docs/quick_start_distributed.html @@ -0,0 +1,986 @@ + + + + + + + + + + +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

+
+
import random
+import matplotlib.pyplot as plt
+
+def plot_sample(df, ax):
+    idxs = df['unique_id'].unique()
+    random.seed(0)
+    sample_idxs = random.choices(idxs, k=4)
+    for uid, axi in zip(sample_idxs, ax.flat):
+        df[df['unique_id'].eq(uid)].set_index('ds').plot(ax=axi, title=uid)
+
+
+
fig, ax = plt.subplots(nrows=2, ncols=2, figsize=(10, 6), gridspec_kw=dict(hspace=0.5))
+plot_sample(series, ax)
+fig.savefig('../figs/quick_start_distributed__sample.png', bbox_inches='tight')
+plt.close()
+
+

+

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)
+
+
/home/jose/mambaforge/envs/mlforecast/lib/python3.10/site-packages/lightgbm/dask.py:525: UserWarning: Parameter n_jobs will be ignored.
+  _log_warning(f"Parameter {param_alias} will be ignored.")
+
+
+
Finding random open ports for workers
+[LightGBM] [Info] Trying to bind port 52367...
+[LightGBM] [Info] Binding port 52367 succeeded
+[LightGBM] [Info] Listening...
+[LightGBM] [Info] Trying to bind port 48789...
+[LightGBM] [Info] Binding port 48789 succeeded
+[LightGBM] [Info] Listening...
+[LightGBM] [Info] Connected to rank 1
+[LightGBM] [Info] Connected to rank 0
+[LightGBM] [Info] Local rank: 0, total number of machines: 2
+[LightGBM] [Info] Local rank: 1, total number of machines: 2
+[LightGBM] [Warning] num_threads is set=1, n_jobs=-1 will be ignored. Current value: num_threads=1
+[LightGBM] [Warning] num_threads is set=1, n_jobs=-1 will be ignored. Current value: num_threads=1
+
+
+
DistributedMLForecast(models=[DaskLGBMForecast], freq=<Day>, lag_features=['lag7'], date_features=[], num_threads=1, engine=None)
+
+
+

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.

+
+
import pandas as pd
+
+
+
fig, ax = plt.subplots(nrows=2, ncols=2, figsize=(10, 6), gridspec_kw=dict(hspace=0.5))
+plot_sample(pd.concat([series, local_preds.set_index('unique_id')]), ax)
+fig.savefig('../figs/quick_start_distributed__sample_prediction.png', bbox_inches='tight')
+plt.close()
+
+

+

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/quick_start_local.html b/docs/quick_start_local.html new file mode 100644 index 00000000..627e9f32 --- /dev/null +++ b/docs/quick_start_local.html @@ -0,0 +1,897 @@ + + + + + + + + + + +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
+
+
+
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

+
+
df.plot(x='ds', y='y', figsize=(10, 6));
+
+

+
+
+

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.

+
+
pd.concat([df, preds]).set_index('ds').plot(figsize=(10, 6));
+
+

+
+
+

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/quick_start_local_files/figure-html/cell-5-output-1.png b/docs/quick_start_local_files/figure-html/cell-5-output-1.png new file mode 100644 index 00000000..66711bf0 Binary files /dev/null and b/docs/quick_start_local_files/figure-html/cell-5-output-1.png differ diff --git a/docs/quick_start_local_files/figure-html/cell-9-output-1.png b/docs/quick_start_local_files/figure-html/cell-9-output-1.png new file mode 100644 index 00000000..545eaab3 Binary files /dev/null and b/docs/quick_start_local_files/figure-html/cell-9-output-1.png differ diff --git a/docs/target_transforms_guide.html b/docs/target_transforms_guide.html new file mode 100644 index 00000000..d937dd7b --- /dev/null +++ b/docs/target_transforms_guide.html @@ -0,0 +1,822 @@ + + + + + + + + + + +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(fname, bbox_inches='tight')
+    plt.close()
+
+
+
plot({'original': serie}, 'figs/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}, 'figs/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}, 'figs/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}, 'figs/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('figs/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}, 'figs/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}, 'figs/target_transforms__log_diffs.png')
+
+

+ + +
+
+ +

Give us a ⭐ on Github

+ +
+ + + + \ No newline at end of file diff --git a/docs/transfer_learning.html b/docs/transfer_learning.html new file mode 100644 index 00000000..c53aca9b --- /dev/null +++ b/docs/transfer_learning.html @@ -0,0 +1,834 @@ + + + + + + + + + +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')
+
+

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)
+fig.savefig('../figs/transfer_learning__eda.png', bbox_inches='tight')
+
+

+
+
+

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)
+fig.savefig('../figs/transfer_learning__forecast.png', bbox_inches='tight')
+
+

+
+
+

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/figs/cross_validation__predictions.png b/figs/cross_validation__predictions.png new file mode 100644 index 00000000..a21b93af 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..d8b910d2 Binary files /dev/null and b/figs/cross_validation__series.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..f5f44735 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..6cb4b33d 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..d646d1dc 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..5fe7f84b 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..41c51d79 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..8e83030f 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..f4864c97 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..efe62e88 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..844dfc9d 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..7db99356 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..f6c9b463 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..2223f922 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..3793639e Binary files /dev/null and b/figs/forecast__predict_intervals_window_size_1.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__mlp.png b/figs/prediction_intervals__mlp.png new file mode 100644 index 00000000..ed73510c Binary files /dev/null and b/figs/prediction_intervals__mlp.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/quick_start_distributed__sample.png b/figs/quick_start_distributed__sample.png new file mode 100644 index 00000000..4d526113 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..4ea00cbf Binary files /dev/null and b/figs/quick_start_distributed__sample_prediction.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__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..9b95858d --- /dev/null +++ b/forecast.html @@ -0,0 +1,3337 @@ + + + + + + + + + + +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,
+             differences:Optional[Iterable[int]]=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.
differencestyping.Optional[typing.Iterable[int]]NoneDifferences to take of the target before computing the features. These are restored at the forecasting step.
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 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
+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
    • +
    • Differences to apply to the target before computing the features, which are then restored when forecasting.
    • +
  • +
  • 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
86988H19619312.70.071271
86989H19619412.30.071271
86990H19619511.90.071271
86991H19619611.70.071271
86992H19619711.40.071271
...............
325187H41395659.09.280574
325188H41395758.021.427570
325189H41395853.07.767965
325190H41395938.07.691257
325191H41396046.015.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']).set_index('unique_id')
+fig, ax = plt.subplots(nrows=2, ncols=2, figsize=(16, 10))
+for uid, axi in zip(sample_ids, ax.flat):
+    results.loc[uid].set_index('ds').plot(ax=axi, title=uid)
+fig.savefig('figs/forecast__predict.png', bbox_inches='tight')
+plt.close()
+
+

+
+

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']).set_index('unique_id')
+fig, ax = plt.subplots(nrows=2, ncols=2, figsize=(16, 10))
+for uid, axi in zip(sample_ids, ax.flat):
+    uid_results = results.loc[uid].set_index('ds')
+    uid_results[['y', 'LGBMRegressor']].plot(ax=axi, title=uid)
+    for lv in [50, 80, 95]:
+        axi.fill_between(
+            uid_results.index, 
+            uid_results[f'LGBMRegressor-lo-{lv}'].values, 
+            uid_results[f'LGBMRegressor-hi-{lv}'].values,
+            label=f'LGBMRegressor-level-{lv}',
+            color='orange',
+            alpha=1 - lv / 100
+        )
+    axi.legend()
+fig.savefig('figs/forecast__predict_intervals.png', bbox_inches='tight')
+plt.close()
+
+

+

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']).set_index('unique_id')
+fig, ax = plt.subplots(nrows=2, ncols=2, figsize=(16, 10))
+for uid, axi in zip(sample_ids, ax.flat):
+    uid_results = results.loc[uid].set_index('ds')
+    uid_results[['y', 'LGBMRegressor']].plot(ax=axi, title=uid)
+    axi.fill_between(
+        uid_results.index, 
+        uid_results['LGBMRegressor-lo-90'].values, 
+        uid_results['LGBMRegressor-hi-90'].values,
+        label='LGBMRegressor-level-90',
+        color='orange',
+        alpha=0.2
+    )
+    axi.legend()
+fig.savefig('figs/forecast__predict_intervals_window_size_1.png', bbox_inches='tight')
+plt.close()
+
+

+
+
+

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, ax = plt.subplots()
+ercot_df.tail(48 * 2).plot(x='ds', y='y', figsize=(20, 7), ax=ax)
+ercot_fcsts.plot(x='ds', y='LGBMRegressor', ax=ax, title='ERCOT forecasts trained on M4-Hourly dataset');
+plt.gcf().savefig('figs/forecast__ercot.png', bbox_inches='tight')
+plt.close()
+
+

+

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 abs(full['LGBMRegressor'] - full['y']).div(full['y']).groupby(full['unique_id']).mean()
+
+
+
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
0H1961011.815.167163
1H1962011.414.767163
2H1963011.114.467163
3H1964010.814.167163
4H1965010.613.867163
..................
13435H413908349.040.262691
13436H413909339.026.603123
13437H413910329.042.545732
13438H413911324.030.053714
13439H413912320.0-13.589900
+ +

13440 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, ax = plt.subplots(nrows=2, ncols=2, figsize=(16, 10))
+
+for uid, axi in zip(sample_ids, ax.flat):
+    subset = cv_results[cv_results['unique_id'].eq(uid)].drop(columns=['unique_id', 'cutoff'])
+    subset.set_index('ds').plot(ax=axi, title=uid)
+fig.savefig('figs/forecast__cross_validation.png', bbox_inches='tight')
+plt.close()
+
+

+
+
fig, ax = plt.subplots(nrows=2, ncols=2, figsize=(16, 10))
+
+for uid, axi in zip(sample_ids, ax.flat):
+    subset = cv_results_intervals[cv_results_intervals['unique_id'].eq(uid)].drop(columns=['unique_id', 'cutoff']).set_index('ds')
+    subset[['y', 'LGBMRegressor']].plot(ax=axi, title=uid)
+    axi.fill_between(
+        subset.index, 
+        subset['LGBMRegressor-lo-90'].values, 
+        subset['LGBMRegressor-hi-90'].values,
+        label='LGBMRegressor-level-90',
+        color='orange',
+        alpha=0.2
+    )
+    axi.legend()
+fig.savefig('figs/forecast__cross_validation_intervals.png', bbox_inches='tight')
+plt.close()
+
+

+
+
+

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-053.9811987945
1id_002000-10-0610.3274017945
2id_002000-10-0717.6574747945
3id_002000-10-0825.8987907945
4id_002000-10-0934.4940407945
..................
26998id_992001-05-1045.3400516935
26999id_992001-05-113.0229486935
27000id_992001-05-1210.1313716935
27001id_992001-05-1314.5724346935
27002id_992001-05-1422.8163576935
+ +

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-053.98119879450.548814
1id_002000-10-0610.32740179450.715189
2id_002000-10-0717.65747479450.602763
3id_002000-10-0825.89879079450.544883
4id_002000-10-0934.49404079450.423655
.....................
26998id_992001-05-1045.34005169350.112841
26999id_992001-05-113.02294869350.883449
27000id_992001-05-1210.13137169350.762250
27001id_992001-05-1314.57243469350.025932
27002id_992001-05-1422.81635769350.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-1542.382751
1id_002001-05-1650.069369
2id_002001-05-171.928786
3id_002001-05-1810.315261
4id_002001-05-1918.833788
............
695id_992001-05-1744.484574
696id_992001-05-181.882713
697id_992001-05-199.057168
698id_992001-05-2015.155541
699id_992001-05-2122.912041
+ +

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    30.643253
+dtype: float64
+
+Inputs:
+Prediction:
+unique_id
+id_99    41.024064
+dtype: float64
+
+
+
+
+ + + + + + + + + + + + + + + + + + + +
static_0static_1lag1
0794534.862245
+ +
+
+
+
+ + + + + + + + + + + + + + + + + + + +
static_0static_1lag1
0794546.396346
+ +
+
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
unique_iddsLGBMRegressor
0id_002001-05-1546.396346
1id_002001-05-1623.651944
2id_012001-05-1514.388954
3id_012001-05-1617.796990
4id_022001-05-1515.640528
............
195id_972001-05-1639.849693
196id_982001-05-158.408627
197id_982001-05-164.290933
198id_992001-05-1533.707579
199id_992001-05-1645.126470
+ +

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..d471e6ae --- /dev/null +++ b/grouped_array.html @@ -0,0 +1,712 @@ + + + + + + + + + +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..f90395a0 --- /dev/null +++ b/index.html @@ -0,0 +1,1031 @@ + + + + + + + + + +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-011.75191772
1id_002000-01-029.19671572
2id_002000-01-0318.57778872
3id_002000-01-0424.52064672
4id_002000-01-0533.41802872
+ +
+
+
+
+
+

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(),
+    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-0469.08283067.76133768.226556
1id_002000-04-0575.70602474.58869975.484774
2id_002000-04-0682.22247381.05828982.853684
3id_002000-04-0789.57763888.73594790.351212
4id_002000-04-0844.14909544.98138446.291173
..................
275id_192000-03-2330.15127031.81482532.592799
276id_192000-03-2431.41810432.65337433.563294
277id_192000-03-2532.84356733.58603334.530912
278id_192000-03-2634.12721034.54147335.507559
279id_192000-03-2734.32920235.45094336.425001
+ +

280 rows × 5 columns

+
+
+
+
+
+

Visualize results

+
+
import matplotlib.pyplot as plt
+import pandas as pd
+
+fig, ax = plt.subplots(nrows=2, ncols=2, figsize=(12, 6), gridspec_kw=dict(hspace=0.3))
+for i, (uid, axi) in enumerate(zip(series['unique_id'].unique(), ax.flat)):
+    fltr = lambda df: df['unique_id'].eq(uid)
+    pd.concat([series.loc[fltr, ['ds', 'y']], predictions.loc[fltr]]).set_index('ds').plot(ax=axi)
+    axi.set(title=uid, xlabel=None)
+    if i % 2 == 0:
+        axi.legend().remove()
+    else:
+        axi.legend(bbox_to_anchor=(1.01, 1.0))
+fig.savefig('figs/index.png', bbox_inches='tight')
+plt.close()
+
+

+
+
+
+

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..0f806741 --- /dev/null +++ b/lgb_cv.html @@ -0,0 +1,1751 @@ + + + + + + + + + + +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,
+             differences:Optional[Iterable[int]]=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.
differencestyping.Optional[typing.Iterable[int]]NoneDifferences to take of the target before computing the features. These are restored at the forecasting step.
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..3008ff77 --- /dev/null +++ b/search.json @@ -0,0 +1,632 @@ +[ + { + "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 differences:Optional[Iterable[int]]=None, num_threads:int=1, \n target_transforms:Optional[List[mlforecast.target_transforms.\n 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\ndifferences\ntyping.Optional[typing.Iterable[int]]\nNone\nDifferences to take of the target before computing the features. These are restored at the forecasting step.\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 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\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\nDifferences to apply to the target before computing the features, which are then restored when forecasting.\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\n86988\nH196\n193\n12.7\n0.071271\n\n\n86989\nH196\n194\n12.3\n0.071271\n\n\n86990\nH196\n195\n11.9\n0.071271\n\n\n86991\nH196\n196\n11.7\n0.071271\n\n\n86992\nH196\n197\n11.4\n0.071271\n\n\n...\n...\n...\n...\n...\n\n\n325187\nH413\n956\n59.0\n9.280574\n\n\n325188\nH413\n957\n58.0\n21.427570\n\n\n325189\nH413\n958\n53.0\n7.767965\n\n\n325190\nH413\n959\n38.0\n7.691257\n\n\n325191\nH413\n960\n46.0\n15.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']).set_index('unique_id')\nfig, ax = plt.subplots(nrows=2, ncols=2, figsize=(16, 10))\nfor uid, axi in zip(sample_ids, ax.flat):\n results.loc[uid].set_index('ds').plot(ax=axi, title=uid)\nfig.savefig('figs/forecast__predict.png', bbox_inches='tight')\nplt.close()\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']).set_index('unique_id')\nfig, ax = plt.subplots(nrows=2, ncols=2, figsize=(16, 10))\nfor uid, axi in zip(sample_ids, ax.flat):\n uid_results = results.loc[uid].set_index('ds')\n uid_results[['y', 'LGBMRegressor']].plot(ax=axi, title=uid)\n for lv in [50, 80, 95]:\n axi.fill_between(\n uid_results.index, \n uid_results[f'LGBMRegressor-lo-{lv}'].values, \n uid_results[f'LGBMRegressor-hi-{lv}'].values,\n label=f'LGBMRegressor-level-{lv}',\n color='orange',\n alpha=1 - lv / 100\n )\n axi.legend()\nfig.savefig('figs/forecast__predict_intervals.png', bbox_inches='tight')\nplt.close()\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']).set_index('unique_id')\nfig, ax = plt.subplots(nrows=2, ncols=2, figsize=(16, 10))\nfor uid, axi in zip(sample_ids, ax.flat):\n uid_results = results.loc[uid].set_index('ds')\n uid_results[['y', 'LGBMRegressor']].plot(ax=axi, title=uid)\n axi.fill_between(\n uid_results.index, \n uid_results['LGBMRegressor-lo-90'].values, \n uid_results['LGBMRegressor-hi-90'].values,\n label='LGBMRegressor-level-90',\n color='orange',\n alpha=0.2\n )\n axi.legend()\nfig.savefig('figs/forecast__predict_intervals_window_size_1.png', bbox_inches='tight')\nplt.close()\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, ax = plt.subplots()\nercot_df.tail(48 * 2).plot(x='ds', y='y', figsize=(20, 7), ax=ax)\nercot_fcsts.plot(x='ds', y='LGBMRegressor', ax=ax, title='ERCOT forecasts trained on M4-Hourly dataset');\nplt.gcf().savefig('figs/forecast__ercot.png', bbox_inches='tight')\nplt.close()\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 abs(full['LGBMRegressor'] - full['y']).div(full['y']).groupby(full['unique_id']).mean()\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\n1\n0\n11.8\n15.167163\n\n\n1\nH196\n2\n0\n11.4\n14.767163\n\n\n2\nH196\n3\n0\n11.1\n14.467163\n\n\n3\nH196\n4\n0\n10.8\n14.167163\n\n\n4\nH196\n5\n0\n10.6\n13.867163\n\n\n...\n...\n...\n...\n...\n...\n\n\n13435\nH413\n908\n3\n49.0\n40.262691\n\n\n13436\nH413\n909\n3\n39.0\n26.603123\n\n\n13437\nH413\n910\n3\n29.0\n42.545732\n\n\n13438\nH413\n911\n3\n24.0\n30.053714\n\n\n13439\nH413\n912\n3\n20.0\n-13.589900\n\n\n\n\n13440 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, ax = plt.subplots(nrows=2, ncols=2, figsize=(16, 10))\n\nfor uid, axi in zip(sample_ids, ax.flat):\n subset = cv_results[cv_results['unique_id'].eq(uid)].drop(columns=['unique_id', 'cutoff'])\n subset.set_index('ds').plot(ax=axi, title=uid)\nfig.savefig('figs/forecast__cross_validation.png', bbox_inches='tight')\nplt.close()\n\n\n\nfig, ax = plt.subplots(nrows=2, ncols=2, figsize=(16, 10))\n\nfor uid, axi in zip(sample_ids, ax.flat):\n subset = cv_results_intervals[cv_results_intervals['unique_id'].eq(uid)].drop(columns=['unique_id', 'cutoff']).set_index('ds')\n subset[['y', 'LGBMRegressor']].plot(ax=axi, title=uid)\n axi.fill_between(\n subset.index, \n subset['LGBMRegressor-lo-90'].values, \n subset['LGBMRegressor-hi-90'].values,\n label='LGBMRegressor-level-90',\n color='orange',\n alpha=0.2\n )\n axi.legend()\nfig.savefig('figs/forecast__cross_validation_intervals.png', bbox_inches='tight')\nplt.close()\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\n3.981198\n79\n45\n\n\n1\nid_00\n2000-10-06\n10.327401\n79\n45\n\n\n2\nid_00\n2000-10-07\n17.657474\n79\n45\n\n\n3\nid_00\n2000-10-08\n25.898790\n79\n45\n\n\n4\nid_00\n2000-10-09\n34.494040\n79\n45\n\n\n...\n...\n...\n...\n...\n...\n\n\n26998\nid_99\n2001-05-10\n45.340051\n69\n35\n\n\n26999\nid_99\n2001-05-11\n3.022948\n69\n35\n\n\n27000\nid_99\n2001-05-12\n10.131371\n69\n35\n\n\n27001\nid_99\n2001-05-13\n14.572434\n69\n35\n\n\n27002\nid_99\n2001-05-14\n22.816357\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\n3.981198\n79\n45\n0.548814\n\n\n1\nid_00\n2000-10-06\n10.327401\n79\n45\n0.715189\n\n\n2\nid_00\n2000-10-07\n17.657474\n79\n45\n0.602763\n\n\n3\nid_00\n2000-10-08\n25.898790\n79\n45\n0.544883\n\n\n4\nid_00\n2000-10-09\n34.494040\n79\n45\n0.423655\n\n\n...\n...\n...\n...\n...\n...\n...\n\n\n26998\nid_99\n2001-05-10\n45.340051\n69\n35\n0.112841\n\n\n26999\nid_99\n2001-05-11\n3.022948\n69\n35\n0.883449\n\n\n27000\nid_99\n2001-05-12\n10.131371\n69\n35\n0.762250\n\n\n27001\nid_99\n2001-05-13\n14.572434\n69\n35\n0.025932\n\n\n27002\nid_99\n2001-05-14\n22.816357\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\n42.382751\n\n\n1\nid_00\n2001-05-16\n50.069369\n\n\n2\nid_00\n2001-05-17\n1.928786\n\n\n3\nid_00\n2001-05-18\n10.315261\n\n\n4\nid_00\n2001-05-19\n18.833788\n\n\n...\n...\n...\n...\n\n\n695\nid_99\n2001-05-17\n44.484574\n\n\n696\nid_99\n2001-05-18\n1.882713\n\n\n697\nid_99\n2001-05-19\n9.057168\n\n\n698\nid_99\n2001-05-20\n15.155541\n\n\n699\nid_99\n2001-05-21\n22.912041\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 30.643253\ndtype: float64\n\nInputs:\nPrediction:\nunique_id\nid_99 41.024064\ndtype: float64\n\n\n\n\n\n\n\n\n\n\nstatic_0\nstatic_1\nlag1\n\n\n\n\n0\n79\n45\n34.862245\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\n46.396346\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\n46.396346\n\n\n1\nid_00\n2001-05-16\n23.651944\n\n\n2\nid_01\n2001-05-15\n14.388954\n\n\n3\nid_01\n2001-05-16\n17.796990\n\n\n4\nid_02\n2001-05-15\n15.640528\n\n\n...\n...\n...\n...\n\n\n195\nid_97\n2001-05-16\n39.849693\n\n\n196\nid_98\n2001-05-15\n8.408627\n\n\n197\nid_98\n2001-05-16\n4.290933\n\n\n198\nid_99\n2001-05-15\n33.707579\n\n\n199\nid_99\n2001-05-16\n45.126470\n\n\n\n\n200 rows × 3 columns\n\n\n\n\n\n\n\nGive us a ⭐ on Github" + }, + { + "objectID": "distributed.models.dask.xgb.html", + "href": "distributed.models.dask.xgb.html", + "title": "DaskXGBForecast", + "section": "", + "text": "Wrapper of xgboost.dask.DaskXGBRegressor that adds a model_ property that contains the fitted model and is sent to the workers in the forecasting step.\n\n\nDaskXGBForecast\n\n DaskXGBForecast (max_depth:Optional[int]=None,\n max_leaves:Optional[int]=None,\n max_bin:Optional[int]=None,\n grow_policy:Optional[str]=None,\n learning_rate:Optional[float]=None,\n n_estimators:int=100, verbosity:Optional[int]=None, obje\n ctive:Union[str,Callable[[numpy.ndarray,numpy.ndarray],T\n uple[numpy.ndarray,numpy.ndarray]],NoneType]=None,\n 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[int,\n numpy.random.mtrand.RandomState,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 gpu_id:Optional[int]=None,\n validate_parameters:Optional[bool]=None,\n predictor:Optional[str]=None,\n enable_categorical:bool=False,\n feature_types:Sequence[str]=None,\n max_cat_to_onehot:Optional[int]=None,\n max_cat_threshold:Optional[int]=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.\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\nint\n100\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 gpu_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[int, numpy.random.mtrand.RandomState, 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\ngpu_id\ntyping.Optional[int]\nNone\nDevice ordinal.\n\n\nvalidate_parameters\ntyping.Optional[bool]\nNone\nGive warnings for unknown parameter.\n\n\npredictor\ntyping.Optional[str]\nNone\nForce XGBoost to use specific predictor, available choices are [cpu_predictor,gpu_predictor].\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.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\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.0Activates early stopping. Validation metric needs to improve at least once inevery early_stopping_rounds round(s) to continue training. Requires at leastone item in eval_set in :py:meth:fit.The method returns the model from the last iteration (not the best one). Ifthere’s more than one item in eval_set, the last entry will be used for earlystopping. If there’s more than one metric in eval_metric, the last metricwill be used for early stopping.If early stopping occurs, the model will have three additional fields::py:attr:best_score, :py:attr:best_iteration and:py:attr:best_ntree_limit... 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)] xgboost.train(params, Xy, callbacks=callbacks)\n\n\nkwargs\ntyping.Any\n\nKeyword arguments for XGBoost Booster object. Full documentation of parameterscan be found :doc:here </parameter>.Attempting to set a parameter via the constructor args and **kwargsdict simultaneously will result in a TypeError... note:: **kwargs unsupported by scikit-learn **kwargs is unsupported by scikit-learn. We do not guarantee that parameters passed via this argument will interact properly with scikit-learn.\n\n\nReturns\nNone\n\n\n\n\n\n\n\n\n\nGive us a ⭐ on Github" + }, + { + "objectID": "distributed.models.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": "docs/cross_validation.html", + "href": "docs/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/cross_validation.html#introduction", + "href": "docs/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/cross_validation.html#install-libraries", + "href": "docs/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\npip install mlforecast lightgbm\n\n\nfrom mlforecast import MLForecast # required to instantiate MLForecast object and use cross-validation method" + }, + { + "objectID": "docs/cross_validation.html#load-and-explore-the-data", + "href": "docs/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\nimport pandas as pd \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\nimport matplotlib.pyplot as plt\n\ndef plot(df, fname, last_n=24 * 14):\n fig, ax = plt.subplots(nrows=2, ncols=2, figsize=(14, 6), gridspec_kw=dict(hspace=0.5))\n uids = df['unique_id'].unique()\n for i, (uid, axi) in enumerate(zip(uids, ax.flat)):\n legend = i % 2 == 0\n df[df['unique_id'].eq(uid)].tail(last_n).set_index('ds').plot(ax=axi, title=uid, legend=legend)\n fig.savefig(fname, bbox_inches='tight')\n plt.close()\n\n\nplot(Y_df, '../figs/cross_validation__series.png')" + }, + { + "objectID": "docs/cross_validation.html#train-model", + "href": "docs/cross_validation.html#train-model", + "title": "Cross validation", + "section": "Train model", + "text": "Train model\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\nmodels = [lgb.LGBMRegressor()]\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/cross_validation.html#perform-time-series-cross-validation", + "href": "docs/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\ndata: training data frame with MLForecast format\nwindow_size (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 data=Y_df,\n window_size=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\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/cross_validation.html#evaluate-results", + "href": "docs/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 first need to install datasetsforecast, a Python library developed by Nixtla that includes a function to compute the RMSE.\n\npip install datasetsforecast\n\n\nfrom datasetsforecast.losses import rmse\n\nThe function to compute the RMSE takes two arguments:\n\nThe actual values.\n\nThe forecasts, in this case, LGBMRegressor.\n\nIn this case we will compute the rmse per time series and cutoff and then we will take the mean of the results.\n\ncv_rmse = crossvalidation_df.groupby(['unique_id', 'cutoff']).apply(lambda df: rmse(df['y'], df['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/cross_validation.html#references", + "href": "docs/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/quick_start_distributed.html", + "href": "docs/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/quick_start_distributed.html#main-concepts", + "href": "docs/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/quick_start_distributed.html#setup", + "href": "docs/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\ncluster = LocalCluster(n_workers=2, threads_per_worker=1) # change this to use a remote cluster\nclient = Client(cluster)" + }, + { + "objectID": "docs/quick_start_distributed.html#data-format", + "href": "docs/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/quick_start_distributed.html#modeling", + "href": "docs/quick_start_distributed.html#modeling", + "title": "Quick start (distributed)", + "section": "Modeling", + "text": "Modeling\n\nimport random\nimport matplotlib.pyplot as plt\n\ndef plot_sample(df, ax):\n idxs = df['unique_id'].unique()\n random.seed(0)\n sample_idxs = random.choices(idxs, k=4)\n for uid, axi in zip(sample_idxs, ax.flat):\n df[df['unique_id'].eq(uid)].set_index('ds').plot(ax=axi, title=uid)\n\n\nfig, ax = plt.subplots(nrows=2, ncols=2, figsize=(10, 6), gridspec_kw=dict(hspace=0.5))\nplot_sample(series, ax)\nfig.savefig('../figs/quick_start_distributed__sample.png', bbox_inches='tight')\nplt.close()\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\n/home/jose/mambaforge/envs/mlforecast/lib/python3.10/site-packages/lightgbm/dask.py:525: UserWarning: Parameter n_jobs will be ignored.\n _log_warning(f\"Parameter {param_alias} will be ignored.\")\n\n\nFinding random open ports for workers\n[LightGBM] [Info] Trying to bind port 52367...\n[LightGBM] [Info] Binding port 52367 succeeded\n[LightGBM] [Info] Listening...\n[LightGBM] [Info] Trying to bind port 48789...\n[LightGBM] [Info] Binding port 48789 succeeded\n[LightGBM] [Info] Listening...\n[LightGBM] [Info] Connected to rank 1\n[LightGBM] [Info] Connected to rank 0\n[LightGBM] [Info] Local rank: 0, total number of machines: 2\n[LightGBM] [Info] Local rank: 1, total number of machines: 2\n[LightGBM] [Warning] num_threads is set=1, n_jobs=-1 will be ignored. Current value: num_threads=1\n[LightGBM] [Warning] num_threads is set=1, n_jobs=-1 will be ignored. Current value: num_threads=1\n\n\nDistributedMLForecast(models=[DaskLGBMForecast], freq=<Day>, lag_features=['lag7'], date_features=[], num_threads=1, engine=None)\n\n\nThe previous line computed the features and trained the model, so now we’re ready to compute our forecasts." + }, + { + "objectID": "docs/quick_start_distributed.html#forecasting", + "href": "docs/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/quick_start_distributed.html#visualize-results", + "href": "docs/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\nimport pandas as pd\n\n\nfig, ax = plt.subplots(nrows=2, ncols=2, figsize=(10, 6), gridspec_kw=dict(hspace=0.5))\nplot_sample(pd.concat([series, local_preds.set_index('unique_id')]), ax)\nfig.savefig('../figs/quick_start_distributed__sample_prediction.png', bbox_inches='tight')\nplt.close()\n\n\nAnd that’s it! You’ve trained a distributed LightGBM model and computed predictions for the next 14 days." + }, + { + "objectID": "docs/install.html", + "href": "docs/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\nIf you want to perform distributed training you have to include the dask extra:\npip install \"mlforecast[dask]\"\nand also either LightGBM or XGBoost.\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\nIf you want to perform distributed training you also have to install dask:\nconda install -c conda-forge dask\nand also either LightGBM or XGBoost.\nGive us a ⭐ on Github" + }, + { + "objectID": "docs/install.html#released-versions", + "href": "docs/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\nIf you want to perform distributed training you have to include the dask extra:\npip install \"mlforecast[dask]\"\nand also either LightGBM or XGBoost.\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\nIf you want to perform distributed training you also have to install dask:\nconda install -c conda-forge dask\nand also either LightGBM or XGBoost." + }, + { + "objectID": "docs/install.html#development-version", + "href": "docs/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/transfer_learning.html", + "href": "docs/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/transfer_learning.html#installing-libraries", + "href": "docs/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/transfer_learning.html#load-m3-data", + "href": "docs/transfer_learning.html#load-m3-data", + "title": "Transfer Learning", + "section": "Load M3 Data", + "text": "Load M3 Data\nThe M3 class will automatically download the complete M3 dataset and process it.\nIt return three Dataframes: Y_df contains the values for the target variables, X_df contains exogenous calendar features and S_df contains static features for each time-series. For this example we will only use Y_df.\nIf you want to use your own data just replace Y_df. Be sure to use a long format and have a simmilar structure than our data set.\n\nY_df_M3, _, _ = M3.load(directory='./', group='Monthly')\n\nIn this tutorial we are only using 1_000 series to speed up computations. Remove the filter to use the whole dataset.\n\nfig = plot_series(Y_df_M3)\nfig.savefig('../figs/transfer_learning__eda.png', bbox_inches='tight')" + }, + { + "objectID": "docs/transfer_learning.html#model-training", + "href": "docs/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/transfer_learning.html#transfer-m3-to-airpassengers", + "href": "docs/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)\nfig.savefig('../figs/transfer_learning__forecast.png', bbox_inches='tight')" + }, + { + "objectID": "docs/transfer_learning.html#evaluate-results", + "href": "docs/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/quick_start_local.html", + "href": "docs/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/quick_start_local.html#main-concepts", + "href": "docs/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/quick_start_local.html#data-format", + "href": "docs/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\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/quick_start_local.html#modeling", + "href": "docs/quick_start_local.html#modeling", + "title": "Quick start (local)", + "section": "Modeling", + "text": "Modeling\n\ndf.plot(x='ds', y='y', figsize=(10, 6));\n\n\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/quick_start_local.html#forecasting", + "href": "docs/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/quick_start_local.html#visualize-results", + "href": "docs/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\npd.concat([df, preds]).set_index('ds').plot(figsize=(10, 6));\n\n\n\n\nAnd that’s it! You’ve trained a linear regression to predict the air passengers for 1961." + }, + { + "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": "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\n\nseries_sizes = series.groupby('unique_id').size()\nassert series_sizes.size == n_series\nassert series_sizes.min() >= min_length\nassert series_sizes.max() <= max_length\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\n0.752139\n18\n10\n\n\n1\nid_00\n2000-01-02\n2.402450\n18\n10\n\n\n2\nid_00\n2000-01-03\n4.339642\n18\n10\n\n\n3\nid_00\n2000-01-04\n6.579317\n18\n10\n\n\n4\nid_00\n2000-01-05\n7.667484\n18\n10\n\n\n...\n...\n...\n...\n...\n...\n\n\n12446\nid_19\n2002-03-11\n2.783477\n89\n42\n\n\n12447\nid_19\n2002-03-12\n10.705175\n89\n42\n\n\n12448\nid_19\n2002-03-13\n20.925285\n89\n42\n\n\n12449\nid_19\n2002-03-14\n29.998780\n89\n42\n\n\n12450\nid_19\n2002-03-15\n38.755054\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": "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": "target_transforms.html", + "href": "target_transforms.html", + "title": "Target transforms", + "section": "", + "text": "import pandas as pd\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\n\n\nLocalStandardScaler\n\n LocalStandardScaler ()\n\nStandardizes each serie by subtracting its mean and dividing by its standard deviation.\n\nseries = generate_daily_series(10, min_length=50, max_length=50)\nsc = LocalStandardScaler()\nsc.set_column_names('unique_id', 'ds', 'y')\npd.testing.assert_frame_equal(\n sc.inverse_transform(sc.fit_transform(series)),\n series,\n)\nsubset = series[series['unique_id'].isin(['id_0', 'id_7'])]\nsc.idxs = [0, 7]\npd.testing.assert_frame_equal(\n sc.inverse_transform(subset),\n subset\n)\n\n\n\n\nGlobalSklearnTransformer\n\n GlobalSklearnTransformer (transformer:sklearn.base.TransformerMixin)\n\nApplies the same scikit-learn transformer to all series.\n\nsk_boxcox = PowerTransformer(method='box-cox', standardize=False)\nboxcox_global = GlobalSklearnTransformer(sk_boxcox)\nsingle_difference = Differences([1])\nseries = generate_daily_series(10)\nfcst = MLForecast(\n models=[LinearRegression()],\n freq='D',\n lags=[1, 2],\n target_transforms=[boxcox_global, single_difference]\n)\nprep = fcst.preprocess(series, dropna=False)\nexpected = (\n pd.Series(\n sk_boxcox.fit_transform(series[['y']])[:, 0], index=series['unique_id']\n ).groupby('unique_id')\n .diff()\n .values\n)\nnp.testing.assert_allclose(prep['y'].values, expected)\n\n\n\n\n\nGive us a ⭐ on Github" + }, + { + "objectID": "distributed.models.dask.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": "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": "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 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\n0.740453\n27\n53\n\n\n1\nid_00\n2000-01-02\n3.595262\n27\n53\n\n\n2\nid_00\n2000-01-03\n6.895835\n27\n53\n\n\n3\nid_00\n2000-01-04\n8.499450\n27\n53\n\n\n4\nid_00\n2000-01-05\n11.321981\n27\n53\n\n\n...\n...\n...\n...\n...\n...\n\n\n4869\nid_19\n2000-03-25\n40.060681\n97\n45\n\n\n4870\nid_19\n2000-03-26\n53.879482\n97\n45\n\n\n4871\nid_19\n2000-03-27\n62.020210\n97\n45\n\n\n4872\nid_19\n2000-03-28\n2.062543\n97\n45\n\n\n4873\nid_19\n2000-03-29\n14.151317\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\n0.740453\n27\n53\n\n\n1\nid_00\n2000-01-02\n3.595262\n27\n53\n\n\n2\nid_00\n2000-01-03\n6.895835\n27\n53\n\n\n3\nid_00\n2000-01-04\n8.499450\n27\n53\n\n\n4\nid_00\n2000-01-05\n11.321981\n27\n53\n\n\n...\n...\n...\n...\n...\n...\n\n\n217\nid_00\n2000-08-05\n1.326319\n27\n53\n\n\n218\nid_00\n2000-08-06\n3.823198\n27\n53\n\n\n219\nid_00\n2000-08-07\n5.955518\n27\n53\n\n\n220\nid_00\n2000-08-08\n8.698637\n27\n53\n\n\n221\nid_00\n2000-08-09\n11.925481\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 differences:Optional[Iterable[int]]=None, num_threads:int=1, \n target_transforms:Optional[List[mlforecast.target_transforms.\n 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": "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 Examples\n else: warn(msg)\n\n\nSparkXGBForecast\n\n SparkXGBForecast (**kwargs)\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 xgboost.XGBRegressor constructor and most of the parameters used in :py:class:xgboost.XGBRegressor fit and predict method.\nSparkXGBRegressor doesn’t support setting gpu_id but support another param use_gpu, see doc below for more details.\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.\ncallbacks: The export and import of the callback functions are at best effort. For details, see :py:attr:xgboost.spark.SparkXGBRegressor.callbacks param doc. validation_indicator_col For params related to xgboost.XGBRegressor training with evaluation dataset’s supervision, set :py:attr:xgboost.spark.SparkXGBRegressor.validation_indicator_col parameter instead of setting the eval_set parameter in xgboost.XGBRegressor fit method. weight_col: To specify the weight of the training and validation dataset, set :py:attr:xgboost.spark.SparkXGBRegressor.weight_col parameter instead of setting sample_weight and sample_weight_eval_set parameter in xgboost.XGBRegressor fit method. xgb_model: Set the value to be the instance returned by :func:xgboost.spark.SparkXGBRegressorModel.get_booster. num_workers: Integer that specifies the number of XGBoost workers to use. Each XGBoost worker corresponds to one spark task. use_gpu: Boolean that specifies whether the executors are running on GPU instances. base_margin_col: To specify the base margins of the training and validation dataset, set :py:attr:xgboost.spark.SparkXGBRegressor.base_margin_col parameter instead of setting base_margin and base_margin_eval_set in the xgboost.XGBRegressor fit method. Note: this isn’t available for distributed training.\n.. Note:: The Parameters chart above contains parameters that need special handling. For a full list of parameters, see entries with Param(parent=... below.\n.. Note:: This API is experimental.\n\n\n\n\nGive us a ⭐ on Github" + }, + { + "objectID": "docs/prediction_intervals.html", + "href": "docs/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/prediction_intervals.html#introduction", + "href": "docs/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/prediction_intervals.html#install-libraries", + "href": "docs/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/prediction_intervals.html#load-and-explore-the-data", + "href": "docs/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)\nfig.savefig('../figs/prediction_intervals__eda.png', bbox_inches='tight')" + }, + { + "objectID": "docs/prediction_intervals.html#train-models", + "href": "docs/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/prediction_intervals.html#plot-prediction-intervals", + "href": "docs/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)\nfig.savefig('../figs/prediction_intervals__knn.png', bbox_inches='tight')\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)\nfig.savefig('../figs/prediction_intervals__lasso.png', bbox_inches='tight')\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)\nfig.savefig('../figs/prediction_intervals__lr.png', bbox_inches='tight')\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)\nfig.savefig('../figs/prediction_intervals__mlp.png', bbox_inches='tight')\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)\nfig.savefig('../figs/prediction_intervals__ridge.png', bbox_inches='tight')\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/prediction_intervals.html#references", + "href": "docs/prediction_intervals.html#references", + "title": "Probabilistic forecasting", + "section": "References", + "text": "References\n\nKamile Stankeviciute, Ahmed M. Alaa and Mihaela van der Schaar (2021). “Conformal Time-Series Forecasting”\nRob J. Hyndman and George Athanasopoulos (2018). “Forecasting principles and practice, The Statistical Forecasting Perspective”." + }, + { + "objectID": "docs/end_to_end_walkthrough.html", + "href": "docs/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\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/end_to_end_walkthrough.html#data-setup", + "href": "docs/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\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/end_to_end_walkthrough.html#eda", + "href": "docs/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\nimport matplotlib.pyplot as plt\n\ndef plot(df, fname, last_n=24 * 14):\n fig, ax = plt.subplots(nrows=2, ncols=2, figsize=(14, 6), gridspec_kw=dict(hspace=0.5))\n uids = df['unique_id'].unique()\n for i, (uid, axi) in enumerate(zip(uids, ax.flat)):\n legend = i % 2 == 0\n df[df['unique_id'].eq(uid)].tail(last_n).set_index('ds').plot(ax=axi, title=uid, legend=legend)\n fig.savefig(fname, bbox_inches='tight')\n plt.close()\n\n\nplot(df, 'figs/end_to_end_walkthrough__eda.png')\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\nplot(prep, 'figs/end_to_end_walkthrough__differences.png')" + }, + { + "objectID": "docs/end_to_end_walkthrough.html#adding-features", + "href": "docs/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 like the following:\n\nfrom mlforecast.target_transforms import BaseTargetTransform\n\nclass StandardScaler(BaseTargetTransform):\n \"\"\"Standardizes the series by subtracting their mean and dividing by their standard deviation.\"\"\"\n def fit_transform(self, df: pd.DataFrame) -> pd.DataFrame:\n self.norm_ = df.groupby(self.id_col)[self.target_col].agg(['mean', 'std'])\n df = df.merge(self.norm_, on=self.id_col)\n df[self.target_col] = (df[self.target_col] - df['mean']) / df['std']\n df = df.drop(columns=['mean', 'std'])\n return df\n\n def inverse_transform(self, df: pd.DataFrame) -> pd.DataFrame:\n df = df.merge(self.norm_, on=self.id_col)\n for col in df.columns.drop([self.id_col, self.time_col, 'mean', 'std']):\n df[col] = df[col] * df['std'] + df['mean']\n df = df.drop(columns=['std', 'mean'])\n return df\n\n\nfcst = MLForecast(\n models=[],\n freq=1,\n lags=[1],\n target_transforms=[StandardScaler()]\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.492285\n-1.382600\n\n\n2\nH196\n3\n-1.574549\n-1.492285\n\n\n3\nH196\n4\n-1.656813\n-1.574549\n\n\n4\nH196\n5\n-1.711656\n-1.656813\n\n\n5\nH196\n6\n-1.793919\n-1.711656\n\n\n...\n...\n...\n...\n...\n\n\n4027\nH413\n1004\n3.061246\n2.423809\n\n\n4028\nH413\n1005\n2.521876\n3.061246\n\n\n4029\nH413\n1006\n0.511497\n2.521876\n\n\n4030\nH413\n1007\n0.217295\n0.511497\n\n\n4031\nH413\n1008\n-0.125941\n0.217295\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=[StandardScaler()]\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\nnp.testing.assert_allclose(preds['Naive'], last_vals['y'])" + }, + { + "objectID": "docs/end_to_end_walkthrough.html#training", + "href": "docs/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/end_to_end_walkthrough.html#forecasting", + "href": "docs/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\nimport pandas as pd\n\n\nplot(pd.concat([df, preds]), 'figs/end_to_end_walkthrough__predictions.png', last_n=24 * 7)" + }, + { + "objectID": "docs/end_to_end_walkthrough.html#updating-series-values", + "href": "docs/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/end_to_end_walkthrough.html#estimating-model-performance", + "href": "docs/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\nplot(cv_result.drop(columns='cutoff'), 'figs/end_to_end_walkthrough__cv.png')\n\n\nWe can compute the RMSE on each split.\n\ndef evaluate_cv(df):\n return df['y'].sub(df['LGBMRegressor']).pow(2).groupby(df['cutoff']).mean().pow(0.5)\n\nsplit_rmse = evaluate_cv(cv_result)\nsplit_rmse\n\ncutoff\n816 29.418172\n864 34.257598\n912 13.145763\n960 35.066261\ndtype: float64\n\n\nAnd the average RMSE across splits.\n\nsplit_rmse.mean()\n\n27.971948676289266\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\n25.87444576840234\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\nplot(cv.cv_preds_.drop(columns='window'), 'figs/end_to_end_walkthrough__lgbcv.png')\n\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)\nplot(pd.concat([df, preds]), 'figs/end_to_end_walkthrough__final_forecast.png')" + }, + { + "objectID": "docs/electricity_peak_forecasting.html", + "href": "docs/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/electricity_peak_forecasting.html#introduction", + "href": "docs/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/electricity_peak_forecasting.html#libraries", + "href": "docs/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/electricity_peak_forecasting.html#load-data", + "href": "docs/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\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\nY_df.plot(x='ds', y='y', figsize=(20, 7))\n\n<AxesSubplot: xlabel='ds'>\n\n\n\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/electricity_peak_forecasting.html#fit-and-forecast-lighgbm-model", + "href": "docs/electricity_peak_forecasting.html#fit-and-forecast-lighgbm-model", + "title": "Detect Demand Peaks", + "section": "Fit and Forecast LighGBM model", + "text": "Fit and Forecast LighGBM 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() # 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/electricity_peak_forecasting.html#peak-detection", + "href": "docs/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/electricity_peak_forecasting.html#next-steps", + "href": "docs/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/target_transforms_guide.html", + "href": "docs/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/target_transforms_guide.html#data-setup", + "href": "docs/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/target_transforms_guide.html#local-transformations", + "href": "docs/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(fname, bbox_inches='tight')\n plt.close()\n\n\nplot({'original': serie}, 'figs/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}, 'figs/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}, 'figs/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}, 'figs/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('figs/target_transforms__zeros.png')" + }, + { + "objectID": "docs/target_transforms_guide.html#global-transformations", + "href": "docs/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}, 'figs/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}, 'figs/target_transforms__log_diffs.png')" + }, + { + "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\n1.751917\n72\n\n\n1\nid_00\n2000-01-02\n9.196715\n72\n\n\n2\nid_00\n2000-01-03\n18.577788\n72\n\n\n3\nid_00\n2000-01-04\n24.520646\n72\n\n\n4\nid_00\n2000-01-05\n33.418028\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(),\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\n69.082830\n67.761337\n68.226556\n\n\n1\nid_00\n2000-04-05\n75.706024\n74.588699\n75.484774\n\n\n2\nid_00\n2000-04-06\n82.222473\n81.058289\n82.853684\n\n\n3\nid_00\n2000-04-07\n89.577638\n88.735947\n90.351212\n\n\n4\nid_00\n2000-04-08\n44.149095\n44.981384\n46.291173\n\n\n...\n...\n...\n...\n...\n...\n\n\n275\nid_19\n2000-03-23\n30.151270\n31.814825\n32.592799\n\n\n276\nid_19\n2000-03-24\n31.418104\n32.653374\n33.563294\n\n\n277\nid_19\n2000-03-25\n32.843567\n33.586033\n34.530912\n\n\n278\nid_19\n2000-03-26\n34.127210\n34.541473\n35.507559\n\n\n279\nid_19\n2000-03-27\n34.329202\n35.450943\n36.425001\n\n\n\n\n280 rows × 5 columns\n\n\n\n\n\nVisualize results\n\nimport matplotlib.pyplot as plt\nimport pandas as pd\n\nfig, ax = plt.subplots(nrows=2, ncols=2, figsize=(12, 6), gridspec_kw=dict(hspace=0.3))\nfor i, (uid, axi) in enumerate(zip(series['unique_id'].unique(), ax.flat)):\n fltr = lambda df: df['unique_id'].eq(uid)\n pd.concat([series.loc[fltr, ['ds', 'y']], predictions.loc[fltr]]).set_index('ds').plot(ax=axi)\n axi.set(title=uid, xlabel=None)\n if i % 2 == 0:\n axi.legend().remove()\n else:\n axi.legend(bbox_to_anchor=(1.01, 1.0))\nfig.savefig('figs/index.png', bbox_inches='tight')\nplt.close()" + }, + { + "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.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.\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": "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" + } +] \ 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..0adb6869 --- /dev/null +++ b/sitemap.xml @@ -0,0 +1,95 @@ + + + + https://Nixtla.github.io/forecast.html + 2023-09-06T21:03:46.298Z + + + https://Nixtla.github.io/distributed.models.dask.xgb.html + 2023-09-06T21:03:41.094Z + + + https://Nixtla.github.io/distributed.models.ray.lgb.html + 2023-09-06T21:03:39.706Z + + + https://Nixtla.github.io/docs/cross_validation.html + 2023-09-06T21:03:38.622Z + + + https://Nixtla.github.io/docs/quick_start_distributed.html + 2023-09-06T21:03:37.466Z + + + https://Nixtla.github.io/docs/install.html + 2023-09-06T21:03:36.142Z + + + https://Nixtla.github.io/docs/transfer_learning.html + 2023-09-06T21:03:33.942Z + + + https://Nixtla.github.io/docs/quick_start_local.html + 2023-09-06T21:03:32.462Z + + + https://Nixtla.github.io/grouped_array.html + 2023-09-06T21:03:31.270Z + + + https://Nixtla.github.io/utils.html + 2023-09-06T21:03:30.074Z + + + https://Nixtla.github.io/distributed.forecast.html + 2023-09-06T21:03:29.037Z + + + https://Nixtla.github.io/target_transforms.html + 2023-09-06T21:03:26.233Z + + + https://Nixtla.github.io/distributed.models.dask.lgb.html + 2023-09-06T21:03:26.937Z + + + https://Nixtla.github.io/distributed.models.spark.lgb.html + 2023-09-06T21:03:29.397Z + + + https://Nixtla.github.io/core.html + 2023-09-06T21:03:30.826Z + + + https://Nixtla.github.io/distributed.models.spark.xgb.html + 2023-09-06T21:03:31.890Z + + + https://Nixtla.github.io/docs/prediction_intervals.html + 2023-09-06T21:03:33.402Z + + + https://Nixtla.github.io/docs/end_to_end_walkthrough.html + 2023-09-06T21:03:35.690Z + + + https://Nixtla.github.io/docs/electricity_peak_forecasting.html + 2023-09-06T21:03:36.850Z + + + https://Nixtla.github.io/docs/target_transforms_guide.html + 2023-09-06T21:03:37.926Z + + + https://Nixtla.github.io/index.html + 2023-09-06T21:03:39.334Z + + + https://Nixtla.github.io/distributed.models.ray.xgb.html + 2023-09-06T21:03:40.150Z + + + https://Nixtla.github.io/lgb_cv.html + 2023-09-06T21:03:42.762Z + + 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..5603fd2c --- /dev/null +++ b/target_transforms.html @@ -0,0 +1,741 @@ + + + + + + + + + +mlforecast - Target transforms + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+ + +
+ +
+ + +
+ + + +
+ +
+
+

Target transforms

+
+ + + +
+ + + + +
+ + +
+ + +
+
import pandas as pd
+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.

+
+
+
+

LocalStandardScaler

+
+
 LocalStandardScaler ()
+
+

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

+
+
series = generate_daily_series(10, min_length=50, max_length=50)
+sc = LocalStandardScaler()
+sc.set_column_names('unique_id', 'ds', 'y')
+pd.testing.assert_frame_equal(
+    sc.inverse_transform(sc.fit_transform(series)),
+    series,
+)
+subset = series[series['unique_id'].isin(['id_0', 'id_7'])]
+sc.idxs = [0, 7]
+pd.testing.assert_frame_equal(
+    sc.inverse_transform(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..cbb3174d --- /dev/null +++ b/utils.html @@ -0,0 +1,1077 @@ + + + + + + + + + +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

+
+
+
+
+
series_sizes = series.groupby('unique_id').size()
+assert series_sizes.size == n_series
+assert series_sizes.min() >= min_length
+assert series_sizes.max() <= max_length
+
+

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-010.7521391810
1id_002000-01-022.4024501810
2id_002000-01-034.3396421810
3id_002000-01-046.5793171810
4id_002000-01-057.6674841810
..................
12446id_192002-03-112.7834778942
12447id_192002-03-1210.7051758942
12448id_192002-03-1320.9252858942
12449id_192002-03-1429.9987808942
12450id_192002-03-1538.7550548942
+ +

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