import pandas as pd
from ..tools import *
import plotly.graph_objects as go


def daily_profile(
    data, cyclic=False, time_origin=None, whs="1h", plot=False, log=False
):
    r"""Average daily activity/light/temperature distribution

    Calculate the daily profile of activity. Data are averaged over all the days.

    Parameters
    ----------
    data : pandas.Series, optional
        Input data series with a DatetimeIndex, where the index specifies the time points and
        the values represent the input variable (e.g., activity, light). Time and value arrays
        are extracted from this series.
    cyclic: bool, optional
        If set to True, two daily profiles are concatenated to ensure
        continuity between the last point of the day and the first one.
        Default is False.
    time_origin: str or pd.Timedelta, optional
        If not None, origin of the time axis for the daily profile.
        Original time bins are translated as time delta with respect to
        this new origin.
        Default is None
        Supported time string: 'AonT', 'AoffT', any 'HH:MM:SS'
    whs: str, optional
        Window half size parameter for the detection of the activity
        onset/offset time. Relevant only if time_origin is set to
        'AonT' or AoffT'.
        Default is '1h'.
    plot: bool, optional
        Whether to plot the daily profile. Default is False.
    log: bool, optional
        Whether the daily profile should be transformed to a log10 scale.

    Returns
    -------
    raw : pandas.Series
        A Series containing the daily activity profile with a 24h index.

    References
    ----------
    This code is derived from the original implementation in pyActigraphy, distributed under the BSD 3-Clause License.
    Original author: Grégory Hammad (gregory.hammad@uliege.be).

    [1] Hammad, G., Reyt, M., Beliy, N., Baillet, M., Deantoni, M., Lesoinne, A., Muto, V., & Schmidt, C. (2021).
    pyActigraphy: Open-source python package for actigraphy data visualization and analysis.
    PLoS Computational Biology, 17(10), 1009514–1009535. https://doi.org/10.1371/journal.pcbi.1009514

    [2] Hammad, G., Wulff, K., Skene, D. J., Münch, M., & Spitschan, M. (2024). Open-Source Python Module for the
    Analysis of Personalized Light Exposure Data from Wearable Light Loggers and Dosimeters.
    LEUKOS, 20(4), 380–389. https://doi.org/10.1080/15502724.2023.2296863
    """

    def _format(to_plot, to_log, profile):
        """
        Internal function that decides whether to present daily profile as pd.Series
        or as an interactive Plotly figure.

        Parameters
        ----------
        to_plot : bool
            Whether the daily profile should be plotted.
        to_log : bool
            Whether the daily profile should be transformed to a log10 scale.
        profile
            The daily profile returned by this function.

        Returns
        -------
        pandas.Series or go.Figure

        References
        ----------
        This code is derived from the original implementation in pyActigraphy, distributed under the BSD 3-Clause License.
        Original author: Grégory Hammad (gregory.hammad@uliege.be).

        [1] Hammad, G., Reyt, M., Beliy, N., Baillet, M., Deantoni, M., Lesoinne, A., Muto, V., & Schmidt, C. (2021).
        pyActigraphy: Open-source python package for actigraphy data visualization and analysis.
        PLoS Computational Biology, 17(10), 1009514–1009535. https://doi.org/10.1371/journal.pcbi.1009514

        [2] Hammad, G., Wulff, K., Skene, D. J., Münch, M., & Spitschan, M. (2024). Open-Source Python Module for the
        Analysis of Personalized Light Exposure Data from Wearable Light Loggers and Dosimeters.
        LEUKOS, 20(4), 380–389. https://doi.org/10.1080/15502724.2023.2296863
        """
        if to_plot:
            layout = go.Layout(
                title="Daily profile",
                xaxis=dict(title="Date time"),
                yaxis=dict(title="Data"),
                showlegend=False,
            )
            if to_log:
                fig = go.Figure(
                    data=[
                        go.Scatter(x=profile.index.astype(str), y=np.log10(profile + 1))
                    ],
                    layout=layout,
                )
                return fig
            else:
                fig = go.Figure(
                    data=[go.Scatter(x=profile.index.astype(str), y=profile)],
                    layout=layout,
                )
                return fig
        else:
            return profile

    if time_origin is None:
        _daily_profile = _average_daily_activity(data, cyclic=cyclic)
        return _format(plot, log, _daily_profile)
    else:
        if cyclic is True:
            raise NotImplementedError(
                "Setting a time origin while cyclic option is True is not "
                "implemented."
            )

        avgdaily = _average_daily_activity(data, cyclic=False)

        if isinstance(time_origin, str):
            # Regex pattern for HH:MM:SS time string
            pattern = re.compile(r"^([0-1]\d|2[0-3])(?::([0-5]\d))(?::([0-5]\d))$")

            if time_origin == "AonT":
                # Convert width half size from Timedelta to a nr of points
                whs = int(pd.Timedelta(whs) / data.index.freq)
                time_origin = _activity_onset_time(avgdaily, whs=whs)

            elif time_origin == "AoffT":
                # Convert width half size from Timedelta to a nr of points
                whs = int(pd.Timedelta(whs) / data.index.freq)
                time_origin = _activity_offset_time(avgdaily, whs=whs)

            elif pattern.match(time_origin):
                time_origin = pd.Timedelta(time_origin)

            else:
                raise ValueError(
                    "Time origin format ({}) not supported.\n".format(time_origin)
                    + "Supported format: {}.".format("HH:MM:SS")
                )

        elif not isinstance(time_origin, pd.Timedelta):
            raise ValueError(
                "Time origin is neither a time string with a supported"
                "format, nor a pd.Timedelta."
            )

        # Round time origin to the required frequency
        time_origin = time_origin.round(data.index.freq)

        shift = int((pd.Timedelta("12h") - time_origin) / data.index.freq)

        _daily_profile = _shift_time_axis(avgdaily, shift)
        return _format(plot, log, _daily_profile)


def daily_profile_auc(data, start_time=None, stop_time=None, time_origin=None):
    r"""AUC of the average daily light profile

    Calculate the area under the curve of the daily profile of light
    exposure. Data are averaged over all the days.

    Parameters
    ----------
    data : pandas.Series, optional
        Input data series with a DatetimeIndex, where the index specifies the time points and
        the values represent the input variable (e.g., activity, light). Time and value arrays
        are extracted from this series.
    start_time: str, optional
        If not set to None, compute AUC from start time.
        Supported time string: 'HH:MM:SS'
        Default is None.
    stop_time: str, optional
        If not set to None, compute AUC until stop time.
        Supported time string: 'HH:MM:SS'
        Default is None.
    time_origin: str or pd.Timedelta, optional
        If not None, origin of the time axis for the daily profile.
        Original time bins are translated as time delta with respect to
        this new origin.
        Default is None
        Supported time string: 'HH:MM:SS'

    Returns
    -------
    auc : float
        Area under the curve.

    References
    ----------
    This code is derived from the original implementation in pyActigraphy, distributed under the BSD 3-Clause License.
    Original author: Grégory Hammad (gregory.hammad@uliege.be).

    [1] Hammad, G., Reyt, M., Beliy, N., Baillet, M., Deantoni, M., Lesoinne, A., Muto, V., & Schmidt, C. (2021).
    pyActigraphy: Open-source python package for actigraphy data visualization and analysis.
    PLoS Computational Biology, 17(10), 1009514–1009535. https://doi.org/10.1371/journal.pcbi.1009514

    [2] Hammad, G., Wulff, K., Skene, D. J., Münch, M., & Spitschan, M. (2024). Open-Source Python Module for the
    Analysis of Personalized Light Exposure Data from Wearable Light Loggers and Dosimeters.
    LEUKOS, 20(4), 380–389. https://doi.org/10.1080/15502724.2023.2296863
    """
    # Compute average daily profile
    avgdaily = _average_daily_activity(data, cyclic=False)

    if time_origin is not None:

        if isinstance(time_origin, str):
            # Regex pattern for HH:MM:SS time string
            pattern = re.compile(r"^([0-1]\d|2[0-3])(?::([0-5]\d))(?::([0-5]\d))$")

            if pattern.match(time_origin):
                time_origin = pd.Timedelta(time_origin)
            else:
                raise ValueError(
                    "Time origin format ({}) not supported.\n".format(time_origin)
                    + "Supported format: HH:MM:SS."
                )

        elif not isinstance(time_origin, pd.Timedelta):
            raise ValueError(
                "Time origin is neither a time string with a supported "
                "format, nor a pd.Timedelta."
            )

        # Round time origin to the required frequency
        time_origin = time_origin.round(data.index.freq)

        shift = int((pd.Timedelta("12h") - time_origin) / data.index.freq)

        avgdaily = _shift_time_axis(avgdaily, shift)

    # Restrict profile to start/stop times
    if start_time is not None:
        start_time = pd.Timedelta(start_time)
    if stop_time is not None:
        stop_time = pd.Timedelta(stop_time)

    # In order to avoid indexing with None, check for that too
    if start_time is not None or stop_time is not None:
        return avgdaily.loc[start_time:stop_time].sum()
    else:
        return avgdaily.sum()


def adat(data, rescale=True, exclude_ends=False):
    """Total average daily activity

    Calculate the total activity counts, averaged over all the days.

    Parameters
    ----------
    data : pandas.Series, optional
        Input data series with a DatetimeIndex, where the index specifies the time points and
        the values represent the input variable (e.g., activity, light). Time and value arrays
        are extracted from this series.
    rescale: bool, optional
        If set to True, the activity counts are rescaled to account for
        masked periods (if any).
        Default is True.
    exclude_ends: bool, optional
        If set to True, the first and last daily periods are excluded from
        the calculation. Useful when the recording does start or end at
        midnigth.
        Default is False.

    Returns
    -------
    adat : int

    References
    ----------
    This code is derived from the original implementation in pyActigraphy, distributed under the BSD 3-Clause License.
    Original author: Grégory Hammad (gregory.hammad@uliege.be).

    [1] Hammad, G., Reyt, M., Beliy, N., Baillet, M., Deantoni, M., Lesoinne, A., Muto, V., & Schmidt, C. (2021).
    pyActigraphy: Open-source python package for actigraphy data visualization and analysis.
    PLoS Computational Biology, 17(10), 1009514–1009535. https://doi.org/10.1371/journal.pcbi.1009514

    [2] Hammad, G., Wulff, K., Skene, D. J., Münch, M., & Spitschan, M. (2024). Open-Source Python Module for the
    Analysis of Personalized Light Exposure Data from Wearable Light Loggers and Dosimeters.
    LEUKOS, 20(4), 380–389. https://doi.org/10.1080/15502724.2023.2296863
    """
    return _average_daily_total_activity(
        data, rescale=rescale, exclude_ends=exclude_ends
    )


def adatp(data, period="7D", rescale=True, exclude_ends=False, verbose=False):
    """Total average daily activity per period

    Calculate the total activity counts, averaged over each consecutive
    period contained in the data. The number of periods

    Parameters
    ----------
    data : pandas.Series, optional
        Input data series with a DatetimeIndex, where the index specifies the time points and
        the values represent the input variable (e.g., activity, light). Time and value arrays
        are extracted from this series.
    period: str, optional
        Time length of the period to be considered. Must be understandable
        by pandas.Timedelta
    rescale: bool, optional
        If set to True, the activity counts are rescaled to account for
        masked periods (if any).
        Default is True.
    exclude_ends: bool, optional
        If set to True, the first and last daily periods are excluded from
        the calculation. Useful when the recording does start or end at
        midnigth.
        Default is False.
    verbose: bool, optional
        If set to True, display the number of periods found in the data.
        Also display the time not accounted for.
        Default is False.

    Returns
    -------
    adatp : list of int


    References
    ----------
    This code is derived from the original implementation in pyActigraphy, distributed under the BSD 3-Clause License.
    Original author: Grégory Hammad (gregory.hammad@uliege.be).

    [1] Hammad, G., Reyt, M., Beliy, N., Baillet, M., Deantoni, M., Lesoinne, A., Muto, V., & Schmidt, C. (2021).
    pyActigraphy: Open-source python package for actigraphy data visualization and analysis.
    PLoS Computational Biology, 17(10), 1009514–1009535. https://doi.org/10.1371/journal.pcbi.1009514

    [2] Hammad, G., Wulff, K., Skene, D. J., Münch, M., & Spitschan, M. (2024). Open-Source Python Module for the
    Analysis of Personalized Light Exposure Data from Wearable Light Loggers and Dosimeters.
    LEUKOS, 20(4), 380–389. https://doi.org/10.1080/15502724.2023.2296863
    """
    intervals = _interval_maker(data.index, period, verbose)

    results = [
        _average_daily_total_activity(
            data[time[0] : time[1]], rescale=rescale, exclude_ends=exclude_ends
        )
        for time in intervals
    ]

    return results


def l5(data):
    r"""L5

    Mean activity/temperature/light, etc., during the 5 least active hours of the day.

    Parameters
    ----------
    data : pandas.Series, optional
        Input data series with a DatetimeIndex, where the index specifies the time points and
        the values represent the input variable (e.g., activity, light). Time and value arrays
        are extracted from this series.

    Returns
    -------
    l5_onset, l5 : float

    Notes
    -----

    The L5 [1]_ variable is calculated as the mean, per acquisition period,
    of the average daily activities during the 5 least active hours.

    .. warning:: The value of this variable depends on the length of the
    acquisition period.

    References
    ----------
    This code is derived from the original implementation in pyActigraphy, distributed under the BSD 3-Clause License.
    Original author: Grégory Hammad (gregory.hammad@uliege.be).

    [1] Van Someren, E.J.W., Lijzenga, C., Mirmiran, M., Swaab, D.F. (1997). Long-Term Fitness Training Improves
    the Circadian Rest-Activity Rhythm in Healthy Elderly Males. Journal of Biological Rhythms, 12(2), 146–156.
    http://doi.org/10.1177/074873049701200206

    [2] Hammad, G., Reyt, M., Beliy, N., Baillet, M., Deantoni, M., Lesoinne, A., Muto, V., & Schmidt, C. (2021).
    pyActigraphy: Open-source python package for actigraphy data visualization and analysis.
    PLoS Computational Biology, 17(10), 1009514–1009535. https://doi.org/10.1371/journal.pcbi.1009514

    [3] Hammad, G., Wulff, K., Skene, D. J., Münch, M., & Spitschan, M. (2024). Open-Source Python Module for the
    Analysis of Personalized Light Exposure Data from Wearable Light Loggers and Dosimeters.
    LEUKOS, 20(4), 380–389. https://doi.org/10.1080/15502724.2023.2296863
    """

    l5_onset, l5 = _lmx(data, "5h", lowest=True)
    return l5_onset, l5


def m10(data):
    r"""M10

    Mean activity/light/temperature, etc. during the 10 most active hours of the day.

    Parameters
    ----------
    data : pandas.Series, optional
        Input data series with a DatetimeIndex, where the index specifies the time points and
        the values represent the input variable (e.g., activity, light). Time and value arrays
        are extracted from this series.

    Returns
    -------
    m10_onset, m10 : float

    Notes
    -----

    The M10 [1]_ variable is calculated as the mean, per acquisition period
    , of the average daily activities during the 10 most active hours.

    .. warning:: The value of this variable depends on the length of the
                 acquisition period.

    References
    ----------
    This code is derived from the original implementation in pyActigraphy, distributed under the BSD 3-Clause License.
    Original author: Grégory Hammad (gregory.hammad@uliege.be).

    [1] Van Someren, E.J.W., Lijzenga, C., Mirmiran, M., Swaab, D.F. (1997). Long-Term Fitness Training Improves
    the Circadian Rest-Activity Rhythm in Healthy Elderly Males. Journal of Biological Rhythms, 12(2), 146–156.
    http://doi.org/10.1177/074873049701200206

    [2] Hammad, G., Reyt, M., Beliy, N., Baillet, M., Deantoni, M., Lesoinne, A., Muto, V., & Schmidt, C. (2021).
    pyActigraphy: Open-source python package for actigraphy data visualization and analysis.
    PLoS Computational Biology, 17(10), 1009514–1009535. https://doi.org/10.1371/journal.pcbi.1009514

    [3] Hammad, G., Wulff, K., Skene, D. J., Münch, M., & Spitschan, M. (2024). Open-Source Python Module for the
    Analysis of Personalized Light Exposure Data from Wearable Light Loggers and Dosimeters.
    LEUKOS, 20(4), 380–389. https://doi.org/10.1080/15502724.2023.2296863
    """
    m10_onset, m10 = _lmx(data, "10h", lowest=False)
    return m10_onset, m10


def ra(data):
    r"""Relative rest/activity amplitude

    Relative amplitude between the mean activity during the 10 most active
    hours of the day and the mean activity during the 5 least active hours
    of the day.

    Parameters
    ----------
    data : pandas.Series, optional
        Input data series with a DatetimeIndex, where the index specifies the time points and
        the values represent the input variable (e.g., activity, light). Time and value arrays
        are extracted from this series.

    Returns
    -------
    ra: float

    Notes
    -----

    The RA [1]_ variable is calculated as:

    .. math::

        RA = \frac{M10 - L5}{M10 + L5}

    References
    ----------
    This code is derived from the original implementation in pyActigraphy, distributed under the BSD 3-Clause License.
    Original author: Grégory Hammad (gregory.hammad@uliege.be).

    [1] Van Someren, E.J.W., Lijzenga, C., Mirmiran, M., Swaab, D.F. (1997). Long-Term Fitness Training Improves the
    Circadian Rest-Activity Rhythm in Healthy Elderly Males. Journal of Biological Rhythms, 12(2), 146–156.
    http://doi.org/10.1177/074873049701200206

    [2] Hammad, G., Reyt, M., Beliy, N., Baillet, M., Deantoni, M., Lesoinne, A., Muto, V., & Schmidt, C. (2021).
    pyActigraphy: Open-source python package for actigraphy data visualization and analysis.
    PLoS Computational Biology, 17(10), 1009514–1009535. https://doi.org/10.1371/journal.pcbi.1009514

    [3] Hammad, G., Wulff, K., Skene, D. J., Münch, M., & Spitschan, M. (2024). Open-Source Python Module for the
    Analysis of Personalized Light Exposure Data from Wearable Light Loggers and Dosimeters.
    LEUKOS, 20(4), 380–389. https://doi.org/10.1080/15502724.2023.2296863
    """
    _, l5 = _lmx(data, "5h", lowest=True)
    _, m10 = _lmx(data, "10h", lowest=False)
    return (m10 - l5) / (m10 + l5)


def l5p(data, period="7D", verbose=False):
    r"""L5 per period

    The L5 variable is calculated for each consecutive period found in the
    actigraphy recording.

    Parameters
    ----------
    data : pandas.Series, optional
        Input data series with a DatetimeIndex, where the index specifies the time points and
        the values represent the input variable (e.g., activity, light). Time and value arrays
        are extracted from this series.
    period: str, optional
        Time period for the calculation of IS
        Default is '7D'.
    verbose: bool, optional
        If set to True, display the number of periods found in the activity
        recording, as well as the time not accounted for.
        Default is False.

    Returns
    -------
    l5p: list of float

    Notes
    -----

    The L5 [1]_ variable is calculated as the mean, per acquisition period,
    of the average daily activities during the 5 least active hours.

    .. warning:: The value of this variable depends on the length of the
                  acquisition period.

    References
    ----------
    This code is derived from the original implementation in pyActigraphy, distributed under the BSD 3-Clause License.
    Original author: Grégory Hammad (gregory.hammad@uliege.be).

    [1] Van Someren, E.J.W., Lijzenga, C., Mirmiran, M., Swaab, D.F. (1997). Long-Term Fitness Training Improves the
    Circadian Rest-Activity Rhythm in Healthy Elderly Males. Journal of Biological Rhythms, 12(2), 146–156.
    http://doi.org/10.1177/074873049701200206

    [2] Hammad, G., Reyt, M., Beliy, N., Baillet, M., Deantoni, M., Lesoinne, A., Muto, V., & Schmidt, C. (2021).
    pyActigraphy: Open-source python package for actigraphy data visualization and analysis.
    PLoS Computational Biology, 17(10), 1009514–1009535. https://doi.org/10.1371/journal.pcbi.1009514

    [3] Hammad, G., Wulff, K., Skene, D. J., Münch, M., & Spitschan, M. (2024). Open-Source Python Module for the
    Analysis of Personalized Light Exposure Data from Wearable Light Loggers and Dosimeters.
    LEUKOS, 20(4), 380–389. https://doi.org/10.1080/15502724.2023.2296863
    """
    intervals = _interval_maker(data.index, period, verbose)

    results = [_lmx(data[time[0] : time[1]], "5h", lowest=True) for time in intervals]
    return [res[1] for res in results]


def m10p(data, period="7D", verbose=False):
    r"""M10 per period

    The M10 variable is calculated for each consecutive period found in the
    actigraphy recording.

    Parameters
    ----------
    data : pandas.Series, optional
        Input data series with a DatetimeIndex, where the index specifies the time points and
        the values represent the input variable (e.g., activity, light). Time and value arrays
        are extracted from this series.
    period: str, optional
        Time period for the calculation of IS
        Default is '7D'.
        If set to True, display the number of periods found in the activity
        recording, as well as the time not accounted for.
        Default is False.
    verbose: bool, optional
        If set to True, display the number of periods found in the activity
        recording, as well as the time not accounted for.
        Default is False.

    Returns
    -------
    m10p: list of float


    Notes
    -----

    The M10 [1]_ variable is calculated as the mean, per acquisition period
    , of the average daily activities during the 10 most active hours.

    .. warning:: The value of this variable depends on the length of the
                     acquisition period.

    References
    ----------
    This code is derived from the original implementation in pyActigraphy, distributed under the BSD 3-Clause License.
    Original author: Grégory Hammad (gregory.hammad@uliege.be).

    [1] Van Someren, E.J.W., Lijzenga, C., Mirmiran, M., Swaab, D.F. (1997). Long-Term Fitness Training Improves the
    Circadian Rest-Activity Rhythm in Healthy Elderly Males. Journal of Biological Rhythms, 12(2), 146–156.
    http://doi.org/10.1177/074873049701200206

    [2] Hammad, G., Reyt, M., Beliy, N., Baillet, M., Deantoni, M., Lesoinne, A., Muto, V., & Schmidt, C. (2021).
    pyActigraphy: Open-source python package for actigraphy data visualization and analysis.
    PLoS Computational Biology, 17(10), 1009514–1009535. https://doi.org/10.1371/journal.pcbi.1009514

    [3] Hammad, G., Wulff, K., Skene, D. J., Münch, M., & Spitschan, M. (2024). Open-Source Python Module for the
    Analysis of Personalized Light Exposure Data from Wearable Light Loggers and Dosimeters.
    LEUKOS, 20(4), 380–389. https://doi.org/10.1080/15502724.2023.2296863
    """
    intervals = _interval_maker(data.index, period, verbose)

    results = [_lmx(data[time[0] : time[1]], "10h", lowest=False) for time in intervals]
    return [res[1] for res in results]


def rap(data, period="7D", verbose=False):
    r"""RA per period

    The RA variable is calculated for each consecutive period found in the
    actigraphy recording.

    Parameters
    ----------
    data : pandas.Series, optional
        Input data series with a DatetimeIndex, where the index specifies the time points and
        the values represent the input variable (e.g., activity, light). Time and value arrays
        are extracted from this series.
    period: str, optional
        Time period for the calculation of IS
        Default is '7D'.
    verbose: bool, optional
        If set to True, display the number of periods found in the activity
        recording, as well as the time not accounted for.
        Default is False.

    Returns
    -------
    rap: list of float

    Notes
    -----

    The RA [1]_ variable is calculated as:

    .. math::

        RA = \frac{M10 - L5}{M10 + L5}

    References
    ----------
    This code is derived from the original implementation in pyActigraphy, distributed under the BSD 3-Clause License.
    Original author: Grégory Hammad (gregory.hammad@uliege.be).

    [1] Van Someren, E.J.W., Lijzenga, C., Mirmiran, M., Swaab, D.F. (1997). Long-Term Fitness Training Improves the
    Circadian Rest-Activity Rhythm in Healthy Elderly Males. Journal of Biological Rhythms, 12(2), 146–156.
    http://doi.org/10.1177/074873049701200206

    [2] Hammad, G., Reyt, M., Beliy, N., Baillet, M., Deantoni, M., Lesoinne, A., Muto, V., & Schmidt, C. (2021).
    pyActigraphy: Open-source python package for actigraphy data visualization and analysis.
    PLoS Computational Biology, 17(10), 1009514–1009535. https://doi.org/10.1371/journal.pcbi.1009514

    [3] Hammad, G., Wulff, K., Skene, D. J., Münch, M., & Spitschan, M. (2024). Open-Source Python Module for the
    Analysis of Personalized Light Exposure Data from Wearable Light Loggers and Dosimeters.
    LEUKOS, 20(4), 380–389. https://doi.org/10.1080/15502724.2023.2296863
    """
    intervals = _interval_maker(data.index, period, verbose)

    results = []

    for time in intervals:
        data_subset = data[time[0] : time[1]]
        _, l5 = _lmx(data_subset, "5h", lowest=True)
        _, m10 = _lmx(data_subset, "10h", lowest=False)
        results.append((m10 - l5) / (m10 + l5))

    return results


def IS(data):
    r"""Interdaily stability

    The Interdaily stability (IS) quantifies the repeatibilty of the
    daily rest-activity pattern over each day contained in the activity
    recording.

    Parameters
    ----------
    data : pandas.Series, optional
        Input data series with a DatetimeIndex, where the index specifies the time points and
        the values represent the input variable (e.g., activity, light). Time and value arrays
        are extracted from this series.

    Returns
    -------
    is: float

    Notes
    -----

    This variable is defined in ref [1]_:

    .. math::

        IS = \frac{d^{24h}}{d^{1h}}

    with:

    .. math::

        d^{1h} = \sum_{i}^{n}\frac{\left(x_{i}-\bar{x}\right)^{2}}{n}

    where :math:`x_{i}` is the number of active (counts higher than a
    predefined threshold) minutes during the :math:`i^{th}` period,
    :math:`\bar{x}` is the mean of all data and :math:`n` is the number of
    periods covered by the actigraphy data and with:

    .. math::

        d^{24h} = \sum_{i}^{p} \frac{
                    \left( \bar{x}_{h,i} - \bar{x} \right)^{2}
                    }{p}

    where :math:`\bar{x}^{h,i}` is the average number of active minutes
    over the :math:`i^{th}` period and :math:`p` is the number of periods
    per day. The average runs over all the days.

    For the record, tt is the 24h value from the chi-square periodogram
    (Sokolove and Bushel1 1978).

    References
    ----------
    This code is derived from the original implementation in pyActigraphy, distributed under the BSD 3-Clause License.
    Original author: Grégory Hammad (gregory.hammad@uliege.be).

    [1] Witting W., Kwa I.H., Eikelenboom P., Mirmiran M., Swaab D.F. Alterations in the circadian rest–activity
    rhythm in aging and Alzheimer׳s disease. Biol Psychiatry. 1990;27:563–572.

    [2] Hammad, G., Reyt, M., Beliy, N., Baillet, M., Deantoni, M., Lesoinne, A., Muto, V., & Schmidt, C. (2021).
    pyActigraphy: Open-source python package for actigraphy data visualization and analysis.
    PLoS Computational Biology, 17(10), 1009514–1009535. https://doi.org/10.1371/journal.pcbi.1009514

    [3] Hammad, G., Wulff, K., Skene, D. J., Münch, M., & Spitschan, M. (2024). Open-Source Python Module for the
    Analysis of Personalized Light Exposure Data from Wearable Light Loggers and Dosimeters.
    LEUKOS, 20(4), 380–389. https://doi.org/10.1080/15502724.2023.2296863
    """
    d_24h = (
        data.groupby([data.index.hour, data.index.minute, data.index.second])
        .mean()
        .var()
    )

    d_1h = data.var()

    return d_24h / d_1h


def ISp(data, period="7D", verbose=False):
    r"""Interdaily stability per period

    The IS is calculated for each consecutive period found in the
    actigraphy recording.

    Parameters
    ----------
    data : pandas.Series, optional
        Input data series with a DatetimeIndex, where the index specifies the time points and
        the values represent the input variable (e.g., activity, light). Time and value arrays
        are extracted from this series.
    period: str, optional
        Time period for the calculation of IS
        Default is '7D'.
    verbose: bool, optional
        If set to True, display the number of periods found in the activity
        recording, as well as the time not accounted for.
        Default is False.

    Returns
    -------
    isp: list of float

    Notes
    -----

    Periods are consecutive and all of the required duration. If the last
    consecutive period is shorter than required, the IS is not calculated
    for that period.


    References
    ----------
    This code is derived from the original implementation in pyActigraphy, distributed under the BSD 3-Clause License.
    Original author: Grégory Hammad (gregory.hammad@uliege.be).

    [1] Hammad, G., Reyt, M., Beliy, N., Baillet, M., Deantoni, M., Lesoinne, A., Muto, V., & Schmidt, C. (2021).
    pyActigraphy: Open-source python package for actigraphy data visualization and analysis.
    PLoS Computational Biology, 17(10), 1009514–1009535. https://doi.org/10.1371/journal.pcbi.1009514

    [2] Hammad, G., Wulff, K., Skene, D. J., Münch, M., & Spitschan, M. (2024). Open-Source Python Module for the
    Analysis of Personalized Light Exposure Data from Wearable Light Loggers and Dosimeters.
    LEUKOS, 20(4), 380–389. https://doi.org/10.1080/15502724.2023.2296863
    """
    intervals = _interval_maker(data.index, period, verbose)

    results = [IS(data[time[0] : time[1]]) for time in intervals]
    return results


def IV(data):
    r"""Intradaily variability

    The Intradaily Variability (IV) quantifies the variability of the
    activity recording. This variable thus measures the rest or activity
    fragmentation.

    Parameters
    ----------
    data : pandas.Series, optional
        Input data series with a DatetimeIndex, where the index specifies the time points and
        the values represent the input variable (e.g., activity, light). Time and value arrays
        are extracted from this series.

    Returns
    -------
    iv: float

    Notes
    -----

    It is defined in ref [1]_:

    .. math::

        IV = \frac{c^{1h}}{d^{1h}}

    with:

    .. math::

        d^{1h} = \sum_{i}^{n}\frac{\left(x_{i}-\bar{x}\right)^{2}}{n}

    where :math:`x_{i}` is the number of active (counts higher than a
    predefined threshold) minutes during the :math:`i^{th}` period,
    :math:`\bar{x}` is the mean of all data and :math:`n` is the number of
    periods covered by the actigraphy data,

    and with:

    .. math::

        c^{1h} = \sum_{i}^{n-1} \frac{
                    \left( x_{i+1} - x_{i} \right)^{2}
                    }{n-1}

    References
    ----------
    This code is derived from the original implementation in pyActigraphy, distributed under the BSD 3-Clause License.
    Original author: Grégory Hammad (gregory.hammad@uliege.be).

    [1] Witting W., Kwa I.H., Eikelenboom P., Mirmiran M., Swaab D.F. Alterations in the circadian rest–activity
    rhythm in aging and Alzheimer׳s disease. Biol Psychiatry. 1990;27:563–572.

    [2] Hammad, G., Reyt, M., Beliy, N., Baillet, M., Deantoni, M., Lesoinne, A., Muto, V., & Schmidt, C. (2021).
    pyActigraphy: Open-source python package for actigraphy data visualization and analysis.
    PLoS Computational Biology, 17(10), 1009514–1009535. https://doi.org/10.1371/journal.pcbi.1009514

    [3] Hammad, G., Wulff, K., Skene, D. J., Münch, M., & Spitschan, M. (2024). Open-Source Python Module for the
    Analysis of Personalized Light Exposure Data from Wearable Light Loggers and Dosimeters.
    LEUKOS, 20(4), 380–389. https://doi.org/10.1080/15502724.2023.2296863
    """
    c_1h = data.diff(1).pow(2).mean()

    d_1h = data.var()

    return c_1h / d_1h


def IVp(data, period="7D", verbose=False):
    r"""Intradaily variability per period

    The IV is calculated for each consecutive period found in the
    actigraphy recording.

    Parameters
    ----------
    data : pandas.Series, optional
        Input data series with a DatetimeIndex, where the index specifies the time points and
        the values represent the input variable (e.g., activity, light). Time and value arrays
        are extracted from this series.
    period: str, optional
        Time period for the calculation of IS
        Default is '7D'.
    verbose: bool, optional
        If set to True, display the number of periods found in the activity
        recording, as well as the time not accounted for.
        Default is False.

    Returns
    -------
    ivp: list of float

    Notes
    -----

    Periods are consecutive and all of the required duration. If the last
    consecutive period is shorter than required, the IV is not calculated
    for that period.


    References
    ----------
    This code is derived from the original implementation in pyActigraphy, distributed under the BSD 3-Clause License.
    Original author: Grégory Hammad (gregory.hammad@uliege.be).

    [1] Hammad, G., Reyt, M., Beliy, N., Baillet, M., Deantoni, M., Lesoinne, A., Muto, V., & Schmidt, C. (2021).
    pyActigraphy: Open-source python package for actigraphy data visualization and analysis.
    PLoS Computational Biology, 17(10), 1009514–1009535. https://doi.org/10.1371/journal.pcbi.1009514

    [2] Hammad, G., Wulff, K., Skene, D. J., Münch, M., & Spitschan, M. (2024). Open-Source Python Module for the
    Analysis of Personalized Light Exposure Data from Wearable Light Loggers and Dosimeters.
    LEUKOS, 20(4), 380–389. https://doi.org/10.1080/15502724.2023.2296863
    """
    intervals = _interval_maker(data.index, period, verbose)

    results = [IV(data[time[0] : time[1]]) for time in intervals]
    return results


def summary_stats(light, bins="24h", agg_func=None):
    r"""Summary statistics.

    Calculate summary statistics (ex: mean, median, etc) according to a
    user-defined (regular or arbitrary) binning.

    Parameters
    ----------
    light : pandas.Series, optional
        Input data series with a DatetimeIndex, where the index specifies the time points and
        the values represent the input variable (in this case, light). Time and value arrays
        are extracted from this series.
    bins : str or list of tuples, optional
        If set to a string, bins is used to define a regular binning where
        every bin is of length "bins". Ex: "2h".
        Otherwise, the list of 2-tuples is used to define an arbitrary
        binning. Ex: \[('2000-01-01 00:00:00','2000-01-01 11:59:00')\].
        Default is '24h'.
    agg_func : list, optional
        List of aggregation functions to be used on every bin.
        Default is \['mean', 'median', 'sum', 'std', 'min', 'max'\].

    Returns
    -------
    ss : pd.DataFrame
        A pandas DataFrame with summary statistics per channel.

    References
    ----------
    This code is derived from the original implementation in pyActigraphy, distributed under the BSD 3-Clause License.
    Original author: Grégory Hammad (gregory.hammad@uliege.be).

    [1] Hammad, G., Reyt, M., Beliy, N., Baillet, M., Deantoni, M., Lesoinne, A., Muto, V., & Schmidt, C. (2021).
    pyActigraphy: Open-source python package for actigraphy data visualization and analysis.
    PLoS Computational Biology, 17(10), 1009514–1009535. https://doi.org/10.1371/journal.pcbi.1009514

    [2] Hammad, G., Wulff, K., Skene, D. J., Münch, M., & Spitschan, M. (2024). Open-Source Python Module for the
    Analysis of Personalized Light Exposure Data from Wearable Light Loggers and Dosimeters.
    LEUKOS, 20(4), 380–389. https://doi.org/10.1080/15502724.2023.2296863
    """
    if agg_func is None:
        agg_func = ["mean", "median", "sum", "std", "min", "max"]
    if isinstance(bins, str):
        summary_stats = light.resample(bins).agg(agg_func)
    elif isinstance(bins, list):
        df_col = []
        for idx, (start, end) in enumerate(bins):
            df_bins = (
                light.loc[start:end, :].apply(agg_func).pivot_table(columns=agg_func)
            )

            channels = df_bins
            channels = channels.loc[:, agg_func]
            df_col.append(pd.concat(channels, axis=1))
            summary_stats = pd.concat(df_col)

    return summary_stats


def light_exposure(light, threshold=None, start_time=None, stop_time=None, agg="mean"):
    r"""Light exposure level

    Calculate the aggregated (mean, median, etc) light exposure level
    per epoch.

    Parameters
    ----------
    light : pandas.Series, optional
        Input data series with a DatetimeIndex, where the index specifies the time points and
        the values represent the input variable (in this case, light). Time and value arrays
        are extracted from this series.
    threshold: float, optional
        If not set to None, discard data below threshold before computing
        exposure levels.
        Default is None.
    start_time: str, optional
        If not set to None, discard data before start time,
        on a daily basis.
        Supported time string: 'HH:MM:SS'
        Default is None.
    stop_time: str, optional
        If not set to None, discard data after stop time, on a daily basis.
        Supported time string: 'HH:MM:SS'
        Default is None.
    agg: str, optional
        Aggregating function used to summarize exposure levels.
        Available functions: 'mean', 'median', 'std', etc.
        Default is 'mean'.

    Returns
    -------
    levels : pd.Series
        A pandas Series with aggreagted light exposure levels per channel

    References
    ----------
    This code is derived from the original implementation in pyActigraphy, distributed under the BSD 3-Clause License.
    Original author: Grégory Hammad (gregory.hammad@uliege.be).

    [1] Hammad, G., Reyt, M., Beliy, N., Baillet, M., Deantoni, M., Lesoinne, A., Muto, V., & Schmidt, C. (2021).
    pyActigraphy: Open-source python package for actigraphy data visualization and analysis.
    PLoS Computational Biology, 17(10), 1009514–1009535. https://doi.org/10.1371/journal.pcbi.1009514

    [2] Hammad, G., Wulff, K., Skene, D. J., Münch, M., & Spitschan, M. (2024). Open-Source Python Module for the
    Analysis of Personalized Light Exposure Data from Wearable Light Loggers and Dosimeters.
    LEUKOS, 20(4), 380–389. https://doi.org/10.1080/15502724.2023.2296863
    """
    light_exposure = _light_exposure(
        light=light, threshold=threshold, start_time=start_time, stop_time=stop_time
    )

    levels = getattr(light_exposure, agg)

    return levels()


def TAT(data, threshold=None, start_time=None, stop_time=None, oformat=None):
    r"""Time above light threshold.

    Calculate the total light exposure time above the threshold.

    Parameters
    ----------
    data : pandas.Series, optional
        Input data series with a DatetimeIndex, where the index specifies the time points and
        the values represent the input variable (e.g., activity, light). Time and value arrays
        are extracted from this series.
    threshold: float, optional
        If not set to None, discard data below threshold before computing
        exposure levels.
        Default is None.
    start_time: str, optional
        If not set to None, discard data before start time,
        on a daily basis.
        Supported time string: 'HH:MM:SS'
        Default is None.
    stop_time: str, optional
        If not set to None, discard data after stop time, on a daily basis.
        Supported time string: 'HH:MM:SS'
        Default is None.
    oformat: str, optional
        Output format. Available formats: 'minute' or 'timedelta'.
        If set to 'minute', the result is in number of minutes.
        If set to 'timedelta', the result is a pd.Timedelta.
        If set to None, the result is in number of epochs.
        Default is None.

    Returns
    -------
    tat : pd.Series
        A pandas Series with aggreagted light exposure levels per channel

    References
    ----------
    This code is derived from the original implementation in pyActigraphy, distributed under the BSD 3-Clause License.
    Original author: Grégory Hammad (gregory.hammad@uliege.be).

    [1] Hammad, G., Reyt, M., Beliy, N., Baillet, M., Deantoni, M., Lesoinne, A., Muto, V., & Schmidt, C. (2021).
    pyActigraphy: Open-source python package for actigraphy data visualization and analysis.
    PLoS Computational Biology, 17(10), 1009514–1009535. https://doi.org/10.1371/journal.pcbi.1009514

    [2] Hammad, G., Wulff, K., Skene, D. J., Münch, M., & Spitschan, M. (2024). Open-Source Python Module for the
    Analysis of Personalized Light Exposure Data from Wearable Light Loggers and Dosimeters.
    LEUKOS, 20(4), 380–389. https://doi.org/10.1080/15502724.2023.2296863
    """
    available_formats = [None, "minute", "timedelta"]

    if oformat not in available_formats:
        raise ValueError(
            "Specified output format ({}) not supported. ".format(oformat)
            + "Available formats are: {}".format(str(available_formats))
        )

    light_exposure_counts = _light_exposure(
        light=data, threshold=threshold, start_time=start_time, stop_time=stop_time
    ).count()

    if oformat == "minute":
        tat = (
            light_exposure_counts * pd.Timedelta(data.index.freq) / pd.Timedelta("1min")
        )
    elif oformat == "timedelta":
        tat = light_exposure_counts * pd.Timedelta(data.index.freq)
    else:
        tat = light_exposure_counts

    return tat


def TATp(data, threshold=None, start_time=None, stop_time=None, oformat=None):
    r"""Time above light threshold (per day).

    Calculate the total light exposure time above the threshold,
    per calendar day.

    Parameters
    ----------
    data : pandas.Series, optional
        Input data series with a DatetimeIndex, where the index specifies the time points and
        the values represent the input variable (e.g., activity, light). Time and value arrays
        are extracted from this series.
    threshold: float, optional
        If not set to None, discard data below threshold before computing
        exposure levels.
        Default is None.
    start_time: str, optional
        If not set to None, discard data before start time,
        on a daily basis.
        Supported time string: 'HH:MM:SS'
        Default is None.
    stop_time: str, optional
        If not set to None, discard data after stop time, on a daily basis.
        Supported time string: 'HH:MM:SS'
        Default is None.
    oformat: str, optional
        Output format. Available formats: 'minute' or 'timedelta'.
        If set to 'minute', the result is in number of minutes.
        If set to 'timedelta', the result is a pd.Timedelta.
        If set to None, the result is in number of epochs.
        Default is None.

    Returns
    -------
    tatp : pd.DataFrame
        A pandas DataFrame with aggreagted light exposure levels
        per channel and per day.

    References
    ----------
    This code is derived from the original implementation in pyActigraphy, distributed under the BSD 3-Clause License.
    Original author: Grégory Hammad (gregory.hammad@uliege.be).

    [1] Hammad, G., Reyt, M., Beliy, N., Baillet, M., Deantoni, M., Lesoinne, A., Muto, V., & Schmidt, C. (2021).
    pyActigraphy: Open-source python package for actigraphy data visualization and analysis.
    PLoS Computational Biology, 17(10), 1009514–1009535. https://doi.org/10.1371/journal.pcbi.1009514

    [2] Hammad, G., Wulff, K., Skene, D. J., Münch, M., & Spitschan, M. (2024). Open-Source Python Module for the
    Analysis of Personalized Light Exposure Data from Wearable Light Loggers and Dosimeters.
    LEUKOS, 20(4), 380–389. https://doi.org/10.1080/15502724.2023.2296863
    """
    available_formats = [None, "minute", "timedelta"]

    if oformat not in available_formats:
        raise ValueError(
            "Specified output format ({}) not supported. ".format(oformat)
            + "Available formats are: {}".format(str(available_formats))
        )

    light_exposure_counts_per_day = (
        _light_exposure(
            light=data, threshold=threshold, start_time=start_time, stop_time=stop_time
        )
        .groupby(data.index.date)
        .count()
    )

    if oformat == "minute":
        tatp = (
            light_exposure_counts_per_day
            * pd.Timedelta(data.index.freq)
            / pd.Timedelta("1min")
        )
    elif oformat == "timedelta":
        tatp = light_exposure_counts_per_day * pd.Timedelta(data.index.freq)
    else:
        tatp = light_exposure_counts_per_day

    return tatp


def VAT(data, threshold=None):
    r"""Values above light threshold.

    Returns the light exposure values above the threshold.

    Parameters
    ----------
    data : pandas.Series, optional
        Input data series with a DatetimeIndex, where the index specifies the time points and
        the values represent the input variable (e.g., activity, light). Time and value arrays
        are extracted from this series.
    threshold: float, optional
        If not set to None, discard data below threshold before computing
        exposure levels.
        Default is None.

    Returns
    -------
    vat : pd.Series
        A pandas Series with light exposure levels per channel

    References
    ----------
    This code is derived from the original implementation in pyActigraphy, distributed under the BSD 3-Clause License.
    Original author: Grégory Hammad (gregory.hammad@uliege.be).

    [1] Hammad, G., Reyt, M., Beliy, N., Baillet, M., Deantoni, M., Lesoinne, A., Muto, V., & Schmidt, C. (2021).
    pyActigraphy: Open-source python package for actigraphy data visualization and analysis.
    PLoS Computational Biology, 17(10), 1009514–1009535. https://doi.org/10.1371/journal.pcbi.1009514

    [2] Hammad, G., Wulff, K., Skene, D. J., Münch, M., & Spitschan, M. (2024). Open-Source Python Module for the
    Analysis of Personalized Light Exposure Data from Wearable Light Loggers and Dosimeters.
    LEUKOS, 20(4), 380–389. https://doi.org/10.1080/15502724.2023.2296863
    """
    return _light_exposure(
        light=data, threshold=threshold, start_time=None, stop_time=None
    )


def get_time_barycentre(data):
    # Normalize each epoch to midnight.
    Y_j = data.index - data.index.normalize()
    # Convert to indices.
    Y_j /= pd.Timedelta(data.index.freq)
    # Compute barycentre
    bc = data.multiply(Y_j, axis=0).sum() / data.sum()

    return bc


def mlit(light, threshold):
    r"""Mean light timing.

    Mean light timing above threshold, MLiT^C.


    Parameters
    ----------
    light : pandas.Series, optional
        Input data series with a DatetimeIndex, where the index specifies the time points and
        the values represent the input variable (in this case, light). Time and value arrays
        are extracted from this series.
    threshold: float
        Threshold value.

    Returns
    -------
    MLiT : pd.DataFrame
        A pandas DataFrame with MLiT^C per channel.

    Notes
    -----

    The MLiT variable is defined in ref [1]_:

    .. math::

        MLiT^C = \frac{\sum_{j}^{m}\sum_{k}^{n} j\times I^{C}_{jk}}{
        \sum_{j}^{m}\sum_{k}^{n} I^{C}_{jk}}

    where :math:`I^{C}_{jk}` is equal to 1 if the light level is higher
    than the threshold C, m is the total number of epochs per day and n is
    the number of days covered by the data.

    References
    ----------
    This code is derived from the original implementation in pyActigraphy, distributed under the BSD 3-Clause License.
    Original author: Grégory Hammad (gregory.hammad@uliege.be).

    [1] Reid K.J., Santostasi G., Baron K.G., Wilson J., Kang J., Zee P.C., Timing and Intensity of Light
    Correlate with Body Weight in Adults. PLoS ONE 9(4): e92251. https://doi.org/10.1371/journal.pone.0092251

    [2] Hammad, G., Reyt, M., Beliy, N., Baillet, M., Deantoni, M., Lesoinne, A., Muto, V., & Schmidt, C. (2021).
    pyActigraphy: Open-source python package for actigraphy data visualization and analysis.
    PLoS Computational Biology, 17(10), 1009514–1009535. https://doi.org/10.1371/journal.pcbi.1009514

    [3] Hammad, G., Wulff, K., Skene, D. J., Münch, M., & Spitschan, M. (2024). Open-Source Python Module for the
    Analysis of Personalized Light Exposure Data from Wearable Light Loggers and Dosimeters.
    LEUKOS, 20(4), 380–389. https://doi.org/10.1080/15502724.2023.2296863
    """
    # Binarized data and convert to float in order to handle 'DivideByZero'
    I_jk = _binarize(data=light, threshold=threshold).astype("float64")

    MLiT = get_time_barycentre(I_jk)

    # Scaling factor: MLiT is now expressed in minutes since midnight.
    MLiT /= pd.Timedelta("1min") / I_jk.index.freq

    return MLiT


def mlitp(light, threshold):
    r"""Mean light timing per day.

    Mean light timing above threshold, MLiT^C, per calendar day.

    Parameters
    ----------
    light : pandas.Series, optional
        Input data series with a DatetimeIndex, where the index specifies the time points and
        the values represent the input variable (in this case, light). Time and value arrays
        are extracted from this series.
    threshold: float
        Threshold value.

    Returns
    -------
    MLiTp : pd.DataFrame
        A pandas DataFrame with MLiT^C per channel and per day.

    Notes
    -----

    The MLiT variable is defined in ref [1]_:

    .. math::

        MLiT^C = \frac{\sum_{j}^{m}\sum_{k}^{n} j\times I^{C}_{jk}}{
        \sum_{j}^{m}\sum_{k}^{n} I^{C}_{jk}}

    where :math:`I^{C}_{jk}` is equal to 1 if the light level is higher
    than the threshold C, m is the total number of epochs per day and n is
    the number of days covered by the data.

    References
    ----------
    This code is derived from the original implementation in pyActigraphy, distributed under the BSD 3-Clause License.
    Original author: Grégory Hammad (gregory.hammad@uliege.be).

    [1] Reid K.J., Santostasi G., Baron K.G., Wilson J., Kang J., Zee P.C., Timing and Intensity of Light Correlate
    with Body Weight in Adults. PLoS ONE 9(4): e92251. https://doi.org/10.1371/journal.pone.0092251

    [2] Hammad, G., Reyt, M., Beliy, N., Baillet, M., Deantoni, M., Lesoinne, A., Muto, V., & Schmidt, C. (2021).
    pyActigraphy: Open-source python package for actigraphy data visualization and analysis.
    PLoS Computational Biology, 17(10), 1009514–1009535. https://doi.org/10.1371/journal.pcbi.1009514

    [3] Hammad, G., Wulff, K., Skene, D. J., Münch, M., & Spitschan, M. (2024). Open-Source Python Module for the
    Analysis of Personalized Light Exposure Data from Wearable Light Loggers and Dosimeters.
    LEUKOS, 20(4), 380–389. https://doi.org/10.1080/15502724.2023.2296863
    """
    # Binarized data and convert to float in order to handle 'DivideByZero'
    I_jk = _binarize(data=light, threshold=threshold).astype("float64")

    # Group data per day:
    MLiTp = I_jk.groupby(I_jk.index.date).apply(get_time_barycentre)

    # Scaling factor: MLiT is now expressed in minutes since midnight.
    MLiTp /= pd.Timedelta("1min") / I_jk.index.freq

    return MLiTp


def get_extremum(data, extremum):
    r"""Light extremum.

    Return the index and the value of the requested extremum (min or max).

    Parameters
    ----------
    data : pandas.Series, optional
        Input data series with a DatetimeIndex, where the index specifies the time points and
        the values represent the input variable (e.g., activity, light). Time and value arrays
        are extracted from this series.
    extremum: str
        Name of the extremum.
        Available: 'min' or 'max'.

    Returns
    -------
    ext : pd.DataFrame
        A pandas DataFrame with extremum info per channel.

    References
    ----------
    This code is derived from the original implementation in pyActigraphy, distributed under the BSD 3-Clause License.
    Original author: Grégory Hammad (gregory.hammad@uliege.be).

    [1] Hammad, G., Reyt, M., Beliy, N., Baillet, M., Deantoni, M., Lesoinne, A., Muto, V., & Schmidt, C. (2021).
    pyActigraphy: Open-source python package for actigraphy data visualization and analysis.
    PLoS Computational Biology, 17(10), 1009514–1009535. https://doi.org/10.1371/journal.pcbi.1009514

    [2] Hammad, G., Wulff, K., Skene, D. J., Münch, M., & Spitschan, M. (2024). Open-Source Python Module for the
    Analysis of Personalized Light Exposure Data from Wearable Light Loggers and Dosimeters.
    LEUKOS, 20(4), 380–389. https://doi.org/10.1080/15502724.2023.2296863
    """
    # Return either the maximum or minimum, as well as the respective timestamp
    if extremum == "max":
        return data.idxmax(), data.max()
    elif extremum == "min":
        return data.idxmin(), data.min()
    else:
        raise ValueError('Extremum must be "min" or "max"')


def lmx(data, length="5h", lowest=True):
    r"""Least or Most light period of length X

    Onset and mean hourly light exposure levels during the X least or most
    bright hours of the day.

    Parameters
    ----------
    data : pandas.Series
        Input data series with a DatetimeIndex, where the index specifies the time points and
        the values represent the input variable (e.g., activity, light). Time and value arrays
        are extracted from this series.
    length: str, optional
        Period length.
        Default is '5h'.
    lowest: bool, optional
        If lowest is set to True, the period of least light exposure is
        considered. Otherwise, consider the period of most light exposure.
        Default is True.

    Returns
    -------
    lmx_t, lmx: (pd.Timedelta, float)
        Onset and mean hourly light exposure level.

    Notes
    -----

    The LMX variable is derived from the L5 and M10 defined in [1]_ as the
    mean hourly activity levels during the 5/10 least/most active hours.

    References
    ----------
    This code is derived from the original implementation in pyActigraphy, distributed under the BSD 3-Clause License.
    Original author: Grégory Hammad (gregory.hammad@uliege.be).

    [1] Van Someren, E.J.W., Lijzenga, C., Mirmiran, M., Swaab, D.F. (1997). Long-Term Fitness Training Improves
    the Circadian Rest-Activity Rhythm in Healthy Elderly Males. Journal of Biological Rhythms, 12(2), 146–156.
    http://doi.org/10.1177/074873049701200206

    [2] Hammad, G., Reyt, M., Beliy, N., Baillet, M., Deantoni, M., Lesoinne, A., Muto, V., & Schmidt, C. (2021).
    pyActigraphy: Open-source python package for actigraphy data visualization and analysis.
    PLoS Computational Biology, 17(10), 1009514–1009535. https://doi.org/10.1371/journal.pcbi.1009514

    [3] Hammad, G., Wulff, K., Skene, D. J., Münch, M., & Spitschan, M. (2024). Open-Source Python Module for the
    Analysis of Personalized Light Exposure Data from Wearable Light Loggers and Dosimeters.
    LEUKOS, 20(4), 380–389. https://doi.org/10.1080/15502724.2023.2296863
    """
    # Calculate time of LMX and the value of LMX
    lmx_ts, lmx = _lmx(data, length, lowest=lowest)

    # Return these values back to the user
    return lmx_ts, lmx


def temporal_centroid(data):
    """
    Compute the temporal centroid of a time series.

    The centroid is calculated as the weighted average of time points, where weights
    are proportional to the values in the time series.

    Parameters
    ----------
    data : pandas.Series
        Input data series with a DatetimeIndex, where the index specifies the time points and
        the values represent the input variable (e.g., activity, light). Time and value arrays
        are extracted from this series.

    Returns
    -------
    pd.Timestamp
        The temporal centroid of the time series, expressed as a timestamp.

    References
    ----------
    [1] Zauner, J., Hartmeyer, S., & Spitschan, M. (2025). LightLogR: Reproducible analysis of personal light
    exposure data. Journal of Open Source Software, 10(107), 7601. https://doi.org/10.21105/joss.07601.
    RRID:SCR_025408
    """
    weights = data / data.sum()
    time = data.index.astype(np.int64)
    temp_centroid = np.round(np.sum(time * weights))
    return pd.to_datetime(temp_centroid)


def spectral_centroid(data):
    """
    Compute the spectral centroid of a time series.

    Parameters
    ----------
    data : pandas.Series
        Input data series with a DatetimeIndex, where the index specifies the time points and
        the values represent the input variable (e.g., activity, light). Time and value arrays
        are extracted from this series.

    Returns
    -------
    pd.Timestamp
        The spectral (frequency) centroid of the time series, expressed as a timestamp.

    References
    ----------
    [1] Zauner, J., Hartmeyer, S., & Spitschan, M. (2025). LightLogR: Reproducible analysis of personal light
    exposure data. Journal of Open Source Software, 10(107), 7601. https://doi.org/10.21105/joss.07601.
    RRID:SCR_025408
    """
    # Infer sampling rate
    delta = (data.index[1] - data.index[0]).total_seconds()

    # Convert sampling rate to Hz (1/samples_per_second)
    t = 1 / delta

    # Number of sample points in the recording
    n = len(data.values)

    # y axis - magnitude corresponding to each frequency - x(fi)
    amplitude = np.abs(fft(data.values))[: n // 2]

    # x axis (frequencies)
    frequencies = fftfreq(n, delta)[: n // 2]

    # Avoid dividing by zero
    if np.sum(amplitude) == 0:
        return None

    spec_centroid = np.sum(amplitude * frequencies) / np.sum(amplitude)
    return spec_centroid


def pRA(data, start=None, period=None):
    r"""Rest->Activity transition probability distribution

    Conditional probability, pRA(t), that an individual would be
    resting at time (t+1) given that the individual had been continuously
    active for the preceding t epochs, defined in [1]_ as:

    .. math::
        pRA(t) = p(A|R_t) = \frac{N_t - N_{t+1}}{N_t}

    with :math:`N_t`, the total number of sequences of rest (i.e. activity
    below threshold) of duration :math:`t` or longer.

    Parameters
    ----------
    threshold: int
        If binarize is set to True, data above this threshold are set to 1
        and to 0 otherwise.
    start: str, optional
        If not None, the actigraphy recording is truncated to
        'start:start+period', each day. Start string format: 'HH:MM:SS'.
        Default is None
    period: str, optional
        Time period for the calculation of pRA.
        Default is None.

    Returns
    -------
    pra: pandas.core.series.Series
        Transition probabilities (pRA(t)), calculated for all t values.
    pra_weights: pandas.core.series.Series
        Weights are defined as the square root of the number of activity
        sequences contributing to each probability estimate.

    Notes
    -----

    pRA is corrected for discontinuities due to sparse data, as defined in
    [1]_.

    References
    ----------
    This code is derived from the original implementation in pyActigraphy, distributed under the BSD 3-Clause License.
    Original author: Grégory Hammad (gregory.hammad@uliege.be).

    [1] Lim, A. S. P., Yu, L., Costa, M. D., Buchman, A. S., Bennett, D. A., Leurgans, S. E., & Saper, C. B. (2011).
    Quantification of the Fragmentation of Rest-Activity Patterns in Elderly Individuals Using a State Transition
    Analysis. Sleep, 34(11), 1569–1581. http://doi.org/10.5665/sleep.1400

    [2] Hammad, G., Reyt, M., Beliy, N., Baillet, M., Deantoni, M., Lesoinne, A., Muto, V., & Schmidt, C. (2021).
    pyActigraphy: Open-source python package for actigraphy data visualization and analysis.
    PLoS Computational Biology, 17(10), 1009514–1009535. https://doi.org/10.1371/journal.pcbi.1009514

    [3] Hammad, G., Wulff, K., Skene, D. J., Münch, M., & Spitschan, M. (2024). Open-Source Python Module for the
    Analysis of Personalized Light Exposure Data from Wearable Light Loggers and Dosimeters.
    LEUKOS, 20(4), 380–389. https://doi.org/10.1080/15502724.2023.2296863
    """

    # Restrict data range to period 'Start, Start+Period'
    if start is not None:
        end = _td_format(pd.Timedelta(start) + pd.Timedelta(period))

        data = data.between_time(start, end)
    else:
        data = data

    # Rest->Activity transition probability:
    pRA, pRA_weights = _transition_prob(data, True)

    return pRA, pRA_weights


def pAR(data, start=None, period=None):
    r"""Activity->Rest transition probability distribution

    Conditional probability, pAR(t), that an individual would be
    active at time (t+1) given that the individual had been continuously
    resting for the preceding t epochs, defined in [1]_ as:

    .. math::
        pAR(t) = p(R|A_t) = \frac{N_t - N_{t+1}}{N_t}

    with :math:`N_t`, the total number of sequences of activity (i.e.
    activity above threshold) of duration :math:`t` or longer.

    Parameters
    ----------
    threshold: int
        If binarize is set to True, data above this threshold are set to 1
        and to 0 otherwise.
    start: str, optional
        If not None, the actigraphy recording is truncated to
        'start:start+period', each day. Start string format: 'HH:MM:SS'.
        Default is None
    period: str, optional
        Time period for the calculation of pAR.
        Default is None.

    Returns
    -------
    par: pandas.core.series.Series
        Transition probabilities (pAR(t)), calculated for all t values.
    par_weights: pandas.core.series.Series
        Weights are defined as the square root of the number of activity
        sequences contributing to each probability estimate.

    Notes
    -----

    pAR is corrected for discontinuities due to sparse data, as defined in
    [1]_.

    References
    ----------
    This code is derived from the original implementation in pyActigraphy, distributed under the BSD 3-Clause License.
    Original author: Grégory Hammad (gregory.hammad@uliege.be).

    [1] Lim, A. S. P., Yu, L., Costa, M. D., Buchman, A. S., Bennett, D. A., Leurgans, S. E., & Saper, C. B. (2011).
    Quantification of the Fragmentation of Rest-Activity Patterns in Elderly Individuals Using a State Transition
    Analysis. Sleep, 34(11), 1569–1581. http://doi.org/10.5665/sleep.1400

    [2] Hammad, G., Reyt, M., Beliy, N., Baillet, M., Deantoni, M., Lesoinne, A., Muto, V., & Schmidt, C. (2021).
    pyActigraphy: Open-source python package for actigraphy data visualization and analysis.
    PLoS Computational Biology, 17(10), 1009514–1009535. https://doi.org/10.1371/journal.pcbi.1009514

    [3] Hammad, G., Wulff, K., Skene, D. J., Münch, M., & Spitschan, M. (2024). Open-Source Python Module for the
    Analysis of Personalized Light Exposure Data from Wearable Light Loggers and Dosimeters.
    LEUKOS, 20(4), 380–389. https://doi.org/10.1080/15502724.2023.2296863
    """

    # Restrict data range to period 'Start, Start+Period'
    if start is not None:
        end = _td_format(pd.Timedelta(start) + pd.Timedelta(period))

        data = data.between_time(start, end)
    else:
        data = data
    # Activity->Rest transition probability:
    pAR, pAR_weights = _transition_prob(data, False)

    return pAR, pAR_weights


def kRA(data, start=None, period=None, frac=0.3, it=0, logit=False, offset="15min"):
    r"""Rest->Activity transition probability

    Weighted average value of pRA(t) within the constant regions, defined
    as the longest stretch within which the LOWESS curve varied by no more
    than 1 standard deviation of the pRA(t) curve [1]_.

    Parameters
    ----------
    threshold: int
        Above this threshold, data are classified as active (1) and as
        rest (0) otherwise.
    start: str, optional
        If not None, the actigraphy recording is truncated to
        'start:start+period', each day. Start string format: 'HH:MM:SS'.
        Special keywords ('AonT' or 'AoffT') are allowed. In this case, the
        start is set to the activity onset ('AonT') or offset ('AoffT')
        time derived from the daily profile. Cf sleep.AonT/AoffT functions
        for more informations.
        Default is None
    period: str, optional
        Time period for the calculation of pRA.
        Default is None.
    frac: float, optional
        Fraction of the data used when estimating each value.
        Default is 0.3.
    it: int, optional
        Number of residual-based reweightings to perform.
        Default is 0.
    logit: bool, optional
        If True, the kRA value is logit-transformed (ln(p/1-p)). Useful
        when kRA is used in a regression model.
        Default is False.
    offset: str, optional
        Time offset with respect to the activity onset and offset times
        used as start times.
        Default is '15min'.

    Returns
    -------
    kra: float

    References
    ----------
    This code is derived from the original implementation in pyActigraphy, distributed under the BSD 3-Clause License.
    Original author: Grégory Hammad (gregory.hammad@uliege.be).

    [1] Lim, A. S. P., Yu, L., Costa, M. D., Buchman, A. S., Bennett, D. A., Leurgans, S. E., & Saper, C. B. (2011).
    Quantification of the Fragmentation of Rest-Activity Patterns in Elderly Individuals Using a State Transition
    Analysis. Sleep, 34(11), 1569–1581. http://doi.org/10.5665/sleep.1400

    [2] Hammad, G., Reyt, M., Beliy, N., Baillet, M., Deantoni, M., Lesoinne, A., Muto, V., & Schmidt, C. (2021).
    pyActigraphy: Open-source python package for actigraphy data visualization and analysis.
    PLoS Computational Biology, 17(10), 1009514–1009535. https://doi.org/10.1371/journal.pcbi.1009514

    [3] Hammad, G., Wulff, K., Skene, D. J., Münch, M., & Spitschan, M. (2024). Open-Source Python Module for the
    Analysis of Personalized Light Exposure Data from Wearable Light Loggers and Dosimeters.
    LEUKOS, 20(4), 380–389. https://doi.org/10.1080/15502724.2023.2296863
    """

    if start is not None and re.match(r"AonT|AoffT", start):
        aont = AonT(data)
        aofft = AoffT(data)

        offset = pd.Timedelta(offset)
        if start == "AonT":
            start_time = str(aont + offset).split(" ")[-1]
            period = str(
                pd.Timedelta("24H") - ((aont + offset) - (aofft - offset))
            ).split(" ")[-1]
        elif start == "AoffT":
            start_time = str(aofft + offset).split(" ")[-1]
            period = str(
                pd.Timedelta("24H") - ((aofft + offset) - (aont - offset))
            ).split(" ")[-1]
    else:
        start_time = start

    # Calculate the pRA probabilities and their weights.
    pra, pra_weights = pRA(data, start=start_time, period=period)
    # Fit the pRA distribution with a LOWESS and return mean value for
    # the constant region (i.e. the region where |pRA-lowess|<1SD)
    kRA = _transition_prob_sustain_region(pra, pra_weights, frac=frac, it=it)
    return np.log(kRA / (1 - kRA)) if logit else kRA


def kAR(data, start=None, period=None, frac=0.3, it=0, logit=False, offset="15min"):
    """Rest->Activity transition probability

    Weighted average value of pAR(t) within the constant regions, defined
    as the longest stretch within which the LOWESS curve varied by no more
    than 1 standard deviation of the pAR(t) curve.

    Parameters
    ----------
    threshold: int
        Above this threshold, data are classified as active (1) and as
        rest (0) otherwise.
    start: str, optional
        If not None, the actigraphy recording is truncated to
        'start:start+period', each day. Start string format: 'HH:MM:SS'.
        Special keywords ('AonT' or 'AoffT') are allowed. In this case, the
        start is set to the activity onset ('AonT') or offset ('AoffT')
        time derived from the daily profile. Cf sleep.AonT/AoffT functions
        for more informations.
        Default is None
    period: str, optional
        Time period for the calculation of pRA.
        Default is None.
    frac: float
        Fraction of the data used when estimating each value.
        Default is 0.3.
    it: int
        Number of residual-based reweightings to perform.
        Default is 0.
    logit: bool, optional
        If True, the kRA value is logit-transformed (ln(p/1-p)). Useful
        when kRA is used in a regression model.
        Default is False.
    freq: str, optional
        Data resampling `frequency string
        <https://pandas.pydata.org/pandas-docs/stable/timeseries.html>`_
        applied to the daily profile if start='AonT' or 'AoffT'.
        Default is None.
    offset: str, optional
        Time offset with respect to the activity onset and offset times
        used as start times.
        Default is '15min'.

    Returns
    -------
    kar: float

    References
    ----------
    This code is derived from the original implementation in pyActigraphy, distributed under the BSD 3-Clause License.
    Original author: Grégory Hammad (gregory.hammad@uliege.be).

    [1] Lim, A. S. P., Yu, L., Costa, M. D., Buchman, A. S., Bennett, D. A., Leurgans, S. E., & Saper, C. B. (2011).
    Quantification of the Fragmentation of Rest-Activity Patterns in Elderly Individuals Using a State Transition
    Analysis. Sleep, 34(11), 1569–1581. http://doi.org/10.5665/sleep.1400

    [2] Hammad, G., Reyt, M., Beliy, N., Baillet, M., Deantoni, M., Lesoinne, A., Muto, V., & Schmidt, C. (2021).
    pyActigraphy: Open-source python package for actigraphy data visualization and analysis.
    PLoS Computational Biology, 17(10), 1009514–1009535. https://doi.org/10.1371/journal.pcbi.1009514

    [3] Hammad, G., Wulff, K., Skene, D. J., Münch, M., & Spitschan, M. (2024). Open-Source Python Module for the
    Analysis of Personalized Light Exposure Data from Wearable Light Loggers and Dosimeters.
    LEUKOS, 20(4), 380–389. https://doi.org/10.1080/15502724.2023.2296863
    """

    if start is not None and re.match(r"AonT|AoffT", start):
        aont = AonT(data)
        aofft = AoffT(data)
        offset = pd.Timedelta(offset)
        if start == "AonT":
            start_time = str(aont + offset).split(" ")[-1]
            period = str(
                pd.Timedelta("24H") - ((aont + offset) - (aofft - offset))
            ).split(" ")[-1]
        elif start == "AoffT":
            start_time = str(aofft + offset).split(" ")[-1]
            period = str(
                pd.Timedelta("24H") - ((aofft + offset) - (aont - offset))
            ).split(" ")[-1]
    else:
        start_time = start

    # Calculate the pAR probabilities and their weights.
    par, par_weights = pAR(data, start=start_time, period=period)
    # Fit the pAR distribution with a LOWESS and return mean value for
    # the constant region (i.e. the region where |pAR-lowess|<1SD)
    kAR = _transition_prob_sustain_region(par, par_weights, frac=frac, it=it)
    return np.log(kAR / (1 - kAR)) if logit else kAR
