def from_group_dataframe(
        cls,
        df: pd.DataFrame,
        group_cols: Union[list[str], str],
        time_col: Optional[str] = None,
        value_cols: Optional[Union[list[str], str]] = None,
        static_cols: Optional[Union[list[str], str]] = None,
        metadata_cols: Optional[Union[list[str], str]] = None,
        fill_missing_dates: Optional[bool] = False,
        freq: Optional[Union[str, int]] = None,
        fillna_value: Optional[float] = None,
        drop_group_cols: Optional[Union[list[str], str]] = None,
        n_jobs: Optional[int] = 1,
        verbose: Optional[bool] = False,
    ) -> list[Self]:
        """
        Build a list of TimeSeries instances grouped by a selection of columns from a DataFrame.
        One column (or the DataFrame index) has to represent the time,
        a list of columns `group_cols` must be used for extracting the individual TimeSeries by groups,
        and a list of columns `value_cols` has to represent the values for the individual time series.
        Values from columns ``group_cols`` and ``static_cols`` are added as static covariates to the resulting
        TimeSeries objects. These can be viewed with `my_series.static_covariates`. Different to `group_cols`,
        `static_cols` only adds the static values but are not used to extract the TimeSeries groups.

        Parameters
        ----------
        df
            The DataFrame
        group_cols
            A string or list of strings representing the columns from the DataFrame by which to extract the
            individual TimeSeries groups.
        time_col
            The time column name. If set, the column will be cast to a pandas DatetimeIndex (if it contains
            timestamps) or a RangeIndex (if it contains integers).
            If not set, the DataFrame index will be used. In this case the DataFrame must contain an index that is
            either a pandas DatetimeIndex, a pandas RangeIndex, or a pandas Index that can be converted to a
            RangeIndex. Be aware that the index must represents the actual index of each individual time series group
            (can contain non-unique values). It is better if the index has no holes; alternatively setting
            `fill_missing_dates` can in some cases solve these issues (filling holes with NaN, or with the provided
            `fillna_value` numeric value, if any).
        value_cols
            A string or list of strings representing the value column(s) to be extracted from the DataFrame. If set to
            `None`, the whole DataFrame will be used.
        static_cols
            A string or list of strings representing static variable columns from the DataFrame that should be
            appended as static covariates to the resulting TimeSeries groups. Different to `group_cols`, the
            DataFrame is not grouped by these columns. Uses the first encountered value per group and column
            (assumes that there is only one unique value). Static covariates can be used as input features to all
            Darts models that support it.
        metadata_cols
            Same as `static_cols` but appended as metadata to the resulting TimeSeries groups. Metadata will never be
            used by the underlying Darts models.
        fill_missing_dates
            Optionally, a boolean value indicating whether to fill missing dates (or indices in case of integer index)
            with NaN values. This requires either a provided `freq` or the possibility to infer the frequency from the
            provided timestamps. See :meth:`_fill_missing_dates() <TimeSeries._fill_missing_dates>` for more info.
        freq
            Optionally, a string or integer representing the frequency of the underlying index. This is useful in order
            to fill in missing values if some dates are missing and `fill_missing_dates` is set to `True`.
            If a string, represents the frequency of the pandas DatetimeIndex (see `offset aliases
            <https://pandas.pydata.org/pandas-docs/stable/user_guide/timeseries.html#offset-aliases>`_ for more info on
            supported frequencies).
            If an integer, represents the step size of the pandas Index or pandas RangeIndex.
        fillna_value
            Optionally, a numeric value to fill missing values (NaNs) with.
        drop_group_cols
            Optionally, a string or list of strings with `group_cols` column(s) to exclude from the static covariates.
        n_jobs
            Optionally, an integer representing the number of parallel jobs to run. Behavior is the same as in the
            `joblib.Parallel` class.
        verbose
            Optionally, a boolean value indicating whether to display a progress bar.

        Returns
        -------
        List[TimeSeries]
            A list containing a univariate or multivariate deterministic TimeSeries per group in the DataFrame.

        Examples
        --------
        >>> import pandas as pd
        >>> from darts import TimeSeries
        >>> from darts.utils.utils import generate_index
        >>>
        >>> # create a DataFrame with two series that have different ids,
        >>> # values, and frequencies
        >>> df_1 = pd.DataFrame({
        >>>     "ID": [0] * 3,
        >>>     "vals": range(3),
        >>>     "time": generate_index("2020-01-01", length=3, freq="D")}
        >>> )
        >>> df_2 = pd.DataFrame({
        >>>     "ID": [1] * 6,
        >>>     "vals": range(6),
        >>>     "time": generate_index("2020-01-01", length=6, freq="h")}
        >>> )
        >>> df = pd.concat([df_1, df_2], axis=0)
        >>>
        >>> # extract the series by "ID" groups from the DataFrame
        >>> series_multi = TimeSeries.from_group_dataframe(
        >>>     df,
        >>>     group_cols="ID",
        >>>     time_col="time"
        >>> )
        >>> len(series_multi), series_multi[0].shape, series_multi[1].shape
        (2, (3, 1, 1), (6, 1, 1))
        """
        if time_col is None and df.index.is_monotonic_increasing:
            logger.warning(
                "UserWarning: `time_col` was not set and `df` has a monotonically increasing (time) index. This "
                "results in time series groups with non-overlapping (time) index. You can ignore this warning if the "
                "index represents the actual index of each individual time series group."
            )

        # group cols: used to extract time series groups from `df`, will also be added as static covariates
        # (except `drop_group_cols`)
        group_cols = [group_cols] if not isinstance(group_cols, list) else group_cols
        if drop_group_cols:
            drop_group_cols = (
                [drop_group_cols]
                if not isinstance(drop_group_cols, list)
                else drop_group_cols
            )
            invalid_cols = set(drop_group_cols) - set(group_cols)
            if invalid_cols:
                raise_log(
                    ValueError(
                        f"Found invalid `drop_group_cols` columns. All columns must be in the passed `group_cols`. "
                        f"Expected any of: {group_cols}, received: {invalid_cols}."
                    ),
                    logger=logger,
                )
            drop_group_col_idx = [
                idx for idx, col in enumerate(group_cols) if col in drop_group_cols
            ]
        else:
            drop_group_cols = []
            drop_group_col_idx = []

        # static covariates: all `group_cols` (except `drop_group_cols`) and `static_cols`
        if static_cols is not None:
            static_cols = (
                [static_cols] if not isinstance(static_cols, list) else static_cols
            )
        else:
            static_cols = []
        static_cov_cols = group_cols + static_cols
        # columns that are used as static covariates but not for grouping
        extract_static_cov_cols = [
            col for col in static_cov_cols if col not in drop_group_cols
        ]

        # metadata: all `metadata_cols`
        if metadata_cols is not None:
            metadata_cols = (
                [metadata_cols]
                if not isinstance(metadata_cols, list)
                else metadata_cols
            )
        else:
            metadata_cols = []
        # columns that are used as metadata but not for grouping or static covariates
        extract_metadata_cols = [
            col for col in metadata_cols if col not in static_cov_cols
        ]

        extract_time_col = [] if time_col is None else [time_col]

        if value_cols is None:
            value_cols = df.columns.drop(
                static_cov_cols + extract_metadata_cols + extract_time_col
            ).tolist()
        extract_value_cols = [value_cols] if isinstance(value_cols, str) else value_cols

        df = df[
            static_cov_cols
            + extract_value_cols
            + extract_time_col
            + extract_metadata_cols
        ]

        if time_col:
            if np.issubdtype(df[time_col].dtype, object) or np.issubdtype(
                df[time_col].dtype, np.datetime64
            ):
                df.index = pd.DatetimeIndex(df[time_col])
                df = df.drop(columns=time_col)
            else:
                df = df.set_index(time_col)

        if df.index.is_monotonic_increasing:
            logger.warning(
                "UserWarning: The (time) index from `df` is monotonically increasing. This "
                "results in time series groups with non-overlapping (time) index. You can ignore this warning if the "
                "index represents the actual index of each individual time series group."
            )

        # sort on entire `df` to avoid having to sort individually later on
        else:
            df = df.sort_index()

        groups = df.groupby(group_cols[0] if len(group_cols) == 1 else group_cols)

        # build progress bar for iterator
        iterator = _build_tqdm_iterator(
            groups,
            verbose=verbose,
            total=len(groups),
            desc="Creating TimeSeries",
        )

        def from_group(static_cov_vals, group):
            split = group[extract_value_cols]

            static_cov_vals = (
                (static_cov_vals,)
                if not isinstance(static_cov_vals, tuple)
                else static_cov_vals
            )
            # optionally, exclude group columns from static covariates
            if drop_group_col_idx:
                if len(drop_group_col_idx) == len(group_cols):
                    static_cov_vals = tuple()
                else:
                    static_cov_vals = tuple(
                        val
                        for idx, val in enumerate(static_cov_vals)
                        if idx not in drop_group_col_idx
                    )

            if static_cols:
                # use first value as static covariate (assume only one unique per group)
                static_cov_vals += tuple(group[static_cols].values[0])

            metadata = None
            if metadata_cols:
                # use first value as metadata (assume only one unique per group)
                metadata = {
                    col: val
                    for col, val in zip(metadata_cols, group[metadata_cols].values[0])
                }

            return cls.from_dataframe(
                df=split,
                fill_missing_dates=fill_missing_dates,
                freq=freq,
                fillna_value=fillna_value,
                static_covariates=(
                    pd.DataFrame([static_cov_vals], columns=extract_static_cov_cols)
                    if extract_static_cov_cols
                    else None
                ),
                metadata=metadata,
            )

        return _parallel_apply(
            iterator,
            from_group,
            n_jobs,
            fn_args=dict(),
            fn_kwargs=dict(),
        )

        Build a list of TimeSeries instances grouped by a selection of columns from a DataFrame.
        One column (or the DataFrame index) has to represent the time,
        a list of columns `group_cols` must be used for extracting the individual TimeSeries by groups,
        and a list of columns `value_cols` has to represent the values for the individual time series.
        Values from columns ``group_cols`` and ``static_cols`` are added as static covariates to the resulting
        TimeSeries objects. These can be viewed with `my_series.static_covariates`. Different to `group_cols`,
        `static_cols` only adds the static values but are not used to extract the TimeSeries groups.

        Parameters
        ----------
        df
            The DataFrame
        group_cols
            A string or list of strings representing the columns from the DataFrame by which to extract the
            individual TimeSeries groups.
        time_col
            The time column name. If set, the column will be cast to a pandas DatetimeIndex (if it contains
            timestamps) or a RangeIndex (if it contains integers).
            If not set, the DataFrame index will be used. In this case the DataFrame must contain an index that is
            either a pandas DatetimeIndex, a pandas RangeIndex, or a pandas Index that can be converted to a
            RangeIndex. Be aware that the index must represents the actual index of each individual time series group
            (can contain non-unique values). It is better if the index has no holes; alternatively setting
            `fill_missing_dates` can in some cases solve these issues (filling holes with NaN, or with the provided
            `fillna_value` numeric value, if any).
        value_cols
            A string or list of strings representing the value column(s) to be extracted from the DataFrame. If set to
            `None`, the whole DataFrame will be used.
        static_cols
            A string or list of strings representing static variable columns from the DataFrame that should be
            appended as static covariates to the resulting TimeSeries groups. Different to `group_cols`, the
            DataFrame is not grouped by these columns. Uses the first encountered value per group and column
            (assumes that there is only one unique value). Static covariates can be used as input features to all
            Darts models that support it.
        metadata_cols
            Same as `static_cols` but appended as metadata to the resulting TimeSeries groups. Metadata will never be
            used by the underlying Darts models.
        fill_missing_dates
            Optionally, a boolean value indicating whether to fill missing dates (or indices in case of integer index)
            with NaN values. This requires either a provided `freq` or the possibility to infer the frequency from the
            provided timestamps. See :meth:`_fill_missing_dates() <TimeSeries._fill_missing_dates>` for more info.
        freq
            Optionally, a string or integer representing the frequency of the underlying index. This is useful in order
            to fill in missing values if some dates are missing and `fill_missing_dates` is set to `True`.
            If a string, represents the frequency of the pandas DatetimeIndex (see `offset aliases
            <https://pandas.pydata.org/pandas-docs/stable/user_guide/timeseries.html#offset-aliases>`_ for more info on
            supported frequencies).
            If an integer, represents the step size of the pandas Index or pandas RangeIndex.
        fillna_value
            Optionally, a numeric value to fill missing values (NaNs) with.
        drop_group_cols
            Optionally, a string or list of strings with `group_cols` column(s) to exclude from the static covariates.
        n_jobs
            Optionally, an integer representing the number of parallel jobs to run. Behavior is the same as in the
            `joblib.Parallel` class.
        verbose
            Optionally, a boolean value indicating whether to display a progress bar.

        Returns
        -------
        List[TimeSeries]
            A list containing a univariate or multivariate deterministic TimeSeries per group in the DataFrame.

        Examples
        --------
        >>> import pandas as pd
        >>> from darts import TimeSeries
        >>> from darts.utils.utils import generate_index
        >>>
        >>> # create a DataFrame with two series that have different ids,
        >>> # values, and frequencies
        >>> df_1 = pd.DataFrame({
        >>>     "ID": [0] * 3,
        >>>     "vals": range(3),
        >>>     "time": generate_index("2020-01-01", length=3, freq="D")}
        >>> )
        >>> df_2 = pd.DataFrame({
        >>>     "ID": [1] * 6,
        >>>     "vals": range(6),
        >>>     "time": generate_index("2020-01-01", length=6, freq="h")}
        >>> )
        >>> df = pd.concat([df_1, df_2], axis=0)
        >>>
        >>> # extract the series by "ID" groups from the DataFrame
        >>> series_multi = TimeSeries.from_group_dataframe(
        >>>     df,
        >>>     group_cols="ID",
        >>>     time_col="time"
        >>> )
        >>> len(series_multi), series_multi[0].shape, series_multi[1].shape
        (2, (3, 1, 1), (6, 1, 1))
        
darts/timeseries.py