Skip to content

Histogram Plot

Histograms of one or more value columns, optionally split by a group column.

Plots raw rows directly (no pre-aggregation). clip_range=(lower, upper) clamps out-of-range values to the edge bins; matplotlib's native range kwarg drops them instead. A Series is plotted as a single histogram; group_col cannot be used with one.

plot(df, value_col=None, group_col=None, title=None, eyebrow=None, subtitle=None, x_label=None, y_label=None, legend_title=None, ax=None, source_text=None, move_legend_outside=False, clip_range=None, use_hatch=False, **kwargs)

Plot a histogram of value_col, optionally split by group_col.

With group_col, one histogram is drawn per unique group value; a list value_col cannot be combined with it. When df is a Series, value_col (if given) becomes the single column name and the Series' own name is discarded; group_col cannot be used with a Series and will error.

Parameters:

Name Type Description Default
df DataFrame | Series

The dataframe (or series) to plot.

required
value_col str or list of str

The column(s) to plot.

None
group_col str

The column used to define different histograms.

None
title str

The title of the plot.

None
eyebrow str

Small uppercase label rendered above the title.

None
subtitle str

Supporting copy rendered below the title.

None
x_label str

X-axis label; omitted means no label.

None
y_label str

Y-axis label; omitted means no label.

None
legend_title str

The title of the legend.

None
ax Axes

Matplotlib axes object to plot on.

None
source_text str

The source text to add to the plot.

None
move_legend_outside bool

Move the legend outside the plot.

False
clip_range tuple[float | None, float | None]

(lower, upper) bounds that clamp out-of-range values to the nearest boundary so they pile up at the edge bins. Pass None on either side for one-sided clipping. Mutually exclusive with matplotlib's range kwarg, which drops out-of-range values instead.

None
use_hatch bool

Whether to use hatching for the bars.

False
**kwargs Any

Additional keyword arguments for pandas' plot function.

{}

Returns:

Name Type Description
SubplotBase SubplotBase

The matplotlib axes object.

Raises:

Type Description
ValueError

If both clip_range and matplotlib's range kwarg are specified.

ValueError

If clip_range is not a 2-tuple or has lower greater than upper.

ValueError

If value_col is a list and group_col is also provided.

ValueError

If df is a DataFrame and value_col is None.

Source code in openretailscience/plots/histogram.py
def plot(
    df: pd.DataFrame | pd.Series,
    value_col: str | list[str] | None = None,
    group_col: str | None = None,
    title: str | None = None,
    eyebrow: str | None = None,
    subtitle: str | None = None,
    x_label: str | None = None,
    y_label: str | None = None,
    legend_title: str | None = None,
    ax: Axes | None = None,
    source_text: str | None = None,
    move_legend_outside: bool = False,
    clip_range: tuple[float | None, float | None] | None = None,
    use_hatch: bool = False,
    **kwargs: Any,  # noqa: ANN401
) -> SubplotBase:
    """Plot a histogram of ``value_col``, optionally split by ``group_col``.

    With ``group_col``, one histogram is drawn per unique group value; a list
    ``value_col`` cannot be combined with it. When ``df`` is a Series, ``value_col`` (if given)
    becomes the single column name and the Series' own ``name`` is discarded; ``group_col``
    cannot be used with a Series and will error.

    Args:
        df (pd.DataFrame | pd.Series): The dataframe (or series) to plot.
        value_col (str or list of str, optional): The column(s) to plot.
        group_col (str, optional): The column used to define different histograms.
        title (str, optional): The title of the plot.
        eyebrow (str, optional): Small uppercase label rendered above the title.
        subtitle (str, optional): Supporting copy rendered below the title.
        x_label (str, optional): X-axis label; omitted means no label.
        y_label (str, optional): Y-axis label; omitted means no label.
        legend_title (str, optional): The title of the legend.
        ax (Axes, optional): Matplotlib axes object to plot on.
        source_text (str, optional): The source text to add to the plot.
        move_legend_outside (bool, optional): Move the legend outside the plot.
        clip_range (tuple[float | None, float | None], optional): ``(lower, upper)`` bounds that
            clamp out-of-range values to the nearest boundary so they pile up at the edge bins.
            Pass ``None`` on either side for one-sided clipping. Mutually exclusive with
            matplotlib's ``range`` kwarg, which drops out-of-range values instead.
        use_hatch (bool, optional): Whether to use hatching for the bars.
        **kwargs: Additional keyword arguments for pandas' ``plot`` function.

    Returns:
        SubplotBase: The matplotlib axes object.

    Raises:
        ValueError: If both clip_range and matplotlib's range kwarg are specified.
        ValueError: If clip_range is not a 2-tuple or has lower greater than upper.
        ValueError: If value_col is a list and group_col is also provided.
        ValueError: If df is a DataFrame and value_col is None.
    """
    if isinstance(value_col, list) and group_col is not None:
        raise ValueError("`value_col` cannot be a list when `group_col` is provided. Please choose one or the other.")

    if clip_range is not None and "range" in kwargs:
        raise ValueError(
            "Cannot specify both `range` and `clip_range`. Use `clip_range` to clamp outliers to the edge bins, "
            "or `range` to drop out-of-range values entirely.",
        )

    value_col = _prepare_value_col(df=df, value_col=value_col)

    if isinstance(df, pd.Series):
        df = df.to_frame(name=value_col[0])

    if clip_range is not None and len(clip_range) != CLIP_RANGE_LENGTH:
        msg = f"clip_range must be a 2-tuple of (lower, upper); got length {len(clip_range)}"
        raise ValueError(msg)

    if clip_range is not None:
        clip_lower, clip_upper = clip_range
        if clip_lower is not None and clip_upper is not None and clip_lower > clip_upper:
            msg = f"clip_range lower ({clip_lower}) must be <= upper ({clip_upper})"
            raise ValueError(msg)
        df = df.assign(**{col: df[col].clip(lower=clip_lower, upper=clip_upper) for col in value_col})

    num_histograms = _get_num_histograms(df=df, value_col=value_col, group_col=group_col)

    default_colors = get_plot_colors(num_histograms)
    colors = kwargs.pop("color", default_colors)

    ax = _plot_histogram(
        df=df,
        value_col=value_col,
        group_col=group_col,
        ax=ax,
        colors=colors,
        num_histograms=num_histograms,
        **kwargs,
    )

    if use_hatch:
        ax = gu.apply_hatches(ax=ax, num_segments=num_histograms)

    return standard_graph_styles(
        ax=ax,
        title=title,
        eyebrow=eyebrow,
        subtitle=subtitle,
        x_label=x_label,
        y_label=y_label,
        legend_title=legend_title,
        move_legend_outside=move_legend_outside,
        show_legend=num_histograms > 1,
        source_text=source_text,
        grid_axis="y",
        x_margin=0,
    )