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

351 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._typing import ( 

26 ArrayLike, 

27 AxisInt, 

28 F, 

29 ReindexMethod, 

30 npt, 

31) 

32from pandas.compat._optional import import_optional_dependency 

33 

34from pandas.core.dtypes.cast import infer_dtype_from 

35from pandas.core.dtypes.common import ( 

36 is_array_like, 

37 is_bool_dtype, 

38 is_numeric_dtype, 

39 is_object_dtype, 

40 needs_i8_conversion, 

41) 

42from pandas.core.dtypes.dtypes import ( 

43 ArrowDtype, 

44 BaseMaskedDtype, 

45 DatetimeTZDtype, 

46) 

47from pandas.core.dtypes.missing import ( 

48 is_valid_na_for_dtype, 

49 isna, 

50 na_value_for_dtype, 

51) 

52 

53if TYPE_CHECKING: 

54 from collections.abc import Callable 

55 from typing import TypeAlias 

56 

57 from pandas import Index 

58 

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

60 

61 

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

63 """ 

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

65 """ 

66 if is_array_like(value): 

67 if len(value) != length: 

68 raise ValueError( 

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

70 f" expected {length}" 

71 ) 

72 value = value[mask] 

73 

74 return value 

75 

76 

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

78 """ 

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

80 with entries equaling value set to True. 

81 

82 Parameters 

83 ---------- 

84 arr : ArrayLike 

85 value : scalar-like 

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

87 by `arr`. 

88 

89 Returns 

90 ------- 

91 np.ndarray[bool] 

92 """ 

93 dtype, value = infer_dtype_from(value) 

94 

95 if ( 

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

97 and lib.is_float(value) 

98 and np.isnan(value) 

99 and not is_nan_na() 

100 ): 

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

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

103 # GH#55127 

104 if isinstance(arr.dtype, BaseMaskedDtype): 

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

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

107 return mask 

108 else: 

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

110 import pyarrow.compute as pc 

111 

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

113 return mask 

114 

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

116 # GH#51237 

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

118 return mask 

119 

120 if isna(value): 

121 return isna(arr) 

122 

123 # GH 21977 

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

125 if ( 

126 is_numeric_dtype(arr.dtype) 

127 and not is_bool_dtype(arr.dtype) 

128 and lib.is_bool(value) 

129 ): 

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

131 pass 

132 elif ( 

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

134 ): 

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

136 pass 

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

138 # GH#29553 prevent numpy deprecation warnings 

139 pass 

140 elif is_object_dtype(arr.dtype): 

141 # pre-compute mask to avoid comparison to NA 

142 # e.g. test_replace_na_in_obj_column 

143 arr_mask = ~isna(arr) 

144 mask[arr_mask] = arr[arr_mask] == value 

145 else: 

146 new_mask = arr == value 

147 

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

149 # usually BooleanArray 

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

151 mask = new_mask 

152 

153 return mask 

154 

155 

156@overload 

157def clean_fill_method( 

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

159 *, 

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

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

162 

163 

164@overload 

165def clean_fill_method( 

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

167 *, 

168 allow_nearest: Literal[True], 

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

170 

171 

172def clean_fill_method( 

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

174 *, 

175 allow_nearest: bool = False, 

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

177 if isinstance(method, str): 

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

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

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

181 if method == "ffill": 

182 method = "pad" 

183 elif method == "bfill": 

184 method = "backfill" 

185 

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

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

188 if allow_nearest: 

189 valid_methods.append("nearest") 

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

191 if method not in valid_methods: 

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

193 return method 

194 

195 

196# interpolation methods that dispatch to np.interp 

197 

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

199 

200# interpolation methods that dispatch to _interpolate_scipy_wrapper 

201 

202SP_METHODS = [ 

203 "nearest", 

204 "zero", 

205 "slinear", 

206 "quadratic", 

207 "cubic", 

208 "barycentric", 

209 "krogh", 

210 "spline", 

211 "polynomial", 

212 "from_derivatives", 

213 "piecewise_polynomial", 

214 "pchip", 

215 "akima", 

216 "cubicspline", 

217] 

218 

219 

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

221 order = kwargs.get("order") 

222 

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

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

225 

226 valid = NP_METHODS + SP_METHODS 

227 if method not in valid: 

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

229 

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

231 if not index.is_monotonic_increasing: 

232 raise ValueError( 

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

234 ) 

235 

236 return method 

237 

238 

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

240 """ 

241 Retrieves the positional index of the first valid value. 

242 

243 Parameters 

244 ---------- 

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

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

247 is_valid: np.ndarray 

248 Mask to find na_values. 

249 

250 Returns 

251 ------- 

252 int or None 

253 """ 

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

255 

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

257 return None 

258 

259 if is_valid.ndim == 2: 

260 # reduce axis 1 

261 is_valid = is_valid.any(axis=1) # type: ignore[assignment] 

262 

263 if how == "first": 

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

265 

266 elif how == "last": 

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

268 

269 chk_notna = is_valid[idxpos] 

270 

271 if not chk_notna: 

272 return None 

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

274 # expected "Optional[int]") 

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

276 

277 

278def validate_limit_direction( 

279 limit_direction: str, 

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

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

282 limit_direction = limit_direction.lower() 

283 if limit_direction not in valid_limit_directions: 

284 raise ValueError( 

285 "Invalid limit_direction: expecting one of " 

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

287 ) 

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

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

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

291 

292 

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

294 if limit_area is not None: 

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

296 limit_area = limit_area.lower() 

297 if limit_area not in valid_limit_areas: 

298 raise ValueError( 

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

300 f"{limit_area}." 

301 ) 

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

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

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

305 

306 

307def infer_limit_direction( 

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

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

310 # Set `limit_direction` depending on `method` 

311 if limit_direction is None: 

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

313 limit_direction = "backward" 

314 else: 

315 limit_direction = "forward" 

316 else: 

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

318 raise ValueError( 

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

320 ) 

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

322 raise ValueError( 

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

324 ) 

325 return limit_direction 

326 

327 

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

329 # create/use the index 

330 if method == "linear": 

331 # prior default 

332 from pandas import RangeIndex 

333 

334 index = RangeIndex(len(index)) 

335 else: 

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

337 is_numeric_or_datetime = ( 

338 is_numeric_dtype(index.dtype) 

339 or isinstance(index.dtype, DatetimeTZDtype) 

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

341 ) 

342 valid = NP_METHODS + SP_METHODS 

343 if method in valid: 

344 if method not in methods and not is_numeric_or_datetime: 

345 raise ValueError( 

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

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

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

349 "interpolating." 

350 ) 

351 else: 

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

353 

354 if isna(index).any(): 

355 raise NotImplementedError( 

356 "Interpolation with NaNs in the index " 

357 "has not been implemented. Try filling " 

358 "those NaNs before interpolating." 

359 ) 

360 return index 

361 

362 

363def interpolate_2d_inplace( 

364 data: np.ndarray, # floating dtype 

365 index: Index, 

366 axis: AxisInt, 

367 method: str = "linear", 

368 limit: int | None = None, 

369 limit_direction: str = "forward", 

370 limit_area: str | None = None, 

371 fill_value: Any | None = None, 

372 mask=None, 

373 **kwargs, 

374) -> None: 

375 """ 

376 Column-wise application of _interpolate_1d. 

377 

378 Notes 

379 ----- 

380 Alters 'data' in-place. 

381 

382 The signature does differ from _interpolate_1d because it only 

383 includes what is needed for Block.interpolate. 

384 """ 

385 # validate the interp method 

386 clean_interp_method(method, index, **kwargs) 

387 

388 if is_valid_na_for_dtype(fill_value, data.dtype): 

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

390 

391 if method == "time": 

392 if not needs_i8_conversion(index.dtype): 

393 raise ValueError( 

394 "time-weighted interpolation only works " 

395 "on Series or DataFrames with a " 

396 "DatetimeIndex" 

397 ) 

398 method = "values" 

399 

400 limit_direction = validate_limit_direction(limit_direction) 

401 limit_area_validated = validate_limit_area(limit_area) 

402 

403 # default limit is unlimited GH #16282 

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

405 

406 indices = _index_to_interp_indices(index, method) 

407 

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

409 # process 1-d slices in the axis direction 

410 

411 _interpolate_1d( 

412 indices=indices, 

413 yvalues=yvalues, 

414 method=method, 

415 limit=limit, 

416 limit_direction=limit_direction, 

417 limit_area=limit_area_validated, 

418 fill_value=fill_value, 

419 bounds_error=False, 

420 mask=mask, 

421 **kwargs, 

422 ) 

423 

424 np.apply_along_axis(func, axis, data) 

425 

426 

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

428 """ 

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

430 """ 

431 xarr = index._values 

432 if needs_i8_conversion(xarr.dtype): 

433 # GH#1646 for dt64tz 

434 xarr = xarr.view("i8") 

435 

436 if method == "linear": 

437 inds = xarr 

438 inds = cast(np.ndarray, inds) 

439 else: 

440 inds = np.asarray(xarr) 

441 

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

443 if inds.dtype == np.object_: 

444 inds = lib.maybe_convert_objects(inds) 

445 

446 return inds 

447 

448 

449def _interpolate_1d( 

450 indices: np.ndarray, 

451 yvalues: np.ndarray, 

452 method: str = "linear", 

453 limit: int | None = None, 

454 limit_direction: str = "forward", 

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

456 fill_value: Any | None = None, 

457 bounds_error: bool = False, 

458 order: int | None = None, 

459 mask=None, 

460 **kwargs, 

461) -> None: 

462 """ 

463 Logic for the 1-d interpolation. The input 

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

465 

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

467 take it as an argument. 

468 

469 Notes 

470 ----- 

471 Fills 'yvalues' in-place. 

472 """ 

473 if mask is not None: 

474 invalid = mask 

475 else: 

476 invalid = isna(yvalues) 

477 valid = ~invalid 

478 

479 if not valid.any(): 

480 return 

481 

482 if valid.all(): 

483 return 

484 

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

486 all_nans = np.flatnonzero(invalid) 

487 

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

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

490 first_valid_index = 0 

491 start_nans = np.arange(first_valid_index) 

492 

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

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

495 last_valid_index = len(yvalues) 

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

497 

498 # preserve_nans contains indices of invalid values, 

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

500 # preserved as NaN after the interpolation. 

501 

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

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

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

505 

506 # set preserve_nans based on direction using _interp_limit 

507 if limit_direction == "forward": 

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

509 elif limit_direction == "backward": 

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

511 else: 

512 # both directions... just use _interp_limit 

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

514 

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

516 # to preserve_nans GH #16284 

517 if limit_area == "inside": 

518 # preserve NaNs on the outside 

519 preserve_nans = np.union1d(preserve_nans, start_nans) 

520 preserve_nans = np.union1d(preserve_nans, end_nans) 

521 elif limit_area == "outside": 

522 # preserve NaNs on the inside 

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

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

525 preserve_nans = np.union1d(preserve_nans, mid_nans) 

526 

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

528 

529 if is_datetimelike: 

530 yvalues = yvalues.view("i8") 

531 

532 if method in NP_METHODS: 

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

534 

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

536 yvalues[invalid] = np.interp( 

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

538 ) 

539 else: 

540 yvalues[invalid] = _interpolate_scipy_wrapper( 

541 indices[valid], 

542 yvalues[valid], 

543 indices[invalid], 

544 method=method, 

545 fill_value=fill_value, 

546 bounds_error=bounds_error, 

547 order=order, 

548 **kwargs, 

549 ) 

550 

551 if mask is not None: 

552 mask[:] = False 

553 mask[preserve_nans] = True 

554 elif is_datetimelike: 

555 yvalues[preserve_nans] = NaT.value 

556 else: 

557 yvalues[preserve_nans] = np.nan 

558 return 

559 

560 

561def _interpolate_scipy_wrapper( 

562 x: np.ndarray, 

563 y: np.ndarray, 

564 new_x: np.ndarray, 

565 method: str, 

566 fill_value=None, 

567 bounds_error: bool = False, 

568 order=None, 

569 **kwargs, 

570): 

571 """ 

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

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

574 the list in _clean_interp_method. 

575 """ 

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

577 import_optional_dependency("scipy", extra=extra) 

578 from scipy import interpolate 

579 

580 new_x = np.asarray(new_x) 

581 

582 # ignores some kwargs that could be passed along. 

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

584 "barycentric": interpolate.barycentric_interpolate, 

585 "krogh": interpolate.krogh_interpolate, 

586 "from_derivatives": _from_derivatives, 

587 "piecewise_polynomial": _from_derivatives, 

588 "cubicspline": _cubicspline_interpolate, 

589 "akima": _akima_interpolate, 

590 "pchip": interpolate.pchip_interpolate, 

591 } 

592 

593 interp1d_methods = [ 

594 "nearest", 

595 "zero", 

596 "slinear", 

597 "quadratic", 

598 "cubic", 

599 "polynomial", 

600 ] 

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

602 if method in interp1d_methods: 

603 if method == "polynomial": 

604 kind = order 

605 else: 

606 kind = method 

607 terp = interpolate.interp1d( 

608 x, y, kind=kind, fill_value=fill_value, bounds_error=bounds_error 

609 ) 

610 new_y = terp(new_x) 

611 elif method == "spline": 

612 # GH #10633, #24014 

613 if isna(order) or (order <= 0): 

614 raise ValueError( 

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

616 ) 

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

618 new_y = terp(new_x) 

619 else: 

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

621 # in some circumstances: check all three 

622 if not x.flags.writeable: 

623 x = x.copy() 

624 if not y.flags.writeable: 

625 y = y.copy() 

626 if not new_x.flags.writeable: 

627 new_x = new_x.copy() 

628 terp = alt_methods.get(method, None) 

629 if terp is None: 

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

631 

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

633 kwargs.pop("downcast", None) 

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

635 return new_y 

636 

637 

638def _from_derivatives( 

639 xi: np.ndarray, 

640 yi: np.ndarray, 

641 x: np.ndarray, 

642 order=None, 

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

644 extrapolate: bool = False, 

645): 

646 """ 

647 Convenience function for interpolate.BPoly.from_derivatives. 

648 

649 Construct a piecewise polynomial in the Bernstein basis, compatible 

650 with the specified values and derivatives at breakpoints. 

651 

652 Parameters 

653 ---------- 

654 xi : array-like 

655 sorted 1D array of x-coordinates 

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

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

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

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

660 derivatives are ignored. 

661 der : int or list 

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

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

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

665 value as 0th derivative. 

666 extrapolate : bool, optional 

667 Whether to extrapolate to ouf-of-bounds points based on first and last 

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

669 

670 See Also 

671 -------- 

672 scipy.interpolate.BPoly.from_derivatives 

673 

674 Returns 

675 ------- 

676 y : scalar or array-like 

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

678 """ 

679 from scipy import interpolate 

680 

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

682 method = interpolate.BPoly.from_derivatives 

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

684 

685 return m(x) 

686 

687 

688def _akima_interpolate( 

689 xi: np.ndarray, 

690 yi: np.ndarray, 

691 x: np.ndarray, 

692 der: int = 0, 

693 axis: AxisInt = 0, 

694): 

695 """ 

696 Convenience function for akima interpolation. 

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

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

699 

700 See `Akima1DInterpolator` for details. 

701 

702 Parameters 

703 ---------- 

704 xi : np.ndarray 

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

706 yi : np.ndarray 

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

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

709 parameter to select correct axis. 

710 x : np.ndarray 

711 Of length M. 

712 der : int, optional 

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

714 value as 0th derivative. 

715 axis : int, optional 

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

717 

718 See Also 

719 -------- 

720 scipy.interpolate.Akima1DInterpolator 

721 

722 Returns 

723 ------- 

724 y : scalar or array-like 

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

726 

727 """ 

728 from scipy import interpolate 

729 

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

731 

732 return P(x, nu=der) 

733 

734 

735def _cubicspline_interpolate( 

736 xi: np.ndarray, 

737 yi: np.ndarray, 

738 x: np.ndarray, 

739 axis: AxisInt = 0, 

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

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

742) -> np.ndarray: 

743 """ 

744 Convenience function for cubic spline data interpolator. 

745 

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

747 

748 Parameters 

749 ---------- 

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

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

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

753 yi : np.ndarray 

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

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

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

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

758 axis : int, optional 

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

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

761 Default is 0. 

762 bc_type : string or 2-tuple, optional 

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

764 boundary conditions, are required to determine all coefficients of 

765 polynomials on each segment [2]_. 

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

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

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

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

770 information on boundary conditions. 

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

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

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

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

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

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

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

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

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

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

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

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

783 derivatives at curve ends: 

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

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

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

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

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

789 and have the shape (n0, n1). 

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

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

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

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

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

795 

796 See Also 

797 -------- 

798 scipy.interpolate.CubicHermiteSpline 

799 

800 Returns 

801 ------- 

802 y : scalar or array-like 

803 The result, of shape (m,) 

804 

805 References 

806 ---------- 

807 .. [1] `Cubic Spline Interpolation 

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

809 on Wikiversity. 

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

811 """ 

812 from scipy import interpolate 

813 

814 P = interpolate.CubicSpline( 

815 xi, yi, axis=axis, bc_type=bc_type, extrapolate=extrapolate 

816 ) 

817 

818 return P(x) 

819 

820 

821def pad_or_backfill_inplace( 

822 values: np.ndarray, 

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

824 axis: AxisInt = 0, 

825 limit: int | None = None, 

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

827) -> None: 

828 """ 

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

830 needed fills inplace, returns the result. 

831 

832 Parameters 

833 ---------- 

834 values: np.ndarray 

835 Input array. 

836 method: str, default "pad" 

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

838 axis: 0 or 1 

839 Interpolation axis 

840 limit: int, optional 

841 Index limit on interpolation. 

842 limit_area: str, optional 

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

844 

845 Notes 

846 ----- 

847 Modifies values in-place. 

848 """ 

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

850 

851 # reshape a 1 dim if needed 

852 if values.ndim == 1: 

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

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

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

856 

857 method = clean_fill_method(method) 

858 tvalues = transf(values) 

859 

860 func = get_fill_func(method, ndim=2) 

861 # _pad_2d and _backfill_2d both modify tvalues inplace 

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

863 

864 

865def _fillna_prep( 

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

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

868 # boilerplate for _pad_1d, _backfill_1d, _pad_2d, _backfill_2d 

869 

870 if mask is None: 

871 mask = isna(values) 

872 

873 return mask 

874 

875 

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

877 """ 

878 Wrapper to handle datetime64 and timedelta64 dtypes. 

879 """ 

880 

881 @wraps(func) 

882 def new_func( 

883 values, 

884 limit: int | None = None, 

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

886 mask=None, 

887 ): 

888 if needs_i8_conversion(values.dtype): 

889 if mask is None: 

890 # This needs to occur before casting to int64 

891 mask = isna(values) 

892 

893 result, mask = func( 

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

895 ) 

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

897 

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

899 

900 return cast(F, new_func) 

901 

902 

903@_datetimelike_compat 

904def _pad_1d( 

905 values: np.ndarray, 

906 limit: int | None = None, 

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

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

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

910 mask = _fillna_prep(values, mask) 

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

912 _fill_limit_area_1d(mask, limit_area) 

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

914 return values, mask 

915 

916 

917@_datetimelike_compat 

918def _backfill_1d( 

919 values: np.ndarray, 

920 limit: int | None = None, 

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

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

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

924 mask = _fillna_prep(values, mask) 

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

926 _fill_limit_area_1d(mask, limit_area) 

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

928 return values, mask 

929 

930 

931@_datetimelike_compat 

932def _pad_2d( 

933 values: np.ndarray, 

934 limit: int | None = None, 

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

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

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

938 mask = _fillna_prep(values, mask) 

939 if limit_area is not None: 

940 _fill_limit_area_2d(mask, limit_area) 

941 

942 if values.size: 

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

944 return values, mask 

945 

946 

947@_datetimelike_compat 

948def _backfill_2d( 

949 values, 

950 limit: int | None = None, 

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

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

953): 

954 mask = _fillna_prep(values, mask) 

955 if limit_area is not None: 

956 _fill_limit_area_2d(mask, limit_area) 

957 

958 if values.size: 

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

960 else: 

961 # for test coverage 

962 pass 

963 return values, mask 

964 

965 

966def _fill_limit_area_1d( 

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

968) -> None: 

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

970 

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

972 When called, mask will no longer faithfully represent when 

973 the corresponding are NA or not. 

974 

975 Parameters 

976 ---------- 

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

978 Mask representing NA values when filling. 

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

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

981 """ 

982 neg_mask = ~mask 

983 first = neg_mask.argmax() 

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

985 if limit_area == "inside": 

986 mask[:first] = False 

987 mask[last + 1 :] = False 

988 elif limit_area == "outside": 

989 mask[first + 1 : last] = False 

990 

991 

992def _fill_limit_area_2d( 

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

994) -> None: 

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

996 

997 When called, mask will no longer faithfully represent when 

998 the corresponding are NA or not. 

999 

1000 Parameters 

1001 ---------- 

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

1003 Mask representing NA values when filling. 

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

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

1006 """ 

1007 neg_mask = ~mask.T 

1008 if limit_area == "outside": 

1009 # Identify inside 

1010 la_mask = ( 

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

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

1013 ) 

1014 else: 

1015 # Identify outside 

1016 la_mask = ( 

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

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

1019 ) 

1020 mask[la_mask.T] = False 

1021 

1022 

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

1024 

1025 

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

1027 method = clean_fill_method(method) 

1028 if ndim == 1: 

1029 return _fill_methods[method] 

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

1031 

1032 

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

1034 if method is None: 

1035 return None 

1036 return clean_fill_method(method, allow_nearest=True) 

1037 

1038 

1039def _interp_limit( 

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

1041) -> np.ndarray: 

1042 """ 

1043 Get indexers of values that won't be filled 

1044 because they exceed the limits. 

1045 

1046 Parameters 

1047 ---------- 

1048 invalid : np.ndarray[bool] 

1049 fw_limit : int or None 

1050 forward limit to index 

1051 bw_limit : int or None 

1052 backward limit to index 

1053 

1054 Returns 

1055 ------- 

1056 set of indexers 

1057 

1058 Notes 

1059 ----- 

1060 This is equivalent to the more readable, but slower 

1061 

1062 .. code-block:: python 

1063 

1064 def _interp_limit(invalid, fw_limit, bw_limit): 

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

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

1067 yield x 

1068 """ 

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

1070 # 1. operate on the reversed array 

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

1072 N = len(invalid) 

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

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

1075 assume_unique = True 

1076 

1077 def inner(invalid, limit: int): 

1078 limit = min(limit, N - 1) 

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

1080 idx = np.union1d( 

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

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

1083 ) 

1084 return idx 

1085 

1086 if fw_limit is not None: 

1087 if fw_limit == 0: 

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

1089 assume_unique = False 

1090 else: 

1091 f_idx = inner(invalid, fw_limit) 

1092 

1093 if bw_limit is not None: 

1094 if bw_limit == 0: 

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

1096 # just use forwards 

1097 return f_idx 

1098 else: 

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

1100 if fw_limit == 0: 

1101 return b_idx 

1102 

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