Skip to content

plot_time_weight

yohou.plotting.forecasting.plot_time_weight(df, *, weight_column='time_weight', groups=None, facet_by='member', facet_n_cols=2, color_palette=None, show_legend=True, title=None, x_label=None, y_label=None, width=None, height=None, connect_gaps=False, resampler=None, line_width=2.0, fill=True, fill_opacity=0.3)

Plot time-based weights as a time series visualization.

Creates a plot showing how time weights vary over the time axis, useful for visualizing weighting schemes used in time-weighted forecasting or scoring (e.g., linear decay, exponential decay, recent-weighted).

Parameters

Name Type Description Default
df DataFrame

Input DataFrame with 'time' column and weight column.

required
weight_column str

Name of the column containing weight values. For panel data, this is the base name (without the group prefix).

"time_weight"
groups list[str] | None

List of panel group prefixes to plot. If None, plots all groups. Creates subplots for each group.

None
facet_by Literal['group', 'member'] | None

Faceting axis for panel data. "group" creates one subplot per group, "member" one per member. None disables faceting. Ignored for non-panel data.

"member"
facet_n_cols int

Number of columns in facet grid when using panel groups.

2
color_palette list[str] | None

Custom color palette as hex codes. If None, uses yohou palette.

None
show_legend bool

Whether to show the legend.

True
title str | None

Plot title. Defaults to "Time Weights".

None
x_label str | None

X-axis label. Defaults to "Time".

None
y_label str | None

Y-axis label. Defaults to "Weight".

None
width int | None

Plot width in pixels.

None
height int | None

Plot height in pixels.

None
connect_gaps bool

Whether to connect gaps in the data with lines.

False
resampler bool | Literal['widget'] | None

Enable plotly-resampler for large datasets. True or "widget" creates a FigureWidgetResampler; False or None uses a plain go.Figure.

None
line_width float

Width of the weight line trace.

2.0
fill bool

Whether to fill the area under the weight curve.

True
fill_opacity float

Opacity of the filled area when fill is True.

0.3

Returns

Type Description
Figure

Plotly figure object.

Raises

Type Description
TypeError

If df is not a Polars DataFrame.

ValueError

If DataFrame is empty, missing 'time' column, or weight_column not found.

Examples

>>> import polars as pl
>>> from yohou.plotting import plot_time_weight
>>> # Create sample data with linear decay weights
>>> n = 100
>>> df = pl.DataFrame({
...     "time": pl.date_range(pl.date(2020, 1, 1), pl.date(2020, 4, 9), "1d", eager=True),
...     "time_weight": [(i + 1) / n for i in range(n)],
... })
>>> # Plot time weights
>>> fig = plot_time_weight(df)
>>> len(fig.data) > 0
True
>>> # Panel data example
>>> df_panel = pl.DataFrame({
...     "time": pl.date_range(pl.date(2020, 1, 1), pl.date(2020, 1, 10), "1d", eager=True),
...     "weight__store_1": [0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 1.0],
...     "weight__store_2": [0.05, 0.1, 0.2, 0.35, 0.5, 0.65, 0.8, 0.9, 0.95, 1.0],
... })
>>> fig = plot_time_weight(df_panel, weight_column="weight")
>>> len(fig.data) >= 2
True

See Also

plot_time_series : Plot basic time series.

Source Code

Show/Hide source
def plot_time_weight(
    df: pl.DataFrame,
    *,
    weight_column: str = "time_weight",
    groups: list[str] | None = None,
    facet_by: Literal["group", "member"] | None = "member",
    facet_n_cols: int = 2,
    color_palette: list[str] | None = None,
    show_legend: bool = True,
    title: str | None = None,
    x_label: str | None = None,
    y_label: str | None = None,
    width: int | None = None,
    height: int | None = None,
    connect_gaps: bool = False,
    resampler: bool | Literal["widget"] | None = None,
    line_width: float = 2.0,
    fill: bool = True,
    fill_opacity: float = 0.3,
) -> go.Figure:
    """
    Plot time-based weights as a time series visualization.

    Creates a plot showing how time weights vary over the time axis,
    useful for visualizing weighting schemes used in time-weighted forecasting
    or scoring (e.g., linear decay, exponential decay, recent-weighted).

    Parameters
    ----------
    df : pl.DataFrame
        Input DataFrame with 'time' column and weight column.
    weight_column : str, default="time_weight"
        Name of the column containing weight values. For panel data,
        this is the base name (without the group prefix).
    groups : list[str] | None, default=None
        List of panel group prefixes to plot. If None, plots all groups.
        Creates subplots for each group.
    facet_by : Literal["group", "member"] | None, default="member"
        Faceting axis for panel data.  ``"group"`` creates one subplot per
        group, ``"member"`` one per member.  ``None`` disables faceting.
        Ignored for non-panel data.
    facet_n_cols : int, default=2
        Number of columns in facet grid when using panel groups.
    color_palette : list[str] | None, default=None
        Custom color palette as hex codes. If None, uses yohou palette.
    show_legend : bool, default=True
        Whether to show the legend.
    title : str | None, default=None
        Plot title. Defaults to "Time Weights".
    x_label : str | None, default=None
        X-axis label. Defaults to "Time".
    y_label : str | None, default=None
        Y-axis label. Defaults to "Weight".
    width : int | None, default=None
        Plot width in pixels.
    height : int | None, default=None
        Plot height in pixels.
    connect_gaps : bool, default=False
        Whether to connect gaps in the data with lines.
    resampler : bool | Literal["widget"] | None, default=None
        Enable plotly-resampler for large datasets.  ``True`` or
        ``"widget"`` creates a ``FigureWidgetResampler``; ``False`` or
        ``None`` uses a plain ``go.Figure``.
    line_width : float, default=2.0
        Width of the weight line trace.
    fill : bool, default=True
        Whether to fill the area under the weight curve.
    fill_opacity : float, default=0.3
        Opacity of the filled area when ``fill`` is True.

    Returns
    -------
    go.Figure
        Plotly figure object.

    Raises
    ------
    TypeError
        If df is not a Polars DataFrame.
    ValueError
        If DataFrame is empty, missing 'time' column, or weight_column not found.

    Examples
    --------
    >>> import polars as pl
    >>> from yohou.plotting import plot_time_weight

    >>> # Create sample data with linear decay weights
    >>> n = 100
    >>> df = pl.DataFrame({
    ...     "time": pl.date_range(pl.date(2020, 1, 1), pl.date(2020, 4, 9), "1d", eager=True),
    ...     "time_weight": [(i + 1) / n for i in range(n)],
    ... })

    >>> # Plot time weights
    >>> fig = plot_time_weight(df)
    >>> len(fig.data) > 0
    True

    >>> # Panel data example
    >>> df_panel = pl.DataFrame({
    ...     "time": pl.date_range(pl.date(2020, 1, 1), pl.date(2020, 1, 10), "1d", eager=True),
    ...     "weight__store_1": [0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 1.0],
    ...     "weight__store_2": [0.05, 0.1, 0.2, 0.35, 0.5, 0.65, 0.8, 0.9, 0.95, 1.0],
    ... })
    >>> fig = plot_time_weight(df_panel, weight_column="weight")
    >>> len(fig.data) >= 2
    True

    See Also
    --------
    [`plot_time_series`][yohou.plotting.plot_time_series] : Plot basic time series.
    """
    # Validate inputs
    validate_plotting_data(df)
    validate_plotting_params(width=width, height=height)

    # Detect panel data structure
    _, panel_groups = inspect_panel(df)

    # Check if this is panel data with prefixed weight columns
    weight_panel_cols: dict[str, list[str]] = {}
    for group_prefix, cols in panel_groups.items():
        for col in cols:
            base_name = col.split("__", 1)[1] if "__" in col else col
            if weight_column in (base_name, col, group_prefix):
                if group_prefix not in weight_panel_cols:
                    weight_panel_cols[group_prefix] = []
                weight_panel_cols[group_prefix].append(col)

    # If we found panel weight columns
    if weight_panel_cols:
        # Filter to requested groups if specified
        if groups is not None:
            weight_panel_cols = {k: v for k, v in weight_panel_cols.items() if k in groups}

        if not weight_panel_cols:
            msg = f"No weight columns found for panel groups: {groups}"
            raise ValueError(msg)

        # Collect unique member names for consistent colouring
        flat_cols = [c for cols in weight_panel_cols.values() for c in cols]
        _, all_members = _group_panel_columns(flat_cols)
        all_group_names = list(weight_panel_cols.keys())

        # Build faceting structure
        if facet_by == "member":
            facets: dict[str, list[str]] = {}
            for member in all_members:
                cols_for_member = []
                for gcols in weight_panel_cols.values():
                    col = next((c for c in gcols if _member_name(c) == member), None)
                    if col:
                        cols_for_member.append(col)
                if cols_for_member:
                    facets[member] = cols_for_member
            sub_names = all_group_names

            def _get_sub_name(col: str) -> str:
                """Return group name from panel column."""
                return col.split("__", 1)[0]

        elif facet_by is None:
            facets = {"All": flat_cols}
            sub_names = [f"{c.split('__', 1)[0]}/{_member_name(c)}" if "__" in c else c for c in flat_cols]

            def _get_sub_name(col: str) -> str:
                """Return composite group/member identifier."""
                gname = col.split("__", 1)[0] if "__" in col else ""
                mname = _member_name(col) if "__" in col else col
                return f"{gname}/{mname}" if gname else mname

        else:
            # facet_by == "group" (default)
            facets = weight_panel_cols
            sub_names = all_members

            def _get_sub_name(col: str) -> str:
                """Return member name from panel column."""
                return _member_name(col)

        # Get colors (one per unique sub-identifier)
        sub_palette = resolve_color_palette(color_palette, len(sub_names))

        # Create subplots
        n_facets = len(facets)
        n_rows = (n_facets + facet_n_cols - 1) // facet_n_cols
        n_cols_grid = min(n_facets, facet_n_cols)
        fig = _create_subplots(
            resampler,
            rows=n_rows,
            cols=n_cols_grid,
            subplot_titles=list(facets.keys()),
            shared_xaxes=True,
            vertical_spacing=0.08,
            horizontal_spacing=0.1,
        )

        seen_subs: set[str] = set()
        for facet_idx, (_, facet_cols) in enumerate(facets.items()):
            row = facet_idx // facet_n_cols + 1
            col_idx = facet_idx % facet_n_cols + 1

            for col in facet_cols:
                sub_name = _get_sub_name(col)
                s_idx = sub_names.index(sub_name)
                color = sub_palette[s_idx % len(sub_palette)]
                first_seen = sub_name not in seen_subs
                seen_subs.add(sub_name)

                # Convert hex to rgba for fill
                rgb = tuple(int(color[i : i + 2], 16) for i in (1, 3, 5))
                rgba_fill = f"rgba({rgb[0]}, {rgb[1]}, {rgb[2]}, {fill_opacity})"

                fig.add_trace(
                    go.Scatter(
                        x=df["time"],
                        y=df[col],
                        mode="lines",
                        line={"color": color, "width": line_width},
                        connectgaps=connect_gaps,
                        fill="tozeroy" if fill else None,
                        fillcolor=rgba_fill if fill else None,
                        name=sub_name,
                        showlegend=first_seen,
                        legendgroup=sub_name,
                        hovertemplate=(f"{sub_name}<br>Time: %{{x}}<br>Weight: %{{y:.3f}}<extra></extra>"),
                    ),
                    row=row,
                    col=col_idx,
                    **((_fill_trace_kwargs(fig)) if fill else {}),
                )

        # Update layout
        title_default = title or "Time Weights by Panel"
        fig.update_layout(
            title=title_default,
            height=height or (300 * n_rows),
            width=width,
            showlegend=show_legend,
        )

        # Update axes labels
        for c in range(1, n_cols_grid + 1):
            fig.update_xaxes(title_text=x_label or "Time", row=n_rows, col=c)
        fig.update_yaxes(title_text=y_label or "Weight")

        return fig

    # Non-panel case: single weight column
    if weight_column not in df.columns:
        msg = f"Weight column '{weight_column}' not found in DataFrame. Available columns: {df.columns}"
        raise ValueError(msg)

    # Get colors
    if color_palette is None:
        color_palette = resolve_color_palette(None, 1)

    color = color_palette[0]

    # Convert hex to rgba for fill
    rgb = tuple(int(color[i : i + 2], 16) for i in (1, 3, 5))
    rgba_fill = f"rgba({rgb[0]}, {rgb[1]}, {rgb[2]}, {fill_opacity})"

    # Create figure
    fig = _create_figure(resampler)

    fig.add_trace(
        go.Scatter(
            x=df["time"],
            y=df[weight_column],
            mode="lines",
            line={"color": color, "width": line_width},
            connectgaps=connect_gaps,
            fill="tozeroy" if fill else None,
            fillcolor=rgba_fill if fill else None,
            name="Time Weight",
            hovertemplate="Time: %{x}<br>Weight: %{y:.3f}<extra></extra>",
        ),
        **((_fill_trace_kwargs(fig)) if fill else {}),
    )

    # Set default labels
    title_default = title or "Time Weights"
    x_label_default = x_label or "Time"
    y_label_default = y_label or "Weight"

    fig = apply_default_layout(
        fig,
        title=title_default,
        x_label=x_label_default,
        y_label=y_label_default,
        width=width,
        height=height,
    )
    fig.update_layout(showlegend=show_legend)

    return fig

Tutorials

The following example notebooks use this component:

  • How to Apply Time-Weighted Training


    Forecasting-Models

    Use time_weight and sample_weight_alignment to emphasise recent or seasonal training samples in PointReductionForecaster, with visualisation of weight curves and alignment strategy comparison.

    View · Open in marimo

  • Quickstart


    Quickstart

    Comprehensive end-to-end tour of yohou beyond the Getting Started tutorials, covering data loading, baseline forecasting, preprocessing pipelines, decomposition, cross-validation search, and interval prediction.

    View · Open in marimo

  • Forecast Visualization


    Visualization

    Visualise point forecasts from single and multiple models, decomposition pipeline components, and time weight decay functions with interactive Plotly.

    View · Open in marimo