Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 27 additions & 0 deletions plotnine/layer.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
from __future__ import annotations

import decimal
import typing
from copy import copy, deepcopy
from typing import Iterable, List, cast, overload
Expand Down Expand Up @@ -257,6 +258,8 @@ def _make_layer_data(self, plot_data: DataLike | None):
else:
Comment thread
has2k1 marked this conversation as resolved.
raise TypeError(f"Data has a bad type: {type(self.data)}")

self.data = _decimal_columns_to_float(self.data)

def _make_layer_mapping(self, plot_mapping: aes):
"""
Create the aesthetic mappings to be used by this layer
Expand Down Expand Up @@ -778,3 +781,27 @@ def _resolve_position(
raise PlotnineError(f"Unknown position of type {type(position_spec)}")

return klass()


def _decimal_columns_to_float(data: pd.DataFrame) -> pd.DataFrame:
"""
Cast columns of decimal.Decimal values to float

polars' `to_pandas()` maps a Decimal column onto a pandas object column
holding `decimal.Decimal` values. plotnine treats object columns as
discrete, so such a column cannot be used with a continuous scale. Casting
it to float lets it behave like any other numeric column.

This runs for every dataframe, so we avoid the expense of `.dropna()`
(which builds a filtered copy) and use a mask to peek at the first
non-null value instead.
"""
for col in data.columns:
series = data[col]
if series.dtype == object:
mask = series.notna()
if mask.any() and isinstance(
series[mask.idxmax()], decimal.Decimal
):
data[col] = series.astype("float64")
return data
22 changes: 22 additions & 0 deletions tests/test_ggplot_internals.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
coord_trans,
facet_null,
geom_bar,
geom_col,
geom_histogram,
geom_line,
geom_point,
Expand All @@ -21,6 +22,7 @@
labs,
lims,
scale_x_continuous,
scale_y_continuous,
stage,
stat_identity,
theme,
Expand Down Expand Up @@ -365,6 +367,26 @@ def to_pandas(self):
assert p2 == "to_pandas"


def test_decimal_columns():
# A pandas object column of decimal.Decimal values (e.g. from polars'
# to_pandas()) would otherwise be treated as discrete and rejected by a
# continuous scale. See GH #1033.
from decimal import Decimal

data = pd.DataFrame(
{
"x": ["a", "b", "c"],
"y": [Decimal(1), Decimal(2), Decimal(3)],
}
)
p = (
ggplot(data, aes("x", "y"))
+ geom_col()
+ scale_y_continuous(limits=(0, 5))
)
p._build()


def test_callable_as_data():
def _fn(data):
return data.rename(columns={"xx": "x", "yy": "y"})
Expand Down
Loading