1"""
2Quantilization functions and related stuff
3"""
4
5from __future__ import annotations
6
7from typing import (
8 TYPE_CHECKING,
9 Any,
10 Literal,
11 cast,
12)
13
14import numpy as np
15
16from pandas._libs import (
17 Timedelta,
18 Timestamp,
19 lib,
20)
21from pandas.util._decorators import set_module
22
23from pandas.core.dtypes.common import (
24 ensure_platform_int,
25 is_bool_dtype,
26 is_integer,
27 is_list_like,
28 is_numeric_dtype,
29 is_scalar,
30)
31from pandas.core.dtypes.dtypes import (
32 CategoricalDtype,
33 DatetimeTZDtype,
34 ExtensionDtype,
35)
36from pandas.core.dtypes.generic import ABCSeries
37from pandas.core.dtypes.missing import isna
38
39from pandas import (
40 Categorical,
41 Index,
42 IntervalIndex,
43)
44import pandas.core.algorithms as algos
45from pandas.core.arrays.datetimelike import dtype_to_unit
46from pandas.core.col import Expression
47
48if TYPE_CHECKING:
49 from collections.abc import Callable
50
51 from pandas._typing import (
52 DtypeObj,
53 IntervalLeftRight,
54 TimeUnit,
55 )
56
57
58@set_module("pandas")
59def cut(
60 x,
61 bins,
62 right: bool = True,
63 labels=None,
64 retbins: bool = False,
65 precision: int = 3,
66 include_lowest: bool = False,
67 duplicates: str = "raise",
68 ordered: bool = True,
69):
70 """
71 Bin values into discrete intervals.
72
73 Use `cut` when you need to segment and sort data values into bins. This
74 function is also useful for going from a continuous variable to a
75 categorical variable. For example, `cut` could convert ages to groups of
76 age ranges. Supports binning into an equal number of bins, or a
77 pre-specified array of bins.
78
79 Parameters
80 ----------
81 x : 1d ndarray or Series
82 The input array to be binned. Must be 1-dimensional.
83 bins : int, sequence of scalars, or IntervalIndex
84 The criteria to bin by.
85
86 * int : Defines the number of equal-width bins in the range of `x`. The
87 range of `x` is extended by .1% on each side to include the minimum
88 and maximum values of `x`.
89 * sequence of scalars : Defines the bin edges allowing for non-uniform
90 width. No extension of the range of `x` is done.
91 * IntervalIndex : Defines the exact bins to be used. Note that
92 IntervalIndex for `bins` must be non-overlapping.
93
94 right : bool, default True
95 Indicates whether `bins` includes the rightmost edge or not. If
96 ``right == True`` (the default), then the `bins` ``[1, 2, 3, 4]``
97 indicate (1,2], (2,3], (3,4]. This argument is ignored when
98 `bins` is an IntervalIndex.
99 labels : array or False, default None
100 Specifies the labels for the returned bins. Must be the same length as
101 the resulting bins. If False, returns only integer indicators of the
102 bins. This affects the type of the output container (see below).
103 This argument is ignored when `bins` is an IntervalIndex. If True,
104 raises an error. When `ordered=False`, labels must be provided.
105 retbins : bool, default False
106 Whether to return the bins or not. Useful when bins is provided
107 as a scalar.
108 precision : int, default 3
109 The precision at which to store and display the bins labels.
110 include_lowest : bool, default False
111 Whether values equal to the first bin edge are placed in the first bin.
112 Because the returned intervals must all be closed on the same side,
113 the left edge of the first default label is lowered slightly, so
114 integer bins get float labels. Values between the lowered edge and the
115 first bin edge are still NA. No effect when ``right=False`` or when
116 `bins` is an IntervalIndex.
117 duplicates : {'raise', 'drop'}, default 'raise'
118 If bin edges are not unique, raise ValueError or drop non-uniques.
119 ordered : bool, default True
120 Whether the labels are ordered or not. Applies to returned types
121 Categorical and Series (with Categorical dtype). If True,
122 the resulting categorical will be ordered. If False, the resulting
123 categorical will be unordered (labels must be provided).
124
125 Returns
126 -------
127 out : Categorical, Series, or ndarray
128 An array-like object representing the respective bin for each value
129 of `x`. The type depends on the value of `labels`.
130
131 * None (default) : returns a Series for Series `x` or a
132 Categorical for all other inputs. The values stored within
133 are Interval dtype.
134
135 * sequence of scalars : returns a Series for Series `x` or a
136 Categorical for all other inputs. The values stored within
137 are whatever the type in the sequence is.
138
139 * False : returns a 1d ndarray or Series of integers.
140
141 bins : numpy.ndarray or IntervalIndex.
142 The computed or specified bins. Only returned when `retbins=True`.
143 For scalar or sequence `bins`, this is an ndarray with the computed
144 bins. If set `duplicates=drop`, `bins` will drop non-unique bin. For
145 an IntervalIndex `bins`, this is equal to `bins`.
146
147 See Also
148 --------
149 qcut : Discretize variable into equal-sized buckets based on rank
150 or based on sample quantiles.
151 Categorical : Array type for storing data that come from a
152 fixed set of values.
153 Series : One-dimensional array with axis labels (including time series).
154 IntervalIndex : Immutable Index implementing an ordered, sliceable set.
155 numpy.histogram_bin_edges: Function to calculate only the edges of the bins
156 used by the histogram function.
157
158 Notes
159 -----
160 Any NA values will be NA in the result. Values that fall outside the range
161 of `bins` (or, when `bins` is a non-contiguous IntervalIndex, between two
162 bins) are not assigned to any bin and will be NA in the resulting Series or
163 Categorical object.
164
165 ``numpy.histogram_bin_edges`` can be used along with cut to calculate bins according
166 to some predefined methods.
167
168 See :ref:`the user guide <reshaping.tile.cut>` for more examples.
169
170 Examples
171 --------
172 Discretize into three equal-sized bins.
173
174 >>> pd.cut(np.array([1, 7, 5, 4, 6, 3]), 3)
175 ... # doctest: +ELLIPSIS
176 [(0.994, 3.0], (5.0, 7.0], (3.0, 5.0], (3.0, 5.0], (5.0, 7.0], ...
177 Categories (3, interval[float64, right]): [(0.994, 3.0] < (3.0, 5.0] ...
178
179 >>> pd.cut(np.array([1, 7, 5, 4, 6, 3]), 3, retbins=True)
180 ... # doctest: +ELLIPSIS
181 ([(0.994, 3.0], (5.0, 7.0], (3.0, 5.0], (3.0, 5.0], (5.0, 7.0], ...
182 Categories (3, interval[float64, right]): [(0.994, 3.0] < (3.0, 5.0] ...
183 array([0.994, 3. , 5. , 7. ]))
184
185 Discovers the same bins, but assign them specific labels. Notice that
186 the returned Categorical's categories are `labels` and is ordered.
187
188 >>> pd.cut(np.array([1, 7, 5, 4, 6, 3]), 3, labels=["bad", "medium", "good"])
189 ['bad', 'good', 'medium', 'medium', 'good', 'bad']
190 Categories (3, str): ['bad' < 'medium' < 'good']
191
192 ``ordered=False`` will result in unordered categories when labels are passed.
193 This parameter can be used to allow non-unique labels:
194
195 >>> pd.cut(np.array([1, 7, 5, 4, 6, 3]), 3, labels=["B", "A", "B"], ordered=False)
196 ['B', 'B', 'A', 'A', 'B', 'B']
197 Categories (2, str): ['A', 'B']
198
199 ``labels=False`` implies you just want the bins back.
200
201 >>> pd.cut([0, 1, 1, 2], bins=4, labels=False)
202 array([0, 1, 1, 3])
203
204 Passing a Series as an input returns a Series with categorical dtype:
205
206 >>> s = pd.Series(np.array([2, 4, 6, 8, 10]), index=["a", "b", "c", "d", "e"])
207 >>> pd.cut(s, 3)
208 ... # doctest: +ELLIPSIS
209 a (1.992, 4.667]
210 b (1.992, 4.667]
211 c (4.667, 7.333]
212 d (7.333, 10.0]
213 e (7.333, 10.0]
214 dtype: category
215 Categories (3, interval[float64, right]): [(1.992, 4.667] < (4.667, ...
216
217 Passing a Series as an input returns a Series with mapping value.
218 It is used to map numerically to intervals based on bins.
219
220 >>> s = pd.Series(np.array([2, 4, 6, 8, 10]), index=["a", "b", "c", "d", "e"])
221 >>> pd.cut(s, [0, 2, 4, 6, 8, 10], labels=False, retbins=True, right=False)
222 ... # doctest: +ELLIPSIS
223 (a 1.0
224 b 2.0
225 c 3.0
226 d 4.0
227 e NaN
228 dtype: float64,
229 array([ 0, 2, 4, 6, 8, 10]))
230
231 Use `drop` optional when bins is not unique
232
233 >>> pd.cut(
234 ... s,
235 ... [0, 2, 4, 6, 10, 10],
236 ... labels=False,
237 ... retbins=True,
238 ... right=False,
239 ... duplicates="drop",
240 ... )
241 ... # doctest: +ELLIPSIS
242 (a 1.0
243 b 2.0
244 c 3.0
245 d 3.0
246 e NaN
247 dtype: float64,
248 array([ 0, 2, 4, 6, 10]))
249
250 Passing an IntervalIndex for `bins` results in those categories exactly.
251 Notice that values not covered by the IntervalIndex are set to NaN. 0
252 is to the left of the first bin (which is closed on the right), and 1.5
253 falls between two bins.
254
255 >>> bins = pd.IntervalIndex.from_tuples([(0, 1), (2, 3), (4, 5)])
256 >>> pd.cut([0, 0.5, 1.5, 2.5, 4.5], bins)
257 [NaN, (0.0, 1.0], NaN, (2.0, 3.0], (4.0, 5.0]]
258 Categories (3, interval[int64, right]): [(0, 1] < (2, 3] < (4, 5]]
259
260 Using np.histogram_bin_edges with cut
261
262 >>> pd.cut(
263 ... np.array([1, 7, 5, 4]),
264 ... bins=np.histogram_bin_edges(np.array([1, 7, 5, 4]), bins="auto"),
265 ... )
266 ... # doctest: +ELLIPSIS
267 [NaN, (5.0, 7.0], (3.0, 5.0], (3.0, 5.0]]
268 Categories (3, interval[float64, right]): [(1.0, 3.0] < (3.0, 5.0] < (5.0, 7.0]]
269 """
270 # NOTE: this binning code is changed a bit from histogram for var(x) == 0
271
272 original = x
273 x_idx = _preprocess_for_cut(x)
274 x_idx, _ = _coerce_to_type(x_idx)
275
276 if not np.iterable(bins):
277 bins = _nbins_to_bins(x_idx, bins, right)
278
279 elif isinstance(bins, IntervalIndex):
280 if bins.is_overlapping:
281 raise ValueError("Overlapping IntervalIndex is not accepted.")
282
283 else:
284 bins = Index(bins)
285 if not bins.is_monotonic_increasing:
286 raise ValueError("bins must increase monotonically.")
287
288 fac, bins = _bins_to_cuts(
289 x_idx,
290 bins,
291 right=right,
292 labels=labels,
293 precision=precision,
294 include_lowest=include_lowest,
295 duplicates=duplicates,
296 ordered=ordered,
297 )
298
299 return _postprocess_for_cut(fac, bins, retbins, original)
300
301
302@set_module("pandas")
303def qcut(
304 x,
305 q,
306 labels=None,
307 retbins: bool = False,
308 precision: int = 3,
309 duplicates: str = "raise",
310):
311 """
312 Quantile-based discretization function.
313
314 Discretize variable into equal-sized buckets based on rank or based
315 on sample quantiles. For example 1000 values for 10 quantiles would
316 produce a Categorical object indicating quantile membership for each data point.
317
318 Parameters
319 ----------
320 x : 1d ndarray or Series
321 Input Numpy array or pandas Series object to be discretized.
322 q : int or list-like of float
323 Number of quantiles. 10 for deciles, 4 for quartiles, etc. Alternately
324 array of quantiles, e.g. [0, .25, .5, .75, 1.] for quartiles.
325 labels : array or False, default None
326 Used as labels for the resulting bins. Must be of the same length as
327 the resulting bins. If False, return only integer indicators of the
328 bins. If True, raises an error.
329 retbins : bool, optional
330 Whether to return the (bins, labels) or not. Can be useful if bins
331 is given as a scalar.
332 precision : int, optional
333 The precision at which to store and display the bins labels.
334 duplicates : {default 'raise', 'drop'}, optional
335 If bin edges are not unique, raise ValueError or drop non-uniques.
336
337 Returns
338 -------
339 out : Categorical or Series or array of integers if labels is False
340 The return type (Categorical or Series) depends on the input: a Series
341 of type category if input is a Series else Categorical. Bins are
342 represented as categories when categorical data is returned.
343 bins : ndarray of floats
344 Returned only if `retbins` is True.
345
346 See Also
347 --------
348 cut : Bin values into discrete intervals.
349 Series.quantile : Return value at the given quantile.
350
351 Notes
352 -----
353 Out of bounds values will be NA in the resulting Categorical object.
354 See :ref:`the user guide <reshaping.tile>` for more examples.
355
356 Examples
357 --------
358 >>> pd.qcut(range(5), 4)
359 ... # doctest: +ELLIPSIS
360 [(-0.001, 1.0], (-0.001, 1.0], (1.0, 2.0], (2.0, 3.0], (3.0, 4.0]]
361 Categories (4, interval[float64, right]): [(-0.001, 1.0] < (1.0, 2.0] ...
362
363 >>> pd.qcut(range(5), 3, labels=["good", "medium", "bad"])
364 ... # doctest: +SKIP
365 [good, good, medium, bad, bad]
366 Categories (3, str): [good < medium < bad]
367
368 >>> pd.qcut(range(5), 4, labels=False)
369 array([0, 0, 1, 2, 3])
370 """
371 if isinstance(x, Expression):
372 return x._call_with_func(
373 qcut, x=x, q=q, labels=labels, retbins=retbins, precision=precision
374 )
375 original = x
376 x_idx = _preprocess_for_cut(x)
377 x_idx, _ = _coerce_to_type(x_idx)
378
379 if is_integer(q):
380 if q < 1:
381 raise ValueError("`q` should be a positive integer.")
382 quantiles = np.linspace(0, 1, q + 1)
383 # Round up rather than to nearest if not representable in base 2
384 np.putmask(
385 quantiles,
386 q * quantiles != np.arange(q + 1),
387 np.nextafter(quantiles, 1),
388 )
389 elif is_list_like(q):
390 quantiles = q
391 else:
392 raise ValueError("`q` should be a positive integer or list-like of quantiles.")
393
394 bins = x_idx.to_series().dropna().quantile(quantiles)
395
396 fac, bins = _bins_to_cuts(
397 x_idx,
398 Index(bins),
399 labels=labels,
400 precision=precision,
401 include_lowest=True,
402 duplicates=duplicates,
403 )
404
405 return _postprocess_for_cut(fac, bins, retbins, original)
406
407
408def _nbins_to_bins(x_idx: Index, nbins: int, right: bool) -> Index:
409 """
410 If a user passed an integer N for bins, convert this to a sequence of N
411 equal(ish)-sized bins.
412 """
413 if is_scalar(nbins) and nbins < 1:
414 raise ValueError("`bins` should be a positive integer.")
415
416 if x_idx.size == 0:
417 raise ValueError("Cannot cut empty array")
418
419 rng = (x_idx.min(), x_idx.max())
420 mn, mx = rng
421
422 if is_numeric_dtype(x_idx.dtype) and (np.isinf(mn) or np.isinf(mx)): # type: ignore[call-overload]
423 # GH#24314
424 raise ValueError(
425 "cannot specify integer `bins` when input data contains infinity"
426 )
427
428 if mn == mx: # adjust end points before binning
429 if _is_dt_or_td(x_idx.dtype):
430 # using seconds=1 is pretty arbitrary here
431 # error: Argument 1 to "dtype_to_unit" has incompatible type
432 # "dtype[Any] | ExtensionDtype"; expected "DatetimeTZDtype | dtype[Any]"
433 unit = dtype_to_unit(x_idx.dtype) # type: ignore[arg-type]
434 td = Timedelta(seconds=1).as_unit(cast("TimeUnit", unit))
435 # Use DatetimeArray/TimedeltaArray method instead of linspace
436 # error: Item "ExtensionArray" of "ExtensionArray | ndarray[Any, Any]"
437 # has no attribute "_generate_range"
438 bins = x_idx._values._generate_range( # type: ignore[union-attr]
439 start=mn - td, # type: ignore[operator]
440 end=mx + td, # type: ignore[operator]
441 periods=nbins + 1,
442 freq=None,
443 unit=unit,
444 )
445 else:
446 mn -= 0.001 * abs(mn) if mn != 0 else 0.001 # type: ignore[operator, arg-type]
447 mx += 0.001 * abs(mx) if mx != 0 else 0.001 # type: ignore[operator, arg-type]
448
449 bins = np.linspace(mn, mx, nbins + 1, endpoint=True)
450 else: # adjust end points after binning
451 if _is_dt_or_td(x_idx.dtype):
452 # Use DatetimeArray/TimedeltaArray method instead of linspace
453
454 # error: Argument 1 to "dtype_to_unit" has incompatible type
455 # "dtype[Any] | ExtensionDtype"; expected "DatetimeTZDtype | dtype[Any]"
456 unit = dtype_to_unit(x_idx.dtype) # type: ignore[arg-type]
457 # error: Item "ExtensionArray" of "ExtensionArray | ndarray[Any, Any]"
458 # has no attribute "_generate_range"
459 bins = x_idx._values._generate_range( # type: ignore[union-attr]
460 start=mn, end=mx, periods=nbins + 1, freq=None, unit=unit
461 )
462 else:
463 bins = np.linspace(mn, mx, nbins + 1, endpoint=True) # type: ignore[call-overload]
464 adj = (mx - mn) * 0.001 # type: ignore[operator] # 0.1% of the range
465 if right:
466 bins[0] -= adj
467 else:
468 bins[-1] += adj
469
470 return Index(bins, copy=False)
471
472
473def _bins_to_cuts(
474 x_idx: Index,
475 bins: Index,
476 right: bool = True,
477 labels=None,
478 precision: int = 3,
479 include_lowest: bool = False,
480 duplicates: str = "raise",
481 ordered: bool = True,
482):
483 if not ordered and labels is None:
484 raise ValueError("'labels' must be provided if 'ordered = False'")
485
486 if duplicates not in ["raise", "drop"]:
487 raise ValueError(
488 "invalid value for 'duplicates' parameter, valid options are: raise, drop"
489 )
490
491 result: Categorical | np.ndarray
492
493 if isinstance(bins, IntervalIndex):
494 # we have a fast-path here
495 ids = bins.get_indexer(x_idx)
496 cat_dtype = CategoricalDtype(bins, ordered=True)
497 result = Categorical.from_codes(ids, dtype=cat_dtype, validate=False)
498 return result, bins
499
500 unique_bins = algos.unique(bins)
501 if len(unique_bins) < len(bins) and len(bins) != 2:
502 if duplicates == "raise":
503 raise ValueError(
504 f"Bin edges must be unique: {bins!r}.\n"
505 f"You can drop duplicate edges by setting the 'duplicates' kwarg"
506 )
507 bins = unique_bins
508
509 side: Literal["left", "right"] = "left" if right else "right"
510
511 try:
512 ids = bins.searchsorted(x_idx, side=side)
513 except TypeError as err:
514 # e.g. test_datetime_nan_error if bins are DatetimeArray and x_idx
515 # is integers
516 if x_idx.dtype.kind == "m":
517 raise ValueError("bins must be of timedelta64 dtype") from err
518 elif x_idx.dtype.kind == bins.dtype.kind == "M":
519 raise ValueError(
520 "Cannot use timezone-naive bins with timezone-aware values, "
521 "or vice-versa"
522 ) from err
523 elif x_idx.dtype.kind == "M":
524 raise ValueError("bins must be of datetime64 dtype") from err
525 else:
526 raise
527 ids = ensure_platform_int(ids)
528
529 if include_lowest:
530 ids[x_idx == bins[0]] = 1
531
532 na_mask = isna(x_idx) | (ids == len(bins)) | (ids == 0)
533 has_nas = na_mask.any()
534
535 if labels is not False:
536 if not (labels is None or is_list_like(labels)):
537 raise ValueError(
538 "Bin labels must either be False, None or passed in as a "
539 "list-like argument"
540 )
541
542 if labels is None:
543 labels = _format_labels(
544 bins, precision, right=right, include_lowest=include_lowest
545 )
546 elif ordered and len(set(labels)) != len(labels):
547 raise ValueError(
548 "labels must be unique if ordered=True; pass ordered=False "
549 "for duplicate labels"
550 )
551 elif len(labels) != len(bins) - 1:
552 raise ValueError(
553 "Bin labels must be one fewer than the number of bin edges"
554 )
555
556 if not isinstance(getattr(labels, "dtype", None), CategoricalDtype):
557 labels = Categorical(
558 labels,
559 categories=labels if len(set(labels)) == len(labels) else None,
560 ordered=ordered,
561 )
562 # TODO: handle mismatch between categorical label order and pandas.cut order.
563 np.putmask(ids, na_mask, 0)
564 result = algos.take_nd(labels, ids - 1)
565
566 else:
567 result = ids - 1
568 if has_nas:
569 result = result.astype(np.float64)
570 np.putmask(result, na_mask, np.nan)
571
572 return result, bins
573
574
575def _coerce_to_type(x: Index) -> tuple[Index, DtypeObj | None]:
576 """
577 if the passed data is of datetime/timedelta, bool or nullable int type,
578 this method converts it to numeric so that cut or qcut method can
579 handle it
580 """
581 dtype: DtypeObj | None = None
582
583 if _is_dt_or_td(x.dtype):
584 dtype = x.dtype
585 elif is_bool_dtype(x.dtype):
586 # GH 20303
587 x = x.astype(np.int64)
588 # To support cut and qcut for IntegerArray we convert to float dtype.
589 # Will properly support in the future.
590 # https://github.com/pandas-dev/pandas/pull/31290
591 # https://github.com/pandas-dev/pandas/issues/31389
592 elif isinstance(x.dtype, ExtensionDtype) and is_numeric_dtype(x.dtype):
593 x_arr = x.to_numpy(dtype=np.float64, na_value=np.nan)
594 x = Index(x_arr, copy=False)
595
596 return Index(x), dtype
597
598
599def _is_dt_or_td(dtype: DtypeObj) -> bool:
600 # Note: the dtype here comes from an Index.dtype, so we know that any
601 # dt64/td64 dtype is of a supported unit.
602 return isinstance(dtype, DatetimeTZDtype) or lib.is_np_dtype(dtype, "mM")
603
604
605def _format_labels(
606 bins: Index,
607 precision: int,
608 right: bool = True,
609 include_lowest: bool = False,
610) -> IntervalIndex:
611 """based on the dtype, return our labels"""
612 closed: IntervalLeftRight = "right" if right else "left"
613
614 formatter: Callable[[Any], Timestamp] | Callable[[Any], Timedelta]
615
616 if _is_dt_or_td(bins.dtype):
617 # error: Argument 1 to "dtype_to_unit" has incompatible type
618 # "dtype[Any] | ExtensionDtype"; expected "DatetimeTZDtype | dtype[Any]"
619 unit = dtype_to_unit(bins.dtype) # type: ignore[arg-type]
620 unit = cast("TimeUnit", unit)
621 formatter = lambda x: x
622 adjust = lambda x: x - Timedelta(1, unit=unit).as_unit(unit)
623 else:
624 precision = _infer_precision(precision, bins)
625 formatter = lambda x: _round_frac(x, precision)
626 adjust = lambda x: x - 10 ** (-precision)
627
628 breaks = [formatter(b) for b in bins]
629 if right and include_lowest:
630 # adjust lhs of first interval by precision to account for being right closed
631 breaks[0] = adjust(breaks[0])
632
633 if _is_dt_or_td(bins.dtype):
634 # error: "Index" has no attribute "as_unit"
635 breaks = type(bins)(breaks).as_unit(unit) # type: ignore[attr-defined]
636
637 return IntervalIndex.from_breaks(breaks, closed=closed)
638
639
640def _preprocess_for_cut(x) -> Index:
641 """
642 handles preprocessing for cut where we convert passed
643 input to array, strip the index information and store it
644 separately
645 """
646 # Check that the passed array is a Pandas or Numpy object
647 # We don't want to strip away a Pandas data-type here (e.g. datetimetz)
648 ndim = getattr(x, "ndim", None)
649 if ndim is None:
650 x = np.asarray(x)
651 if x.ndim != 1:
652 raise ValueError("Input array must be 1 dimensional")
653
654 return Index(x, copy=False)
655
656
657def _postprocess_for_cut(fac, bins, retbins: bool, original):
658 """
659 handles post processing for the cut method where
660 we combine the index information if the originally passed
661 datatype was a series
662 """
663 if isinstance(original, ABCSeries):
664 fac = original._constructor(fac, index=original.index, name=original.name)
665
666 if not retbins:
667 return fac
668
669 if isinstance(bins, Index) and is_numeric_dtype(bins.dtype):
670 bins = bins._values
671
672 return fac, bins
673
674
675def _round_frac(x, precision: int):
676 """
677 Round the fractional part of the given number
678 """
679 if not np.isfinite(x) or x == 0:
680 return x
681 else:
682 frac, whole = np.modf(x)
683 if whole == 0:
684 digits = -int(np.floor(np.log10(abs(frac)))) - 1 + precision
685 else:
686 digits = precision
687 return np.around(x, digits)
688
689
690def _infer_precision(base_precision: int, bins: Index) -> int:
691 """
692 Infer an appropriate precision for _round_frac
693 """
694 for precision in range(base_precision, 20):
695 levels = np.asarray([_round_frac(b, precision) for b in bins])
696 if algos.unique(levels).size == bins.size:
697 return precision
698 return base_precision # default