Skip to content

ENH: add quantile #340

Open

Description

@lucyleeow

It was decided not to add quantile to array-api (data-apis/array-api#795), partly because scipy added array API support to their quantile (scipy/scipy#22352) and partly due to complications (many methods, weight support, nan support).

This RFC is mostly motivated by usage in scikit-learn (as seen from data-apis/array-api#795 (comment) its not used much in other packages).

Although scipy does have an array API quantile, due to our min scipy version, we are currently not able to use it (e.g., scikit-learn/scikit-learn#31671).

More generally, it seems to be a commonly enough used function (which is implemented many array libraries) to warrant considering adding it to here, and having a consistent, single source.

Not sure who would be interested but maybe:
cc @betatim @ogrisel @lorentzenchr (and @rgommers who suggested it)

Activity

  1. lucascolley commented on Jun 30, 2025

    @lucascolley
    Member

    PR welcome! We can always deprecate or declare legacy once the SciPy implementation is sufficiently old enough

  2. betatim commented on Jun 30, 2025

    @betatim
    Member

    What do you think of basically copying the scipy implementation, checking what version of scipy is installed and then based on that decide to use scipy or the version in array-api-extra?

  3. lucascolley commented on Jun 30, 2025

    @lucascolley
    Member

    yes, the SciPy version should work for https://github.com/data-apis/array-api-extra/blob/main/src/array_api_extra/_lib/_funcs.py (pending any test failures), and delegation to existing backends (including SciPy in this case) can happen in https://github.com/data-apis/array-api-extra/blob/main/src/array_api_extra/_delegation.py.

    Contributor docs at https://data-apis.org/array-api-extra/contributing.html !

  4. changed the title [-]RFC Add `quantile` ?[/-] [+]ENH: add `quantile`[/+] on Jun 30, 2025
  5. lorentzenchr commented on Jun 30, 2025

    @lorentzenchr

    The version in scipy,stats.quantile does not (yet?) support (sample) weights, while scikit-learn uses them in several places (trend: growing, see _weighted_percentile). The real benefit would be to have an Array-API compatible version with weights. (On the other hand, 2 methods would do for me: inverted_cdf, linear).

  6. lucyleeow commented on Jul 2, 2025

    @lucyleeow
    Author

    Thanks all, I didn't expect such a quick positive response! And thanks for tackling this @betatim !

    @OmarManzoor raised a good point about most array libraries already implementing their own quantile (which may be faster than our implementation). However, there are many ways to calculate quantile.

    The following packages implement all 9 methods from the Hyndman and Fan paper 1 :

    These packages implement the older, mostly discontinuous ('linear', 'lower', 'higher', 'midpoint', 'nearest') methods that numpy used to have (but still support for backwards compatibility), which are not described in the Hyndman and Fan paper (except 'linear'):

    In light of this, and for future proofing, having our own implementation would be great!

    I am also hopeful that implementing here means that we have a single path towards pushing for weight and nan support 😬 both of which we would like to use in scikit-learn.

    Numpy added weight support for "inverted_cdf" in numpy/numpy#24254 (thanks @lorentzenchr ). Ideally we would add support for another method, whose output matches that of median (among other desirable properties). "linear" (continuous) and "average_inverted_cdf" (discrete) have both been suggested (#340 (comment), numpy/numpy#24254 (comment))

    NaN support is possibly less compelling (but it would be nice). We've recently added nan support to SplineTransformer but would like to do so for KBinsDiscretizer and maybe RobustScaler and QuantileTransformer.

    Footnotes

    1. R. J. Hyndman and Y. Fan, “Sample quantiles in statistical packages,” The American Statistician, 50(4), pp. 361-365, 1996 ↩

  7. betatim commented on Jul 2, 2025

    @betatim
    Member

    I've not educated myself about the (many) different ways of calculating quantiles. I think two things are desirable: (1) use "native" implementations as much as possible and (2) provide a quantile function that supports what users want. It sounds like there is some tension between the two :-/

    Things we should IMHO not aim for: (1) provide as many different methods for computing quantiles (if it happens for free then that is fine, but I'm not sure it is worth putting effort into this) and (2) changing what quantile does once it exists in array-api-extra (let's try and "get it right" from the start so that we don't have to deal with deprecations and all that)

    In particular sample weights seem to not have a lot of support in libraries. Does this mean we should not rely on the underlying array libs and "roll our own"?

  8. lucascolley commented on Jul 2, 2025

    @lucascolley
    Member

    There has been discussion in SciPy about how there may be various competing interpretations for a weights parameter, hence why stats.quantile does not have one (yet). I can try to find the discussions if that is helpful.

  9. lucyleeow commented on Jul 2, 2025

    @lucyleeow
    Author

    In particular sample weights seem to not have a lot of support in libraries.

    Sample weights is no simple feat. Just see this PR in numpy, trying to add sample weights, which was active for almost 6 years (!!) and then closed, unmerged.
    Sebastian outlines the problems well, here.

    When you see the terms fweights, aweights etc, I think they are referring to the stata terms (who coined 'analytical' weights). It does not help that people call them different terms, here is a summary in case it is useful:

    • frequency weights
    • analytical (stata) weights, aka precision weights, aka reliability weights, aka inverse variance weights (ref)
    • probability weights, aka sampling weights

    I have (suprisingly) found a decent source describing these different interpretations of weight and how they are handled: http://www.stata.com/manuals13/u20.pdf#u20.23Weightedestimation (see section 20.23) It talks about weights generally, and not specifically wrt quantile.

    My limited understanding is that the only difference in handling is whether the weights are normalized to n or not. This is mentioned in R Hmisc (I feel like we can trust R to get stats right, right?): https://search.r-project.org/CRAN/refmans/Hmisc/html/wtd.stats.html (see normwt parameter). Though honestly, I'd need to look more deeply into this.

    use "native" implementations as much as possible

    I had the same thought initially. We could even get past the different implementations part because I think they all implement 'linear', but because we sort of 'need' weight and nan support, I think we have to roll our own?

  10. betatim commented on Jul 2, 2025

    @betatim
    Member

    What about using the inverted_cdf method that is implemented in numpy and supports weights? That way we have a good implementation to base ours on, we get sample weight support. Is the inverted_cdf method "too weird"/why is it not the default in Numpy (except for backwards compatibility issues)?

  11. lucyleeow commented on Jul 3, 2025

    @lucyleeow
    Author

    Honestly, for large n I feel like any method would be okay (and may be considered within statistical uncertainty). See scikit-learn/scikit-learn#17370 (comment) and scikit-learn/scikit-learn#17370 (comment). With that said:

    With the general equation $Q_{i}(p)=(1-\gamma)X_{(j)}+\gamma X_{(j+1)}$

    inverted_cdf is probably the simplest method:

    Index is defined as $j\le pn\lt j+1$ ($n$ is sample size and $p$ is percentile)
    then:

    $$\gamma = \left\{ \begin{array}{ll} 1 & \text{if } g > 0 \\\ 0 & \text{if } g = 0 \end{array} \right.$$

    where $g=pn-j$ (essentially the 'fractional' component above $pn$)

    But it does not have any nice symmetry properties (which is why we added _average_weighted_percentile in sklearn, see scikit-learn/scikit-learn#17370 (comment)). In particular:

    1. the number of samples below or equal to $Q_{i}(p)$ is the same as the number of samples above or equal to $Q_{i}(1-p)$ - i.e. the 2 tail ends of the quantiles are the same
    2. the quantile level of $X_{(k)}$ plus the quantile level of $X_{(n-k+1)}$ is equal to 1. i.e., the cdf is symmetrical around p=0.5

    (Honestly I don't intuitively have a feel on how these are different, except I guess for continuous quantile methods, the cdf/plotting position formula would vary, so they don't mean the same?)

    Also, Q(0.5) is only equal to sample median when n is odd.

    • average_inverted_cdf satisfies the first symmetry property and Q(0.5) is always equal to sample median.
    • linear satisfies the second symmetry property but not the first and Q(0.5) is always equal to sample median. It's also continuous.

    Practically - weight support for inverted_cdf would probably be good enough (🤷) but weight support for average_inverted_cdf, or linear would probably be nicer and help some of our tests pass (e.g., scikit-learn/scikit-learn#29818 and scikit-learn/scikit-learn#16298) 😅
    edit: actually, when sample_weight=None we use np.percentile which defaults to 'linear' method, so it would be nice if the calculation with weights didn't differ too much from the calculation without weights.

  12. lucyleeow commented on Jul 10, 2025

    @lucyleeow
    Author

    @lucascolley did you manage to find discussions in SciPy around weight support? Maybe there are people more knowledgeable in stats in SciPy? (ref #340 (comment))

  13. 12 remaining items

  14. lucyleeow commented on Oct 21, 2025

    @lucyleeow
    Author

    only support method="linear" and propagate NaNs. This way, we can delegate for all major array libs (including dask and jax).

    Not sure if delegating is possible, it relies on all packages having consistent behaviour, which we'd need to check. Just taking a quick look at tensorflow tfp.stats.quantiles - and they take num_quantiles, instead of q.

    Also it seems that scipy and numpy axis behaviour also differs: scipy/scipy#23832

  15. cakedev0 commented on Oct 21, 2025

    @cakedev0
    Contributor

    I think tensorflow is not a supported backend of array-api-extra (see https://github.com/data-apis/array-api-extra/blob/main/src/array_api_extra/_lib/_backends.py).
    I already wrote the delegation code for numpy, cupy, torch, dask and jax, and based on the docs, it should be fine (but we'll see if the tests pass).

    Also it seems that scipy and numpy axis behaviour also differs: scipy/scipy#23832

    Ah, thanks for the pointer, I've been scratching my head on this broadcasting thing for a while, this should help ^^

  16. lucyleeow commented on Oct 21, 2025

    @lucyleeow
    Author

    think tensorflow is not a supported backend of array-api-extra

    Ah interesting, I'm sure they had a good reason. Tensorflow is listed as a stakeholder (in array API standard): https://data-apis.org/array-api/latest/purpose_and_scope.html#stakeholders

    If we aren't interested in tensorflow, then seems reasonable, depending on how much tweaking is required for each individual package (as this may be difficult to maintain going forward). Do you want to open a draft PR and we can take a look?

  17. lucyleeow commented on Oct 21, 2025

    @lucyleeow
    Author

    Ah interesting, I'm sure they had a good reason

    Out of interest, why exclude tensorflow @lucascolley ?

  18. lucascolley commented on Oct 21, 2025

    @lucascolley
    Member

    I don't think we are concerned with supporting tensorflow in array-api-extra at least, since it isn't supported by array-api-compat: https://data-apis.org/array-api-compat/supported-array-libraries.html. I'm missing historical context for the relationship between tensorflow and the consortium.

  19. lucyleeow commented on Oct 21, 2025

    @lucyleeow
    Author

    I've been scratching my head on this broadcasting thing for a while,

    For a general guide to broadcasting, this is nice: https://quansight-labs.github.io/ndindex/indexing-guide/other-topics.html#broadcasting

  20. cakedev0 commented on Oct 21, 2025

    @cakedev0
    Contributor

    Sorry, what I meant by "broadcasting thing" was not broadcasting in general but what was done in numpy/scipy for the quantile function related to broadcasting.

    In numpy, there is an error: ValueError: q must be a scalar or 1d. (but it's a bit weird, because it works with 2D q, see issue above).

    So I propose to support only scalar and 1d for q. This should be enough for what is needed in scikit-learn right? And it would make the implementation quite easier I think.

  21. lucascolley commented on Oct 21, 2025

    @lucascolley
    Member

    yes, a good rule of thumb is to only implement a sensible subset of the APIs based on what we need, for more complex functions.

  22. cakedev0 commented on Oct 21, 2025

    @cakedev0
    Contributor

    Ok great! I should be able to make my draft PR readable and tested soon 👍

  23. cakedev0 commented on Oct 22, 2025

    @cakedev0
    Contributor

    I'm extending the scope of my PR. In the end, I prefer doing most of it in one-go to reduce context switchs (and I feel this will be true for reviewers too), because it's not so hard now that I have a clear understanding of the scope and the desired shapes/broadcasting behavior.

    New scope:

    • support for weights
    • support for methods: "linear", "inverted_cdf", "averaged_inverted_cdf"
    • NaNs: unclear yet, but I think that sklearn needs something like nan_policy="omit" for weighted quantile. And usual behavior is nan_policy="propagte" (numpy's behavior for instance). So I might implement both. Or just always omit. What do you think?
  24. lucyleeow commented on Oct 23, 2025

    @lucyleeow
    Author

    A thought (which may or may not be much of a concern): will it be odd that we only support 'linear' if the array library is one of the supported backends but 'average_inverted_cdf' (and not 'linear') otherwise?

    I does mean when writing code, I probably wouldn't use 'linear' because it's not as widely supported as 'average_inverted_cdf'.

    We could of course add support for 'linear', but in that case, we may as well add support for the other continuous methods as well.

    But I am not 100% we have full agreement that limited support of method(s) would be the way to go, WDYT @betatim @ogrisel and maybe @lesteve

  25. cakedev0 commented on Oct 23, 2025

    @cakedev0
    Contributor

    will it be odd that we only support 'linear' if the array library is one of the supported backends but 'average_inverted_cdf' (and not 'linear') otherwise?

    I'm not sure to understand what you mean. For me, the case "otherwise" is empty, because in my understanding "otherwise" means no array-api-compat and hence no ways to get a working xp.

  26. lucyleeow commented on Oct 23, 2025

    @lucyleeow
    Author

    Ah I may have some mis-understanding. In array API extra, are the 'extra' functions ONLY for array libraries supported by array-api-compat?
    I thought they were meant to be written general enough to support 'any' library that supports the standard?

  27. cakedev0 commented on Oct 23, 2025

    @cakedev0
    Contributor

    Hum... My understanding is that any library that supports the standard can be trivially added to array-api-compat so I think it's fair to assume that the set "libraries that support the standard but aren't in array-api-compat" is empty.

    But that's just my understanding, so I'll let @lucascolley or someone else confirm.

  28. cakedev0 commented on Oct 23, 2025

    @cakedev0
    Contributor

    I thought they were meant to be written general enough to support 'any' library that supports the standard?

    They are. They are tested against array_api_strict and array_api_strictest (unless explicitly skipped, a few tests skip strictest, but none skip strict I think).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions