Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.11/site-packages/pandas/core/missing.py: 16%

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

360 statements  

1""" 

2Routines for filling missing data. 

3""" 

4 

5from __future__ import annotations 

6 

7from functools import wraps 

8from typing import ( 

9 TYPE_CHECKING, 

10 Any, 

11 Literal, 

12 cast, 

13 overload, 

14) 

15 

16import numpy as np 

17 

18from pandas._config import is_nan_na 

19 

20from pandas._libs import ( 

21 NaT, 

22 algos, 

23 lib, 

24) 

25from pandas.compat._optional import import_optional_dependency 

26 

27from pandas.core.dtypes.cast import infer_dtype_from 

28from pandas.core.dtypes.common import ( 

29 is_array_like_deprecate_non_pandas, 

30 is_arrow_temporal_dtype, 

31 is_bool_dtype, 

32 is_numeric_dtype, 

33 is_object_dtype, 

34 needs_i8_conversion, 

35) 

36from pandas.core.dtypes.dtypes import ( 

37 ArrowDtype, 

38 BaseMaskedDtype, 

39 DatetimeTZDtype, 

40) 

41from pandas.core.dtypes.missing import ( 

42 is_valid_na_for_dtype, 

43 isna, 

44 na_value_for_dtype, 

45) 

46 

47if TYPE_CHECKING: 

48 from collections.abc import Callable 

49 from typing import TypeAlias 

50 

51 from pandas._typing import ( 

52 ArrayLike, 

53 AxisInt, 

54 F, 

55 ReindexMethod, 

56 npt, 

57 ) 

58 

59 from pandas import Index 

60 

61 _CubicBC: TypeAlias = Literal["not-a-knot", "clamped", "natural", "periodic"] 

62 

63 

64def check_value_size(value, mask: npt.NDArray[np.bool_], length: int): 

65 """ 

66 Validate the size of the values passed to ExtensionArray.fillna. 

67 """ 

68 if isinstance(value, dict): 

69 raise TypeError( 

70 "ExtensionArray.fillna does not support filling with a dict. " 

71 "Use Series.fillna instead." 

72 ) 

73 if is_array_like_deprecate_non_pandas(value): 

74 if len(value) != length: 

75 raise ValueError( 

76 f"Length of 'value' does not match. Got ({len(value)}) " 

77 f" expected {length}" 

78 ) 

79 value = value[mask] 

80 

81 return value 

82 

83 

84def mask_missing(arr: ArrayLike, value) -> npt.NDArray[np.bool_]: 

85 """ 

86 Return a masking array of same size/shape as arr 

87 with entries equaling value set to True. 

88 

89 Parameters 

90 ---------- 

91 arr : ArrayLike 

92 value : scalar-like 

93 Caller has ensured `not is_list_like(value)` and that it can be held 

94 by `arr`. 

95 

96 Returns 

97 ------- 

98 np.ndarray[bool] 

99 """ 

100 dtype, value = infer_dtype_from(value) 

101 

102 if ( 

103 isinstance(arr.dtype, (BaseMaskedDtype, ArrowDtype)) 

104 and lib.is_float(value) 

105 and np.isnan(value) 

106 and not is_nan_na() 

107 ): 

108 # TODO: this should be done in an EA method? 

109 if arr.dtype.kind == "f": 

110 # GH#55127 

111 if isinstance(arr.dtype, BaseMaskedDtype): 

112 # error: "ExtensionArray" has no attribute "_data" [attr-defined] 

113 mask = np.isnan(arr._data) & ~arr.isna() # type: ignore[attr-defined] 

114 return mask 

115 else: 

116 # error: "ExtensionArray" has no attribute "_pa_array" [attr-defined] 

117 import pyarrow.compute as pc 

118 

119 mask = pc.is_nan(arr._pa_array).fill_null(False).to_numpy() # type: ignore[attr-defined] 

120 return mask 

121 

122 elif arr.dtype.kind in "iu": 

123 # GH#51237 

124 mask = np.zeros(arr.shape, dtype=bool) 

125 return mask 

126 

127 if isna(value): 

128 return isna(arr) 

129 

130 # GH 21977 

131 mask = np.zeros(arr.shape, dtype=bool) 

132 if ( 

133 is_numeric_dtype(arr.dtype) 

134 and not is_bool_dtype(arr.dtype) 

135 and lib.is_bool(value) 

136 ): 

137 # e.g. test_replace_ea_float_with_bool, see GH#62048 

138 pass 

139 elif ( 

140 is_bool_dtype(arr.dtype) and is_numeric_dtype(dtype) and not lib.is_bool(value) 

141 ): 

142 # e.g. test_replace_ea_float_with_bool, see GH#62048 

143 pass 

144 elif is_numeric_dtype(arr.dtype) and isinstance(value, str): 

145 # GH#29553 prevent numpy deprecation warnings 

146 pass 

147 elif is_object_dtype(arr.dtype): 

148 # pre-compute mask to avoid comparison to NA 

149 # e.g. test_replace_na_in_obj_column 

150 arr_mask = ~isna(arr) 

151 mask[arr_mask] = arr[arr_mask] == value 

152 else: 

153 new_mask = arr == value 

154 

155 if not isinstance(new_mask, np.ndarray): 

156 # usually BooleanArray 

157 new_mask = new_mask.to_numpy(dtype=bool, na_value=False) 

158 mask = new_mask 

159 

160 return mask 

161 

162 

163@overload 

164def clean_fill_method( 

165 method: Literal["ffill", "pad", "bfill", "backfill"], 

166 *, 

167 allow_nearest: Literal[False] = ..., 

168) -> Literal["pad", "backfill"]: ... 

169 

170 

171@overload 

172def clean_fill_method( 

173 method: Literal["ffill", "pad", "bfill", "backfill", "nearest"], 

174 *, 

175 allow_nearest: Literal[True], 

176) -> Literal["pad", "backfill", "nearest"]: ... 

177 

178 

179def clean_fill_method( 

180 method: Literal["ffill", "pad", "bfill", "backfill", "nearest"], 

181 *, 

182 allow_nearest: bool = False, 

183) -> Literal["pad", "backfill", "nearest"]: 

184 if isinstance(method, str): 

185 # error: Incompatible types in assignment (expression has type "str", variable 

186 # has type "Literal['ffill', 'pad', 'bfill', 'backfill', 'nearest']") 

187 method = method.lower() # type: ignore[assignment] 

188 if method == "ffill": 

189 method = "pad" 

190 elif method == "bfill": 

191 method = "backfill" 

192 

193 valid_methods = ["pad", "backfill"] 

194 expecting = "pad (ffill) or backfill (bfill)" 

195 if allow_nearest: 

196 valid_methods.append("nearest") 

197 expecting = "pad (ffill), backfill (bfill) or nearest" 

198 if method not in valid_methods: 

199 raise ValueError(f"Invalid fill method. Expecting {expecting}. Got {method}") 

200 return method 

201 

202 

203# interpolation methods that dispatch to np.interp 

204 

205NP_METHODS = ["linear", "time", "index", "values"] 

206 

207# interpolation methods that dispatch to _interpolate_scipy_wrapper 

208 

209SP_METHODS = [ 

210 "nearest", 

211 "zero", 

212 "slinear", 

213 "quadratic", 

214 "cubic", 

215 "barycentric", 

216 "krogh", 

217 "spline", 

218 "polynomial", 

219 "from_derivatives", 

220 "piecewise_polynomial", 

221 "pchip", 

222 "akima", 

223 "cubicspline", 

224] 

225 

226 

227def clean_interp_method(method: str, index: Index, **kwargs) -> str: 

228 order = kwargs.get("order") 

229 

230 if method in ("spline", "polynomial") and order is None: 

231 raise ValueError("You must specify the order of the spline or polynomial.") 

232 

233 valid = NP_METHODS + SP_METHODS 

234 if method not in valid: 

235 raise ValueError(f"method must be one of {valid}. Got '{method}' instead.") 

236 

237 if method in ("krogh", "piecewise_polynomial", "pchip"): 

238 if not index.is_monotonic_increasing: 

239 raise ValueError( 

240 f"{method} interpolation requires that the index be monotonic." 

241 ) 

242 

243 return method 

244 

245 

246def find_valid_index(how: str, is_valid: npt.NDArray[np.bool_]) -> int | None: 

247 """ 

248 Retrieves the positional index of the first valid value. 

249 

250 Parameters 

251 ---------- 

252 how : {'first', 'last'} 

253 Use this parameter to change between the first or last valid index. 

254 is_valid: np.ndarray 

255 Mask to find na_values. 

256 

257 Returns 

258 ------- 

259 int or None 

260 """ 

261 assert how in ["first", "last"] 

262 

263 if len(is_valid) == 0: # early stop 

264 return None 

265 

266 if is_valid.ndim == 2: 

267 # reduce axis 1 

268 is_valid = is_valid.any(axis=1) 

269 

270 if how == "first": 

271 idxpos = is_valid[::].argmax() 

272 

273 elif how == "last": 

274 idxpos = len(is_valid) - 1 - is_valid[::-1].argmax() 

275 

276 chk_notna = is_valid[idxpos] 

277 

278 if not chk_notna: 

279 return None 

280 # Incompatible return value type (got "signedinteger[Any]", 

281 # expected "Optional[int]") 

282 return idxpos # type: ignore[return-value] 

283 

284 

285def validate_limit_direction( 

286 limit_direction: str, 

287) -> Literal["forward", "backward", "both"]: 

288 valid_limit_directions = ["forward", "backward", "both"] 

289 limit_direction = limit_direction.lower() 

290 if limit_direction not in valid_limit_directions: 

291 raise ValueError( 

292 "Invalid limit_direction: expecting one of " 

293 f"{valid_limit_directions}, got '{limit_direction}'." 

294 ) 

295 # error: Incompatible return value type (got "str", expected 

296 # "Literal['forward', 'backward', 'both']") 

297 return limit_direction # type: ignore[return-value] 

298 

299 

300def validate_limit_area(limit_area: str | None) -> Literal["inside", "outside"] | None: 

301 if limit_area is not None: 

302 valid_limit_areas = ["inside", "outside"] 

303 limit_area = limit_area.lower() 

304 if limit_area not in valid_limit_areas: 

305 raise ValueError( 

306 f"Invalid limit_area: expecting one of {valid_limit_areas}, got " 

307 f"{limit_area}." 

308 ) 

309 # error: Incompatible return value type (got "Optional[str]", expected 

310 # "Optional[Literal['inside', 'outside']]") 

311 return limit_area # type: ignore[return-value] 

312 

313 

314def infer_limit_direction( 

315 limit_direction: Literal["backward", "forward", "both"] | None, method: str 

316) -> Literal["backward", "forward", "both"]: 

317 # Set `limit_direction` depending on `method` 

318 if limit_direction is None: 

319 if method in ("backfill", "bfill"): 

320 limit_direction = "backward" 

321 else: 

322 limit_direction = "forward" 

323 else: 

324 if method in ("pad", "ffill") and limit_direction != "forward": 

325 raise ValueError( 

326 f"`limit_direction` must be 'forward' for method `{method}`" 

327 ) 

328 if method in ("backfill", "bfill") and limit_direction != "backward": 

329 raise ValueError( 

330 f"`limit_direction` must be 'backward' for method `{method}`" 

331 ) 

332 return limit_direction 

333 

334 

335def get_interp_index(method, index: Index) -> Index: 

336 # create/use the index 

337 if method == "linear": 

338 # prior default 

339 from pandas import RangeIndex 

340 

341 index = RangeIndex(len(index)) 

342 else: 

343 methods = {"index", "values", "nearest", "time"} 

344 is_numeric_or_datetime = ( 

345 is_numeric_dtype(index.dtype) 

346 or isinstance(index.dtype, DatetimeTZDtype) 

347 or lib.is_np_dtype(index.dtype, "mM") 

348 ) 

349 valid = NP_METHODS + SP_METHODS 

350 if method in valid: 

351 if method not in methods and not is_numeric_or_datetime: 

352 raise ValueError( 

353 "Index column must be numeric or datetime type when " 

354 f"using {method} method other than linear. " 

355 "Try setting a numeric or datetime index column before " 

356 "interpolating." 

357 ) 

358 else: 

359 raise ValueError(f"Can not interpolate with method={method}.") 

360 

361 if isna(index).any(): 

362 raise NotImplementedError( 

363 "Interpolation with NaNs in the index " 

364 "has not been implemented. Try filling " 

365 "those NaNs before interpolating." 

366 ) 

367 return index 

368 

369 

370def interpolate_2d_inplace( 

371 data: np.ndarray, # floating dtype 

372 index: Index, 

373 axis: AxisInt, 

374 method: str = "linear", 

375 limit: int | None = None, 

376 limit_direction: str = "forward", 

377 limit_area: str | None = None, 

378 fill_value: Any | None = None, 

379 mask=None, 

380 **kwargs, 

381) -> None: 

382 """ 

383 Column-wise application of _interpolate_1d. 

384 

385 Notes 

386 ----- 

387 Alters 'data' in-place. 

388 

389 The signature does differ from _interpolate_1d because it only 

390 includes what is needed for Block.interpolate. 

391 """ 

392 # validate the interp method 

393 clean_interp_method(method, index, **kwargs) 

394 

395 if is_valid_na_for_dtype(fill_value, data.dtype): 

396 fill_value = na_value_for_dtype(data.dtype, compat=False) 

397 

398 if method == "time": 

399 if not needs_i8_conversion(index.dtype) and not is_arrow_temporal_dtype( 

400 index.dtype 

401 ): 

402 raise ValueError( 

403 "time-weighted interpolation only works " 

404 "on Series or DataFrames with a " 

405 "DatetimeIndex" 

406 ) 

407 method = "values" 

408 

409 limit_direction = validate_limit_direction(limit_direction) 

410 limit_area_validated = validate_limit_area(limit_area) 

411 

412 # default limit is unlimited GH #16282 

413 limit = algos.validate_limit(nobs=None, limit=limit) 

414 

415 indices = _index_to_interp_indices(index, method) 

416 

417 def func(yvalues: np.ndarray) -> None: 

418 # process 1-d slices in the axis direction 

419 

420 _interpolate_1d( 

421 indices=indices, 

422 yvalues=yvalues, 

423 method=method, 

424 limit=limit, 

425 limit_direction=limit_direction, 

426 limit_area=limit_area_validated, 

427 fill_value=fill_value, 

428 bounds_error=False, 

429 mask=mask, 

430 **kwargs, 

431 ) 

432 

433 np.apply_along_axis(func, axis, data) 

434 

435 

436def _arrow_temporal_to_i8(arr: ArrayLike) -> np.ndarray: 

437 """ 

438 View an ArrowDtype timestamp/duration array as int64. 

439 

440 ``np.asarray`` is not usable here: for tz-aware timestamps it yields an 

441 object array of ``Timestamp`` objects, which cannot be viewed as i8. 

442 

443 Parameters 

444 ---------- 

445 arr : ArrowExtensionArray 

446 Array whose dtype satisfies ``is_arrow_temporal_dtype``. 

447 

448 Returns 

449 ------- 

450 np.ndarray 

451 int64 view of the underlying values, with ``iNaT`` for missing values. 

452 """ 

453 pa = import_optional_dependency("pyarrow") 

454 if pa.types.is_timestamp(arr.dtype.pyarrow_dtype): # type: ignore[union-attr] 

455 return arr._to_datetimearray().asi8 # type: ignore[union-attr] 

456 return arr._to_timedeltaarray().asi8 # type: ignore[union-attr] 

457 

458 

459def _index_to_interp_indices(index: Index, method: str) -> np.ndarray: 

460 """ 

461 Convert Index to ndarray of indices to pass to NumPy/SciPy. 

462 """ 

463 xarr = index._values 

464 if needs_i8_conversion(xarr.dtype): 

465 # GH#1646 for dt64tz 

466 xarr = xarr.view("i8") 

467 elif is_arrow_temporal_dtype(xarr.dtype): 

468 xarr = _arrow_temporal_to_i8(xarr) 

469 

470 if method == "linear": 

471 inds = xarr 

472 inds = cast("np.ndarray", inds) 

473 else: 

474 inds = np.asarray(xarr) 

475 

476 if method in ("values", "index"): 

477 if inds.dtype == np.object_: 

478 inds = lib.maybe_convert_objects(inds) 

479 

480 return inds 

481 

482 

483def _interpolate_1d( 

484 indices: np.ndarray, 

485 yvalues: np.ndarray, 

486 method: str = "linear", 

487 limit: int | None = None, 

488 limit_direction: str = "forward", 

489 limit_area: Literal["inside", "outside"] | None = None, 

490 fill_value: Any | None = None, 

491 bounds_error: bool = False, 

492 order: int | None = None, 

493 mask=None, 

494 **kwargs, 

495) -> None: 

496 """ 

497 Logic for the 1-d interpolation. The input 

498 indices and yvalues will each be 1-d arrays of the same length. 

499 

500 Bounds_error is currently hardcoded to False since non-scipy ones don't 

501 take it as an argument. 

502 

503 Notes 

504 ----- 

505 Fills 'yvalues' in-place. 

506 """ 

507 if mask is not None: 

508 invalid = mask 

509 else: 

510 invalid = isna(yvalues) 

511 valid = ~invalid 

512 

513 if not valid.any(): 

514 return 

515 

516 if valid.all(): 

517 return 

518 

519 # These index pointers to invalid values... i.e. {0, 1, etc... 

520 all_nans = np.flatnonzero(invalid) 

521 

522 first_valid_index = find_valid_index(how="first", is_valid=valid) 

523 if first_valid_index is None: # no nan found in start 

524 first_valid_index = 0 

525 start_nans = np.arange(first_valid_index) 

526 

527 last_valid_index = find_valid_index(how="last", is_valid=valid) 

528 if last_valid_index is None: # no nan found in end 

529 last_valid_index = len(yvalues) 

530 end_nans = np.arange(1 + last_valid_index, len(valid)) 

531 

532 # preserve_nans contains indices of invalid values, 

533 # but in this case, it is the final set of indices that need to be 

534 # preserved as NaN after the interpolation. 

535 

536 # For example if limit_direction='forward' then preserve_nans will 

537 # contain indices of NaNs at the beginning of the series, and NaNs that 

538 # are more than 'limit' away from the prior non-NaN. 

539 

540 # set preserve_nans based on direction using _interp_limit 

541 if limit_direction == "forward": 

542 preserve_nans = np.union1d(start_nans, _interp_limit(invalid, limit, 0)) 

543 elif limit_direction == "backward": 

544 preserve_nans = np.union1d(end_nans, _interp_limit(invalid, 0, limit)) 

545 else: 

546 # both directions... just use _interp_limit 

547 preserve_nans = np.unique(_interp_limit(invalid, limit, limit)) 

548 

549 # if limit_area is set, add either mid or outside indices 

550 # to preserve_nans GH #16284 

551 if limit_area == "inside": 

552 # preserve NaNs on the outside 

553 preserve_nans = np.union1d(preserve_nans, start_nans) 

554 preserve_nans = np.union1d(preserve_nans, end_nans) 

555 elif limit_area == "outside": 

556 # preserve NaNs on the inside 

557 mid_nans = np.setdiff1d(all_nans, start_nans, assume_unique=True) 

558 mid_nans = np.setdiff1d(mid_nans, end_nans, assume_unique=True) 

559 preserve_nans = np.union1d(preserve_nans, mid_nans) 

560 

561 is_datetimelike = yvalues.dtype.kind in "mM" 

562 

563 if is_datetimelike: 

564 yvalues = yvalues.view("i8") 

565 

566 if method in NP_METHODS: 

567 # np.interp requires sorted X values, #21037 

568 

569 indexer = np.argsort(indices[valid]) 

570 yvalues[invalid] = np.interp( 

571 indices[invalid], indices[valid][indexer], yvalues[valid][indexer] 

572 ) 

573 else: 

574 yvalues[invalid] = _interpolate_scipy_wrapper( 

575 indices[valid], 

576 yvalues[valid], 

577 indices[invalid], 

578 method=method, 

579 fill_value=fill_value, 

580 bounds_error=bounds_error, 

581 order=order, 

582 **kwargs, 

583 ) 

584 

585 if mask is not None: 

586 mask[:] = False 

587 mask[preserve_nans] = True 

588 elif is_datetimelike: 

589 yvalues[preserve_nans] = NaT.value 

590 else: 

591 yvalues[preserve_nans] = np.nan 

592 return 

593 

594 

595def _interpolate_scipy_wrapper( 

596 x: np.ndarray, 

597 y: np.ndarray, 

598 new_x: np.ndarray, 

599 method: str, 

600 fill_value=None, 

601 bounds_error: bool = False, 

602 order=None, 

603 **kwargs, 

604): 

605 """ 

606 Passed off to scipy.interpolate.interp1d. method is scipy's kind. 

607 Returns an array interpolated at new_x. Add any new methods to 

608 the list in _clean_interp_method. 

609 """ 

610 extra = f"{method} interpolation requires SciPy." 

611 import_optional_dependency("scipy", extra=extra) 

612 from scipy import interpolate 

613 

614 new_x = np.asarray(new_x) 

615 

616 # ignores some kwargs that could be passed along. 

617 alt_methods: dict[str, Callable[..., np.ndarray]] = { 

618 "barycentric": interpolate.barycentric_interpolate, 

619 "krogh": interpolate.krogh_interpolate, 

620 "from_derivatives": _from_derivatives, 

621 "piecewise_polynomial": _from_derivatives, 

622 "cubicspline": _cubicspline_interpolate, 

623 "akima": _akima_interpolate, 

624 "pchip": interpolate.pchip_interpolate, 

625 } 

626 

627 interp1d_methods = [ 

628 "nearest", 

629 "zero", 

630 "slinear", 

631 "quadratic", 

632 "cubic", 

633 "polynomial", 

634 ] 

635 terp: Callable[..., np.ndarray] | None 

636 if method in interp1d_methods: 

637 if method == "polynomial": 

638 kind = order 

639 else: 

640 kind = method 

641 terp = interpolate.interp1d( 

642 x, y, kind=kind, fill_value=fill_value, bounds_error=bounds_error 

643 ) 

644 new_y = terp(new_x) # pyright: ignore[reportOptionalCall] 

645 elif method == "spline": 

646 # GH #10633, #24014 

647 if isna(order) or (order <= 0): # pyright: ignore[reportOptionalOperand] 

648 raise ValueError( 

649 f"order needs to be specified and greater than 0; got order: {order}" 

650 ) 

651 terp = interpolate.UnivariateSpline(x, y, k=order, **kwargs) 

652 new_y = terp(new_x) # pyright: ignore[reportOptionalCall] 

653 else: 

654 # GH 7295: need to be able to write for some reason 

655 # in some circumstances: check all three 

656 if not x.flags.writeable: 

657 x = x.copy() 

658 if not y.flags.writeable: 

659 y = y.copy() 

660 if not new_x.flags.writeable: 

661 new_x = new_x.copy() 

662 terp = alt_methods.get(method, None) 

663 if terp is None: 

664 raise ValueError(f"Can not interpolate with method={method}.") 

665 

666 # Make sure downcast is not in kwargs for alt methods 

667 kwargs.pop("downcast", None) 

668 new_y = terp(x, y, new_x, **kwargs) 

669 return new_y 

670 

671 

672def _from_derivatives( 

673 xi: np.ndarray, 

674 yi: np.ndarray, 

675 x: np.ndarray, 

676 order=None, 

677 der: int | list[int] | None = 0, 

678 extrapolate: bool = False, 

679): 

680 """ 

681 Convenience function for interpolate.BPoly.from_derivatives. 

682 

683 Construct a piecewise polynomial in the Bernstein basis, compatible 

684 with the specified values and derivatives at breakpoints. 

685 

686 Parameters 

687 ---------- 

688 xi : array-like 

689 sorted 1D array of x-coordinates 

690 yi : array-like or list of array-likes 

691 yi[i][j] is the j-th derivative known at xi[i] 

692 order: None or int or array-like of ints. Default: None. 

693 Specifies the degree of local polynomials. If not None, some 

694 derivatives are ignored. 

695 der : int or list 

696 How many derivatives to extract; None for all potentially nonzero 

697 derivatives (that is a number equal to the number of points), or a 

698 list of derivatives to extract. This number includes the function 

699 value as 0th derivative. 

700 extrapolate : bool, optional 

701 Whether to extrapolate to out-of-bounds points based on first and last 

702 intervals, or to return NaNs. Default: True. 

703 

704 See Also 

705 -------- 

706 scipy.interpolate.BPoly.from_derivatives 

707 

708 Returns 

709 ------- 

710 y : scalar or array-like 

711 The result, of length R or length M or M by R. 

712 """ 

713 from scipy import interpolate 

714 

715 # return the method for compat with scipy version & backwards compat 

716 method = interpolate.BPoly.from_derivatives 

717 m = method(xi, yi.reshape(-1, 1), orders=order, extrapolate=extrapolate) 

718 

719 return m(x) 

720 

721 

722def _akima_interpolate( 

723 xi: np.ndarray, 

724 yi: np.ndarray, 

725 x: np.ndarray, 

726 der: int = 0, 

727 axis: AxisInt = 0, 

728): 

729 """ 

730 Convenience function for akima interpolation. 

731 xi and yi are arrays of values used to approximate some function f, 

732 with ``yi = f(xi)``. 

733 

734 See `Akima1DInterpolator` for details. 

735 

736 Parameters 

737 ---------- 

738 xi : np.ndarray 

739 A sorted list of x-coordinates, of length N. 

740 yi : np.ndarray 

741 A 1-D array of real values. `yi`'s length along the interpolation 

742 axis must be equal to the length of `xi`. If N-D array, use axis 

743 parameter to select correct axis. 

744 x : np.ndarray 

745 Of length M. 

746 der : int, optional 

747 How many derivatives to extract. This number includes the function 

748 value as 0th derivative. 

749 axis : int, optional 

750 Axis in the yi array corresponding to the x-coordinate values. 

751 

752 See Also 

753 -------- 

754 scipy.interpolate.Akima1DInterpolator 

755 

756 Returns 

757 ------- 

758 y : scalar or array-like 

759 The result, of length R or length M or M by R, 

760 

761 """ 

762 from scipy import interpolate 

763 

764 P = interpolate.Akima1DInterpolator(xi, yi, axis=axis) 

765 

766 return P(x, nu=der) 

767 

768 

769def _cubicspline_interpolate( 

770 xi: np.ndarray, 

771 yi: np.ndarray, 

772 x: np.ndarray, 

773 axis: AxisInt = 0, 

774 bc_type: _CubicBC | tuple[Any, Any] = "not-a-knot", 

775 extrapolate: Literal["periodic"] | bool | None = None, 

776) -> np.ndarray: 

777 """ 

778 Convenience function for cubic spline data interpolator. 

779 

780 See `scipy.interpolate.CubicSpline` for details. 

781 

782 Parameters 

783 ---------- 

784 xi : np.ndarray, shape (n,) 

785 1-d array containing values of the independent variable. 

786 Values must be real, finite and in strictly increasing order. 

787 yi : np.ndarray 

788 Array containing values of the dependent variable. It can have 

789 arbitrary number of dimensions, but the length along ``axis`` 

790 (see below) must match the length of ``x``. Values must be finite. 

791 x : np.ndarray, shape (m,) 

792 axis : int, optional 

793 Axis along which `y` is assumed to be varying. Meaning that for 

794 ``x[i]`` the corresponding values are ``np.take(y, i, axis=axis)``. 

795 Default is 0. 

796 bc_type : string or 2-tuple, optional 

797 Boundary condition type. Two additional equations, given by the 

798 boundary conditions, are required to determine all coefficients of 

799 polynomials on each segment [2]_. 

800 If `bc_type` is a string, then the specified condition will be applied 

801 at both ends of a spline. Available conditions are: 

802 * 'not-a-knot' (default): The first and second segment at a curve end 

803 are the same polynomial. It is a good default when there is no 

804 information on boundary conditions. 

805 * 'periodic': The interpolated functions is assumed to be periodic 

806 of period ``x[-1] - x[0]``. The first and last value of `y` must be 

807 identical: ``y[0] == y[-1]``. This boundary condition will result in 

808 ``y'[0] == y'[-1]`` and ``y''[0] == y''[-1]``. 

809 * 'clamped': The first derivative at curves ends are zero. Assuming 

810 a 1D `y`, ``bc_type=((1, 0.0), (1, 0.0))`` is the same condition. 

811 * 'natural': The second derivative at curve ends are zero. Assuming 

812 a 1D `y`, ``bc_type=((2, 0.0), (2, 0.0))`` is the same condition. 

813 If `bc_type` is a 2-tuple, the first and the second value will be 

814 applied at the curve start and end respectively. The tuple values can 

815 be one of the previously mentioned strings (except 'periodic') or a 

816 tuple `(order, deriv_values)` allowing to specify arbitrary 

817 derivatives at curve ends: 

818 * `order`: the derivative order, 1 or 2. 

819 * `deriv_value`: array-like containing derivative values, shape must 

820 be the same as `y`, excluding ``axis`` dimension. For example, if 

821 `y` is 1D, then `deriv_value` must be a scalar. If `y` is 3D with 

822 the shape (n0, n1, n2) and axis=2, then `deriv_value` must be 2D 

823 and have the shape (n0, n1). 

824 extrapolate : {bool, 'periodic', None}, optional 

825 If bool, determines whether to extrapolate to out-of-bounds points 

826 based on first and last intervals, or to return NaNs. If 'periodic', 

827 periodic extrapolation is used. If None (default), ``extrapolate`` is 

828 set to 'periodic' for ``bc_type='periodic'`` and to True otherwise. 

829 

830 See Also 

831 -------- 

832 scipy.interpolate.CubicHermiteSpline 

833 

834 Returns 

835 ------- 

836 y : scalar or array-like 

837 The result, of shape (m,) 

838 

839 References 

840 ---------- 

841 .. [1] `Cubic Spline Interpolation 

842 <https://en.wikiversity.org/wiki/Cubic_Spline_Interpolation>`_ 

843 on Wikiversity. 

844 .. [2] Carl de Boor, "A Practical Guide to Splines", Springer-Verlag, 1978. 

845 """ 

846 from scipy import interpolate 

847 

848 P = interpolate.CubicSpline( 

849 xi, yi, axis=axis, bc_type=bc_type, extrapolate=extrapolate 

850 ) 

851 

852 return P(x) 

853 

854 

855def pad_or_backfill_inplace( 

856 values: np.ndarray, 

857 method: Literal["pad", "backfill"] = "pad", 

858 axis: AxisInt = 0, 

859 limit: int | None = None, 

860 limit_area: Literal["inside", "outside"] | None = None, 

861) -> None: 

862 """ 

863 Perform an actual interpolation of values, values will be make 2-d if 

864 needed fills inplace, returns the result. 

865 

866 Parameters 

867 ---------- 

868 values: np.ndarray 

869 Input array. 

870 method: str, default "pad" 

871 Interpolation method. Could be "bfill" or "pad" 

872 axis: 0 or 1 

873 Interpolation axis 

874 limit: int, optional 

875 Index limit on interpolation. 

876 limit_area: str, optional 

877 Limit area for interpolation. Can be "inside" or "outside" 

878 

879 Notes 

880 ----- 

881 Modifies values in-place. 

882 """ 

883 transf = (lambda x: x) if axis == 0 else (lambda x: x.T) 

884 

885 # reshape a 1 dim if needed 

886 if values.ndim == 1: 

887 if axis != 0: # pragma: no cover 

888 raise AssertionError("cannot interpolate on an ndim == 1 with axis != 0") 

889 values = values.reshape((1, *values.shape)) 

890 

891 method = clean_fill_method(method) 

892 tvalues = transf(values) 

893 

894 func = get_fill_func(method, ndim=2) 

895 # _pad_2d and _backfill_2d both modify tvalues inplace 

896 func(tvalues, limit=limit, limit_area=limit_area) 

897 

898 

899def _fillna_prep( 

900 values, mask: npt.NDArray[np.bool_] | None = None 

901) -> npt.NDArray[np.bool_]: 

902 # boilerplate for _pad_1d, _backfill_1d, _pad_2d, _backfill_2d 

903 

904 if mask is None: 

905 mask = isna(values) 

906 

907 return mask 

908 

909 

910def _datetimelike_compat(func: F) -> F: 

911 """ 

912 Wrapper to handle datetime64 and timedelta64 dtypes. 

913 """ 

914 

915 @wraps(func) 

916 def new_func( 

917 values, 

918 limit: int | None = None, 

919 limit_area: Literal["inside", "outside"] | None = None, 

920 mask=None, 

921 ): 

922 if needs_i8_conversion(values.dtype): 

923 if mask is None: 

924 # This needs to occur before casting to int64 

925 mask = isna(values) 

926 

927 result, mask = func( 

928 values.view("i8"), limit=limit, limit_area=limit_area, mask=mask 

929 ) 

930 return result.view(values.dtype), mask 

931 

932 return func(values, limit=limit, limit_area=limit_area, mask=mask) 

933 

934 return cast("F", new_func) 

935 

936 

937@_datetimelike_compat 

938def _pad_1d( 

939 values: np.ndarray, 

940 limit: int | None = None, 

941 limit_area: Literal["inside", "outside"] | None = None, 

942 mask: npt.NDArray[np.bool_] | None = None, 

943) -> tuple[np.ndarray, npt.NDArray[np.bool_]]: 

944 mask = _fillna_prep(values, mask) 

945 if limit_area is not None and not mask.all(): 

946 _fill_limit_area_1d(mask, limit_area) 

947 algos.pad_inplace(values, mask, limit=limit) 

948 return values, mask 

949 

950 

951@_datetimelike_compat 

952def _backfill_1d( 

953 values: np.ndarray, 

954 limit: int | None = None, 

955 limit_area: Literal["inside", "outside"] | None = None, 

956 mask: npt.NDArray[np.bool_] | None = None, 

957) -> tuple[np.ndarray, npt.NDArray[np.bool_]]: 

958 mask = _fillna_prep(values, mask) 

959 if limit_area is not None and not mask.all(): 

960 _fill_limit_area_1d(mask, limit_area) 

961 algos.backfill_inplace(values, mask, limit=limit) 

962 return values, mask 

963 

964 

965@_datetimelike_compat 

966def _pad_2d( 

967 values: np.ndarray, 

968 limit: int | None = None, 

969 limit_area: Literal["inside", "outside"] | None = None, 

970 mask: npt.NDArray[np.bool_] | None = None, 

971) -> tuple[np.ndarray, npt.NDArray[np.bool_]]: 

972 mask = _fillna_prep(values, mask) 

973 if limit_area is not None: 

974 _fill_limit_area_2d(mask, limit_area) 

975 

976 if values.size: 

977 algos.pad_2d_inplace(values, mask, limit=limit) 

978 return values, mask 

979 

980 

981@_datetimelike_compat 

982def _backfill_2d( 

983 values, 

984 limit: int | None = None, 

985 limit_area: Literal["inside", "outside"] | None = None, 

986 mask: npt.NDArray[np.bool_] | None = None, 

987): 

988 mask = _fillna_prep(values, mask) 

989 if limit_area is not None: 

990 _fill_limit_area_2d(mask, limit_area) 

991 

992 if values.size: 

993 algos.backfill_2d_inplace(values, mask, limit=limit) 

994 else: 

995 # for test coverage 

996 pass 

997 return values, mask 

998 

999 

1000def _fill_limit_area_1d( 

1001 mask: npt.NDArray[np.bool_], limit_area: Literal["outside", "inside"] 

1002) -> None: 

1003 """Prepare 1d mask for ffill/bfill with limit_area. 

1004 

1005 Caller is responsible for checking at least one value of mask is False. 

1006 When called, mask will no longer faithfully represent when 

1007 the corresponding are NA or not. 

1008 

1009 Parameters 

1010 ---------- 

1011 mask : np.ndarray[bool, ndim=1] 

1012 Mask representing NA values when filling. 

1013 limit_area : { "outside", "inside" } 

1014 Whether to limit filling to outside or inside the outer most non-NA value. 

1015 """ 

1016 neg_mask = ~mask 

1017 first = neg_mask.argmax() 

1018 last = len(neg_mask) - neg_mask[::-1].argmax() - 1 

1019 if limit_area == "inside": 

1020 mask[:first] = False 

1021 mask[last + 1 :] = False 

1022 elif limit_area == "outside": 

1023 mask[first + 1 : last] = False 

1024 

1025 

1026def _fill_limit_area_2d( 

1027 mask: npt.NDArray[np.bool_], limit_area: Literal["outside", "inside"] 

1028) -> None: 

1029 """Prepare 2d mask for ffill/bfill with limit_area. 

1030 

1031 When called, mask will no longer faithfully represent when 

1032 the corresponding are NA or not. 

1033 

1034 Parameters 

1035 ---------- 

1036 mask : np.ndarray[bool, ndim=1] 

1037 Mask representing NA values when filling. 

1038 limit_area : { "outside", "inside" } 

1039 Whether to limit filling to outside or inside the outer most non-NA value. 

1040 """ 

1041 neg_mask = ~mask.T 

1042 if limit_area == "outside": 

1043 # Identify inside 

1044 la_mask = ( 

1045 np.maximum.accumulate(neg_mask, axis=0) 

1046 & np.maximum.accumulate(neg_mask[::-1], axis=0)[::-1] 

1047 ) 

1048 else: 

1049 # Identify outside 

1050 la_mask = ( 

1051 ~np.maximum.accumulate(neg_mask, axis=0) 

1052 | ~np.maximum.accumulate(neg_mask[::-1], axis=0)[::-1] 

1053 ) 

1054 mask[la_mask.T] = False 

1055 

1056 

1057_fill_methods = {"pad": _pad_1d, "backfill": _backfill_1d} 

1058 

1059 

1060def get_fill_func(method, ndim: int = 1): 

1061 method = clean_fill_method(method) 

1062 if ndim == 1: 

1063 return _fill_methods[method] 

1064 return {"pad": _pad_2d, "backfill": _backfill_2d}[method] 

1065 

1066 

1067def clean_reindex_fill_method(method) -> ReindexMethod | None: 

1068 if method is None: 

1069 return None 

1070 return clean_fill_method(method, allow_nearest=True) 

1071 

1072 

1073def _interp_limit( 

1074 invalid: npt.NDArray[np.bool_], fw_limit: int | None, bw_limit: int | None 

1075) -> np.ndarray: 

1076 """ 

1077 Get indexers of values that won't be filled 

1078 because they exceed the limits. 

1079 

1080 Parameters 

1081 ---------- 

1082 invalid : np.ndarray[bool] 

1083 fw_limit : int or None 

1084 forward limit to index 

1085 bw_limit : int or None 

1086 backward limit to index 

1087 

1088 Returns 

1089 ------- 

1090 set of indexers 

1091 

1092 Notes 

1093 ----- 

1094 This is equivalent to the more readable, but slower 

1095 

1096 .. code-block:: python 

1097 

1098 def _interp_limit(invalid, fw_limit, bw_limit): 

1099 for x in np.where(invalid)[0]: 

1100 if invalid[max(0, x - fw_limit) : x + bw_limit + 1].all(): 

1101 yield x 

1102 """ 

1103 # handle forward first; the backward direction is the same except 

1104 # 1. operate on the reversed array 

1105 # 2. subtract the returned indices from N - 1 

1106 N = len(invalid) 

1107 f_idx = np.array([], dtype=np.int64) 

1108 b_idx = np.array([], dtype=np.int64) 

1109 assume_unique = True 

1110 

1111 def inner(invalid, limit: int): 

1112 limit = min(limit, N - 1) 

1113 windowed = np.lib.stride_tricks.sliding_window_view(invalid, limit + 1).all(1) 

1114 idx = np.union1d( 

1115 np.where(windowed)[0] + limit, 

1116 np.where((~invalid[: limit + 1]).cumsum() == 0)[0], 

1117 ) 

1118 return idx 

1119 

1120 if fw_limit is not None: 

1121 if fw_limit == 0: 

1122 f_idx = np.where(invalid)[0] 

1123 assume_unique = False 

1124 else: 

1125 f_idx = inner(invalid, fw_limit) 

1126 

1127 if bw_limit is not None: 

1128 if bw_limit == 0: 

1129 # then we don't even need to care about backwards 

1130 # just use forwards 

1131 return f_idx 

1132 else: 

1133 b_idx = N - 1 - inner(invalid[::-1], bw_limit) 

1134 if fw_limit == 0: 

1135 return b_idx 

1136 

1137 return np.intersect1d(f_idx, b_idx, assume_unique=assume_unique)