1from __future__ import annotations
2
3import inspect
4import re
5from typing import (
6 TYPE_CHECKING,
7 Any,
8 Literal,
9 Self,
10 cast,
11 final,
12)
13import warnings
14
15import numpy as np
16
17from pandas._libs import (
18 NaT,
19 algos as libalgos,
20 internals as libinternals,
21 lib,
22)
23from pandas._libs.internals import (
24 BlockPlacement,
25 BlockValuesRefs,
26)
27from pandas._libs.missing import NA
28from pandas.errors import (
29 AbstractMethodError,
30 OutOfBoundsDatetime,
31 Pandas4Warning,
32)
33from pandas.util._decorators import cache_readonly
34from pandas.util._exceptions import find_stack_level
35from pandas.util._validators import validate_bool_kwarg
36
37from pandas.core.dtypes.astype import (
38 astype_array_safe,
39 astype_is_view,
40)
41from pandas.core.dtypes.cast import (
42 LossySetitemError,
43 can_hold_element,
44 construct_1d_object_array_from_listlike,
45 convert_dtypes,
46 find_result_type,
47 np_can_hold_element,
48)
49from pandas.core.dtypes.common import (
50 is_1d_only_ea_dtype,
51 is_float_dtype,
52 is_integer_dtype,
53 is_list_like,
54 is_scalar,
55 is_string_dtype,
56)
57from pandas.core.dtypes.dtypes import (
58 CategoricalDtype,
59 DatetimeTZDtype,
60 ExtensionDtype,
61 IntervalDtype,
62 NumpyEADtype,
63 PeriodDtype,
64)
65from pandas.core.dtypes.generic import (
66 ABCDataFrame,
67 ABCIndex,
68 ABCNumpyExtensionArray,
69 ABCSeries,
70)
71from pandas.core.dtypes.inference import is_re
72from pandas.core.dtypes.missing import (
73 is_valid_na_for_dtype,
74 isna,
75 na_value_for_dtype,
76)
77
78from pandas.core import missing
79import pandas.core.algorithms as algos
80from pandas.core.array_algos.putmask import (
81 extract_bool_array,
82 putmask_inplace,
83 putmask_without_repeat,
84 setitem_datetimelike_compat,
85 validate_putmask,
86)
87from pandas.core.array_algos.quantile import quantile_compat
88from pandas.core.array_algos.replace import (
89 compare_or_regex_search,
90 replace_regex,
91 should_use_regex,
92)
93from pandas.core.array_algos.transforms import shift
94from pandas.core.arrays import (
95 DatetimeArray,
96 ExtensionArray,
97 IntervalArray,
98 NumpyExtensionArray,
99 PeriodArray,
100 TimedeltaArray,
101)
102from pandas.core.arrays.string_ import StringDtype
103from pandas.core.base import PandasObject
104import pandas.core.common as com
105from pandas.core.computation import expressions
106from pandas.core.construction import (
107 ensure_wrapped_if_datetimelike,
108 extract_array,
109)
110from pandas.core.indexers import (
111 check_setitem_lengths,
112 is_scalar_indexer,
113 length_of_indexer,
114)
115from pandas.core.indexes.base import get_values_for_csv
116
117if TYPE_CHECKING:
118 from collections.abc import (
119 Callable,
120 Generator,
121 Iterable,
122 Sequence,
123 )
124
125 from pandas._typing import (
126 ArrayLike,
127 AxisInt,
128 DtypeBackend,
129 DtypeObj,
130 FillnaOptions,
131 IgnoreRaise,
132 InterpolateOptions,
133 QuantileInterpolation,
134 Shape,
135 npt,
136 )
137
138 from pandas.core.api import Index
139 from pandas.core.arrays._mixins import NDArrayBackedExtensionArray
140
141# comparison is faster than is_object_dtype
142_dtype_obj = np.dtype("object")
143
144
145class Block(PandasObject, libinternals.Block):
146 """
147 Canonical n-dimensional unit of homogeneous dtype contained in a pandas
148 data structure
149
150 Index-ignorant; let the container take care of that
151 """
152
153 values: np.ndarray | ExtensionArray
154 ndim: int
155 refs: BlockValuesRefs
156 __init__: Callable[..., None]
157
158 __slots__ = ()
159 is_numeric = False
160
161 @final
162 @cache_readonly
163 def _validate_ndim(self) -> bool:
164 """
165 We validate dimension for blocks that can hold 2D values, which for now
166 means numpy dtypes or EA dtypes like DatetimeTZDtype and PeriodDtype.
167 """
168 return not is_1d_only_ea_dtype(self.dtype)
169
170 @final
171 @cache_readonly
172 def is_object(self) -> bool:
173 return self.values.dtype == _dtype_obj
174
175 @final
176 @cache_readonly
177 def is_extension(self) -> bool:
178 return not lib.is_np_dtype(self.values.dtype)
179
180 @final
181 @cache_readonly
182 def _can_consolidate(self) -> bool:
183 # We _could_ consolidate for DatetimeTZDtype but don't for now.
184 return not self.is_extension
185
186 @final
187 @cache_readonly
188 def _consolidate_key(self):
189 return self._can_consolidate, self.dtype.name
190
191 @final
192 @cache_readonly
193 def _can_hold_na(self) -> bool:
194 """
195 Can we store NA values in this Block?
196 """
197 dtype = self.dtype
198 if isinstance(dtype, np.dtype):
199 return dtype.kind not in "iub"
200 return dtype._can_hold_na
201
202 @final
203 @property
204 def is_bool(self) -> bool:
205 """
206 We can be bool if a) we are bool dtype or b) object dtype with bool objects.
207 """
208 return self.values.dtype == np.dtype(bool)
209
210 @final
211 def external_values(self):
212 return external_values(self.values)
213
214 @final
215 @cache_readonly
216 def fill_value(self):
217 # Used in reindex_indexer
218 return na_value_for_dtype(self.dtype, compat=False)
219
220 @final
221 def _standardize_fill_value(self, value):
222 # if we are passed a scalar None, convert it here
223 if self.dtype != _dtype_obj and is_valid_na_for_dtype(value, self.dtype):
224 value = self.fill_value
225 return value
226
227 @property
228 def mgr_locs(self) -> BlockPlacement:
229 return self._mgr_locs
230
231 @mgr_locs.setter
232 def mgr_locs(self, new_mgr_locs: BlockPlacement) -> None:
233 self._mgr_locs = new_mgr_locs
234
235 @final
236 def make_block(
237 self,
238 values,
239 placement: BlockPlacement | None = None,
240 refs: BlockValuesRefs | None = None,
241 ) -> Block:
242 """
243 Create a new block, with type inference propagate any values that are
244 not specified
245 """
246 if placement is None:
247 placement = self._mgr_locs
248 if self.is_extension:
249 values = ensure_block_shape(values, ndim=self.ndim)
250
251 return new_block(values, placement=placement, ndim=self.ndim, refs=refs)
252
253 @final
254 def make_block_same_class(
255 self,
256 values,
257 placement: BlockPlacement | None = None,
258 refs: BlockValuesRefs | None = None,
259 ) -> Self:
260 """Wrap given values in a block of same type as self."""
261 # Pre-2.0 we called ensure_wrapped_if_datetimelike because fastparquet
262 # relied on it, as of 2.0 the caller is responsible for this.
263 if placement is None:
264 placement = self._mgr_locs
265
266 # We assume maybe_coerce_values has already been called
267 return type(self)(values, placement=placement, ndim=self.ndim, refs=refs)
268
269 @final
270 def __repr__(self) -> str:
271 # don't want to print out all of the items here
272 name = type(self).__name__
273 if self.ndim == 1:
274 result = f"{name}: {len(self)} dtype: {self.dtype}"
275 else:
276 shape = " x ".join([str(s) for s in self.shape])
277 result = f"{name}: {self.mgr_locs.indexer}, {shape}, dtype: {self.dtype}"
278
279 return result
280
281 @final
282 def __len__(self) -> int:
283 return len(self.values)
284
285 @final
286 def slice_block_columns(self, slc: slice) -> Self:
287 """
288 Perform __getitem__-like, return result as block.
289 """
290 new_mgr_locs = self._mgr_locs[slc]
291
292 new_values = self._slice(slc)
293 refs = self.refs
294 return type(self)(new_values, new_mgr_locs, self.ndim, refs=refs)
295
296 @final
297 def take_block_columns(self, indices: npt.NDArray[np.intp]) -> Self:
298 """
299 Perform __getitem__-like, return result as block.
300
301 Only supports slices that preserve dimensionality.
302 """
303 # Note: only called from is from internals.concat, and we can verify
304 # that never happens with 1-column blocks, i.e. never for ExtensionBlock.
305
306 new_mgr_locs = self._mgr_locs[indices]
307
308 new_values = self._slice(indices)
309 return type(self)(new_values, new_mgr_locs, self.ndim, refs=None)
310
311 @final
312 def getitem_block_columns(
313 self, slicer: slice, new_mgr_locs: BlockPlacement, ref_inplace_op: bool = False
314 ) -> Self:
315 """
316 Perform __getitem__-like, return result as block.
317
318 Only supports slices that preserve dimensionality.
319 """
320 new_values = self._slice(slicer)
321 refs = self.refs if not ref_inplace_op or self.refs.has_reference() else None
322 return type(self)(new_values, new_mgr_locs, self.ndim, refs=refs)
323
324 @final
325 def _can_hold_element(self, element: Any) -> bool:
326 """require the same dtype as ourselves"""
327 element = extract_array(element, extract_numpy=True)
328 return can_hold_element(self.values, element)
329
330 @final
331 def should_store(self, value: ArrayLike) -> bool:
332 """
333 Should we set self.values[indexer] = value inplace or do we need to cast?
334
335 Parameters
336 ----------
337 value : np.ndarray or ExtensionArray
338
339 Returns
340 -------
341 bool
342 """
343 return value.dtype == self.dtype
344
345 # ---------------------------------------------------------------------
346 # Apply/Reduce and Helpers
347
348 @final
349 def apply(self, func, **kwargs) -> list[Block]:
350 """
351 apply the function to my values; return a block if we are not
352 one
353 """
354 result = func(self.values, **kwargs)
355
356 result = maybe_coerce_values(result)
357 return self._split_op_result(result)
358
359 @final
360 def reduce(self, func) -> Block:
361 # We will apply the function and reshape the result into a single-row
362 # Block with the same mgr_locs; squeezing will be done at a higher level
363 assert self.ndim == 2
364
365 result = func(self.values)
366
367 if self.values.ndim == 1:
368 res_values = result
369 else:
370 res_values = result.reshape(-1, 1)
371
372 return self.make_block(res_values)
373
374 @final
375 def _split_op_result(self, result: ArrayLike) -> list[Block]:
376 # See also: split_and_operate
377 if result.ndim > 1 and isinstance(result.dtype, ExtensionDtype):
378 # TODO(EA2D): unnecessary with 2D EAs
379 # if we get a 2D ExtensionArray, we need to split it into 1D pieces
380 nbs = []
381 for i, loc in enumerate(self._mgr_locs):
382 if not is_1d_only_ea_dtype(result.dtype):
383 vals = result[i : i + 1]
384 else:
385 vals = result[i]
386
387 bp = BlockPlacement(loc)
388 block = self.make_block(values=vals, placement=bp)
389 nbs.append(block)
390 return nbs
391
392 nb = self.make_block(result)
393
394 return [nb]
395
396 @final
397 def _split(self) -> Generator[Block]:
398 """
399 Split a block into a list of single-column blocks.
400 """
401 assert self.ndim == 2
402
403 for i, ref_loc in enumerate(self._mgr_locs):
404 vals = self.values[slice(i, i + 1)]
405
406 bp = BlockPlacement(ref_loc)
407 nb = type(self)(vals, placement=bp, ndim=2, refs=self.refs)
408 yield nb
409
410 @final
411 def split_and_operate(self, func, *args, **kwargs) -> list[Block]:
412 """
413 Split the block and apply func column-by-column.
414
415 Parameters
416 ----------
417 func : Block method
418 *args
419 **kwargs
420
421 Returns
422 -------
423 List[Block]
424 """
425 assert self.ndim == 2 and self.shape[0] != 1
426
427 res_blocks = []
428 for nb in self._split():
429 rbs = func(nb, *args, **kwargs)
430 res_blocks.extend(rbs)
431 return res_blocks
432
433 # ---------------------------------------------------------------------
434 # Up/Down-casting
435
436 @final
437 def coerce_to_target_dtype(self, other, raise_on_upcast: bool) -> Block:
438 """
439 coerce the current block to a dtype compat for other
440 we will return a block, possibly object, and not raise
441
442 we can also safely try to coerce to the same dtype
443 and will receive the same block
444 """
445 new_dtype = find_result_type(self.values.dtype, other)
446 if new_dtype == self.dtype:
447 if lib.is_np_dtype(new_dtype, "mM"):
448 # GH#61671 e.g. datetime64[ns] column and a replacement value
449 # outside ns range. find_common_type picked the highest
450 # resolution which can't represent the value.
451 raise OutOfBoundsDatetime(
452 f"Incompatible (high-resolution) value for "
453 f"dtype='{self.dtype}'. "
454 "Explicitly cast before operating."
455 )
456 # GH#52927 avoid RecursionError
457 raise AssertionError(
458 "Something has gone wrong, please report a bug at "
459 "https://github.com/pandas-dev/pandas/issues"
460 )
461
462 # In a future version of pandas, the default will be that
463 # setting `nan` into an integer series won't raise.
464 if (
465 is_scalar(other)
466 and is_integer_dtype(self.values.dtype)
467 and isna(other)
468 and other is not NaT
469 and not (
470 isinstance(other, (np.datetime64, np.timedelta64)) and np.isnat(other)
471 )
472 ):
473 raise_on_upcast = False
474 elif (
475 isinstance(other, np.ndarray)
476 and other.ndim == 1
477 and is_integer_dtype(self.values.dtype)
478 and is_float_dtype(other.dtype)
479 and lib.has_only_ints_or_nan(other)
480 ):
481 raise_on_upcast = False
482
483 if raise_on_upcast:
484 raise TypeError(f"Invalid value '{other}' for dtype '{self.values.dtype}'")
485 if self.values.dtype == new_dtype:
486 raise AssertionError(
487 f"Did not expect new dtype {new_dtype} to equal self.dtype "
488 f"{self.values.dtype}. Please report a bug at "
489 "https://github.com/pandas-dev/pandas/issues."
490 )
491 try:
492 return self.astype(new_dtype)
493 except OutOfBoundsDatetime as err:
494 # e.g. GH#56419 if self.dtype is a low-resolution dt64 and we try to
495 # upcast to a higher-resolution dt64, we may have entries that are
496 # out of bounds for the higher resolution.
497 # Re-raise with a more informative message.
498 raise OutOfBoundsDatetime(
499 f"Incompatible (high-resolution) value for dtype='{self.dtype}'. "
500 "Explicitly cast before operating."
501 ) from err
502
503 @final
504 def convert(self) -> list[Block]:
505 """
506 Attempt to coerce any object types to better types. Return a copy
507 of the block (if copy = True).
508 """
509 if not self.is_object:
510 return [self.copy(deep=False)]
511
512 if self.ndim != 1 and self.shape[0] != 1:
513 blocks = self.split_and_operate(Block.convert)
514 if all(blk.dtype.kind == "O" for blk in blocks):
515 # Avoid fragmenting the block if convert is a no-op
516 return [self.copy(deep=False)]
517 return blocks
518
519 values = self.values
520 if values.ndim == 2:
521 # the check above ensures we only get here with values.shape[0] == 1,
522 # avoid doing .ravel as that might make a copy
523 values = values[0]
524
525 res_values = lib.maybe_convert_objects(
526 values, # type: ignore[arg-type]
527 convert_non_numeric=True,
528 )
529 refs = None
530 if res_values is values or (
531 isinstance(res_values, NumpyExtensionArray)
532 and res_values._ndarray is values
533 ):
534 refs = self.refs
535
536 res_values = ensure_block_shape(res_values, self.ndim)
537 res_values = maybe_coerce_values(res_values)
538 return [self.make_block(res_values, refs=refs)]
539
540 def convert_dtypes(
541 self,
542 infer_objects: bool = True,
543 convert_string: bool = True,
544 convert_integer: bool = True,
545 convert_boolean: bool = True,
546 convert_floating: bool = True,
547 dtype_backend: DtypeBackend = "numpy_nullable",
548 ) -> list[Block]:
549 if infer_objects and self.is_object:
550 blks = self.convert()
551 else:
552 blks = [self]
553
554 if not any(
555 [convert_floating, convert_integer, convert_boolean, convert_string]
556 ):
557 return [b.copy(deep=False) for b in blks]
558
559 rbs = []
560 for blk in blks:
561 # Determine dtype column by column
562 sub_blks = (
563 [blk] if blk.ndim == 1 or blk.shape[0] == 1 else list(blk._split())
564 )
565 dtypes = [
566 convert_dtypes(
567 b.values,
568 convert_string,
569 convert_integer,
570 convert_boolean,
571 convert_floating,
572 infer_objects,
573 dtype_backend,
574 )
575 for b in sub_blks
576 ]
577 if all(dtype == blk.dtype for dtype in dtypes):
578 # Avoid block splitting if no dtype changes
579 rbs.append(blk.copy(deep=False))
580 continue
581
582 for dtype, b in zip(dtypes, sub_blks, strict=True):
583 rbs.append(b.astype(dtype=dtype, squeeze=b.ndim != 1))
584 return rbs
585
586 # ---------------------------------------------------------------------
587 # Array-Like Methods
588
589 @final
590 @cache_readonly
591 def dtype(self) -> DtypeObj:
592 return self.values.dtype
593
594 @final
595 def astype(
596 self,
597 dtype: DtypeObj,
598 errors: IgnoreRaise = "raise",
599 squeeze: bool = False,
600 ) -> Block:
601 """
602 Coerce to the new dtype.
603
604 Parameters
605 ----------
606 dtype : np.dtype or ExtensionDtype
607 errors : str, {'raise', 'ignore'}, default 'raise'
608 - ``raise`` : allow exceptions to be raised
609 - ``ignore`` : suppress exceptions. On error return original object
610 squeeze : bool, default False
611 squeeze values to ndim=1 if only one column is given
612
613 Returns
614 -------
615 Block
616 """
617 values = self.values
618 if squeeze and values.ndim == 2 and is_1d_only_ea_dtype(dtype):
619 if values.shape[0] != 1:
620 raise ValueError("Can not squeeze with more than one column.")
621 values = values[0, :] # type: ignore[call-overload]
622
623 new_values = astype_array_safe(values, dtype, errors=errors)
624
625 new_values = maybe_coerce_values(new_values)
626
627 refs = None
628 if astype_is_view(values.dtype, new_values.dtype):
629 refs = self.refs
630
631 newb = self.make_block(new_values, refs=refs)
632 if newb.shape != self.shape:
633 raise TypeError(
634 f"cannot set astype for dtype "
635 f"({self.dtype.name} [{self.shape}]) to different shape "
636 f"({newb.dtype.name} [{newb.shape}])"
637 )
638 return newb
639
640 @final
641 def get_values_for_csv(
642 self, *, float_format, date_format, decimal, na_rep: str = "nan", quoting=None
643 ) -> Block:
644 """convert to our native types format"""
645 result = get_values_for_csv(
646 self.values,
647 na_rep=na_rep,
648 quoting=quoting,
649 float_format=float_format,
650 date_format=date_format,
651 decimal=decimal,
652 )
653 return self.make_block(result)
654
655 @final
656 def copy(self, *, deep: bool) -> Self:
657 """copy constructor"""
658 values = self.values
659 refs: BlockValuesRefs | None
660 if deep:
661 if isinstance(values, np.ndarray):
662 # "K" preserves memory layout; default "C" flips contiguity
663 # for non-C-order blocks (GH#60469).
664 values = values.copy(order="K")
665 else:
666 values = values.copy()
667 refs = None
668 else:
669 values = values.view()
670 refs = self.refs
671 return type(self)(values, placement=self._mgr_locs, ndim=self.ndim, refs=refs)
672
673 # ---------------------------------------------------------------------
674 # Copy-on-Write Helpers
675
676 def _maybe_copy(self, inplace: bool, deep: bool = True) -> Self:
677 if inplace and not self.refs.has_reference():
678 return self
679 return self.copy(deep=deep)
680
681 @final
682 def _get_refs_and_copy(self, inplace: bool):
683 refs = None
684 copy = not inplace
685 if inplace:
686 if self.refs.has_reference():
687 copy = True
688 else:
689 refs = self.refs
690 return copy, refs
691
692 # ---------------------------------------------------------------------
693 # Replace
694
695 @final
696 def replace(
697 self,
698 to_replace,
699 value,
700 inplace: bool = False,
701 # mask may be pre-computed if we're called from replace_list
702 mask: npt.NDArray[np.bool_] | None = None,
703 ) -> list[Block]:
704 """
705 replace the to_replace value with value, possible to create new
706 blocks here this is just a call to putmask.
707 """
708
709 # Note: the checks we do in NDFrame.replace ensure we never get
710 # here with listlike to_replace or value, as those cases
711 # go through replace_list
712 values = self.values
713
714 if not self._can_hold_element(to_replace):
715 # We cannot hold `to_replace`, so we know immediately that
716 # replacing it is a no-op.
717 # Note: If to_replace were a list, NDFrame.replace would call
718 # replace_list instead of replace.
719 return [self._maybe_copy(inplace, deep=False)]
720
721 if mask is None:
722 mask = missing.mask_missing(values, to_replace)
723 if not mask.any():
724 # Note: we get here with test_replace_extension_other incorrectly
725 # bc _can_hold_element is incorrect.
726 return [self._maybe_copy(inplace, deep=False)]
727
728 elif self._can_hold_element(value) or (self.dtype == "string" and is_re(value)):
729 # TODO(CoW): Maybe split here as well into columns where mask has True
730 # and rest?
731 blk = self._maybe_copy(inplace)
732 putmask_inplace(blk.values, mask, value)
733 return [blk]
734
735 elif self.ndim == 1 or self.shape[0] == 1:
736 if value is None or value is NA:
737 blk = self.astype(np.dtype(object))
738 else:
739 blk = self.coerce_to_target_dtype(value, raise_on_upcast=False)
740 return blk.replace(
741 to_replace=to_replace,
742 value=value,
743 inplace=True,
744 mask=mask,
745 )
746
747 else:
748 # split so that we only upcast where necessary
749 blocks = []
750 for i, nb in enumerate(self._split()):
751 blocks.extend(
752 type(self).replace(
753 nb,
754 to_replace=to_replace,
755 value=value,
756 inplace=True,
757 mask=mask[i : i + 1],
758 )
759 )
760 return blocks
761
762 @final
763 def _replace_regex(
764 self,
765 to_replace,
766 value,
767 inplace: bool = False,
768 mask=None,
769 ) -> list[Block]:
770 """
771 Replace elements by the given value.
772
773 Parameters
774 ----------
775 to_replace : object or pattern
776 Scalar to replace or regular expression to match.
777 value : object
778 Replacement object.
779 inplace : bool, default False
780 Perform inplace modification.
781 mask : array-like of bool, optional
782 True indicate corresponding element is ignored.
783
784 Returns
785 -------
786 List[Block]
787 """
788 if not is_re(to_replace) and not self._can_hold_element(to_replace):
789 # i.e. only if self.is_object is True, but could in principle include a
790 # String ExtensionBlock
791 return [self.copy(deep=False)]
792
793 if is_re(to_replace) and _regex_target_dtype(self.dtype) not in [
794 object,
795 "string",
796 ]:
797 # only object or string dtype can hold strings, and a regex object
798 # will only match strings
799 return [self.copy(deep=False)]
800
801 if not (
802 self._can_hold_element(value) or (self.dtype == "string" and is_re(value))
803 ):
804 # GH#57733 - astype to object may return a block sharing memory
805 # with self (e.g. StringArray backed by object ndarray). Since
806 # replace_regex mutates values in-place, we must ensure the
807 # block's data is independent.
808 block = self.astype(np.dtype(object))
809 if astype_is_view(self.dtype, np.dtype(object)):
810 block = block._maybe_copy(inplace=True)
811 else:
812 block = self._maybe_copy(inplace)
813
814 rx = re.compile(to_replace)
815
816 replace_regex(block.values, rx, value, mask)
817 return [block]
818
819 @final
820 def replace_list(
821 self,
822 src_list: Iterable[Any],
823 dest_list: Sequence[Any],
824 inplace: bool = False,
825 regex: bool = False,
826 ) -> list[Block]:
827 """
828 See BlockManager.replace_list docstring.
829 """
830 values = self.values
831
832 # Exclude anything that we know we won't contain
833 pairs = [
834 (x, y)
835 for x, y in zip(src_list, dest_list, strict=True)
836 if (self._can_hold_element(x) or (self.dtype == "string" and is_re(x)))
837 ]
838 if not pairs:
839 return [self.copy(deep=False)]
840
841 src_len = len(pairs) - 1
842
843 if is_string_dtype(values.dtype) or (
844 regex and is_string_dtype(_regex_target_dtype(values.dtype))
845 ):
846 # Calculate the mask once, prior to the call of comp
847 # in order to avoid repeating the same computations
848 na_mask = ~isna(values)
849 masks: Iterable[npt.NDArray[np.bool_]] = (
850 extract_bool_array(
851 compare_or_regex_search(values, s[0], regex=regex, mask=na_mask),
852 )
853 for s in pairs
854 )
855 else:
856 # GH#38086 faster if we know we dont need to check for regex
857 masks = (missing.mask_missing(values, s[0]) for s in pairs)
858 # Materialize if inplace = True, since the masks can change
859 # as we replace
860 if inplace:
861 masks = list(masks)
862
863 # Don't set up refs here, otherwise we will think that we have
864 # references when we check again later
865 rb = [self]
866
867 for i, ((src, dest), mask) in enumerate(zip(pairs, masks, strict=True)):
868 new_rb: list[Block] = []
869
870 # GH-39338: _replace_coerce can split a block into
871 # single-column blocks, so track the index so we know
872 # where to index into the mask
873 for blk_num, blk in enumerate(rb):
874 if len(rb) == 1:
875 m = mask
876 else:
877 mib = mask
878 assert not isinstance(mib, bool)
879 m = mib[blk_num : blk_num + 1]
880
881 # error: Argument "mask" to "_replace_coerce" of "Block" has
882 # incompatible type "Union[ExtensionArray, ndarray[Any, Any], bool]";
883 # expected "ndarray[Any, dtype[bool_]]"
884 result = blk._replace_coerce(
885 to_replace=src,
886 value=dest,
887 mask=m,
888 inplace=inplace,
889 regex=regex,
890 )
891
892 if i != src_len:
893 # This is ugly, but we have to get rid of intermediate refs.
894 self_blk_ids = {
895 id(ref()): idx
896 for idx, ref in enumerate(self.refs.referenced_blocks)
897 }
898 # Blocks still sharing memory with self are temporary, so drop
899 # them from self's refs. Pop highest index first: popping in
900 # ascending order shifts every later index down, so with two or
901 # more of them the later pops hit the wrong entry or ran off the
902 # end (GH#61972).
903 drop = {
904 self_blk_ids[id(res_blk)]
905 for res_blk in result
906 if res_blk.refs is self.refs
907 and id(res_blk) in self_blk_ids
908 and res_blk is not self
909 }
910 for idx in sorted(drop, reverse=True):
911 self.refs.referenced_blocks.pop(idx)
912 # Note: blocks whose refs are not self.refs are left alone. Those
913 # refs still track every live block sharing that data, which may
914 # include sibling sub-blocks from _split on an intermediate
915 # block, and always include the block itself. Emptying them would
916 # leave those blocks untracked, so writes through a later view
917 # would not trigger copy-on-write (GH#58966).
918 new_rb.extend(result)
919 rb = new_rb
920
921 # The drop above assumes every block it removes is a short-lived intermediate,
922 # but a block can survive to the end untouched (a later pair whose mask is
923 # empty hands it straight back). Re-register whatever we are actually
924 # returning, otherwise it stays invisible to has_reference() and writes
925 # through a later view skip copy-on-write (GH#58966).
926 # Blocks split off the same parent share one refs, so cache the ids per refs
927 # rather than rescanning the list for each of them.
928 registered: dict[int, set[int]] = {}
929 for ret_blk in rb:
930 refs = ret_blk.refs
931 ids = registered.get(id(refs))
932 if ids is None:
933 ids = {id(ref()) for ref in refs.referenced_blocks}
934 registered[id(refs)] = ids
935 if id(ret_blk) not in ids:
936 refs.add_reference(ret_blk)
937 ids.add(id(ret_blk))
938 return rb
939
940 @final
941 def _replace_coerce(
942 self,
943 to_replace,
944 value,
945 mask: npt.NDArray[np.bool_],
946 inplace: bool = True,
947 regex: bool = False,
948 ) -> list[Block]:
949 """
950 Replace value corresponding to the given boolean array with another
951 value.
952
953 Parameters
954 ----------
955 to_replace : object or pattern
956 Scalar to replace or regular expression to match.
957 value : object
958 Replacement object.
959 mask : np.ndarray[bool]
960 True indicate corresponding element is ignored.
961 inplace : bool, default True
962 Perform inplace modification.
963 regex : bool, default False
964 If true, perform regular expression substitution.
965
966 Returns
967 -------
968 List[Block]
969 """
970 if should_use_regex(regex, to_replace):
971 return self._replace_regex(
972 to_replace,
973 value,
974 inplace=inplace,
975 mask=mask,
976 )
977 else:
978 if value is None:
979 # gh-45601, gh-45836, gh-46634
980 if mask.any():
981 has_ref = self.refs.has_reference()
982 nb = self.astype(np.dtype(object))
983 if not inplace:
984 nb = nb.copy(deep=True)
985 elif inplace and has_ref and nb.refs.has_reference():
986 # no copy in astype and we had refs before
987 nb = nb.copy(deep=True)
988 putmask_inplace(nb.values, mask, value)
989 return [nb]
990 return [self.copy(deep=False)]
991 return self.replace(
992 to_replace=to_replace,
993 value=value,
994 inplace=inplace,
995 mask=mask,
996 )
997
998 # ---------------------------------------------------------------------
999 # 2D Methods - Shared by NumpyBlock and NDArrayBackedExtensionBlock
1000 # but not ExtensionBlock
1001
1002 def _maybe_squeeze_arg(self, arg: np.ndarray) -> np.ndarray:
1003 """
1004 For compatibility with 1D-only ExtensionArrays.
1005 """
1006 return arg
1007
1008 def _unwrap_setitem_indexer(self, indexer):
1009 """
1010 For compatibility with 1D-only ExtensionArrays.
1011 """
1012 return indexer
1013
1014 # NB: this cannot be made cache_readonly because in mgr.set_values we pin
1015 # new .values that can have different shape GH#42631
1016 @property
1017 def shape(self) -> Shape:
1018 return self.values.shape
1019
1020 def iget(self, i: int | tuple[int, int] | tuple[slice, int]) -> np.ndarray:
1021 # In the case where we have a tuple[slice, int], the slice will always
1022 # be slice(None)
1023 # Note: only reached with self.ndim == 2
1024 # Invalid index type "Union[int, Tuple[int, int], Tuple[slice, int]]"
1025 # for "Union[ndarray[Any, Any], ExtensionArray]"; expected type
1026 # "Union[int, integer[Any]]"
1027 return self.values[i] # type: ignore[index]
1028
1029 def _slice(
1030 self, slicer: slice | npt.NDArray[np.bool_] | npt.NDArray[np.intp]
1031 ) -> ArrayLike:
1032 """return a slice of my values"""
1033
1034 return self.values[slicer]
1035
1036 def set_inplace(self, locs, values: ArrayLike, copy: bool = False) -> None:
1037 """
1038 Modify block values in-place with new item value.
1039
1040 If copy=True, first copy the underlying values in place before modifying
1041 (for Copy-on-Write).
1042
1043 Notes
1044 -----
1045 `set_inplace` never creates a new array or new Block, whereas `setitem`
1046 _may_ create a new array and always creates a new Block.
1047
1048 Caller is responsible for checking values.dtype == self.dtype.
1049 """
1050 if copy:
1051 self.values = self.values.copy()
1052 self.values[locs] = values
1053
1054 @final
1055 def take_nd(
1056 self,
1057 indexer: npt.NDArray[np.intp],
1058 axis: AxisInt,
1059 new_mgr_locs: BlockPlacement | None = None,
1060 fill_value=lib.no_default,
1061 ) -> Block:
1062 """
1063 Take values according to indexer and return them as a block.
1064 """
1065 values = self.values
1066
1067 if fill_value is lib.no_default:
1068 fill_value = self.fill_value
1069 allow_fill = False
1070 else:
1071 allow_fill = True
1072
1073 # Note: algos.take_nd has upcast logic similar to coerce_to_target_dtype
1074 new_values = algos.take_nd(
1075 values, indexer, axis=axis, allow_fill=allow_fill, fill_value=fill_value
1076 )
1077
1078 # Called from three places in managers, all of which satisfy
1079 # these assertions
1080 if isinstance(self, ExtensionBlock):
1081 # NB: in this case, the 'axis' kwarg will be ignored in the
1082 # algos.take_nd call above.
1083 assert not (self.ndim == 1 and new_mgr_locs is None)
1084 assert not (axis == 0 and new_mgr_locs is None)
1085
1086 if new_mgr_locs is None:
1087 new_mgr_locs = self._mgr_locs
1088
1089 if new_values.dtype != self.dtype:
1090 return self.make_block(new_values, new_mgr_locs)
1091 else:
1092 return self.make_block_same_class(new_values, new_mgr_locs)
1093
1094 def _unstack(
1095 self,
1096 unstacker,
1097 fill_value,
1098 new_placement: npt.NDArray[np.intp],
1099 needs_masking: npt.NDArray[np.bool_],
1100 ):
1101 """
1102 Return a list of unstacked blocks of self
1103
1104 Parameters
1105 ----------
1106 unstacker : reshape._Unstacker
1107 fill_value : int
1108 Only used in ExtensionBlock._unstack
1109 new_placement : np.ndarray[np.intp]
1110 needs_masking : np.ndarray[bool]
1111 Only used in ExtensionBlock._unstack
1112
1113 Returns
1114 -------
1115 blocks : list of Block
1116 New blocks of unstacked values.
1117 """
1118 new_values = unstacker.get_new_values(self.values.T, fill_value=fill_value)
1119 new_values = new_values.T
1120
1121 # get_new_values can return a view of self.values on the identity-indexer
1122 # fast path (see _Unstacker._make_sorted_values); every other path copies
1123 # via take_nd. Gate on that, then the O(1) may_share_memory, so CoW
1124 # tracks the reference. GH#65107. For NDArrayBacked EAs (e.g. tz-aware
1125 # datetime, period) np.asarray boxes to object, so may_share_memory on
1126 # the EA objects is always False; compare the backing ._ndarray instead
1127 # as get_result does, else the shared view goes untracked. GH#65748
1128 if isinstance(self.values, np.ndarray):
1129 source, unstacked = self.values, new_values
1130 else:
1131 values = cast("NDArrayBackedExtensionArray", self.values)
1132 source, unstacked = values._ndarray, new_values._ndarray
1133
1134 if unstacker._indexer_is_identity and np.may_share_memory(unstacked, source):
1135 refs = self.refs
1136 else:
1137 refs = None
1138
1139 bp = BlockPlacement(new_placement)
1140 blocks = [new_block_2d(new_values, placement=bp, refs=refs)]
1141 return blocks
1142
1143 # ---------------------------------------------------------------------
1144
1145 def setitem(self, indexer, value) -> Block:
1146 """
1147 Attempt self.values[indexer] = value, possibly creating a new array.
1148
1149 Parameters
1150 ----------
1151 indexer : tuple, list-like, array-like, slice, int
1152 The subset of self.values to set
1153 value : object
1154 The value being set
1155
1156 Returns
1157 -------
1158 Block
1159
1160 Notes
1161 -----
1162 `indexer` is a direct slice/positional indexer. `value` must
1163 be a compatible shape.
1164 """
1165
1166 value = self._standardize_fill_value(value)
1167
1168 values = cast("np.ndarray", self.values)
1169 if self.ndim == 2:
1170 values = values.T
1171
1172 # length checking
1173 check_setitem_lengths(indexer, value, values)
1174
1175 if self.dtype != _dtype_obj:
1176 # GH48933: extract_array would convert a pd.Series value to np.ndarray
1177 value = extract_array(value, extract_numpy=True)
1178 try:
1179 casted = np_can_hold_element(values.dtype, value)
1180 except LossySetitemError:
1181 # current dtype cannot store value, coerce to common dtype
1182 nb = self.coerce_to_target_dtype(value, raise_on_upcast=True)
1183 return nb.setitem(indexer, value)
1184 else:
1185 if self.dtype == _dtype_obj and not is_scalar_indexer(indexer, values.ndim):
1186 if isinstance(
1187 indexer, (ABCSeries, ABCIndex, np.ndarray, list, range, slice)
1188 ):
1189 num_set = length_of_indexer(indexer, values)
1190 else:
1191 num_set = len(values[indexer])
1192 casted = setitem_datetimelike_compat(values, num_set, casted)
1193
1194 if (
1195 self.ndim == 1
1196 and isinstance(casted, list)
1197 and len(casted) > 0
1198 and isinstance(casted[0], (tuple, list, np.ndarray))
1199 ):
1200 # Prevent numpy from unpacking nested containers
1201 # (e.g. tuples) into a scalar cell during assignment. GH#37629
1202 # Only for 1D blocks: on a 2D block a list of lists/tuples is
1203 # a genuine 2D value whose rows must not be wrapped. GH#65264
1204 casted = construct_1d_object_array_from_listlike(casted)
1205
1206 self = self._maybe_copy(inplace=True)
1207 values = cast("np.ndarray", self.values.T)
1208 if isinstance(casted, np.ndarray) and casted.ndim == 1 and len(casted) == 1:
1209 # NumPy 1.25 deprecation: https://github.com/numpy/numpy/pull/10615
1210 casted = casted[0, ...]
1211 try:
1212 values[indexer] = casted
1213 except (TypeError, ValueError) as err:
1214 if is_list_like(casted):
1215 raise ValueError(
1216 "setting an array element with a sequence."
1217 ) from err
1218 raise
1219 return self
1220
1221 def putmask(self, mask, new) -> list[Block]:
1222 """
1223 putmask the data to the block; it is possible that we may create a
1224 new dtype of block
1225
1226 Return the resulting block(s).
1227
1228 Parameters
1229 ----------
1230 mask : np.ndarray[bool], SparseArray[bool], or BooleanArray
1231 new : an ndarray/object
1232
1233 Returns
1234 -------
1235 List[Block]
1236 """
1237 orig_mask = mask
1238 values = cast("np.ndarray", self.values)
1239 mask, noop = validate_putmask(values.T, mask)
1240 assert not isinstance(new, (ABCIndex, ABCSeries, ABCDataFrame))
1241
1242 if new is lib.no_default:
1243 new = self.fill_value
1244
1245 new = self._standardize_fill_value(new)
1246 new = extract_array(new, extract_numpy=True)
1247
1248 if noop:
1249 return [self.copy(deep=False)]
1250
1251 try:
1252 casted = np_can_hold_element(values.dtype, new)
1253
1254 self = self._maybe_copy(inplace=True)
1255 values = cast("np.ndarray", self.values)
1256
1257 putmask_without_repeat(values.T, mask, casted)
1258 return [self]
1259 except LossySetitemError:
1260 if self.ndim == 1 or self.shape[0] == 1:
1261 # no need to split columns
1262
1263 if not is_list_like(new):
1264 # using just new[indexer] can't save us the need to cast
1265 return self.coerce_to_target_dtype(
1266 new, raise_on_upcast=True
1267 ).putmask(mask, new)
1268 else:
1269 indexer = mask.nonzero()[0]
1270 nb = self.setitem(indexer, new[indexer])
1271 return [nb]
1272
1273 else:
1274 is_array = isinstance(new, np.ndarray)
1275
1276 res_blocks = []
1277 for i, nb in enumerate(self._split()):
1278 n = new
1279 if is_array:
1280 # we have a different value per-column
1281 n = new[:, i : i + 1]
1282
1283 submask = orig_mask[:, i : i + 1]
1284 rbs = nb.putmask(submask, n)
1285 res_blocks.extend(rbs)
1286 return res_blocks
1287
1288 def where(self, other, cond) -> list[Block]:
1289 """
1290 evaluate the block; return result block(s) from the result
1291
1292 Parameters
1293 ----------
1294 other : an ndarray/object
1295 cond : np.ndarray[bool], SparseArray[bool], or BooleanArray
1296
1297 Returns
1298 -------
1299 List[Block]
1300 """
1301 assert cond.ndim == self.ndim
1302 assert not isinstance(other, (ABCIndex, ABCSeries, ABCDataFrame))
1303
1304 transpose = self.ndim == 2
1305
1306 cond = extract_bool_array(cond)
1307
1308 # EABlocks override where
1309 values = cast("np.ndarray", self.values)
1310 orig_other = other
1311 if transpose:
1312 values = values.T
1313
1314 icond, noop = validate_putmask(values, ~cond)
1315 if noop:
1316 return [self.copy(deep=False)]
1317
1318 if other is lib.no_default:
1319 other = self.fill_value
1320
1321 other = self._standardize_fill_value(other)
1322
1323 try:
1324 # try/except here is equivalent to a self._can_hold_element check,
1325 # but this gets us back 'casted' which we will reuse below;
1326 # without using 'casted', expressions.where may do unwanted upcasts.
1327 casted = np_can_hold_element(values.dtype, other)
1328 except (ValueError, TypeError, LossySetitemError):
1329 # we cannot coerce, return a compat dtype
1330
1331 if self.ndim == 1 or self.shape[0] == 1:
1332 # no need to split columns
1333
1334 block = self.coerce_to_target_dtype(other, raise_on_upcast=False)
1335 return block.where(orig_other, cond)
1336
1337 else:
1338 is_array = isinstance(other, (np.ndarray, ExtensionArray))
1339
1340 res_blocks = []
1341 for i, nb in enumerate(self._split()):
1342 oth = other
1343 if is_array:
1344 # we have a different value per-column
1345 oth = other[:, i : i + 1]
1346
1347 submask = cond[:, i : i + 1]
1348 rbs = nb.where(oth, submask)
1349 res_blocks.extend(rbs)
1350 return res_blocks
1351
1352 else:
1353 other = casted
1354 alt = setitem_datetimelike_compat(values, icond.sum(), other) # type: ignore[arg-type]
1355 if isinstance(other, tuple) and values.dtype == _dtype_obj:
1356 # GH#37681 np.where/np.putmask unpack tuples, so wrap in an
1357 # object array to ensure the tuple is treated as a scalar.
1358 fill_arr = np.empty(1, dtype=object)
1359 fill_arr[0] = other
1360 result = values.copy()
1361 np.putmask(result, icond, fill_arr)
1362 elif alt is not other:
1363 if is_list_like(other) and len(other) < len(values):
1364 # call np.where with other to get the appropriate ValueError
1365 np.where(~icond, values, other)
1366 raise NotImplementedError(
1367 "This should not be reached; call to np.where above is "
1368 "expected to raise ValueError. Please report a bug at "
1369 "github.com/pandas-dev/pandas"
1370 )
1371 result = values.copy()
1372 np.putmask(result, icond, alt)
1373 else:
1374 # By the time we get here, we should have all Series/Index
1375 # args extracted to ndarray
1376 if (
1377 is_list_like(other)
1378 and not isinstance(other, np.ndarray)
1379 and len(other) == self.shape[-1]
1380 ):
1381 # If we don't do this broadcasting here, then expressions.where
1382 # will broadcast a 1D other to be row-like instead of
1383 # column-like.
1384 other = np.array(other).reshape(values.shape)
1385 # If lengths don't match (or len(other)==1), we will raise
1386 # inside expressions.where, see test_series_where
1387
1388 # Note: expressions.where may upcast.
1389 result = expressions.where(~icond, values, other)
1390 # The np_can_hold_element check _should_ ensure that we always
1391 # have result.dtype == self.dtype here.
1392
1393 if transpose:
1394 result = result.T
1395
1396 return [self.make_block(result)]
1397
1398 def fillna(
1399 self,
1400 value,
1401 limit: int | None = None,
1402 inplace: bool = False,
1403 ) -> list[Block]:
1404 """
1405 fillna on the block with the value. If we fail, then convert to
1406 block to hold objects instead and try again
1407 """
1408 # Caller is responsible for validating limit; if int it is strictly positive.
1409 # Caller is also responsible for unboxing Series/Index values, GH#22954
1410 inplace = validate_bool_kwarg(inplace, "inplace")
1411
1412 if not self._can_hold_na:
1413 # can short-circuit the isna call
1414 return [self.copy(deep=False)]
1415
1416 # Fast path: single-pass Cython for scalar fill values on
1417 # float and object dtypes with limit or inplace. Avoids
1418 # mask allocation and cumsum computation. GH#42147
1419 if (
1420 is_scalar(value)
1421 and self.dtype
1422 in (np.dtype("float64"), np.dtype("float32"), np.dtype("object"))
1423 and (limit is not None or inplace)
1424 ):
1425 values = cast("np.ndarray", self.values)
1426 try:
1427 casted = np_can_hold_element(values.dtype, value)
1428 except (LossySetitemError, NotImplementedError):
1429 pass
1430 else:
1431 copy, refs = self._get_refs_and_copy(inplace)
1432 new_values = values.copy() if copy else values
1433
1434 if new_values.ndim == 1:
1435 filled = libalgos.scalar_fillna_inplace(
1436 new_values, casted, limit=limit
1437 )
1438 else:
1439 filled = libalgos.scalar_fillna_2d_inplace(
1440 new_values, casted, limit=limit
1441 )
1442
1443 if filled == 0:
1444 return [self.copy(deep=False)]
1445
1446 return [self.make_block_same_class(new_values, refs=refs)]
1447
1448 # Standard path: generate mask, then apply via putmask/where
1449 mask = isna(self.values)
1450 mask, noop = validate_putmask(self.values, mask)
1451 if noop:
1452 return [self.copy(deep=False)]
1453
1454 if not self._can_hold_element(value) and not is_valid_na_for_dtype(
1455 value, self.dtype
1456 ):
1457 # GH#45153 fillna with incompatible value requiring any
1458 # dtype casting is deprecated.
1459 warnings.warn(
1460 f"'{type(value).__name__}' is not supported as a "
1461 f"fill value for {self.dtype} dtype. In a future "
1462 f"version, calling fillna with an incompatible "
1463 f"value will raise. Explicitly cast to a common "
1464 f"dtype before filling.",
1465 Pandas4Warning,
1466 stacklevel=find_stack_level(),
1467 )
1468
1469 if limit is not None:
1470 mask[mask.cumsum(self.values.ndim - 1) > limit] = False
1471
1472 if inplace:
1473 nbs = self.putmask(mask.T, value)
1474 else:
1475 nbs = self.where(value, ~mask.T)
1476 return extend_blocks(nbs)
1477
1478 def pad_or_backfill(
1479 self,
1480 *,
1481 method: FillnaOptions,
1482 inplace: bool = False,
1483 limit: int | None = None,
1484 limit_area: Literal["inside", "outside"] | None = None,
1485 ) -> list[Block]:
1486 if not self._can_hold_na:
1487 # If there are no NAs, then interpolate is a no-op
1488 return [self.copy(deep=False)]
1489
1490 copy, refs = self._get_refs_and_copy(inplace)
1491
1492 # Dispatch to the NumpyExtensionArray method.
1493 # We know self.array_values is a NumpyExtensionArray bc EABlock overrides
1494 vals = cast("NumpyExtensionArray", self.array_values)
1495 new_values = vals.T._pad_or_backfill(
1496 method=method,
1497 limit=limit,
1498 limit_area=limit_area,
1499 copy=copy,
1500 ).T
1501
1502 data = extract_array(new_values, extract_numpy=True)
1503 return [self.make_block_same_class(data, refs=refs)]
1504
1505 @final
1506 def interpolate(
1507 self,
1508 *,
1509 method: InterpolateOptions,
1510 index: Index,
1511 inplace: bool = False,
1512 limit: int | None = None,
1513 limit_direction: Literal["forward", "backward", "both"] = "forward",
1514 limit_area: Literal["inside", "outside"] | None = None,
1515 **kwargs,
1516 ) -> list[Block]:
1517 inplace = validate_bool_kwarg(inplace, "inplace")
1518
1519 if not self._can_hold_na:
1520 # If there are no NAs, then interpolate is a no-op
1521 return [self.copy(deep=False)]
1522
1523 if self.dtype == _dtype_obj:
1524 # GH#53631
1525 name = {1: "Series", 2: "DataFrame"}[self.ndim]
1526 raise TypeError(f"{name} cannot interpolate with object dtype.")
1527
1528 copy, refs = self._get_refs_and_copy(inplace)
1529
1530 # Dispatch to the EA method.
1531 new_values = self.array_values.interpolate(
1532 method=method,
1533 axis=self.ndim - 1,
1534 index=index,
1535 limit=limit,
1536 limit_direction=limit_direction,
1537 limit_area=limit_area,
1538 copy=copy,
1539 **kwargs,
1540 )
1541 data = extract_array(new_values, extract_numpy=True)
1542 return [self.make_block_same_class(data, refs=refs)]
1543
1544 @final
1545 def diff(self, n: int) -> list[Block]:
1546 """return block for the diff of the values"""
1547 # only reached with ndim == 2
1548 new_values = algos.diff(self.values, n, axis=self.values.ndim - 1)
1549 return [self.make_block(values=new_values)]
1550
1551 def shift(self, periods: int, fill_value: Any = None) -> list[Block]:
1552 """shift the block by periods, possibly upcast"""
1553 # convert integer to float if necessary. need to do a lot more than
1554 # that, handle boolean etc also
1555
1556 # Note: periods is never 0 here, as that is handled at the top of
1557 # NDFrame.shift. If that ever changes, we can do a check for periods=0
1558 # and possibly avoid coercing.
1559
1560 if not lib.is_scalar(fill_value) and self.dtype != _dtype_obj:
1561 # with object dtype there is nothing to promote, and the user can
1562 # pass pretty much any weird fill_value they like
1563 # see test_shift_object_non_scalar_fill
1564 raise ValueError("fill_value must be a scalar")
1565
1566 fill_value = self._standardize_fill_value(fill_value)
1567
1568 try:
1569 # error: Argument 1 to "np_can_hold_element" has incompatible type
1570 # "Union[dtype[Any], ExtensionDtype]"; expected "dtype[Any]"
1571 casted = np_can_hold_element(
1572 self.dtype, # type: ignore[arg-type]
1573 fill_value,
1574 )
1575 except LossySetitemError:
1576 if self.dtype.kind not in "iubSUV" or not is_valid_na_for_dtype(
1577 fill_value, self.dtype
1578 ):
1579 # GH#53802
1580 warnings.warn(
1581 "shifting with a fill value that cannot be held by "
1582 "original dtype is deprecated and will raise in a future "
1583 "version. Explicitly cast to the desired dtype before "
1584 "shifting instead.",
1585 Pandas4Warning,
1586 stacklevel=find_stack_level(),
1587 )
1588 nb = self.coerce_to_target_dtype(fill_value, raise_on_upcast=False)
1589 return nb.shift(periods, fill_value=fill_value)
1590
1591 else:
1592 values = cast("np.ndarray", self.values)
1593 new_values = shift(values, periods, casted)
1594 return [self.make_block_same_class(new_values)]
1595
1596 @final
1597 def quantile(
1598 self,
1599 qs: Index, # with dtype float64
1600 interpolation: QuantileInterpolation = "linear",
1601 ) -> Block:
1602 """
1603 compute the quantiles of the
1604
1605 Parameters
1606 ----------
1607 qs : Index
1608 The quantiles to be computed in float64.
1609 interpolation : str, default 'linear'
1610 Type of interpolation.
1611
1612 Returns
1613 -------
1614 Block
1615 """
1616 # We should always have ndim == 2 because Series dispatches to DataFrame
1617 assert self.ndim == 2
1618 assert is_list_like(qs) # caller is responsible for this
1619
1620 result = quantile_compat(self.values, np.asarray(qs._values), interpolation)
1621 # ensure_block_shape needed for cases where we start with EA and result
1622 # is ndarray, e.g. IntegerArray, SparseArray
1623 result = ensure_block_shape(result, ndim=2)
1624 return new_block_2d(result, placement=self._mgr_locs)
1625
1626 @final
1627 def round(self, decimals: int) -> Self:
1628 """
1629 Rounds the values.
1630 If the block is not of an integer or float dtype, nothing happens.
1631 This is consistent with DataFrame.round behavior.
1632 (Note: Series.round would raise)
1633
1634 Parameters
1635 ----------
1636 decimals: int,
1637 Number of decimal places to round to.
1638 Caller is responsible for validating this
1639 """
1640 if not self.is_numeric or self.is_bool:
1641 if isinstance(self.values, (DatetimeArray, TimedeltaArray, PeriodArray)):
1642 # GH#57781
1643 # TODO: also the ArrowDtype analogues?
1644 warnings.warn(
1645 "obj.round has no effect with datetime, timedelta, "
1646 "or period dtypes. Use obj.dt.round(...) instead.",
1647 UserWarning,
1648 stacklevel=find_stack_level(),
1649 )
1650 return self.copy(deep=False)
1651 values = self.values.round(decimals)
1652
1653 refs = None
1654 if values is self.values:
1655 refs = self.refs
1656
1657 return self.make_block_same_class(values, refs=refs)
1658
1659 # ---------------------------------------------------------------------
1660 # Abstract Methods Overridden By EABackedBlock and NumpyBlock
1661
1662 def delete(self, loc) -> list[Block]:
1663 """Deletes the locs from the block.
1664
1665 We split the block to avoid copying the underlying data. We create new
1666 blocks for every connected segment of the initial block that is not deleted.
1667 The new blocks point to the initial array.
1668
1669 Assumes `loc` is strictly increasing when list-like.
1670 """
1671 if not is_list_like(loc):
1672 loc = [loc]
1673
1674 if self.ndim == 1:
1675 values = cast("np.ndarray", self.values)
1676 values = np.delete(values, loc)
1677 mgr_locs = self._mgr_locs.delete(loc)
1678 return [type(self)(values, placement=mgr_locs, ndim=self.ndim)]
1679
1680 if np.max(loc) >= self.values.shape[0]:
1681 raise IndexError
1682
1683 # Add one out-of-bounds indexer as maximum to collect
1684 # all columns after our last indexer if any
1685 loc = np.concatenate([loc, [self.values.shape[0]]])
1686 mgr_locs_arr = self._mgr_locs.as_array
1687 new_blocks: list[Block] = []
1688
1689 previous_loc = -1
1690 # TODO(CoW): This is tricky, if parent block goes out of scope
1691 # all split blocks are referencing each other even though they
1692 # don't share data
1693 refs = self.refs if self.refs.has_reference() else None
1694 for idx in loc:
1695 if idx == previous_loc + 1:
1696 # There is no column between current and last idx
1697 pass
1698 else:
1699 # No overload variant of "__getitem__" of "ExtensionArray" matches
1700 # argument type "Tuple[slice, slice]"
1701 values = self.values[previous_loc + 1 : idx, :] # type: ignore[call-overload]
1702 locs = mgr_locs_arr[previous_loc + 1 : idx]
1703 nb = type(self)(
1704 values, placement=BlockPlacement(locs), ndim=self.ndim, refs=refs
1705 )
1706 new_blocks.append(nb)
1707
1708 previous_loc = idx
1709
1710 return new_blocks
1711
1712 @property
1713 def is_view(self) -> bool:
1714 """return a boolean if I am possibly a view"""
1715 raise AbstractMethodError(self)
1716
1717 @property
1718 def array_values(self) -> ExtensionArray:
1719 """
1720 The array that Series.array returns. Always an ExtensionArray.
1721 """
1722 raise AbstractMethodError(self)
1723
1724 def get_values(self, dtype: DtypeObj | None = None) -> np.ndarray:
1725 """
1726 return an internal format, currently just the ndarray
1727 this is often overridden to handle to_dense like operations
1728 """
1729 raise AbstractMethodError(self)
1730
1731
1732class EABackedBlock(Block):
1733 """
1734 Mixin for Block subclasses backed by ExtensionArray.
1735 """
1736
1737 values: ExtensionArray
1738
1739 @final
1740 def shift(self, periods: int, fill_value: Any = None) -> list[Block]:
1741 """
1742 Shift the block by `periods`.
1743
1744 Dispatches to underlying ExtensionArray and re-boxes in an
1745 ExtensionBlock.
1746 """
1747 new_values = self.values.shift(periods=periods, fill_value=fill_value)
1748 return [self.make_block_same_class(new_values)]
1749
1750 @final
1751 def setitem(self, indexer, value):
1752 """
1753 Attempt self.values[indexer] = value, possibly creating a new array.
1754
1755 This differs from Block.setitem by not allowing setitem to change
1756 the dtype of the Block.
1757
1758 Parameters
1759 ----------
1760 indexer : tuple, list-like, array-like, slice, int
1761 The subset of self.values to set
1762 value : object
1763 The value being set
1764
1765 Returns
1766 -------
1767 Block
1768
1769 Notes
1770 -----
1771 `indexer` is a direct slice/positional indexer. `value` must
1772 be a compatible shape.
1773 """
1774 orig_indexer = indexer
1775 orig_value = value
1776
1777 indexer = self._unwrap_setitem_indexer(indexer)
1778 value = self._maybe_squeeze_arg(value)
1779
1780 values = self.values
1781 if values._readonly:
1782 # GH#38547 raise here instead of letting the ValueError from
1783 # EA.__setitem__ reach the except clause below, which would
1784 # misinterpret it as a cast failure. Also covers 3rd-party EAs,
1785 # whose __setitem__ does not check the flag.
1786 raise ValueError("Cannot modify read-only array")
1787 target = values
1788 if values.ndim == 2:
1789 target, indexer, value = self._setitem_target(indexer, value)
1790 check_setitem_lengths(indexer, value, values)
1791
1792 try:
1793 target[indexer] = value
1794 except (ValueError, TypeError):
1795 if isinstance(self.dtype, IntervalDtype):
1796 # see TestSetitemFloatIntervalWithIntIntervalValues
1797 nb = self.coerce_to_target_dtype(orig_value, raise_on_upcast=True)
1798 return nb.setitem(orig_indexer, orig_value)
1799
1800 elif isinstance(self, NDArrayBackedExtensionBlock):
1801 if values.ndim == 2 and _unbroadcastable_shape(target, indexer, value):
1802 # GH#68521 not for a 1D block: that would change the
1803 # exception type of Series setitem; see
1804 # test_iloc_setitem_1d_ea_block_shape_mismatch_keeps_its_message
1805 # target is values.T, already in frame order, so the
1806 # error's own message reports the shape mismatch correctly
1807 raise
1808 nb = self.coerce_to_target_dtype(orig_value, raise_on_upcast=True)
1809 return nb.setitem(orig_indexer, orig_value)
1810
1811 else:
1812 raise
1813
1814 else:
1815 return self
1816
1817 def _setitem_target(self, indexer, value):
1818 """
1819 Get the array, indexer and value for setitem on a 2-D block.
1820 """
1821 # GH#45419 Adapt indexer/value to storage layout (nblocks, nrows)
1822 # instead of transposing values, since EA.T may not be a view.
1823 if not isinstance(indexer, tuple):
1824 indexer = (indexer, slice(None))
1825 if len(indexer) == 2:
1826 indexer = indexer[::-1]
1827 if isinstance(value, np.ndarray) and value.ndim == 2:
1828 value = value.T
1829 return self.values, indexer, value
1830
1831 @final
1832 def where(self, other, cond) -> list[Block]:
1833 arr = self.values.T
1834
1835 cond = extract_bool_array(cond)
1836
1837 orig_other = other
1838 orig_cond = cond
1839 other = self._maybe_squeeze_arg(other)
1840 cond = self._maybe_squeeze_arg(cond)
1841
1842 if other is lib.no_default:
1843 other = self.fill_value
1844
1845 icond, noop = validate_putmask(arr, ~cond)
1846 if noop:
1847 # GH#44181, GH#45135
1848 # Avoid a) raising for Interval/PeriodDtype and b) unnecessary object upcast
1849 return [self.copy(deep=False)]
1850
1851 try:
1852 res_values = arr._where(cond, other).T
1853 except OutOfBoundsDatetime:
1854 raise
1855 except (ValueError, TypeError):
1856 if self.ndim == 1 or self.shape[0] == 1:
1857 if isinstance(self.dtype, (IntervalDtype, StringDtype)):
1858 # TestSetitemFloatIntervalWithIntIntervalValues
1859 blk = self.coerce_to_target_dtype(orig_other, raise_on_upcast=False)
1860 if (
1861 self.ndim == 2
1862 and isinstance(orig_cond, np.ndarray)
1863 and orig_cond.ndim == 1
1864 and not is_1d_only_ea_dtype(blk.dtype)
1865 ):
1866 orig_cond = orig_cond[:, None]
1867 return blk.where(orig_other, orig_cond)
1868
1869 elif isinstance(self, NDArrayBackedExtensionBlock):
1870 # NB: not (yet) the same as
1871 # isinstance(values, NDArrayBackedExtensionArray)
1872 blk = self.coerce_to_target_dtype(orig_other, raise_on_upcast=False)
1873 return blk.where(orig_other, orig_cond)
1874
1875 else:
1876 raise
1877
1878 else:
1879 # Same pattern we use in Block.putmask
1880 is_array = isinstance(orig_other, (np.ndarray, ExtensionArray))
1881
1882 res_blocks = []
1883 for i, nb in enumerate(self._split()):
1884 n = orig_other
1885 if is_array:
1886 # we have a different value per-column
1887 n = orig_other[:, i : i + 1]
1888
1889 submask = orig_cond[:, i : i + 1]
1890 rbs = nb.where(n, submask)
1891 res_blocks.extend(rbs)
1892 return res_blocks
1893
1894 nb = self.make_block_same_class(res_values)
1895 return [nb]
1896
1897 @final
1898 def putmask(self, mask, new) -> list[Block]:
1899 """
1900 See Block.putmask.__doc__
1901 """
1902 mask = extract_bool_array(mask)
1903 if new is lib.no_default:
1904 new = self.fill_value
1905
1906 orig_new = new
1907 orig_mask = mask
1908 new = self._maybe_squeeze_arg(new)
1909 mask = self._maybe_squeeze_arg(mask)
1910
1911 if not mask.any():
1912 return [self.copy(deep=False)]
1913
1914 self = self._maybe_copy(inplace=True)
1915 values = self.values
1916 if values._readonly:
1917 # GH#38547 as in EABackedBlock.setitem, raise instead of letting
1918 # the except clause below misinterpret the EA-level ValueError
1919 # as a cast failure.
1920 raise ValueError("Cannot modify read-only array")
1921 if values.ndim == 2:
1922 # GH#64620 Reorient the read-only inputs to the block's storage
1923 # layout and putmask into self.values in place. We must not
1924 # transpose self.values: EA.T is only zero-copy when
1925 # dtype._can_fast_transpose, so a transpose could yield a copy and
1926 # _putmask would silently mutate that throwaway instead of self.
1927 # mask/new are read-only, so transposing them is always correct
1928 # (mirrors value = value.T in EABackedBlock.setitem); failing to
1929 # transpose new fills masked cells from the wrong column.
1930 mask = mask.T
1931 if isinstance(new, (np.ndarray, ExtensionArray)) and new.ndim == 2:
1932 new = new.T
1933
1934 try:
1935 # Caller is responsible for ensuring matching lengths
1936 values._putmask(mask, new)
1937 except OutOfBoundsDatetime:
1938 raise
1939 except (TypeError, ValueError):
1940 if self.ndim == 1 or self.shape[0] == 1:
1941 if isinstance(self.dtype, IntervalDtype):
1942 # Discussion about what we want to support in the general
1943 # case GH#39584
1944 blk = self.coerce_to_target_dtype(orig_new, raise_on_upcast=True)
1945 return blk.putmask(orig_mask, orig_new)
1946
1947 elif isinstance(self, NDArrayBackedExtensionBlock):
1948 # NB: not (yet) the same as
1949 # isinstance(values, NDArrayBackedExtensionArray)
1950 blk = self.coerce_to_target_dtype(orig_new, raise_on_upcast=True)
1951 return blk.putmask(orig_mask, orig_new)
1952
1953 else:
1954 raise
1955
1956 else:
1957 # Same pattern we use in Block.putmask
1958 is_array = isinstance(orig_new, (np.ndarray, ExtensionArray))
1959
1960 res_blocks = []
1961 for i, nb in enumerate(self._split()):
1962 n = orig_new
1963 if is_array:
1964 # we have a different value per-column
1965 n = orig_new[:, i : i + 1]
1966
1967 submask = orig_mask[:, i : i + 1]
1968 rbs = nb.putmask(submask, n)
1969 res_blocks.extend(rbs)
1970 return res_blocks
1971
1972 return [self]
1973
1974 @final
1975 def delete(self, loc) -> list[Block]:
1976 # This will be unnecessary if/when __array_function__ is implemented
1977 if self.ndim == 1:
1978 values = self.values.delete(loc)
1979 mgr_locs = self._mgr_locs.delete(loc)
1980 return [type(self)(values, placement=mgr_locs, ndim=self.ndim)]
1981 elif self.values.ndim == 1:
1982 # We get here through to_stata
1983 return []
1984 return super().delete(loc)
1985
1986 @final
1987 @cache_readonly
1988 def array_values(self) -> ExtensionArray:
1989 return self.values
1990
1991 @final
1992 def get_values(self, dtype: DtypeObj | None = None) -> np.ndarray:
1993 """
1994 return object dtype as boxed values, such as Timestamps/Timedelta
1995 """
1996 values: ArrayLike = self.values
1997 if dtype == _dtype_obj:
1998 values = values.astype(object)
1999 # TODO(EA2D): reshape not needed with 2D EAs
2000 return np.asarray(values).reshape(self.shape)
2001
2002 @final
2003 def pad_or_backfill(
2004 self,
2005 *,
2006 method: FillnaOptions,
2007 inplace: bool = False,
2008 limit: int | None = None,
2009 limit_area: Literal["inside", "outside"] | None = None,
2010 ) -> list[Block]:
2011 values = self.values
2012
2013 kwargs: dict[str, Any] = {"method": method, "limit": limit}
2014 if "limit_area" in inspect.signature(values._pad_or_backfill).parameters:
2015 kwargs["limit_area"] = limit_area
2016 elif limit_area is not None:
2017 raise NotImplementedError(
2018 f"{type(values).__name__} does not implement limit_area "
2019 "(added in pandas 2.2). 3rd-party ExtensionArray authors "
2020 "need to add this argument to _pad_or_backfill."
2021 )
2022
2023 if values.ndim == 2:
2024 # NDArrayBackedExtensionArray.fillna assumes axis=0
2025 new_values = values.T._pad_or_backfill(**kwargs).T
2026 else:
2027 new_values = values._pad_or_backfill(**kwargs)
2028 return [self.make_block_same_class(new_values)]
2029
2030
2031class ExtensionBlock(EABackedBlock):
2032 """
2033 Block for holding extension types.
2034
2035 Notes
2036 -----
2037 This holds all 3rd-party extension array types. It's also the immediate
2038 parent class for our internal extension types' blocks.
2039
2040 ExtensionArrays are limited to 1-D.
2041 """
2042
2043 values: ExtensionArray
2044
2045 def fillna(
2046 self,
2047 value,
2048 limit: int | None = None,
2049 inplace: bool = False,
2050 ) -> list[Block]:
2051 # Caller is responsible for unboxing Series/Index values, GH#22954
2052 if isinstance(self.dtype, (IntervalDtype, StringDtype)):
2053 # Block.fillna handles coercion (test_fillna_interval)
2054 if isinstance(self.dtype, IntervalDtype) and limit is not None:
2055 raise ValueError("limit must be None")
2056 return super().fillna(
2057 value=value,
2058 limit=limit,
2059 inplace=inplace,
2060 )
2061 if self._can_hold_na and not self.values._hasna:
2062 refs = self.refs
2063 new_values = self.values
2064 else:
2065 copy, refs = self._get_refs_and_copy(inplace)
2066
2067 try:
2068 new_values = self.values.fillna(value=value, limit=limit, copy=copy)
2069 except TypeError:
2070 # 3rd party EA that has not implemented copy keyword yet
2071 refs = None
2072 new_values = self.values.fillna(value=value, limit=limit)
2073 # issue the warning *after* retrying, in case the TypeError
2074 # was caused by an invalid fill_value
2075 warnings.warn(
2076 # GH#53278
2077 "ExtensionArray.fillna added a 'copy' keyword in pandas "
2078 "2.1.0. In a future version, ExtensionArray subclasses will "
2079 "need to implement this keyword or an exception will be "
2080 "raised. In the interim, the keyword is ignored by "
2081 f"{type(self.values).__name__}.",
2082 Pandas4Warning,
2083 stacklevel=find_stack_level(),
2084 )
2085
2086 return [self.make_block_same_class(new_values, refs=refs)]
2087
2088 @cache_readonly
2089 def shape(self) -> Shape:
2090 # TODO(EA2D): override unnecessary with 2D EAs
2091 if self.ndim == 1:
2092 return (len(self.values),)
2093 return len(self._mgr_locs), len(self.values)
2094
2095 def iget(self, i: int | tuple[int, int] | tuple[slice, int]):
2096 # In the case where we have a tuple[slice, int], the slice will always
2097 # be slice(None)
2098 # We _could_ make the annotation more specific, but mypy would
2099 # complain about override mismatch:
2100 # Literal[0] | tuple[Literal[0], int] | tuple[slice, int]
2101
2102 # Note: only reached with self.ndim == 2
2103
2104 if isinstance(i, tuple):
2105 # TODO(EA2D): unnecessary with 2D EAs
2106 col, loc = i
2107 if not com.is_null_slice(col) and col != 0:
2108 raise IndexError(f"{self} only contains one item")
2109 if isinstance(col, slice):
2110 # the is_null_slice check above assures that col is slice(None)
2111 # so what we want is a view on all our columns and row loc
2112 if loc < 0:
2113 loc += len(self.values)
2114 # Note: loc:loc+1 vs [[loc]] makes a difference when called
2115 # from fast_xs because we want to get a view back.
2116 return self.values[loc : loc + 1]
2117 return self.values[loc]
2118 else:
2119 if i != 0:
2120 raise IndexError(f"{self} only contains one item")
2121 return self.values
2122
2123 def set_inplace(self, locs, values: ArrayLike, copy: bool = False) -> None:
2124 # When an ndarray, we should have locs.tolist() == [0]
2125 # When a BlockPlacement we should have list(locs) == [0]
2126 if copy:
2127 self.values = self.values.copy()
2128 self.values[:] = values
2129
2130 def _maybe_squeeze_arg(self, arg):
2131 """
2132 If necessary, squeeze a (N, 1) ndarray to (N,)
2133 """
2134 # e.g. if we are passed a 2D mask for putmask
2135 if (
2136 isinstance(arg, (np.ndarray, ExtensionArray))
2137 and arg.ndim == self.values.ndim + 1
2138 ):
2139 # TODO(EA2D): unnecessary with 2D EAs
2140 assert arg.shape[1] == 1
2141 # error: No overload variant of "__getitem__" of "ExtensionArray"
2142 # matches argument type "Tuple[slice, int]"
2143 arg = arg[:, 0] # type: ignore[call-overload]
2144 elif isinstance(arg, ABCDataFrame):
2145 # 2022-01-06 only reached for setitem
2146 # TODO: should we avoid getting here with DataFrame?
2147 assert arg.shape[1] == 1
2148 arg = arg._ixs(0, axis=1)._values
2149
2150 return arg
2151
2152 def _unwrap_setitem_indexer(self, indexer):
2153 """
2154 Adapt a 2D-indexer to our 1D values.
2155
2156 This is intended for 'setitem', not 'iget' or '_slice'.
2157 """
2158 # TODO: ATM this doesn't work for iget/_slice, can we change that?
2159
2160 if isinstance(indexer, tuple) and len(indexer) == 2:
2161 # TODO(EA2D): not needed with 2D EAs
2162 # Should never have length > 2. Caller is responsible for checking.
2163 # Length 1 is reached vis setitem_single_block and setitem_single_column
2164 # each of which pass indexer=(pi,)
2165 if all(isinstance(x, np.ndarray) and x.ndim == 2 for x in indexer):
2166 # GH#44703 went through indexing.maybe_convert_ix
2167 first, second = indexer
2168 if not (
2169 second.size == 1 and (second == 0).all() and first.shape[1] == 1
2170 ):
2171 raise NotImplementedError(
2172 "This should not be reached. Please report a bug at "
2173 "github.com/pandas-dev/pandas/"
2174 )
2175 indexer = first[:, 0]
2176
2177 elif lib.is_integer(indexer[1]) and indexer[1] == 0:
2178 # reached via setitem_single_block passing the whole indexer
2179 indexer = indexer[0]
2180
2181 elif com.is_null_slice(indexer[1]):
2182 indexer = indexer[0]
2183
2184 elif com.is_bool_indexer(indexer[1]) and len(indexer[1]) == 1:
2185 if indexer[1][0]:
2186 indexer = indexer[0]
2187 else:
2188 indexer = []
2189
2190 elif is_list_like(indexer[1]) and indexer[1][0] == 0:
2191 indexer = indexer[0]
2192
2193 else:
2194 raise NotImplementedError(
2195 "This should not be reached. Please report a bug at "
2196 "github.com/pandas-dev/pandas/"
2197 )
2198 return indexer
2199
2200 @property
2201 def is_view(self) -> bool:
2202 """Extension arrays are never treated as views."""
2203 return False
2204
2205 # error: Cannot override writeable attribute with read-only property
2206 @cache_readonly
2207 def is_numeric(self) -> bool: # type: ignore[override]
2208 return self.values.dtype._is_numeric
2209
2210 def _slice(
2211 self, slicer: slice | npt.NDArray[np.bool_] | npt.NDArray[np.intp]
2212 ) -> ExtensionArray:
2213 """
2214 Return a slice of my values.
2215
2216 Parameters
2217 ----------
2218 slicer : slice, ndarray[int], or ndarray[bool]
2219 Valid (non-reducing) indexer for self.values.
2220
2221 Returns
2222 -------
2223 ExtensionArray
2224 """
2225 # Notes: ndarray[bool] is only reachable when via get_rows_with_mask, which
2226 # is only for Series, i.e. self.ndim == 1.
2227
2228 # return same dims as we currently have
2229 if self.ndim == 2:
2230 # reached via getitem_block via _slice_take_blocks_ax0
2231 # TODO(EA2D): won't be necessary with 2D EAs
2232
2233 if not isinstance(slicer, slice):
2234 raise AssertionError(
2235 "invalid slicing for a 1-ndim ExtensionArray", slicer
2236 )
2237 # GH#32959 only full-slicers along fake-dim0 are valid
2238 # TODO(EA2D): won't be necessary with 2D EAs
2239 # range(1) instead of self._mgr_locs to avoid exception on [::-1]
2240 # see test_iloc_getitem_slice_negative_step_ea_block
2241 new_locs = range(1)[slicer]
2242 if not len(new_locs):
2243 raise AssertionError(
2244 "invalid slicing for a 1-ndim ExtensionArray", slicer
2245 )
2246 slicer = slice(None)
2247
2248 return self.values[slicer]
2249
2250 @final
2251 def slice_block_rows(self, slicer: slice) -> Self:
2252 """
2253 Perform __getitem__-like specialized to slicing along index.
2254 """
2255 # GH#42787 in principle this is equivalent to values[..., slicer], but we don't
2256 # require subclasses of ExtensionArray to support that form (for now).
2257 new_values = self.values[slicer]
2258 return type(self)(new_values, self._mgr_locs, ndim=self.ndim, refs=self.refs)
2259
2260 def _unstack(
2261 self,
2262 unstacker,
2263 fill_value,
2264 new_placement: npt.NDArray[np.intp],
2265 needs_masking: npt.NDArray[np.bool_],
2266 ):
2267 # ExtensionArray-safe unstack.
2268 # We override Block._unstack, which unstacks directly on the
2269 # values of the array. For EA-backed blocks, this would require
2270 # converting to a 2-D ndarray of objects.
2271 # Instead, we unstack an ndarray of integer positions, followed by
2272 # a `take` on the actual values.
2273
2274 # Caller is responsible for ensuring self.shape[-1] == len(unstacker.index)
2275 new_values = unstacker.arange_result
2276
2277 # needs_masking[i] calculated once in BlockManager.unstack tells
2278 # us if there are any -1s in the relevant indices. When False,
2279 # that allows us to go through a faster path in 'take', among
2280 # other things avoiding e.g. Categorical._validate_scalar.
2281 blocks = [
2282 # TODO: could cast to object depending on fill_value?
2283 type(self)(
2284 self.values.take(
2285 indices, allow_fill=needs_masking[i], fill_value=fill_value
2286 ),
2287 BlockPlacement(place),
2288 ndim=2,
2289 )
2290 for i, (indices, place) in enumerate(
2291 zip(new_values.T, new_placement, strict=True)
2292 )
2293 ]
2294 return blocks
2295
2296
2297class NumpyBlock(Block):
2298 values: np.ndarray
2299 __slots__ = ()
2300
2301 @property
2302 def is_view(self) -> bool:
2303 """return a boolean if I am possibly a view"""
2304 return self.values.base is not None
2305
2306 @property
2307 def array_values(self) -> ExtensionArray:
2308 return NumpyExtensionArray(self.values)
2309
2310 def get_values(self, dtype: DtypeObj | None = None) -> np.ndarray:
2311 if dtype == _dtype_obj:
2312 return self.values.astype(_dtype_obj)
2313 return self.values
2314
2315 @cache_readonly
2316 def is_numeric(self) -> bool: # type: ignore[override]
2317 dtype = self.values.dtype
2318 kind = dtype.kind
2319
2320 return kind in "fciub"
2321
2322
2323class NDArrayBackedExtensionBlock(EABackedBlock):
2324 """
2325 Block backed by an NDArrayBackedExtensionArray
2326 """
2327
2328 values: NDArrayBackedExtensionArray
2329
2330 @property
2331 def is_view(self) -> bool:
2332 """return a boolean if I am possibly a view"""
2333 # check the ndarray values of the DatetimeIndex values
2334 return self.values._ndarray.base is not None
2335
2336 def _setitem_target(self, indexer, value):
2337 # GH#68521 .T is a view here, so write through it with the key
2338 # in frame order and let numpy do the broadcasting
2339 if not isinstance(indexer, tuple):
2340 # a 0-d ndarray key breaks EA.__setitem__; a tuple avoids it
2341 indexer = (indexer,)
2342 return self.values.T, indexer, value
2343
2344
2345class DatetimeLikeBlock(NDArrayBackedExtensionBlock):
2346 """Block for datetime64[ns], timedelta64[ns]."""
2347
2348 __slots__ = ()
2349 is_numeric = False
2350 values: DatetimeArray | TimedeltaArray
2351
2352
2353def _unbroadcastable_shape(values: ArrayLike, indexer, value) -> bool:
2354 """
2355 Whether ``value`` cannot be broadcast into ``values[indexer]``.
2356
2357 Returns False both when the value does fit and when that cannot be
2358 determined, so a caller can only use a True result to conclude a failed
2359 setitem is a shape problem rather than a dtype one.
2360 """
2361 try:
2362 target_shape = np.shape(values[indexer])
2363 value_shape = np.shape(value)
2364 except (IndexError, TypeError, ValueError):
2365 return False
2366 # assignment also drops leading length-1 axes of the value, which
2367 # broadcasting on its own does not, e.g. ``arr[0] = np.array([x])``
2368 while len(value_shape) > len(target_shape) and value_shape[0] == 1:
2369 value_shape = value_shape[1:]
2370 try:
2371 return np.broadcast_shapes(target_shape, value_shape) != target_shape
2372 except ValueError:
2373 return True
2374
2375
2376# -----------------------------------------------------------------
2377# Constructor Helpers
2378
2379
2380def maybe_coerce_values(values: ArrayLike) -> ArrayLike:
2381 """
2382 Input validation for values passed to __init__. Ensure that
2383 any datetime64/timedelta64 dtypes are in nanoseconds. Ensure
2384 that we do not have string dtypes.
2385
2386 Parameters
2387 ----------
2388 values : np.ndarray or ExtensionArray
2389
2390 Returns
2391 -------
2392 values : np.ndarray or ExtensionArray
2393 """
2394 # Caller is responsible for ensuring NumpyExtensionArray is already extracted.
2395
2396 if isinstance(values, np.ndarray):
2397 values = ensure_wrapped_if_datetimelike(values)
2398
2399 if issubclass(values.dtype.type, str):
2400 values = np.array(values, dtype=object)
2401
2402 return values
2403
2404
2405def _regex_target_dtype(dtype: DtypeObj) -> DtypeObj:
2406 """
2407 The dtype a regex is actually matched against. A Categorical's elements have
2408 its categories' dtype (GH#38447).
2409 """
2410 if isinstance(dtype, CategoricalDtype):
2411 return dtype.categories.dtype
2412 return dtype
2413
2414
2415def get_block_type(dtype: DtypeObj) -> type[Block]:
2416 """
2417 Find the appropriate Block subclass to use for the given values and dtype.
2418
2419 Parameters
2420 ----------
2421 dtype : numpy or pandas dtype
2422
2423 Returns
2424 -------
2425 cls : class, subclass of Block
2426 """
2427 if isinstance(dtype, DatetimeTZDtype):
2428 return DatetimeLikeBlock
2429 elif isinstance(dtype, PeriodDtype):
2430 return NDArrayBackedExtensionBlock
2431 elif isinstance(dtype, ExtensionDtype):
2432 # Note: need to be sure NumpyExtensionArray is unwrapped before we get here
2433 return ExtensionBlock
2434
2435 # We use kind checks because it is much more performant
2436 # than is_foo_dtype
2437 kind = dtype.kind
2438 if kind in "Mm":
2439 return DatetimeLikeBlock
2440
2441 return NumpyBlock
2442
2443
2444def new_block_2d(
2445 values: ArrayLike, placement: BlockPlacement, refs: BlockValuesRefs | None = None
2446) -> Block:
2447 # new_block specialized to case with
2448 # ndim=2
2449 # isinstance(placement, BlockPlacement)
2450 # check_ndim/ensure_block_shape already checked
2451 klass = get_block_type(values.dtype)
2452
2453 values = maybe_coerce_values(values)
2454 return klass(values, ndim=2, placement=placement, refs=refs)
2455
2456
2457def new_block(
2458 values,
2459 placement: BlockPlacement,
2460 *,
2461 ndim: int,
2462 refs: BlockValuesRefs | None = None,
2463) -> Block:
2464 # caller is responsible for ensuring:
2465 # - values is NOT a NumpyExtensionArray
2466 # - check_ndim/ensure_block_shape already checked
2467 # - maybe_coerce_values already called/unnecessary
2468 klass = get_block_type(values.dtype)
2469 return klass(values, ndim=ndim, placement=placement, refs=refs)
2470
2471
2472def check_ndim(values, placement: BlockPlacement, ndim: int) -> None:
2473 """
2474 ndim inference and validation.
2475
2476 Validates that values.ndim and ndim are consistent.
2477 Validates that len(values) and len(placement) are consistent.
2478
2479 Parameters
2480 ----------
2481 values : array-like
2482 placement : BlockPlacement
2483 ndim : int
2484
2485 Raises
2486 ------
2487 ValueError : the number of dimensions do not match
2488 """
2489
2490 if values.ndim > ndim:
2491 # Check for both np.ndarray and ExtensionArray
2492 raise ValueError(
2493 f"Wrong number of dimensions. values.ndim > ndim [{values.ndim} > {ndim}]"
2494 )
2495
2496 if not is_1d_only_ea_dtype(values.dtype):
2497 # TODO(EA2D): special case not needed with 2D EAs
2498 if values.ndim != ndim:
2499 raise ValueError(
2500 "Wrong number of dimensions. "
2501 f"values.ndim != ndim [{values.ndim} != {ndim}]"
2502 )
2503 if len(placement) != len(values):
2504 raise ValueError(
2505 f"Wrong number of items passed {len(values)}, "
2506 f"placement implies {len(placement)}"
2507 )
2508 elif ndim == 2 and len(placement) != 1:
2509 # TODO(EA2D): special case unnecessary with 2D EAs
2510 raise ValueError("need to split")
2511
2512
2513def extract_pandas_array(
2514 values: ArrayLike, dtype: DtypeObj | None, ndim: int | None
2515) -> tuple[ArrayLike, DtypeObj | None]:
2516 """
2517 Ensure that we don't allow NumpyExtensionArray / NumpyEADtype in internals.
2518 """
2519 # For now, blocks should be backed by ndarrays when possible.
2520 if isinstance(values, ABCNumpyExtensionArray):
2521 values = values.to_numpy()
2522 if ndim and ndim > 1:
2523 # TODO(EA2D): special case not needed with 2D EAs
2524 values = np.atleast_2d(values)
2525
2526 if isinstance(dtype, NumpyEADtype):
2527 dtype = dtype.numpy_dtype
2528
2529 return values, dtype
2530
2531
2532# -----------------------------------------------------------------
2533
2534
2535def extend_blocks(result, blocks=None) -> list[Block]:
2536 """return a new extended blocks, given the result"""
2537 if blocks is None:
2538 blocks = []
2539 if isinstance(result, list):
2540 for r in result:
2541 if isinstance(r, list):
2542 blocks.extend(r)
2543 else:
2544 blocks.append(r)
2545 else:
2546 assert isinstance(result, Block), type(result)
2547 blocks.append(result)
2548 return blocks
2549
2550
2551def ensure_block_shape(values: ArrayLike, ndim: int = 1) -> ArrayLike:
2552 """
2553 Reshape if possible to have values.ndim == ndim.
2554 """
2555
2556 if values.ndim < ndim:
2557 if not is_1d_only_ea_dtype(values.dtype):
2558 # TODO(EA2D): https://github.com/pandas-dev/pandas/issues/23023
2559 # block.shape is incorrect for "2D" ExtensionArrays
2560 # We can't, and don't need to, reshape.
2561 values = cast("np.ndarray | DatetimeArray | TimedeltaArray", values)
2562 values = values.reshape(1, -1)
2563
2564 return values
2565
2566
2567def external_values(values: ArrayLike) -> ArrayLike:
2568 """
2569 The array that Series.values returns (public attribute).
2570
2571 This has some historical constraints, and is overridden in block
2572 subclasses to return the correct array (e.g. period returns
2573 object ndarray and datetimetz a datetime64[ns] ndarray instead of
2574 proper extension array).
2575 """
2576 if isinstance(values, (PeriodArray, IntervalArray)):
2577 return values.astype(object)
2578 elif isinstance(values, (DatetimeArray, TimedeltaArray)):
2579 # NB: for datetime64tz this is different from np.asarray(values), since
2580 # that returns an object-dtype ndarray of Timestamps.
2581 # Avoid raising in .astype in casting from dt64tz to dt64
2582 values = values._ndarray
2583
2584 if isinstance(values, np.ndarray):
2585 values = values.view()
2586 values.flags.writeable = False
2587 else:
2588 # ExtensionArrays
2589 # TODO decide on read-only https://github.com/pandas-dev/pandas/issues/63099
2590 # values = values.view()
2591 # values._readonly = True
2592 pass
2593
2594 return values