Skip to content

plot_spectrum

yohou.plotting.signal.plot_spectrum(df, *, columns=None, log_scale=False, 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, line_width=2.0, show_peaks=False, n_peaks=3)

Plot periodogram (power spectral density) for frequency domain analysis.

Creates a periodogram showing the power spectral density via FFT, useful for identifying dominant frequencies and periodic patterns in the data. Hover text always includes the corresponding period (1/frequency) and detected peaks are annotated with their period in sample units.

Parameters

Name Type Description Default
df DataFrame

Input DataFrame with 'time' column and numeric columns.

required
columns str | list[str] | None

Column(s) to analyze. If None, uses all numeric columns except 'time'.

None
log_scale bool

Use logarithmic scale for PSD axis.

False
groups list[str] | None

Panel group prefixes to plot.

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.

2
color_palette list[str] | None

Custom color palette.

None
show_legend bool

Whether to show the legend.

True
title str | None

Plot title.

None
x_label str | None

X-axis label. Defaults to "Frequency (cycles/sample)".

None
y_label str | None

Y-axis label. Defaults to "Power Spectral Density".

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
line_width float

Width of the line traces.

2.0
show_peaks bool

Whether to annotate dominant frequency peaks.

False
n_peaks int

Number of peaks to annotate when show_peaks is True.

3

Returns

Type Description
Figure

Plotly figure object.

Examples

>>> import polars as pl
>>> import numpy as np
>>> from yohou.plotting import plot_spectrum
>>> # Create sample time series with periodic component
>>> t = np.arange(100)
>>> y = np.sin(2 * np.pi * 0.1 * t) + 0.5 * np.sin(2 * np.pi * 0.25 * t)
>>> df = pl.DataFrame({
...     "time": pl.date_range(pl.date(2020, 1, 1), pl.date(2020, 4, 9), "1d", eager=True),
...     "y": y,
... })
>>> # Plot spectrum
>>> fig = plot_spectrum(df, columns="y")
>>> len(fig.data) > 0
True

See Also

plot_phase : Plot phase spectrum.

Source Code

Show/Hide source
def plot_spectrum(
    df: pl.DataFrame,
    *,
    columns: str | list[str] | None = None,
    log_scale: bool = False,
    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,
    line_width: float = 2.0,
    show_peaks: bool = False,
    n_peaks: int = 3,
) -> go.Figure:
    """Plot periodogram (power spectral density) for frequency domain analysis.

    Creates a periodogram showing the power spectral density via FFT, useful
    for identifying dominant frequencies and periodic patterns in the data.
    Hover text always includes the corresponding period (1/frequency) and
    detected peaks are annotated with their period in sample units.

    Parameters
    ----------
    df : pl.DataFrame
        Input DataFrame with 'time' column and numeric columns.
    columns : str | list[str] | None, default=None
        Column(s) to analyze. If None, uses all numeric columns except 'time'.
    log_scale : bool, default=False
        Use logarithmic scale for PSD axis.
    groups : list[str] | None, default=None
        Panel group prefixes to plot.
    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.
    color_palette : list[str] | None, default=None
        Custom color palette.
    show_legend : bool, default=True
        Whether to show the legend.
    title : str | None, default=None
        Plot title.
    x_label : str | None, default=None
        X-axis label.  Defaults to "Frequency (cycles/sample)".
    y_label : str | None, default=None
        Y-axis label.  Defaults to "Power Spectral Density".
    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.
    line_width : float, default=2.0
        Width of the line traces.
    show_peaks : bool, default=False
        Whether to annotate dominant frequency peaks.
    n_peaks : int, default=3
        Number of peaks to annotate when ``show_peaks`` is True.

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

    Examples
    --------
    >>> import polars as pl
    >>> import numpy as np
    >>> from yohou.plotting import plot_spectrum

    >>> # Create sample time series with periodic component
    >>> t = np.arange(100)
    >>> y = np.sin(2 * np.pi * 0.1 * t) + 0.5 * np.sin(2 * np.pi * 0.25 * t)
    >>> df = pl.DataFrame({
    ...     "time": pl.date_range(pl.date(2020, 1, 1), pl.date(2020, 4, 9), "1d", eager=True),
    ...     "y": y,
    ... })

    >>> # Plot spectrum
    >>> fig = plot_spectrum(df, columns="y")
    >>> len(fig.data) > 0
    True

    See Also
    --------
    [`plot_phase`][yohou.plotting.plot_phase] : Plot phase spectrum.
    """
    # Validate inputs
    validate_plotting_data(df)
    validate_plotting_params(width=width, height=height)

    # Auto-detect panel data
    if groups is None and columns is None and _auto_detect_panel(df):
        groups = []

    if groups is not None:
        _panel_cols = resolve_panel_columns(df, groups, columns)
        _panel_colors = resolve_color_palette(color_palette, len(_panel_cols))
        _legend_tracker = LegendTracker()

        def _render_periodogram(ctx: RenderContext) -> None:
            """Render spectral periodogram with optional log scaling for a single column."""
            base = [c for c in ctx.sub_df.columns if c != "time"][0]
            y_arr = ctx.sub_df[base].to_numpy()
            freqs, psd = scipy_periodogram(y_arr)
            ctx.fig.add_trace(
                go.Scatter(
                    x=freqs[1:].tolist(),
                    y=psd[1:].tolist(),
                    mode="lines",
                    line={"width": line_width, "color": _panel_colors[ctx.entity_idx]},
                    name=ctx.display_name,
                    legendgroup=ctx.display_name,
                    showlegend=_legend_tracker.should_show(ctx.display_name),
                    connectgaps=connect_gaps,
                ),
                row=ctx.row,
                col=ctx.col,
            )
            if log_scale:
                ctx.fig.update_yaxes(type="log", row=ctx.row, col=ctx.col)

        effective_facet_by = facet_by or "member"
        fig = facet_figure(
            df,
            _render_periodogram,
            groups=groups,
            columns=columns,
            facet_n_cols=facet_n_cols,
            facet_by=effective_facet_by,
            title=title or "Periodogram",
            x_label=x_label or "Frequency (cycles/sample)",
            y_label=y_label or "Power Spectral Density",
            width=width,
            height=height,
            shared_xaxes=False,
        )
        fig.update_layout(showlegend=show_legend)
        return fig

    # Non-panel case: column-mode facet_figure
    plot_columns = validate_plotting_data(df, columns=columns, exclude=["time"])
    _colors = resolve_color_palette(color_palette, len(plot_columns))
    _col_colors = dict(zip(plot_columns, _colors, strict=False))

    def _render_spectrum(ctx: RenderContext) -> None:
        """Render periodogram for one column into a subplot."""
        base = ctx.display_name
        col_color = _col_colors[base]
        y = ctx.sub_df[base].to_numpy()
        freqs, power = scipy_periodogram(y)
        freqs = freqs[1:]
        power = power[1:]
        periods = np.where(freqs > 0, 1.0 / freqs, np.inf)
        hover = (
            f"<b>{base}</b><br>"
            f"Frequency: %{{x:.4f}}<br>"
            f"Period: %{{customdata:.1f}} samples<br>"
            f"PSD: %{{y:.4f}}<extra></extra>"
        )
        ctx.fig.add_trace(
            go.Scatter(
                x=freqs,
                y=power,
                mode="lines",
                line={"width": line_width, "color": col_color},
                name=base,
                customdata=periods,
                connectgaps=connect_gaps,
                hovertemplate=hover,
            ),
            row=ctx.row,
            col=ctx.col,
        )
        if show_peaks and n_peaks > 0:
            peak_indices = np.argsort(power)[-n_peaks:][::-1]
            peak_freqs = freqs[peak_indices]
            peak_powers = power[peak_indices]
            peak_periods = np.where(peak_freqs > 0, 1.0 / peak_freqs, np.inf)
            hover_peak = (
                f"<b>{base} Peak</b><br>"
                f"Frequency: %{{x:.4f}}<br>"
                f"Period: %{{customdata:.1f}} samples<br>"
                f"PSD: %{{y:.4f}}<extra></extra>"
            )
            ctx.fig.add_trace(
                go.Scatter(
                    x=peak_freqs,
                    y=peak_powers,
                    mode="markers+text",
                    marker={"size": 8, "color": col_color, "symbol": "diamond"},
                    name=f"{base} (peaks)",
                    customdata=peak_periods,
                    text=[f"T={p:.0f}" for p in peak_periods],
                    textposition="top center",
                    hovertemplate=hover_peak,
                ),
                row=ctx.row,
                col=ctx.col,
            )

    fig = facet_figure(
        df,
        _render_spectrum,
        columns=plot_columns,
        facet_n_cols=facet_n_cols,
        title=title or "Periodogram",
        x_label=x_label or "Frequency (cycles/sample)",
        y_label=y_label or "Power Spectral Density",
        width=width,
        height=height,
        shared_xaxes=False,
    )
    if log_scale:
        fig.update_yaxes(type="log")
    fig.update_layout(showlegend=show_legend)

    return fig

Tutorials

The following example notebooks use this component:

  • How to Visualize Signal Processing


    Visualization

    Butterworth low-pass filtering with frequency spectrum analysis and phase shift inspection on half-hourly electricity demand data using Plotly.

    View · Open in marimo