From 9275573a1f9115b94a6db227e9338abae5863151 Mon Sep 17 00:00:00 2001 From: CyberNative AI LLC Date: Fri, 23 Oct 2026 10:10:55 -0700 Subject: [PATCH] Fix px.ecdf counting missing values as observations Rows whose value is missing (NaN/None, , null) were included in the cumulative sum and the group total, so the ECDF stopped short of 1 and had an extra point at the missing x. Drop them before sorting. --- CHANGELOG.md | 1 + plotly/express/_core.py | 2 ++ .../test_optional/test_px/test_px_functions.py | 17 +++++++++++++++++ 3 files changed, 20 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index d0986ae579..1e3c038eeb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -22,6 +22,7 @@ This project adheres to [Semantic Versioning](http://semver.org/). - Fix `px.sunburst`, `px.treemap` and `px.icicle` listing sectors in a different order on every run when `path` is used with a Polars DataFrame; sectors now follow their order of first appearance for all dataframe backends [[#5766](https://github.com/plotly/plotly.py/pull/5766)], with thanks to @Irahan2 for the contribution! - Fix `mpl_to_plotly` conversion of matplotlib contour lines and line collections: close contour rings ending with `Z` codes, support dash styles, convert date x-axes, separate disjoint subpaths with `None` separators, hide line collection traces from the legend by default, and group consecutive same-style lines into single traces [[#5770](https://github.com/plotly/plotly.py/pull/5770)], with thanks to @robertoffmoura for the contribution! - Fix serialization of box/lasso select data for figures with multiple subplots [[#5710](https://github.com/plotly/plotly.py/pull/5710)], with thanks to @flpi-sun for the contribution! +- Fix `px.ecdf` counting rows with a missing value as observations, so the curve no longer stops short of 1 (or 100%) and no longer adds a point at a missing x [[#5842](https://github.com/plotly/plotly.py/pull/5842)] ## [7.1.0] - 2026-09-15 diff --git a/plotly/express/_core.py b/plotly/express/_core.py index 9994720b09..2ffcc8bd90 100644 --- a/plotly/express/_core.py +++ b/plotly/express/_core.py @@ -2710,6 +2710,8 @@ def make_figure(args, constructor, trace_patch=None, layout_patch=None): base = args["x"] if args["orientation"] == "v" else args["y"] var = args["x"] if args["orientation"] == "h" else args["y"] ascending = args.get("ecdfmode", "standard") != "reversed" + # missing values are not observations: exclude them from the ECDF + group = group.filter(~nw.col(base).is_null()) group = group.sort(by=base, descending=not ascending, nulls_last=True) group_sum = group.get_column( var diff --git a/tests/test_optional/test_px/test_px_functions.py b/tests/test_optional/test_px/test_px_functions.py index 84a451733b..bc771da084 100644 --- a/tests/test_optional/test_px/test_px_functions.py +++ b/tests/test_optional/test_px/test_px_functions.py @@ -688,3 +688,20 @@ def test_empty_histogram(): fig = px.histogram() assert len(fig.data) == 1 assert fig.data[0].type == "histogram" + + +@pytest.mark.parametrize( + "ecdfmode,expected", + [ + ("standard", [1 / 3, 2 / 3, 1]), + ("complementary", [2 / 3, 1 / 3, 0]), + ("reversed", [1, 2 / 3, 1 / 3]), + ], +) +def test_ecdf_ignores_missing_values(constructor, ecdfmode, expected): + # a missing value is not an observation: it must not count towards the total + # or produce a point of its own + data = constructor({"value": [3.0, 1.0, 2.0, None]}) + fig = px.ecdf(data, x="value", ecdfmode=ecdfmode) + assert_array_equal(fig.data[0].x, [1.0, 2.0, 3.0]) + assert np.allclose(fig.data[0].y, expected)