Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.11/site-packages/pandas/core/reshape/tile.py: 15%

Shortcuts on this page

r m x   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

182 statements  

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