1from __future__ import annotations
2
3from collections.abc import (
4 Callable,
5 Hashable,
6 Sequence,
7)
8import itertools
9from typing import (
10 TYPE_CHECKING,
11 Any,
12 Literal,
13 NoReturn,
14 Self,
15 cast,
16 final,
17)
18import warnings
19
20import numpy as np
21
22from pandas._config.config import get_option
23
24from pandas._libs import (
25 algos as libalgos,
26 internals as libinternals,
27 lib,
28)
29from pandas._libs.internals import (
30 BlockPlacement,
31 BlockValuesRefs,
32)
33from pandas._libs.tslibs import Timestamp
34from pandas.errors import (
35 AbstractMethodError,
36 PerformanceWarning,
37)
38from pandas.util._decorators import cache_readonly
39from pandas.util._exceptions import find_stack_level
40from pandas.util._validators import validate_bool_kwarg
41
42from pandas.core.dtypes.cast import (
43 find_common_type,
44 infer_dtype_from_scalar,
45 np_can_hold_element,
46)
47from pandas.core.dtypes.common import (
48 ensure_platform_int,
49 is_1d_only_ea_dtype,
50 is_list_like,
51)
52from pandas.core.dtypes.dtypes import (
53 CategoricalDtype,
54 DatetimeTZDtype,
55 ExtensionDtype,
56 SparseDtype,
57)
58from pandas.core.dtypes.generic import (
59 ABCDataFrame,
60 ABCSeries,
61)
62from pandas.core.dtypes.missing import (
63 array_equals,
64 isna,
65)
66
67import pandas.core.algorithms as algos
68from pandas.core.arrays import DatetimeArray
69from pandas.core.arrays._mixins import NDArrayBackedExtensionArray
70from pandas.core.base import PandasObject
71from pandas.core.construction import (
72 ensure_wrapped_if_datetimelike,
73 extract_array,
74)
75from pandas.core.indexers import maybe_convert_indices
76from pandas.core.indexes.api import (
77 Index,
78 default_index,
79 ensure_index,
80)
81from pandas.core.internals.blocks import (
82 Block,
83 NumpyBlock,
84 ensure_block_shape,
85 extend_blocks,
86 get_block_type,
87 maybe_coerce_values,
88 new_block,
89 new_block_2d,
90)
91from pandas.core.internals.ops import (
92 blockwise_all,
93 operate_blockwise,
94)
95
96if TYPE_CHECKING:
97 from collections.abc import Generator
98
99 from pandas._typing import (
100 ArrayLike,
101 AxisInt,
102 DtypeObj,
103 QuantileInterpolation,
104 Shape,
105 npt,
106 )
107
108 from pandas.api.extensions import ExtensionArray
109
110
111def interleaved_dtype(dtypes: list[DtypeObj]) -> DtypeObj | None:
112 """
113 Find the common dtype for `blocks`.
114
115 Parameters
116 ----------
117 blocks : List[DtypeObj]
118
119 Returns
120 -------
121 dtype : np.dtype, ExtensionDtype, or None
122 None is returned when `blocks` is empty.
123 """
124 if not len(dtypes):
125 return None
126
127 return find_common_type(dtypes)
128
129
130def ensure_np_dtype(dtype: DtypeObj) -> np.dtype:
131 # TODO: https://github.com/pandas-dev/pandas/issues/22791
132 # Give EAs some input on what happens here. Sparse needs this.
133 if isinstance(dtype, SparseDtype):
134 dtype = dtype.subtype
135 dtype = cast(np.dtype, dtype)
136 elif isinstance(dtype, ExtensionDtype):
137 dtype = np.dtype("object")
138 elif dtype == np.dtype(str):
139 dtype = np.dtype("object")
140 return dtype
141
142
143class BaseBlockManager(PandasObject):
144 """
145 Core internal data structure to implement DataFrame, Series, etc.
146
147 Manage a bunch of labeled 2D mixed-type ndarrays. Essentially it's a
148 lightweight blocked set of labeled data to be manipulated by the DataFrame
149 public API class
150
151 Attributes
152 ----------
153 shape
154 ndim
155 axes
156 values
157 items
158
159 Methods
160 -------
161 set_axis(axis, new_labels)
162 copy(deep=True)
163
164 get_dtypes
165
166 apply(func, axes, block_filter_fn)
167
168 get_bool_data
169 get_numeric_data
170
171 get_slice(slice_like, axis)
172 get(label)
173 iget(loc)
174
175 take(indexer, axis)
176 reindex_axis(new_labels, axis)
177 reindex_indexer(new_labels, indexer, axis)
178
179 delete(label)
180 insert(loc, label, value)
181 set(label, value)
182
183 Parameters
184 ----------
185 blocks: Sequence of Block
186 axes: Sequence of Index
187 verify_integrity: bool, default True
188
189 Notes
190 -----
191 This is *not* a public API class
192 """
193
194 __slots__ = ()
195
196 _blknos: npt.NDArray[np.intp]
197 _blklocs: npt.NDArray[np.intp]
198 blocks: tuple[Block, ...]
199 axes: list[Index]
200
201 @property
202 def ndim(self) -> int:
203 raise NotImplementedError
204
205 _known_consolidated: bool
206 _is_consolidated: bool
207
208 def __init__(self, blocks, axes, verify_integrity: bool = True) -> None:
209 raise NotImplementedError
210
211 @final
212 def __len__(self) -> int:
213 return len(self.items)
214
215 @property
216 def shape(self) -> Shape:
217 return tuple(len(ax) for ax in self.axes)
218
219 @classmethod
220 def from_blocks(cls, blocks: list[Block], axes: list[Index]) -> Self:
221 raise NotImplementedError
222
223 @property
224 def blknos(self) -> npt.NDArray[np.intp]:
225 """
226 Suppose we want to find the array corresponding to our i'th column.
227
228 blknos[i] identifies the block from self.blocks that contains this column.
229
230 blklocs[i] identifies the column of interest within
231 self.blocks[self.blknos[i]]
232 """
233 if self._blknos is None:
234 # Note: these can be altered by other BlockManager methods.
235 self._rebuild_blknos_and_blklocs()
236
237 return self._blknos
238
239 @property
240 def blklocs(self) -> npt.NDArray[np.intp]:
241 """
242 See blknos.__doc__
243 """
244 if self._blklocs is None:
245 # Note: these can be altered by other BlockManager methods.
246 self._rebuild_blknos_and_blklocs()
247
248 return self._blklocs
249
250 def make_empty(self, axes=None) -> Self:
251 """return an empty BlockManager with the items axis of len 0"""
252 if axes is None:
253 # TODO shallow copy remaining axis?
254 axes = [default_index(0), *self.axes[1:]]
255
256 # preserve dtype if possible
257 if self.ndim == 1:
258 assert isinstance(self, SingleBlockManager) # for mypy
259 blk = self.blocks[0]
260 arr = blk.values[:0]
261 bp = BlockPlacement(slice(0, 0))
262 nb = blk.make_block_same_class(arr, placement=bp)
263 blocks = [nb]
264 else:
265 blocks = []
266 return type(self).from_blocks(blocks, axes)
267
268 def __bool__(self) -> bool:
269 return True
270
271 def set_axis(self, axis: AxisInt, new_labels: Index) -> None:
272 # Caller is responsible for ensuring we have an Index object.
273 self._validate_set_axis(axis, new_labels)
274 self.axes[axis] = new_labels
275
276 @final
277 def _validate_set_axis(self, axis: AxisInt, new_labels: Index) -> None:
278 # Caller is responsible for ensuring we have an Index object.
279 old_len = len(self.axes[axis])
280 new_len = len(new_labels)
281
282 if axis == 1 and len(self.items) == 0:
283 # If we are setting the index on a DataFrame with no columns,
284 # it is OK to change the length.
285 pass
286
287 elif new_len != old_len:
288 raise ValueError(
289 f"Length mismatch: Expected axis has {old_len} elements, new "
290 f"values have {new_len} elements"
291 )
292
293 @property
294 def is_single_block(self) -> bool:
295 # Assumes we are 2D; overridden by SingleBlockManager
296 return len(self.blocks) == 1
297
298 @property
299 def items(self) -> Index:
300 return self.axes[0]
301
302 def _has_no_reference(self, i: int) -> bool:
303 """
304 Check for column `i` if it has references.
305 (whether it references another array or is itself being referenced)
306 Returns True if the column has no references.
307 """
308 blkno = self.blknos[i]
309 return self._has_no_reference_block(blkno)
310
311 def _has_no_reference_block(self, blkno: int) -> bool:
312 """
313 Check for block `i` if it has references.
314 (whether it references another array or is itself being referenced)
315 Returns True if the block has no references.
316 """
317 return not self.blocks[blkno].refs.has_reference()
318
319 def add_references(self, mgr: BaseBlockManager) -> None:
320 """
321 Adds the references from one manager to another. We assume that both
322 managers have the same block structure.
323 """
324 if len(self.blocks) != len(mgr.blocks):
325 # If block structure changes, then we made a copy
326 return
327 for i, blk in enumerate(self.blocks):
328 blk.refs = mgr.blocks[i].refs
329 blk.refs.add_reference(blk)
330
331 def references_same_values(self, mgr: BaseBlockManager, blkno: int) -> bool:
332 """
333 Checks if two blocks from two different block managers reference the
334 same underlying values.
335 """
336 blk = self.blocks[blkno]
337 return any(blk is ref() for ref in mgr.blocks[blkno].refs.referenced_blocks)
338
339 def get_unique_dtypes(self) -> npt.NDArray[np.object_]:
340 return algos.unique(np.array([blk.dtype for blk in self.blocks], dtype=object))
341
342 def get_dtypes(self) -> npt.NDArray[np.object_]:
343 dtypes = np.array([blk.dtype for blk in self.blocks], dtype=object)
344 return dtypes.take(self.blknos)
345
346 @property
347 def arrays(self) -> list[ArrayLike]:
348 """
349 Quick access to the backing arrays of the Blocks.
350
351 Only for compatibility with ArrayManager for testing convenience.
352 Not to be used in actual code, and return value is not the same as the
353 ArrayManager method (list of 1D arrays vs iterator of 2D ndarrays / 1D EAs).
354
355 Warning! The returned arrays don't handle Copy-on-Write, so this should
356 be used with caution (only in read-mode).
357 """
358 # TODO: Deprecate, usage in Dask
359 # https://github.com/dask/dask/blob/484fc3f1136827308db133cd256ba74df7a38d8c/dask/base.py#L1312
360 return [blk.values for blk in self.blocks]
361
362 def __repr__(self) -> str:
363 output = type(self).__name__
364 for i, ax in enumerate(self.axes):
365 if i == 0:
366 output += f"\nItems: {ax}"
367 else:
368 output += f"\nAxis {i}: {ax}"
369
370 for block in self.blocks:
371 output += f"\n{block}"
372 return output
373
374 def _equal_values(self, other: Self) -> bool:
375 """
376 To be implemented by the subclasses. Only check the column values
377 assuming shape and indexes have already been checked.
378 """
379 raise AbstractMethodError(self)
380
381 @final
382 def equals(self, other: object) -> bool:
383 """
384 Implementation for DataFrame.equals
385 """
386 if not isinstance(other, type(self)):
387 return False
388
389 self_axes, other_axes = self.axes, other.axes
390 if len(self_axes) != len(other_axes):
391 return False
392 if not all(
393 ax1.equals(ax2) for ax1, ax2 in zip(self_axes, other_axes, strict=True)
394 ):
395 return False
396
397 return self._equal_values(other)
398
399 def apply(
400 self,
401 f,
402 align_keys: list[str] | None = None,
403 **kwargs,
404 ) -> Self:
405 """
406 Iterate over the blocks, collect and create a new BlockManager.
407
408 Parameters
409 ----------
410 f : str or callable
411 Name of the Block method to apply.
412 align_keys: List[str] or None, default None
413 **kwargs
414 Keywords to pass to `f`
415
416 Returns
417 -------
418 BlockManager
419 """
420 assert "filter" not in kwargs
421
422 align_keys = align_keys or []
423 result_blocks: list[Block] = []
424 # fillna: Series/DataFrame is responsible for making sure value is aligned
425
426 aligned_args = {k: kwargs[k] for k in align_keys}
427
428 for b in self.blocks:
429 if aligned_args:
430 for k, obj in aligned_args.items():
431 if isinstance(obj, (ABCSeries, ABCDataFrame)):
432 # The caller is responsible for ensuring that
433 # obj.axes[-1].equals(self.items)
434 if obj.ndim == 1:
435 kwargs[k] = obj.iloc[b.mgr_locs.indexer]._values
436 else:
437 kwargs[k] = obj.iloc[:, b.mgr_locs.indexer]._values
438 else:
439 # otherwise we have an ndarray
440 kwargs[k] = obj[b.mgr_locs.indexer]
441
442 if callable(f):
443 applied = b.apply(f, **kwargs)
444 else:
445 applied = getattr(b, f)(**kwargs)
446 result_blocks = extend_blocks(applied, result_blocks)
447
448 out = type(self).from_blocks(result_blocks, [ax.view() for ax in self.axes])
449 return out
450
451 @final
452 def isna(self, func) -> Self:
453 return self.apply("apply", func=func)
454
455 @final
456 def fillna(self, value, limit: int | None, inplace: bool) -> Self:
457 if limit is not None:
458 # Do this validation even if we go through one of the no-op paths
459 limit = libalgos.validate_limit(None, limit=limit)
460
461 return self.apply(
462 "fillna",
463 value=value,
464 limit=limit,
465 inplace=inplace,
466 )
467
468 @final
469 def where(self, other, cond, align: bool) -> Self:
470 if align:
471 align_keys = ["other", "cond"]
472 else:
473 align_keys = ["cond"]
474 other = extract_array(other, extract_numpy=True)
475
476 return self.apply(
477 "where",
478 align_keys=align_keys,
479 other=other,
480 cond=cond,
481 )
482
483 @final
484 def putmask(self, mask, new, align: bool = True) -> Self:
485 if align:
486 align_keys = ["new", "mask"]
487 else:
488 align_keys = ["mask"]
489 new = extract_array(new, extract_numpy=True)
490
491 return self.apply(
492 "putmask",
493 align_keys=align_keys,
494 mask=mask,
495 new=new,
496 )
497
498 @final
499 def round(self, decimals: int) -> Self:
500 return self.apply("round", decimals=decimals)
501
502 @final
503 def replace(self, to_replace, value, inplace: bool) -> Self:
504 inplace = validate_bool_kwarg(inplace, "inplace")
505 # NDFrame.replace ensures the not-is_list_likes here
506 assert not lib.is_list_like(to_replace)
507 assert not lib.is_list_like(value)
508 return self.apply(
509 "replace",
510 to_replace=to_replace,
511 value=value,
512 inplace=inplace,
513 )
514
515 @final
516 def replace_regex(self, **kwargs) -> Self:
517 return self.apply("_replace_regex", **kwargs)
518
519 @final
520 def replace_list(
521 self,
522 src_list: list[Any],
523 dest_list: list[Any],
524 inplace: bool = False,
525 regex: bool = False,
526 ) -> Self:
527 """do a list replace"""
528 inplace = validate_bool_kwarg(inplace, "inplace")
529
530 bm = self.apply(
531 "replace_list",
532 src_list=src_list,
533 dest_list=dest_list,
534 inplace=inplace,
535 regex=regex,
536 )
537 bm._consolidate_inplace()
538 return bm
539
540 def interpolate(self, inplace: bool, **kwargs) -> Self:
541 return self.apply("interpolate", inplace=inplace, **kwargs)
542
543 def pad_or_backfill(self, inplace: bool, **kwargs) -> Self:
544 return self.apply("pad_or_backfill", inplace=inplace, **kwargs)
545
546 def shift(self, periods: int, fill_value) -> Self:
547 if fill_value is lib.no_default:
548 fill_value = None
549
550 return self.apply("shift", periods=periods, fill_value=fill_value)
551
552 def setitem(self, indexer, value) -> Self:
553 """
554 Set values with indexer.
555
556 For SingleBlockManager, this backs s[indexer] = value
557 """
558 if isinstance(indexer, np.ndarray) and indexer.ndim > self.ndim:
559 raise ValueError(f"Cannot set values with ndim > {self.ndim}")
560
561 if not self._has_no_reference(0):
562 # this method is only called if there is a single block -> hardcoded 0
563 # Split blocks to only copy the columns we want to modify
564 if self.ndim == 2 and isinstance(indexer, tuple):
565 blk_loc = self.blklocs[indexer[1]]
566 if is_list_like(blk_loc) and blk_loc.ndim == 2:
567 blk_loc = np.squeeze(blk_loc, axis=0)
568 elif not is_list_like(blk_loc):
569 # Keep dimension and copy data later
570 blk_loc = [blk_loc] # type: ignore[assignment]
571 if len(blk_loc) == 0:
572 return self.copy(deep=False)
573
574 values = self.blocks[0].values
575 if values.ndim == 2:
576 # Block.delete in _iset_split_block requires sorted unique
577 # locs; inverse maps the requested column order onto the
578 # new block (GH#65446)
579 blk_loc, inverse = np.unique(blk_loc, return_inverse=True)
580 values = values[blk_loc]
581 # "T" has no attribute "_iset_split_block"
582 self._iset_split_block( # type: ignore[attr-defined]
583 0, blk_loc, values
584 )
585
586 indexer = list(indexer)
587 # first block equals values we are setting to -> set to all columns
588 if lib.is_integer(indexer[1]):
589 col_indexer = 0
590 elif len(inverse) > 1 and lib.is_range_indexer(
591 inverse, len(blk_loc)
592 ):
593 col_indexer = slice(None) # type: ignore[assignment]
594 else:
595 col_indexer = inverse # type: ignore[assignment]
596 indexer[1] = col_indexer
597
598 row_indexer = indexer[0]
599 if isinstance(col_indexer, np.ndarray):
600 if (
601 isinstance(row_indexer, np.ndarray)
602 and row_indexer.ndim == 1
603 ):
604 # GH#65446: Make the row indexer 2d to take a cross product
605 row_indexer = row_indexer[:, None]
606 elif isinstance(row_indexer, np.ndarray) and row_indexer.ndim == 2:
607 # numpy cannot handle a 2d indexer in combo with a slice
608 row_indexer = np.squeeze(row_indexer, axis=1)
609 if isinstance(row_indexer, np.ndarray) and len(row_indexer) == 0:
610 # numpy does not like empty indexer combined with slice
611 # and we are setting nothing anyway
612 return self
613 indexer[0] = row_indexer
614 self.blocks[0].setitem(tuple(indexer), value)
615 return self
616 # No need to split if we either set all columns or on a single block
617 # manager
618 self = self.copy(deep=True)
619
620 return self.apply("setitem", indexer=indexer, value=value)
621
622 def diff(self, n: int) -> Self:
623 # only reached with self.ndim == 2
624 return self.apply("diff", n=n)
625
626 def astype(self, dtype, errors: str = "raise") -> Self:
627 return self.apply("astype", dtype=dtype, errors=errors)
628
629 def convert(self) -> Self:
630 return self.apply("convert")
631
632 def convert_dtypes(self, **kwargs):
633 return self.apply("convert_dtypes", **kwargs)
634
635 def get_values_for_csv(
636 self, *, float_format, date_format, decimal, na_rep: str = "nan", quoting=None
637 ) -> Self:
638 """
639 Convert values to native types (strings / python objects) that are used
640 in formatting (repr / csv).
641 """
642 return self.apply(
643 "get_values_for_csv",
644 na_rep=na_rep,
645 quoting=quoting,
646 float_format=float_format,
647 date_format=date_format,
648 decimal=decimal,
649 )
650
651 @property
652 def any_extension_types(self) -> bool:
653 """Whether any of the blocks in this manager are extension blocks"""
654 return any(block.is_extension for block in self.blocks)
655
656 @property
657 def is_view(self) -> bool:
658 """return a boolean if we are a single block and are a view"""
659 if len(self.blocks) == 1:
660 return self.blocks[0].is_view
661
662 # It is technically possible to figure out which blocks are views
663 # e.g. [ b.values.base is not None for b in self.blocks ]
664 # but then we have the case of possibly some blocks being a view
665 # and some blocks not. setting in theory is possible on the non-view
666 # blocks. But this is a bit
667 # complicated
668
669 return False
670
671 def _get_data_subset(self, predicate: Callable) -> Self:
672 blocks = [blk for blk in self.blocks if predicate(blk.values)]
673 return self._combine(blocks)
674
675 def _get_data_subset_indices(self, predicate: Callable) -> np.ndarray:
676 blocks = [blk for blk in self.blocks if predicate(blk.values)]
677 indexer = np.sort(np.concatenate([b.mgr_locs.as_array for b in blocks]))
678 return indexer
679
680 def get_bool_data(self) -> Self:
681 """
682 Select blocks that are bool-dtype and columns from object-dtype blocks
683 that are all-bool.
684 """
685
686 new_blocks = []
687
688 for blk in self.blocks:
689 if blk.dtype == bool:
690 new_blocks.append(blk)
691
692 elif blk.is_object:
693 new_blocks.extend(nb for nb in blk._split() if nb.is_bool)
694
695 return self._combine(new_blocks)
696
697 def get_numeric_data(self) -> Self:
698 numeric_blocks = [blk for blk in self.blocks if blk.is_numeric]
699 if len(numeric_blocks) == len(self.blocks):
700 # Avoid somewhat expensive _combine
701 # TODO(CoW) need to return a shallow copy here?
702 return self
703 return self._combine(numeric_blocks)
704
705 def _combine(self, blocks: list[Block], index: Index | None = None) -> Self:
706 """return a new manager with the blocks"""
707 if len(blocks) == 0:
708 if self.ndim == 2:
709 # retain our own Index dtype
710 if index is not None:
711 axes = [self.items[:0], index]
712 else:
713 axes = [self.items[:0], *self.axes[1:]]
714 return self.make_empty(axes)
715 return self.make_empty()
716
717 # FIXME: optimization potential
718 indexer = np.sort(np.concatenate([b.mgr_locs.as_array for b in blocks]))
719 inv_indexer = lib.get_reverse_indexer(indexer, self.shape[0])
720
721 new_blocks: list[Block] = []
722 for b in blocks:
723 nb = b.copy(deep=False)
724 nb.mgr_locs = BlockPlacement(inv_indexer[nb.mgr_locs.indexer])
725 new_blocks.append(nb)
726
727 axes = list(self.axes)
728 # TODO shallow copy of axes?
729 if index is not None:
730 axes[-1] = index
731 axes[0] = self.items.take(indexer)
732
733 return type(self).from_blocks(new_blocks, axes)
734
735 @property
736 def nblocks(self) -> int:
737 return len(self.blocks)
738
739 def copy(self, *, deep: bool) -> Self:
740 """
741 Make deep or shallow copy of BlockManager
742
743 Parameters
744 ----------
745 deep : bool, string or None, default True
746 If False, return a shallow copy (do not copy data)
747
748 Returns
749 -------
750 BlockManager
751 """
752 # TODO: Should deep=True be respected for axes?
753 new_axes = [ax.view() for ax in self.axes]
754
755 res = self.apply("copy", deep=deep)
756 res.axes = new_axes
757
758 if self.ndim > 1:
759 # Avoid needing to re-compute these
760 blknos = self._blknos
761 if blknos is not None:
762 res._blknos = blknos.copy()
763 res._blklocs = self._blklocs.copy()
764
765 if deep:
766 res._consolidate_inplace()
767 return res
768
769 def is_consolidated(self) -> bool:
770 return True
771
772 def consolidate(self) -> Self:
773 """
774 Join together blocks having same dtype
775
776 Returns
777 -------
778 y : BlockManager
779 """
780 if self.is_consolidated():
781 return self
782
783 # TODO shallow copy is not needed here?
784 bm = type(self)(self.blocks, self.axes, verify_integrity=False)
785 bm._is_consolidated = False
786 bm._consolidate_inplace()
787 return bm
788
789 def _consolidate_inplace(self) -> None:
790 return
791
792 @final
793 def reindex_axis(
794 self,
795 new_index: Index,
796 axis: AxisInt,
797 fill_value=None,
798 only_slice: bool = False,
799 ) -> Self:
800 """
801 Conform data manager to new index.
802 """
803 new_index, indexer = self.axes[axis].reindex(new_index)
804
805 return self.reindex_indexer(
806 new_index,
807 indexer,
808 axis=axis,
809 fill_value=fill_value,
810 only_slice=only_slice,
811 )
812
813 def reindex_indexer(
814 self,
815 new_axis: Index,
816 indexer: npt.NDArray[np.intp] | None,
817 axis: AxisInt,
818 fill_value=None,
819 allow_dups: bool = False,
820 only_slice: bool = False,
821 *,
822 use_na_proxy: bool = False,
823 ) -> Self:
824 """
825 Parameters
826 ----------
827 new_axis : Index
828 indexer : ndarray[intp] or None
829 axis : int
830 fill_value : object, default None
831 allow_dups : bool, default False
832 only_slice : bool, default False
833 Whether to take views, not copies, along columns.
834 use_na_proxy : bool, default False
835 Whether to use an np.void ndarray for newly introduced columns.
836
837 pandas-indexer with -1's only.
838 """
839 if indexer is None:
840 if new_axis is self.axes[axis]:
841 # TODO(CoW) need to handle CoW?
842 return self
843
844 result = self.copy(deep=False)
845 result.axes = list(self.axes)
846 result.axes[axis] = new_axis
847 return result
848
849 # Should be intp, but in some cases we get int64 on 32bit builds
850 assert isinstance(indexer, np.ndarray)
851
852 # some axes don't allow reindexing with dups
853 if not allow_dups:
854 self.axes[axis]._validate_can_reindex(indexer)
855
856 if axis >= self.ndim:
857 raise IndexError("Requested axis not found in manager")
858
859 if axis == 0:
860 new_blocks = list(
861 self._slice_take_blocks_ax0(
862 indexer,
863 fill_value=fill_value,
864 only_slice=only_slice,
865 use_na_proxy=use_na_proxy,
866 )
867 )
868 else:
869 new_blocks = []
870 for blk in self.blocks:
871 if blk.dtype == np.void:
872 # GH#58316: np.void placeholders cast to b'' when
873 # reindexed; preserve np.void so _setitem_single_column
874 # can later infer the correct dtype
875 vals = np.empty((blk.values.shape[0], len(indexer)), dtype=np.void)
876 new_blocks.append(NumpyBlock(vals, blk.mgr_locs, ndim=2))
877 else:
878 new_blocks.append(
879 blk.take_nd(
880 indexer,
881 axis=1,
882 fill_value=(
883 fill_value if fill_value is not None else blk.fill_value
884 ),
885 )
886 )
887
888 new_axes = list(self.axes)
889 new_axes[axis] = new_axis
890 if self.ndim == 2:
891 new_axes[1 - axis] = self.axes[1 - axis].view()
892
893 new_mgr = type(self).from_blocks(new_blocks, new_axes)
894 if axis == 1:
895 # We can avoid the need to rebuild these
896 new_mgr._blknos = self.blknos.copy()
897 new_mgr._blklocs = self.blklocs.copy()
898 return new_mgr
899
900 def _slice_take_blocks_ax0(
901 self,
902 slice_or_indexer: slice | np.ndarray,
903 fill_value=lib.no_default,
904 only_slice: bool = False,
905 *,
906 use_na_proxy: bool = False,
907 ref_inplace_op: bool = False,
908 ) -> Generator[Block]:
909 """
910 Slice/take blocks along axis=0.
911
912 Overloaded for SingleBlock
913
914 Parameters
915 ----------
916 slice_or_indexer : slice or np.ndarray[int64]
917 fill_value : scalar, default lib.no_default
918 only_slice : bool, default False
919 If True, we always return views on existing arrays, never copies.
920 This is used when called from ops.blockwise.operate_blockwise.
921 use_na_proxy : bool, default False
922 Whether to use an np.void ndarray for newly introduced columns.
923 ref_inplace_op: bool, default False
924 Don't track refs if True because we operate inplace
925
926 Yields
927 ------
928 Block : New Block
929 """
930 allow_fill = fill_value is not lib.no_default
931
932 sl_type, slobj, sllen = _preprocess_slice_or_indexer(
933 slice_or_indexer, self.shape[0], allow_fill=allow_fill
934 )
935
936 if self.is_single_block:
937 blk = self.blocks[0]
938
939 if sl_type == "slice":
940 # GH#32959 EABlock would fail since we can't make 0-width
941 # TODO(EA2D): special casing unnecessary with 2D EAs
942 if sllen == 0:
943 return
944 bp = BlockPlacement(slice(0, sllen))
945 yield blk.getitem_block_columns(slobj, new_mgr_locs=bp)
946 return
947 elif not allow_fill or self.ndim == 1:
948 if allow_fill and fill_value is None:
949 fill_value = blk.fill_value
950
951 if not allow_fill and only_slice:
952 # GH#33597 slice instead of take, so we get
953 # views instead of copies
954 for i, ml in enumerate(slobj):
955 yield blk.getitem_block_columns(
956 slice(ml, ml + 1),
957 new_mgr_locs=BlockPlacement(i),
958 ref_inplace_op=ref_inplace_op,
959 )
960 else:
961 bp = BlockPlacement(slice(0, sllen))
962 yield blk.take_nd(
963 slobj,
964 axis=0,
965 new_mgr_locs=bp,
966 fill_value=fill_value,
967 )
968 return
969
970 if sl_type == "slice":
971 blknos = self.blknos[slobj]
972 blklocs = self.blklocs[slobj]
973 else:
974 blknos = algos.take_nd(
975 self.blknos, slobj, fill_value=-1, allow_fill=allow_fill
976 )
977 blklocs = algos.take_nd(
978 self.blklocs, slobj, fill_value=-1, allow_fill=allow_fill
979 )
980
981 # When filling blknos, make sure blknos is updated before appending to
982 # blocks list, that way new blkno is exactly len(blocks).
983 group = not only_slice
984 for blkno, mgr_locs in libinternals.get_blkno_placements(blknos, group=group):
985 if blkno == -1:
986 # If we've got here, fill_value was not lib.no_default
987
988 dtype, _ = infer_dtype_from_scalar(fill_value)
989 if is_1d_only_ea_dtype(dtype) and len(mgr_locs) > 1:
990 # Handle 1D-only extension dtypes by creating separate blocks
991 # (GH#63993)
992 placements = [BlockPlacement(col_idx) for col_idx in mgr_locs]
993 else:
994 placements = [mgr_locs]
995
996 for placement in placements:
997 yield self._make_na_block(
998 placement=placement,
999 fill_value=fill_value,
1000 use_na_proxy=use_na_proxy,
1001 )
1002 else:
1003 blk = self.blocks[blkno]
1004
1005 # Otherwise, slicing along items axis is necessary.
1006 if not blk._can_consolidate and not blk._validate_ndim:
1007 # i.e. we dont go through here for DatetimeTZBlock
1008 # A non-consolidatable block, it's easy, because there's
1009 # only one item and each mgr loc is a copy of that single
1010 # item.
1011 deep = False
1012 for mgr_loc in mgr_locs:
1013 newblk = blk.copy(deep=deep)
1014 newblk.mgr_locs = BlockPlacement(slice(mgr_loc, mgr_loc + 1))
1015 yield newblk
1016
1017 else:
1018 # GH#32779 to avoid the performance penalty of copying,
1019 # we may try to only slice
1020 taker = blklocs[mgr_locs.indexer]
1021 max_len = max(len(mgr_locs), taker.max() + 1)
1022 taker = lib.maybe_indices_to_slice(taker, max_len)
1023
1024 if isinstance(taker, slice):
1025 nb = blk.getitem_block_columns(taker, new_mgr_locs=mgr_locs)
1026 yield nb
1027 elif only_slice:
1028 # GH#33597 slice instead of take, so we get
1029 # views instead of copies
1030 for i, ml in zip(taker, mgr_locs, strict=True):
1031 slc = slice(i, i + 1)
1032 bp = BlockPlacement(ml)
1033 nb = blk.getitem_block_columns(slc, new_mgr_locs=bp)
1034 # We have np.shares_memory(nb.values, blk.values)
1035 yield nb
1036 else:
1037 nb = blk.take_nd(taker, axis=0, new_mgr_locs=mgr_locs)
1038 yield nb
1039
1040 def _make_na_block(
1041 self, placement: BlockPlacement, fill_value=None, use_na_proxy: bool = False
1042 ) -> Block:
1043 # Note: we only get here with self.ndim == 2
1044
1045 if use_na_proxy:
1046 assert fill_value is None
1047 shape = (len(placement), self.shape[1])
1048 vals = np.empty(shape, dtype=np.void)
1049 nb = NumpyBlock(vals, placement, ndim=2)
1050 return nb
1051
1052 if fill_value is None or fill_value is np.nan:
1053 fill_value = np.nan
1054 # GH45857 avoid unnecessary upcasting
1055 dtype = interleaved_dtype([blk.dtype for blk in self.blocks])
1056 if dtype is not None and np.issubdtype(dtype.type, np.floating):
1057 fill_value = dtype.type(fill_value)
1058
1059 shape = (len(placement), self.shape[1])
1060
1061 dtype, fill_value = infer_dtype_from_scalar(fill_value)
1062 block_values = make_na_array(dtype, shape, fill_value)
1063 return new_block_2d(block_values, placement=placement)
1064
1065 def take(
1066 self,
1067 indexer: npt.NDArray[np.intp],
1068 axis: AxisInt = 1,
1069 verify: bool = True,
1070 ) -> Self:
1071 """
1072 Take items along any axis.
1073
1074 indexer : np.ndarray[np.intp]
1075 axis : int, default 1
1076 verify : bool, default True
1077 Check that all entries are between 0 and len(self) - 1, inclusive.
1078 Pass verify=False if this check has been done by the caller.
1079
1080 Returns
1081 -------
1082 BlockManager
1083 """
1084 # Caller is responsible for ensuring indexer annotation is accurate
1085
1086 n = self.shape[axis]
1087 indexer = maybe_convert_indices(indexer, n, verify=verify)
1088
1089 new_labels = self.axes[axis].take(indexer)
1090 return self.reindex_indexer(
1091 new_axis=new_labels,
1092 indexer=indexer,
1093 axis=axis,
1094 allow_dups=True,
1095 )
1096
1097
1098class BlockManager(libinternals.BlockManager, BaseBlockManager):
1099 """
1100 BaseBlockManager that holds 2D blocks.
1101 """
1102
1103 ndim = 2
1104
1105 # ----------------------------------------------------------------
1106 # Constructors
1107
1108 def __init__(
1109 self,
1110 blocks: Sequence[Block],
1111 axes: Sequence[Index],
1112 verify_integrity: bool = True,
1113 ) -> None:
1114 if verify_integrity:
1115 # Assertion disabled for performance
1116 # assert all(isinstance(x, Index) for x in axes)
1117
1118 for block in blocks:
1119 if self.ndim != block.ndim:
1120 raise AssertionError(
1121 f"Number of Block dimensions ({block.ndim}) must equal "
1122 f"number of axes ({self.ndim})"
1123 )
1124 # As of 2.0, the caller is responsible for ensuring that
1125 # DatetimeTZBlock with block.ndim == 2 has block.values.ndim ==2;
1126 # previously there was a special check for fastparquet compat.
1127
1128 self._verify_integrity()
1129
1130 def _verify_integrity(self) -> None:
1131 mgr_shape = self.shape
1132 tot_items = sum(len(x.mgr_locs) for x in self.blocks)
1133 for block in self.blocks:
1134 if block.shape[1:] != mgr_shape[1:]:
1135 raise_construction_error(tot_items, block.shape[1:], self.axes)
1136 if len(self.items) != tot_items:
1137 raise AssertionError(
1138 "Number of manager items must equal union of "
1139 f"block items\n# manager items: {len(self.items)}, # "
1140 f"tot_items: {tot_items}"
1141 )
1142
1143 @classmethod
1144 def from_blocks(cls, blocks: list[Block], axes: list[Index]) -> Self:
1145 """
1146 Constructor for BlockManager and SingleBlockManager with same signature.
1147 """
1148 return cls(blocks, axes, verify_integrity=False)
1149
1150 # ----------------------------------------------------------------
1151 # Indexing
1152
1153 def fast_xs(self, loc: int) -> SingleBlockManager:
1154 """
1155 Return the array corresponding to `frame.iloc[loc]`.
1156
1157 Parameters
1158 ----------
1159 loc : int
1160
1161 Returns
1162 -------
1163 np.ndarray or ExtensionArray
1164 """
1165 if len(self.blocks) == 1:
1166 # TODO: this could be wrong if blk.mgr_locs is not slice(None)-like;
1167 # is this ruled out in the general case?
1168 result: np.ndarray | ExtensionArray = self.blocks[0].iget(
1169 (slice(None), loc)
1170 )
1171 # in the case of a single block, the new block is a view
1172 bp = BlockPlacement(slice(0, len(result)))
1173 block = new_block(
1174 result,
1175 placement=bp,
1176 ndim=1,
1177 refs=self.blocks[0].refs,
1178 )
1179 return SingleBlockManager(block, self.axes[0].view())
1180
1181 dtype = interleaved_dtype([blk.dtype for blk in self.blocks])
1182
1183 n = len(self)
1184
1185 if isinstance(dtype, ExtensionDtype):
1186 # TODO: use object dtype as workaround for non-performant
1187 # EA.__setitem__ methods. (primarily ArrowExtensionArray.__setitem__
1188 # when iteratively setting individual values)
1189 # https://github.com/pandas-dev/pandas/pull/54508#issuecomment-1675827918
1190 result = np.empty(n, dtype=object)
1191 else:
1192 result = np.empty(n, dtype=dtype)
1193 result = ensure_wrapped_if_datetimelike(result)
1194
1195 for blk in self.blocks:
1196 # Such assignment may incorrectly coerce NaT to None
1197 # result[blk.mgr_locs] = blk._slice((slice(None), loc))
1198 for i, rl in enumerate(blk.mgr_locs):
1199 item = blk.iget((i, loc))
1200 if (
1201 result.dtype.kind in "iub"
1202 and lib.is_float(item)
1203 and isna(item)
1204 and isinstance(blk.dtype, CategoricalDtype)
1205 ):
1206 # GH#58954 caused bc interleaved_dtype is wrong for Categorical
1207 # TODO(GH#38240) this will be unnecessary
1208 # Note that doing this in a try/except would work for the
1209 # integer case, but not for bool, which will cast the NaN
1210 # entry to True.
1211 if result.dtype.kind == "b":
1212 new_dtype = object
1213 else:
1214 new_dtype = np.float64
1215 result = result.astype(new_dtype)
1216 result[rl] = item
1217
1218 if isinstance(dtype, ExtensionDtype):
1219 cls = dtype.construct_array_type()
1220 result = cls._from_sequence(result, dtype=dtype)
1221
1222 bp = BlockPlacement(slice(0, len(result)))
1223 block = new_block(result, placement=bp, ndim=1)
1224 return SingleBlockManager(block, self.axes[0].view())
1225
1226 def iget(self, i: int, track_ref: bool = True) -> SingleBlockManager:
1227 """
1228 Return the data as a SingleBlockManager.
1229 """
1230 block = self.blocks[self.blknos[i]]
1231 values = block.iget(self.blklocs[i])
1232
1233 # shortcut for select a single-dim from a 2-dim BM
1234 bp = BlockPlacement(slice(0, len(values)))
1235 nb = type(block)(
1236 values, placement=bp, ndim=1, refs=block.refs if track_ref else None
1237 )
1238 return SingleBlockManager(nb, self.axes[1].view())
1239
1240 def iget_values(self, i: int) -> ArrayLike:
1241 """
1242 Return the data for column i as the values (ndarray or ExtensionArray).
1243
1244 Warning! The returned array is a view but doesn't handle Copy-on-Write,
1245 so this should be used with caution.
1246 """
1247 # TODO(CoW) making the arrays read-only might make this safer to use?
1248 block = self.blocks[self.blknos[i]]
1249 values = block.iget(self.blklocs[i])
1250 return values
1251
1252 @property
1253 def column_arrays(self) -> list[np.ndarray]:
1254 """
1255 Used in the JSON C code to access column arrays.
1256 This optimizes compared to using `iget_values` by converting each
1257
1258 Warning! This doesn't handle Copy-on-Write, so should be used with
1259 caution (current use case of consuming this in the JSON code is fine).
1260 """
1261 # This is an optimized equivalent to
1262 # result = [self.iget_values(i) for i in range(len(self.items))]
1263 result: list[np.ndarray | None] = [None] * len(self.items)
1264
1265 for blk in self.blocks:
1266 mgr_locs = blk._mgr_locs
1267 values = blk.array_values._values_for_json()
1268 if values.ndim == 1:
1269 # TODO(EA2D): special casing not needed with 2D EAs
1270 result[mgr_locs[0]] = values
1271
1272 else:
1273 for i, loc in enumerate(mgr_locs):
1274 result[loc] = values[i]
1275
1276 # error: Incompatible return value type (got "List[None]",
1277 # expected "List[ndarray[Any, Any]]")
1278 return result # type: ignore[return-value]
1279
1280 def iset(
1281 self,
1282 loc: int | slice | np.ndarray,
1283 value: ArrayLike,
1284 inplace: bool = False,
1285 refs: BlockValuesRefs | None = None,
1286 ) -> None:
1287 """
1288 Set new item in-place. Does not consolidate. Adds new Block if not
1289 contained in the current set of items
1290 """
1291
1292 # FIXME: refactor, clearly separate broadcasting & zip-like assignment
1293 # can prob also fix the various if tests for sparse/categorical
1294 if self._blklocs is None and self.ndim > 1:
1295 self._rebuild_blknos_and_blklocs()
1296
1297 # Note: we exclude DTA/TDA here
1298 value_is_extension_type = is_1d_only_ea_dtype(value.dtype)
1299 if not value_is_extension_type:
1300 if value.ndim == 2:
1301 value = value.T
1302 else:
1303 value = ensure_block_shape(value, ndim=2)
1304
1305 if value.shape[1:] != self.shape[1:]:
1306 raise AssertionError(
1307 "Shape of new values must be compatible with manager shape"
1308 )
1309
1310 if lib.is_integer(loc):
1311 # We have 6 tests where loc is _not_ an int.
1312 # In this case, get_blkno_placements will yield only one tuple,
1313 # containing (self._blknos[loc], BlockPlacement(slice(0, 1, 1)))
1314
1315 # Check if we can use _iset_single fastpath
1316 loc = cast(int, loc)
1317 blkno = self.blknos[loc]
1318 blk = self.blocks[blkno]
1319 if len(blk._mgr_locs) == 1: # TODO: fastest way to check this?
1320 return self._iset_single(
1321 loc,
1322 value,
1323 inplace=inplace,
1324 blkno=blkno,
1325 blk=blk,
1326 refs=refs,
1327 )
1328
1329 # error: Incompatible types in assignment (expression has type
1330 # "List[Union[int, slice, ndarray]]", variable has type "Union[int,
1331 # slice, ndarray]")
1332 loc = [loc] # type: ignore[assignment]
1333
1334 # categorical/sparse/datetimetz
1335 if value_is_extension_type:
1336
1337 def value_getitem(placement):
1338 return value
1339
1340 else:
1341
1342 def value_getitem(placement):
1343 return value[placement.indexer]
1344
1345 # Accessing public blknos ensures the public versions are initialized
1346 blknos = self.blknos[loc]
1347 blklocs = self.blklocs[loc].copy()
1348
1349 unfit_mgr_locs = []
1350 unfit_val_locs = []
1351 removed_blknos = []
1352 for blkno_l, val_locs in libinternals.get_blkno_placements(blknos, group=True):
1353 blk = self.blocks[blkno_l]
1354 blk_locs = blklocs[val_locs.indexer]
1355 if inplace and blk.should_store(value):
1356 # Updating inplace -> check if we need to do Copy-on-Write
1357 if not self._has_no_reference_block(blkno_l):
1358 self._iset_split_block(
1359 blkno_l, blk_locs, value_getitem(val_locs), refs=refs
1360 )
1361 else:
1362 blk.set_inplace(blk_locs, value_getitem(val_locs))
1363 continue
1364 else:
1365 unfit_mgr_locs.append(blk.mgr_locs.as_array[blk_locs])
1366 unfit_val_locs.append(val_locs)
1367
1368 # If all block items are unfit, schedule the block for removal.
1369 if len(val_locs) == len(blk.mgr_locs):
1370 removed_blknos.append(blkno_l)
1371 continue
1372 else:
1373 # Defer setting the new values to enable consolidation
1374 self._iset_split_block(blkno_l, blk_locs, refs=refs)
1375
1376 if removed_blknos:
1377 # Remove blocks & update blknos accordingly
1378 is_deleted = np.zeros(self.nblocks, dtype=np.bool_)
1379 is_deleted[removed_blknos] = True
1380
1381 new_blknos = np.empty(self.nblocks, dtype=np.intp)
1382 new_blknos.fill(-1)
1383 new_blknos[~is_deleted] = np.arange(self.nblocks - len(removed_blknos))
1384 self._blknos = new_blknos[self._blknos]
1385 self.blocks = tuple(
1386 blk for i, blk in enumerate(self.blocks) if i not in set(removed_blknos)
1387 )
1388
1389 if unfit_val_locs:
1390 unfit_idxr = np.concatenate(unfit_mgr_locs)
1391 unfit_count = len(unfit_idxr)
1392
1393 new_blocks: list[Block] = []
1394 if value_is_extension_type:
1395 # This code (ab-)uses the fact that EA blocks contain only
1396 # one item.
1397 # TODO(EA2D): special casing unnecessary with 2D EAs
1398 new_blocks.extend(
1399 new_block_2d(
1400 values=value,
1401 placement=BlockPlacement(slice(mgr_loc, mgr_loc + 1)),
1402 refs=refs,
1403 )
1404 for mgr_loc in unfit_idxr
1405 )
1406
1407 self._blknos[unfit_idxr] = np.arange(unfit_count) + len(self.blocks)
1408 self._blklocs[unfit_idxr] = 0
1409
1410 else:
1411 # unfit_val_locs contains BlockPlacement objects
1412 unfit_val_items = unfit_val_locs[0].append(unfit_val_locs[1:])
1413
1414 new_blocks.append(
1415 new_block_2d(
1416 values=value_getitem(unfit_val_items),
1417 placement=BlockPlacement(unfit_idxr),
1418 refs=refs,
1419 )
1420 )
1421
1422 self._blknos[unfit_idxr] = len(self.blocks)
1423 self._blklocs[unfit_idxr] = np.arange(unfit_count)
1424
1425 self.blocks += tuple(new_blocks)
1426
1427 # Newly created block's dtype may already be present.
1428 self._known_consolidated = False
1429
1430 def _iset_split_block(
1431 self,
1432 blkno_l: int,
1433 blk_locs: np.ndarray | list[int],
1434 value: ArrayLike | None = None,
1435 refs: BlockValuesRefs | None = None,
1436 ) -> None:
1437 """Removes columns from a block by splitting the block.
1438
1439 Avoids copying the whole block through slicing and updates the manager
1440 after determining the new block structure. Optionally adds a new block,
1441 otherwise has to be done by the caller.
1442
1443 Parameters
1444 ----------
1445 blkno_l: The block number to operate on, relevant for updating the manager
1446 blk_locs: The locations of our block that should be deleted.
1447 value: The value to set as a replacement.
1448 refs: The reference tracking object of the value to set.
1449 """
1450 blk = self.blocks[blkno_l]
1451
1452 if self._blklocs is None:
1453 self._rebuild_blknos_and_blklocs()
1454
1455 nbs_tup = tuple(blk.delete(blk_locs))
1456 if value is not None:
1457 locs = blk.mgr_locs.as_array[blk_locs]
1458 first_nb = new_block_2d(value, BlockPlacement(locs), refs=refs)
1459 else:
1460 first_nb = nbs_tup[0]
1461 nbs_tup = tuple(nbs_tup[1:])
1462
1463 nr_blocks = len(self.blocks)
1464 blocks_tup = (
1465 *self.blocks[:blkno_l],
1466 first_nb,
1467 *self.blocks[blkno_l + 1 :],
1468 *nbs_tup,
1469 )
1470 self.blocks = blocks_tup
1471
1472 if not nbs_tup and value is not None:
1473 # No need to update anything if split did not happen
1474 return
1475
1476 self._blklocs[first_nb.mgr_locs.indexer] = np.arange(len(first_nb))
1477
1478 for i, nb in enumerate(nbs_tup):
1479 self._blklocs[nb.mgr_locs.indexer] = np.arange(len(nb))
1480 self._blknos[nb.mgr_locs.indexer] = i + nr_blocks
1481
1482 def _iset_single(
1483 self,
1484 loc: int,
1485 value: ArrayLike,
1486 inplace: bool,
1487 blkno: int,
1488 blk: Block,
1489 refs: BlockValuesRefs | None = None,
1490 ) -> None:
1491 """
1492 Fastpath for iset when we are only setting a single position and
1493 the Block currently in that position is itself single-column.
1494
1495 In this case we can swap out the entire Block and blklocs and blknos
1496 are unaffected.
1497 """
1498 # Caller is responsible for verifying value.shape
1499
1500 if inplace and blk.should_store(value):
1501 copy = not self._has_no_reference_block(blkno)
1502 iloc = self.blklocs[loc]
1503 blk.set_inplace(slice(iloc, iloc + 1), value, copy=copy)
1504 return
1505
1506 nb = new_block_2d(value, placement=blk._mgr_locs, refs=refs)
1507 old_blocks = self.blocks
1508 new_blocks = (*old_blocks[:blkno], nb, *old_blocks[blkno + 1 :])
1509 self.blocks = new_blocks
1510 return
1511
1512 def column_setitem(
1513 self, loc: int, idx: int | slice | np.ndarray, value, inplace_only: bool = False
1514 ) -> None:
1515 """
1516 Set values ("setitem") into a single column (not setting the full column).
1517
1518 This is a method on the BlockManager level, to avoid creating an
1519 intermediate Series at the DataFrame level (`s = df[loc]; s[idx] = value`)
1520 """
1521 if not self._has_no_reference(loc):
1522 blkno = self.blknos[loc]
1523 # Split blocks to only copy the column we want to modify
1524 blk_loc = self.blklocs[loc]
1525 # Copy our values
1526 values = self.blocks[blkno].values
1527 if values.ndim == 1:
1528 values = values.copy()
1529 else:
1530 # Use [blk_loc] as indexer to keep ndim=2, this already results in a
1531 # copy
1532 values = values[[blk_loc]]
1533 self._iset_split_block(blkno, [blk_loc], values)
1534
1535 # this manager is only created temporarily to mutate the values in place
1536 # so don't track references, otherwise the `setitem` would perform CoW again
1537 col_mgr = self.iget(loc, track_ref=False)
1538 if inplace_only:
1539 col_mgr.setitem_inplace(idx, value)
1540 else:
1541 new_mgr = col_mgr.setitem((idx,), value)
1542 self.iset(loc, new_mgr._block.values, inplace=True)
1543
1544 def insert(self, loc: int, item: Hashable, value: ArrayLike, refs=None) -> None:
1545 """
1546 Insert item at selected position.
1547
1548 Parameters
1549 ----------
1550 loc : int
1551 item : hashable
1552 value : np.ndarray or ExtensionArray
1553 refs : The reference tracking object of the value to set.
1554 """
1555 new_axis = self.items.insert(loc, item)
1556
1557 if value.ndim == 2:
1558 value = value.T
1559 if len(value) > 1:
1560 raise ValueError(
1561 f"Expected a 1D array, got an array with shape {value.T.shape}"
1562 )
1563 else:
1564 value = ensure_block_shape(value, ndim=self.ndim)
1565
1566 bp = BlockPlacement(slice(loc, loc + 1))
1567 block = new_block_2d(values=value, placement=bp, refs=refs)
1568
1569 if not len(self.blocks):
1570 # Fastpath
1571 self._blklocs = np.array([0], dtype=np.intp)
1572 self._blknos = np.array([0], dtype=np.intp)
1573 else:
1574 self._insert_update_mgr_locs(loc)
1575 self._insert_update_blklocs_and_blknos(loc)
1576
1577 self.axes[0] = new_axis
1578 self.blocks += (block,)
1579
1580 self._known_consolidated = False
1581
1582 if (
1583 get_option("performance_warnings")
1584 and sum(not block.is_extension for block in self.blocks) > 100
1585 ):
1586 warnings.warn(
1587 "DataFrame is highly fragmented. This is usually the result "
1588 "of calling `frame.insert` many times, which has poor performance. "
1589 "Consider joining all columns at once using pd.concat(axis=1) "
1590 "instead. To get a de-fragmented frame, use `newframe = frame.copy()`",
1591 PerformanceWarning,
1592 stacklevel=find_stack_level(),
1593 )
1594
1595 def _insert_update_mgr_locs(self, loc) -> None:
1596 """
1597 When inserting a new Block at location 'loc', we increment
1598 all of the mgr_locs of blocks above that by one.
1599 """
1600 # Faster version of set(arr) for sequences of small numbers
1601 blknos = np.bincount(self.blknos[loc:]).nonzero()[0]
1602 for blkno in blknos:
1603 # .620 this way, .326 of which is in increment_above
1604 blk = self.blocks[blkno]
1605 blk._mgr_locs = blk._mgr_locs.increment_above(loc)
1606
1607 def _insert_update_blklocs_and_blknos(self, loc) -> None:
1608 """
1609 When inserting a new Block at location 'loc', we update our
1610 _blklocs and _blknos.
1611 """
1612
1613 # Accessing public blklocs ensures the public versions are initialized
1614 if loc == self.blklocs.shape[0]:
1615 # np.append is a lot faster, let's use it if we can.
1616 self._blklocs = np.append(self._blklocs, 0)
1617 self._blknos = np.append(self._blknos, len(self.blocks))
1618 elif loc == 0:
1619 # As of numpy 1.26.4, np.concatenate faster than np.append
1620 self._blklocs = np.concatenate([[0], self._blklocs])
1621 self._blknos = np.concatenate([[len(self.blocks)], self._blknos])
1622 else:
1623 new_blklocs, new_blknos = libinternals.update_blklocs_and_blknos(
1624 self.blklocs, self.blknos, loc, len(self.blocks)
1625 )
1626 self._blklocs = new_blklocs
1627 self._blknos = new_blknos
1628
1629 def idelete(self, indexer) -> BlockManager:
1630 """
1631 Delete selected locations, returning a new BlockManager.
1632 """
1633 is_deleted = np.zeros(self.shape[0], dtype=np.bool_)
1634 is_deleted[indexer] = True
1635 taker = (~is_deleted).nonzero()[0]
1636
1637 nbs = self._slice_take_blocks_ax0(taker, only_slice=True, ref_inplace_op=True)
1638 new_columns = self.items[~is_deleted]
1639 axes = [new_columns, self.axes[1]]
1640 return type(self)(tuple(nbs), axes, verify_integrity=False)
1641
1642 # ----------------------------------------------------------------
1643 # Block-wise Operation
1644
1645 def grouped_reduce(self, func: Callable) -> Self:
1646 """
1647 Apply grouped reduction function blockwise, returning a new BlockManager.
1648
1649 Parameters
1650 ----------
1651 func : grouped reduction function
1652
1653 Returns
1654 -------
1655 BlockManager
1656 """
1657 result_blocks: list[Block] = []
1658
1659 for blk in self.blocks:
1660 if blk.is_object:
1661 # split on object-dtype blocks bc some columns may raise
1662 # while others do not.
1663 for sb in blk._split():
1664 applied = sb.apply(func)
1665 result_blocks = extend_blocks(applied, result_blocks)
1666 else:
1667 applied = blk.apply(func)
1668 result_blocks = extend_blocks(applied, result_blocks)
1669
1670 if len(result_blocks) == 0:
1671 nrows = 0
1672 else:
1673 nrows = result_blocks[0].values.shape[-1]
1674 index = default_index(nrows)
1675
1676 # TODO shallow copy columns?
1677 return type(self).from_blocks(result_blocks, [self.axes[0].view(), index])
1678
1679 def reduce(self, func: Callable) -> Self:
1680 """
1681 Apply reduction function blockwise, returning a single-row BlockManager.
1682
1683 Parameters
1684 ----------
1685 func : reduction function
1686
1687 Returns
1688 -------
1689 BlockManager
1690 """
1691 # If 2D, we assume that we're operating column-wise
1692 assert self.ndim == 2
1693
1694 res_blocks = [blk.reduce(func) for blk in self.blocks]
1695 index = default_index(1) # placeholder
1696 # shallow copy self.items not needed because DataFrame._reduce does a getitem
1697 new_mgr = type(self).from_blocks(res_blocks, [self.items, index])
1698 return new_mgr
1699
1700 def operate_blockwise(self, other: BlockManager, array_op) -> BlockManager:
1701 """
1702 Apply array_op blockwise with another (aligned) BlockManager.
1703 """
1704 return operate_blockwise(self, other, array_op)
1705
1706 def _equal_values(self: BlockManager, other: BlockManager) -> bool:
1707 """
1708 Used in .equals defined in base class. Only check the column values
1709 assuming shape and indexes have already been checked.
1710 """
1711 return blockwise_all(self, other, array_equals)
1712
1713 def quantile(
1714 self,
1715 *,
1716 qs: Index, # with dtype float 64
1717 interpolation: QuantileInterpolation = "linear",
1718 ) -> Self:
1719 """
1720 Iterate over blocks applying quantile reduction.
1721 This routine is intended for reduction type operations and
1722 will do inference on the generated blocks.
1723
1724 Parameters
1725 ----------
1726 interpolation : type of interpolation, default 'linear'
1727 qs : list of the quantiles to be computed
1728
1729 Returns
1730 -------
1731 BlockManager
1732 """
1733 # Series dispatches to DataFrame for quantile, which allows us to
1734 # simplify some of the code here and in the blocks
1735 assert self.ndim >= 2
1736 assert is_list_like(qs) # caller is responsible for this
1737
1738 new_axes = [self.axes[0].view(), Index(qs, dtype=np.float64)]
1739
1740 blocks = [
1741 blk.quantile(qs=qs, interpolation=interpolation) for blk in self.blocks
1742 ]
1743
1744 return type(self)(blocks, new_axes)
1745
1746 # ----------------------------------------------------------------
1747
1748 def unstack(self, unstacker, fill_value) -> BlockManager:
1749 """
1750 Return a BlockManager with all blocks unstacked.
1751
1752 Parameters
1753 ----------
1754 unstacker : reshape._Unstacker
1755 fill_value : Any
1756 fill_value for newly introduced missing values.
1757
1758 Returns
1759 -------
1760 unstacked : BlockManager
1761 """
1762 new_columns = unstacker.get_new_columns(self.items)
1763 new_index = unstacker.new_index
1764
1765 allow_fill = not unstacker.mask_all
1766 if allow_fill:
1767 # calculating the full mask once and passing it to Block._unstack is
1768 # faster than letting calculating it in each repeated call
1769 new_mask2D = (~unstacker.mask).reshape(*unstacker.full_shape)
1770 needs_masking = new_mask2D.any(axis=0)
1771 else:
1772 needs_masking = np.zeros(unstacker.full_shape[1], dtype=bool)
1773
1774 new_blocks: list[Block] = []
1775 columns_mask: list[np.ndarray] = []
1776
1777 if len(self.items) == 0:
1778 factor = 1
1779 else:
1780 fac = len(new_columns) / len(self.items)
1781 assert fac == int(fac)
1782 factor = int(fac)
1783
1784 for blk in self.blocks:
1785 mgr_locs = blk.mgr_locs
1786 new_placement = mgr_locs.tile_for_unstack(factor)
1787
1788 blocks, mask = blk._unstack(
1789 unstacker,
1790 fill_value,
1791 new_placement=new_placement,
1792 needs_masking=needs_masking,
1793 )
1794
1795 new_blocks.extend(blocks)
1796 columns_mask.extend(mask)
1797
1798 # Block._unstack should ensure this holds,
1799 assert mask.sum() == sum(len(nb._mgr_locs) for nb in blocks)
1800 # In turn this ensures that in the BlockManager call below
1801 # we have len(new_columns) == sum(x.shape[0] for x in new_blocks)
1802 # which suffices to allow us to pass verify_inegrity=False
1803
1804 new_columns = new_columns[columns_mask]
1805
1806 bm = BlockManager(new_blocks, [new_columns, new_index], verify_integrity=False)
1807 return bm
1808
1809 def to_iter_dict(self) -> Generator[tuple[str, Self]]:
1810 """
1811 Yield a tuple of (str(dtype), BlockManager)
1812
1813 Returns
1814 -------
1815 values : a tuple of (str(dtype), BlockManager)
1816 """
1817 key = lambda block: str(block.dtype)
1818 for dtype, blocks in itertools.groupby(sorted(self.blocks, key=key), key=key):
1819 # TODO(EA2D): the combine will be unnecessary with 2D EAs
1820 yield dtype, self._combine(list(blocks))
1821
1822 def as_array(
1823 self,
1824 dtype: np.dtype | None = None,
1825 copy: bool = False,
1826 na_value: object = lib.no_default,
1827 ) -> np.ndarray:
1828 """
1829 Convert the blockmanager data into a numpy array.
1830
1831 Parameters
1832 ----------
1833 dtype : np.dtype or None, default None
1834 Data type of the return array.
1835 copy : bool, default False
1836 If True then guarantee that a copy is returned. A value of
1837 False does not guarantee that the underlying data is not
1838 copied.
1839 na_value : object, default lib.no_default
1840 Value to be used as the missing value sentinel.
1841
1842 Returns
1843 -------
1844 arr : ndarray
1845 """
1846 passed_nan = lib.is_float(na_value) and isna(na_value)
1847
1848 if len(self.blocks) == 0:
1849 arr = np.empty(self.shape, dtype=float)
1850 return arr.transpose()
1851
1852 if self.is_single_block:
1853 blk = self.blocks[0]
1854
1855 if na_value is not lib.no_default:
1856 # We want to copy when na_value is provided to avoid
1857 # mutating the original object
1858 if lib.is_np_dtype(blk.dtype, "f") and passed_nan:
1859 # We are already numpy-float and na_value=np.nan
1860 pass
1861 else:
1862 copy = True
1863
1864 if blk.is_extension:
1865 # Avoid implicit conversion of extension blocks to object
1866
1867 # error: Item "ndarray" of "Union[ndarray, ExtensionArray]" has no
1868 # attribute "to_numpy"
1869 arr = blk.values.to_numpy( # type: ignore[union-attr]
1870 dtype=dtype,
1871 na_value=na_value,
1872 copy=copy,
1873 ).reshape(blk.shape)
1874 elif not copy:
1875 arr = np.asarray(blk.values, dtype=dtype)
1876 else:
1877 arr = np.array(blk.values, dtype=dtype, copy=copy)
1878 if passed_nan and blk.dtype.kind in "mM":
1879 arr[isna(blk.values)] = na_value
1880
1881 if not copy:
1882 arr = arr.view()
1883 arr.flags.writeable = False
1884 else:
1885 arr = self._interleave(dtype=dtype, na_value=na_value)
1886 # The underlying data was copied within _interleave, so no need
1887 # to further copy if copy=True or setting na_value
1888
1889 if na_value is lib.no_default:
1890 pass
1891 elif arr.dtype.kind == "f" and passed_nan:
1892 pass
1893 else:
1894 arr[isna(arr)] = na_value
1895
1896 return arr.transpose()
1897
1898 def _interleave(
1899 self,
1900 dtype: np.dtype | None = None,
1901 na_value: object = lib.no_default,
1902 ) -> np.ndarray:
1903 """
1904 Return ndarray from blocks with specified item order
1905 Items must be contained in the blocks
1906 """
1907 if not dtype:
1908 # Incompatible types in assignment (expression has type
1909 # "Optional[Union[dtype[Any], ExtensionDtype]]", variable has
1910 # type "Optional[dtype[Any]]")
1911 dtype = interleaved_dtype( # type: ignore[assignment]
1912 [blk.dtype for blk in self.blocks]
1913 )
1914
1915 # error: Argument 1 to "ensure_np_dtype" has incompatible type
1916 # "Optional[dtype[Any]]"; expected "Union[dtype[Any], ExtensionDtype]"
1917 dtype = ensure_np_dtype(dtype) # type: ignore[arg-type]
1918 result = np.empty(self.shape, dtype=dtype)
1919
1920 itemmask = np.zeros(self.shape[0])
1921
1922 if dtype == np.dtype("object") and na_value is lib.no_default:
1923 # much more performant than using to_numpy below
1924 for blk in self.blocks:
1925 rl = blk.mgr_locs
1926 arr = blk.get_values(dtype)
1927 result[rl.indexer] = arr
1928 itemmask[rl.indexer] = 1
1929 return result
1930
1931 for blk in self.blocks:
1932 rl = blk.mgr_locs
1933 if blk.is_extension:
1934 # Avoid implicit conversion of extension blocks to object
1935
1936 # error: Item "ndarray" of "Union[ndarray, ExtensionArray]" has no
1937 # attribute "to_numpy"
1938 arr = blk.values.to_numpy( # type: ignore[union-attr]
1939 dtype=dtype,
1940 na_value=na_value,
1941 )
1942 else:
1943 arr = blk.get_values(dtype)
1944 result[rl.indexer] = arr
1945 if na_value is not lib.no_default and blk.dtype.kind in "mM":
1946 result[rl.indexer][isna(arr)] = na_value
1947 itemmask[rl.indexer] = 1
1948
1949 if not itemmask.all():
1950 raise AssertionError("Some items were not contained in blocks")
1951
1952 return result
1953
1954 # ----------------------------------------------------------------
1955 # Consolidation
1956
1957 def is_consolidated(self) -> bool:
1958 """
1959 Return True if more than one block with the same dtype
1960 """
1961 if not self._known_consolidated:
1962 self._consolidate_check()
1963 return self._is_consolidated
1964
1965 def _consolidate_check(self) -> None:
1966 if len(self.blocks) == 1:
1967 # fastpath
1968 self._is_consolidated = True
1969 self._known_consolidated = True
1970 return
1971 dtypes = [blk.dtype for blk in self.blocks if blk._can_consolidate]
1972 self._is_consolidated = len(dtypes) == len(set(dtypes))
1973 self._known_consolidated = True
1974
1975 def _consolidate_inplace(self) -> None:
1976 if not self.is_consolidated():
1977 self.blocks = _consolidate(self.blocks)
1978 self._is_consolidated = True
1979 self._known_consolidated = True
1980 self._rebuild_blknos_and_blklocs()
1981
1982 # ----------------------------------------------------------------
1983 # Concatenation
1984
1985 @classmethod
1986 def concat_horizontal(cls, mgrs: list[Self], axes: list[Index]) -> Self:
1987 """
1988 Concatenate uniformly-indexed BlockManagers horizontally.
1989 """
1990 offset = 0
1991 blocks: list[Block] = []
1992 for mgr in mgrs:
1993 for blk in mgr.blocks:
1994 # We need to do getitem_block here otherwise we would be altering
1995 # blk.mgr_locs in place, which would render it invalid. This is only
1996 # relevant in the copy=False case.
1997 nb = blk.slice_block_columns(slice(None))
1998 nb._mgr_locs = nb._mgr_locs.add(offset)
1999 blocks.append(nb)
2000
2001 offset += len(mgr.items)
2002
2003 # TODO relevant axis already shallow-copied at caller?
2004 new_mgr = cls(tuple(blocks), axes)
2005 return new_mgr
2006
2007 @classmethod
2008 def concat_vertical(cls, mgrs: list[Self], axes: list[Index]) -> Self:
2009 """
2010 Concatenate uniformly-indexed BlockManagers vertically.
2011 """
2012 raise NotImplementedError("This logic lives (for now) in internals.concat")
2013
2014
2015class SingleBlockManager(BaseBlockManager):
2016 """manage a single block with"""
2017
2018 @property
2019 def ndim(self) -> Literal[1]:
2020 return 1
2021
2022 _is_consolidated = True
2023 _known_consolidated = True
2024 __slots__ = ()
2025 is_single_block = True
2026
2027 def __init__(
2028 self,
2029 block: Block,
2030 axis: Index,
2031 verify_integrity: bool = False,
2032 ) -> None:
2033 # Assertions disabled for performance
2034 # assert isinstance(block, Block), type(block)
2035 # assert isinstance(axis, Index), type(axis)
2036
2037 self.axes = [axis]
2038 self.blocks = (block,)
2039
2040 @classmethod
2041 def from_blocks(
2042 cls,
2043 blocks: list[Block],
2044 axes: list[Index],
2045 ) -> Self:
2046 """
2047 Constructor for BlockManager and SingleBlockManager with same signature.
2048 """
2049 assert len(blocks) == 1
2050 assert len(axes) == 1
2051 return cls(blocks[0], axes[0], verify_integrity=False)
2052
2053 @classmethod
2054 def from_array(
2055 cls, array: ArrayLike, index: Index, refs: BlockValuesRefs | None = None
2056 ) -> SingleBlockManager:
2057 """
2058 Constructor for if we have an array that is not yet a Block.
2059 """
2060 array = maybe_coerce_values(array)
2061 bp = BlockPlacement(slice(0, len(index)))
2062 block = new_block(array, placement=bp, ndim=1, refs=refs)
2063 return cls(block, index)
2064
2065 def to_2d_mgr(self, columns: Index) -> BlockManager:
2066 """
2067 Manager analogue of Series.to_frame
2068 """
2069 blk = self.blocks[0]
2070 arr = ensure_block_shape(blk.values, ndim=2)
2071 bp = BlockPlacement(0)
2072 new_blk = type(blk)(arr, placement=bp, ndim=2, refs=blk.refs)
2073 axes = [columns, self.axes[0].view()]
2074 return BlockManager([new_blk], axes=axes, verify_integrity=False)
2075
2076 def _has_no_reference(self, i: int = 0) -> bool:
2077 """
2078 Check for column `i` if it has references.
2079 (whether it references another array or is itself being referenced)
2080 Returns True if the column has no references.
2081 """
2082 return not self.blocks[0].refs.has_reference()
2083
2084 def __getstate__(self):
2085 block_values = [b.values for b in self.blocks]
2086 block_items = [self.items[b.mgr_locs.indexer] for b in self.blocks]
2087 axes_array = list(self.axes)
2088
2089 extra_state = {
2090 "0.14.1": {
2091 "axes": axes_array,
2092 "blocks": [
2093 {"values": b.values, "mgr_locs": b.mgr_locs.indexer}
2094 for b in self.blocks
2095 ],
2096 }
2097 }
2098
2099 # First three elements of the state are to maintain forward
2100 # compatibility with 0.13.1.
2101 return axes_array, block_values, block_items, extra_state
2102
2103 def __setstate__(self, state) -> None:
2104 def unpickle_block(values, mgr_locs, ndim: int) -> Block:
2105 # TODO(EA2D): ndim would be unnecessary with 2D EAs
2106 # older pickles may store e.g. DatetimeIndex instead of DatetimeArray
2107 values = extract_array(values, extract_numpy=True)
2108 if not isinstance(mgr_locs, BlockPlacement):
2109 mgr_locs = BlockPlacement(mgr_locs)
2110
2111 values = maybe_coerce_values(values)
2112 return new_block(values, placement=mgr_locs, ndim=ndim)
2113
2114 if isinstance(state, tuple) and len(state) >= 4 and "0.14.1" in state[3]:
2115 state = state[3]["0.14.1"]
2116 self.axes = [ensure_index(ax) for ax in state["axes"]]
2117 ndim = len(self.axes)
2118 self.blocks = tuple(
2119 unpickle_block(b["values"], b["mgr_locs"], ndim=ndim)
2120 for b in state["blocks"]
2121 )
2122 else:
2123 raise NotImplementedError("pre-0.14.1 pickles are no longer supported")
2124
2125 self._post_setstate()
2126
2127 def _post_setstate(self) -> None:
2128 pass
2129
2130 @cache_readonly
2131 def _block(self) -> Block:
2132 return self.blocks[0]
2133
2134 @final
2135 @property
2136 def array(self) -> ArrayLike:
2137 """
2138 Quick access to the backing array of the Block.
2139 """
2140 return self.blocks[0].values
2141
2142 # error: Cannot override writeable attribute with read-only property
2143 @property
2144 def _blknos(self) -> None: # type: ignore[override]
2145 """compat with BlockManager"""
2146 return None
2147
2148 # error: Cannot override writeable attribute with read-only property
2149 @property
2150 def _blklocs(self) -> None: # type: ignore[override]
2151 """compat with BlockManager"""
2152 return None
2153
2154 def get_rows_with_mask(self, indexer: npt.NDArray[np.bool_]) -> Self:
2155 # similar to get_slice, but not restricted to slice indexer
2156 blk = self._block
2157 if len(indexer) > 0 and indexer.all():
2158 return type(self)(blk.copy(deep=False), self.index)
2159 array = blk.values[indexer]
2160
2161 if isinstance(indexer, np.ndarray) and indexer.dtype.kind == "b":
2162 # boolean indexing always gives a copy with numpy
2163 refs = None
2164 else:
2165 # TODO(CoW) in theory only need to track reference if new_array is a view
2166 refs = blk.refs
2167
2168 bp = BlockPlacement(slice(0, len(array)))
2169 block = type(blk)(array, placement=bp, ndim=1, refs=refs)
2170
2171 new_idx = self.index[indexer]
2172 return type(self)(block, new_idx)
2173
2174 def get_slice(self, slobj: slice, axis: AxisInt = 0) -> SingleBlockManager:
2175 # Assertion disabled for performance
2176 # assert isinstance(slobj, slice), type(slobj)
2177 if axis >= self.ndim:
2178 raise IndexError("Requested axis not found in manager")
2179
2180 blk = self._block
2181 array = blk.values[slobj]
2182 bp = BlockPlacement(slice(0, len(array)))
2183 # TODO this method is only used in groupby SeriesSplitter at the moment,
2184 # so passing refs is not yet covered by the tests
2185 block = type(blk)(array, placement=bp, ndim=1, refs=blk.refs)
2186 new_index = self.index._getitem_slice(slobj)
2187 return type(self)(block, new_index)
2188
2189 @property
2190 def index(self) -> Index:
2191 return self.axes[0]
2192
2193 @property
2194 def dtype(self) -> DtypeObj:
2195 return self._block.dtype
2196
2197 def get_dtypes(self) -> npt.NDArray[np.object_]:
2198 return np.array([self._block.dtype], dtype=object)
2199
2200 def external_values(self):
2201 """The array that Series.values returns"""
2202 return self._block.external_values()
2203
2204 def internal_values(self):
2205 """The array that Series._values returns"""
2206 return self._block.values
2207
2208 def array_values(self) -> ExtensionArray:
2209 """The array that Series.array returns"""
2210 return self._block.array_values
2211
2212 def get_numeric_data(self) -> Self:
2213 if self._block.is_numeric:
2214 return self.copy(deep=False)
2215 return self.make_empty()
2216
2217 @property
2218 def _can_hold_na(self) -> bool:
2219 return self._block._can_hold_na
2220
2221 def setitem_inplace(self, indexer, value) -> None:
2222 """
2223 Set values with indexer.
2224
2225 For SingleBlockManager, this backs s[indexer] = value
2226
2227 This is an inplace version of `setitem()`, mutating the manager/values
2228 in place, not returning a new Manager (and Block), and thus never changing
2229 the dtype.
2230 """
2231 if not self._has_no_reference(0):
2232 self.blocks = (self._block.copy(deep=True),)
2233 self._reset_cache()
2234
2235 arr = self.array
2236
2237 # EAs will do this validation in their own __setitem__ methods.
2238 if isinstance(arr, np.ndarray):
2239 # Note: checking for ndarray instead of np.dtype means we exclude
2240 # dt64/td64, which do their own validation.
2241 value = np_can_hold_element(arr.dtype, value)
2242
2243 if isinstance(value, np.ndarray) and value.ndim == 1 and len(value) == 1:
2244 # NumPy 1.25 deprecation: https://github.com/numpy/numpy/pull/10615
2245 value = value[0, ...]
2246
2247 arr[indexer] = value
2248
2249 def idelete(self, indexer) -> SingleBlockManager:
2250 """
2251 Delete single location from SingleBlockManager.
2252
2253 Ensures that self.blocks doesn't become empty.
2254 """
2255 nb = self._block.delete(indexer)[0]
2256 self.blocks = (nb,)
2257 self.axes[0] = self.axes[0].delete(indexer)
2258 self._reset_cache()
2259 return self
2260
2261 def fast_xs(self, loc):
2262 """
2263 fast path for getting a cross-section
2264 return a view of the data
2265 """
2266 raise NotImplementedError("Use series._values[loc] instead")
2267
2268 def set_values(self, values: ArrayLike) -> None:
2269 """
2270 Set the values of the single block in place.
2271
2272 Use at your own risk! This does not check if the passed values are
2273 valid for the current Block/SingleBlockManager (length, dtype, etc),
2274 and this does not properly keep track of references.
2275 """
2276 # NOTE(CoW) Currently this is only used for FrameColumnApply.series_generator
2277 # which handles CoW by setting the refs manually if necessary
2278 self.blocks[0].values = values
2279 self.blocks[0]._mgr_locs = BlockPlacement(slice(len(values)))
2280
2281 def _equal_values(self, other: Self) -> bool:
2282 """
2283 Used in .equals defined in base class. Only check the column values
2284 assuming shape and indexes have already been checked.
2285 """
2286 # For SingleBlockManager (i.e.Series)
2287 if other.ndim != 1:
2288 return False
2289 left = self.blocks[0].values
2290 right = other.blocks[0].values
2291 return array_equals(left, right)
2292
2293 def grouped_reduce(self, func):
2294 arr = self.array
2295 res = func(arr)
2296 index = default_index(len(res))
2297
2298 mgr = type(self).from_array(res, index)
2299 return mgr
2300
2301
2302# --------------------------------------------------------------------
2303# Constructor Helpers
2304
2305
2306def create_block_manager_from_blocks(
2307 blocks: list[Block],
2308 axes: list[Index],
2309 consolidate: bool = True,
2310 verify_integrity: bool = True,
2311) -> BlockManager:
2312 # If verify_integrity=False, then caller is responsible for checking
2313 # all(x.shape[-1] == len(axes[1]) for x in blocks)
2314 # sum(x.shape[0] for x in blocks) == len(axes[0])
2315 # set(x for blk in blocks for x in blk.mgr_locs) == set(range(len(axes[0])))
2316 # all(blk.ndim == 2 for blk in blocks)
2317 # This allows us to safely pass verify_integrity=False
2318
2319 try:
2320 mgr = BlockManager(blocks, axes, verify_integrity=verify_integrity)
2321
2322 except ValueError as err:
2323 arrays = [blk.values for blk in blocks]
2324 tot_items = sum(arr.shape[0] for arr in arrays)
2325 raise_construction_error(tot_items, arrays[0].shape[1:], axes, err)
2326
2327 if consolidate:
2328 mgr._consolidate_inplace()
2329 return mgr
2330
2331
2332def create_block_manager_from_column_arrays(
2333 arrays: list[ArrayLike],
2334 axes: list[Index],
2335 consolidate: bool,
2336 refs: list,
2337) -> BlockManager:
2338 # Assertions disabled for performance (caller is responsible for verifying)
2339 # assert isinstance(axes, list)
2340 # assert all(isinstance(x, Index) for x in axes)
2341 # assert all(isinstance(x, (np.ndarray, ExtensionArray)) for x in arrays)
2342 # assert all(type(x) is not NumpyExtensionArray for x in arrays)
2343 # assert all(x.ndim == 1 for x in arrays)
2344 # assert all(len(x) == len(axes[1]) for x in arrays)
2345 # assert len(arrays) == len(axes[0])
2346 # These last three are sufficient to allow us to safely pass
2347 # verify_integrity=False below.
2348
2349 try:
2350 blocks = _form_blocks(arrays, consolidate, refs)
2351 mgr = BlockManager(blocks, axes, verify_integrity=False)
2352 except ValueError as e:
2353 raise_construction_error(len(arrays), arrays[0].shape, axes, e)
2354 if consolidate:
2355 mgr._consolidate_inplace()
2356 return mgr
2357
2358
2359def raise_construction_error(
2360 tot_items: int,
2361 block_shape: Shape,
2362 axes: list[Index],
2363 e: ValueError | None = None,
2364) -> NoReturn:
2365 """raise a helpful message about our construction"""
2366 passed = tuple(map(int, [tot_items, *block_shape]))
2367 # Correcting the user facing error message during dataframe construction
2368 if len(passed) <= 2:
2369 passed = passed[::-1]
2370
2371 implied = tuple(len(ax) for ax in axes)
2372 # Correcting the user facing error message during dataframe construction
2373 if len(implied) <= 2:
2374 implied = implied[::-1]
2375
2376 # We return the exception object instead of raising it so that we
2377 # can raise it in the caller; mypy plays better with that
2378 if passed == implied and e is not None:
2379 raise e
2380 if block_shape[0] == 0:
2381 raise ValueError("Empty data passed with indices specified.")
2382 raise ValueError(f"Shape of passed values is {passed}, indices imply {implied}")
2383
2384
2385# -----------------------------------------------------------------------
2386
2387
2388def _grouping_func(tup: tuple[int, ArrayLike]) -> tuple[int, DtypeObj]:
2389 dtype = tup[1].dtype
2390
2391 if is_1d_only_ea_dtype(dtype):
2392 # We know these won't be consolidated, so don't need to group these.
2393 # This avoids expensive comparisons of CategoricalDtype objects
2394 sep = id(dtype)
2395 else:
2396 sep = 0
2397
2398 return sep, dtype
2399
2400
2401def _form_blocks(arrays: list[ArrayLike], consolidate: bool, refs: list) -> list[Block]:
2402 tuples = enumerate(arrays)
2403
2404 if not consolidate:
2405 return _tuples_to_blocks_no_consolidate(tuples, refs)
2406
2407 # when consolidating, we can ignore refs (either stacking always copies,
2408 # or the EA is already copied in the calling dict_to_mgr)
2409
2410 # group by dtype
2411 grouper = itertools.groupby(tuples, _grouping_func)
2412
2413 nbs: list[Block] = []
2414 for (_, dtype), tup_block in grouper:
2415 block_type = get_block_type(dtype)
2416
2417 if isinstance(dtype, np.dtype):
2418 is_dtlike = dtype.kind in "mM"
2419
2420 if issubclass(dtype.type, (str, bytes)):
2421 dtype = np.dtype(object)
2422
2423 values, placement = _stack_arrays(tup_block, dtype)
2424 if is_dtlike:
2425 values = ensure_wrapped_if_datetimelike(values)
2426 blk = block_type(values, placement=BlockPlacement(placement), ndim=2)
2427 nbs.append(blk)
2428
2429 elif is_1d_only_ea_dtype(dtype):
2430 dtype_blocks = [
2431 block_type(x[1], placement=BlockPlacement(x[0]), ndim=2)
2432 for x in tup_block
2433 ]
2434 nbs.extend(dtype_blocks)
2435
2436 else:
2437 dtype_blocks = [
2438 block_type(
2439 ensure_block_shape(x[1], 2), placement=BlockPlacement(x[0]), ndim=2
2440 )
2441 for x in tup_block
2442 ]
2443 nbs.extend(dtype_blocks)
2444 return nbs
2445
2446
2447def _tuples_to_blocks_no_consolidate(tuples, refs) -> list[Block]:
2448 # tuples produced within _form_blocks are of the form (placement, array)
2449 return [
2450 new_block_2d(
2451 ensure_block_shape(arr, ndim=2), placement=BlockPlacement(i), refs=ref
2452 )
2453 for ((i, arr), ref) in zip(tuples, refs, strict=True)
2454 ]
2455
2456
2457def _stack_arrays(tuples, dtype: np.dtype):
2458 placement, arrays = zip(*tuples, strict=True)
2459
2460 first = arrays[0]
2461 shape = (len(arrays), *first.shape)
2462
2463 stacked = np.empty(shape, dtype=dtype)
2464 for i, arr in enumerate(arrays):
2465 stacked[i] = arr
2466
2467 return stacked, placement
2468
2469
2470def _consolidate(blocks: tuple[Block, ...]) -> tuple[Block, ...]:
2471 """
2472 Merge blocks having same dtype, exclude non-consolidating blocks
2473 """
2474 # sort by _can_consolidate, dtype
2475 gkey = lambda x: x._consolidate_key
2476 grouper = itertools.groupby(sorted(blocks, key=gkey), gkey)
2477
2478 new_blocks: list[Block] = []
2479 for (_can_consolidate, dtype), group_blocks in grouper:
2480 merged_blocks, _ = _merge_blocks(
2481 list(group_blocks), dtype=dtype, can_consolidate=_can_consolidate
2482 )
2483 new_blocks = extend_blocks(merged_blocks, new_blocks)
2484 return tuple(new_blocks)
2485
2486
2487def _merge_blocks(
2488 blocks: list[Block], dtype: DtypeObj, can_consolidate: bool
2489) -> tuple[list[Block], bool]:
2490 if len(blocks) == 1:
2491 return blocks, False
2492
2493 if can_consolidate:
2494 # TODO: optimization potential in case all mgrs contain slices and
2495 # combination of those slices is a slice, too.
2496 new_mgr_locs = np.concatenate([b.mgr_locs.as_array for b in blocks])
2497
2498 new_values: ArrayLike
2499
2500 if isinstance(blocks[0].dtype, np.dtype):
2501 # error: List comprehension has incompatible type List[Union[ndarray,
2502 # ExtensionArray]]; expected List[Union[complex, generic,
2503 # Sequence[Union[int, float, complex, str, bytes, generic]],
2504 # Sequence[Sequence[Any]], SupportsArray]]
2505 new_values = np.vstack([b.values for b in blocks]) # type: ignore[misc]
2506 else:
2507 bvals = [blk.values for blk in blocks]
2508 bvals2 = cast(Sequence[NDArrayBackedExtensionArray], bvals)
2509 new_values = bvals2[0]._concat_same_type(bvals2, axis=0)
2510
2511 argsort = np.argsort(new_mgr_locs)
2512 new_values = new_values[argsort]
2513 new_mgr_locs = new_mgr_locs[argsort]
2514
2515 bp = BlockPlacement(new_mgr_locs)
2516 return [new_block_2d(new_values, placement=bp)], True
2517
2518 # can't consolidate --> no merge
2519 return blocks, False
2520
2521
2522def _preprocess_slice_or_indexer(
2523 slice_or_indexer: slice | np.ndarray, length: int, allow_fill: bool
2524):
2525 if isinstance(slice_or_indexer, slice):
2526 return (
2527 "slice",
2528 slice_or_indexer,
2529 libinternals.slice_len(slice_or_indexer, length),
2530 )
2531 else:
2532 if (
2533 not isinstance(slice_or_indexer, np.ndarray)
2534 or slice_or_indexer.dtype.kind != "i"
2535 ):
2536 dtype = getattr(slice_or_indexer, "dtype", None)
2537 raise TypeError(type(slice_or_indexer), dtype)
2538
2539 indexer = ensure_platform_int(slice_or_indexer)
2540 if not allow_fill:
2541 indexer = maybe_convert_indices(indexer, length)
2542 return "fancy", indexer, len(indexer)
2543
2544
2545def make_na_array(dtype: DtypeObj, shape: Shape, fill_value) -> ArrayLike:
2546 if isinstance(dtype, DatetimeTZDtype):
2547 # NB: exclude e.g. pyarrow[dt64tz] dtypes
2548 ts = Timestamp(fill_value).as_unit(dtype.unit)
2549 i8values = np.full(shape, ts._value)
2550 dt64values = i8values.view(f"M8[{dtype.unit}]")
2551 return DatetimeArray._simple_new(dt64values, dtype=dtype)
2552
2553 elif is_1d_only_ea_dtype(dtype):
2554 dtype = cast(ExtensionDtype, dtype)
2555 cls = dtype.construct_array_type()
2556
2557 missing_arr = cls._from_sequence([], dtype=dtype)
2558 ncols, nrows = shape
2559 assert ncols == 1, ncols
2560 empty_arr = -1 * np.ones((nrows,), dtype=np.intp)
2561 return missing_arr.take(empty_arr, allow_fill=True, fill_value=fill_value)
2562 elif isinstance(dtype, ExtensionDtype):
2563 # TODO: no tests get here, a handful would if we disabled
2564 # the dt64tz special-case above (which is faster)
2565 cls = dtype.construct_array_type()
2566 missing_arr = cls._empty(shape=shape, dtype=dtype)
2567 missing_arr[:] = fill_value
2568 return missing_arr
2569 else:
2570 # NB: we should never get here with dtype integer or bool;
2571 # if we did, the missing_arr.fill would cast to gibberish
2572 missing_arr_np = np.empty(shape, dtype=dtype)
2573 missing_arr_np.fill(fill_value)
2574
2575 if dtype.kind in "mM":
2576 missing_arr_np = ensure_wrapped_if_datetimelike(missing_arr_np)
2577 return missing_arr_np