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

Shortcuts on this page

r m x   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

198 statements  

1from __future__ import annotations 

2 

3from typing import ( 

4 TYPE_CHECKING, 

5 cast, 

6) 

7 

8import numpy as np 

9 

10from pandas._libs import ( 

11 NaT, 

12 algos as libalgos, 

13 internals as libinternals, 

14 lib, 

15) 

16from pandas._libs.missing import NA 

17from pandas.util._decorators import cache_readonly 

18 

19from pandas.core.dtypes.cast import ( 

20 ensure_dtype_can_hold_na, 

21 find_common_type, 

22) 

23from pandas.core.dtypes.common import ( 

24 is_1d_only_ea_dtype, 

25 needs_i8_conversion, 

26) 

27from pandas.core.dtypes.concat import concat_compat 

28from pandas.core.dtypes.dtypes import ExtensionDtype 

29from pandas.core.dtypes.missing import is_valid_na_for_dtype 

30 

31from pandas.core.construction import ensure_wrapped_if_datetimelike 

32from pandas.core.internals.blocks import ( 

33 ensure_block_shape, 

34 new_block_2d, 

35) 

36from pandas.core.internals.managers import ( 

37 BlockManager, 

38 make_na_array, 

39) 

40 

41if TYPE_CHECKING: 

42 from collections.abc import ( 

43 Generator, 

44 Sequence, 

45 ) 

46 

47 from pandas._typing import ( 

48 ArrayLike, 

49 AxisInt, 

50 DtypeObj, 

51 Shape, 

52 ) 

53 

54 from pandas import Index 

55 from pandas.core.internals.blocks import ( 

56 Block, 

57 BlockPlacement, 

58 ) 

59 

60 

61def concatenate_managers( 

62 mgrs_indexers, axes: list[Index], concat_axis: AxisInt, copy: bool 

63) -> BlockManager: 

64 """ 

65 Concatenate block managers into one. 

66 

67 Parameters 

68 ---------- 

69 mgrs_indexers : list of (BlockManager, {axis: indexer,...}) tuples 

70 axes : list of Index 

71 concat_axis : int 

72 copy : bool 

73 

74 Returns 

75 ------- 

76 BlockManager 

77 """ 

78 

79 needs_copy = copy and concat_axis == 0 

80 

81 # Assertions disabled for performance 

82 # for tup in mgrs_indexers: 

83 # # caller is responsible for ensuring this 

84 # indexers = tup[1] 

85 # assert concat_axis not in indexers 

86 

87 if concat_axis == 0: 

88 mgrs = _maybe_reindex_columns_na_proxy(axes, mgrs_indexers, needs_copy) 

89 return mgrs[0].concat_horizontal(mgrs, axes) 

90 

91 if len(mgrs_indexers) > 0 and mgrs_indexers[0][0].nblocks > 0: 

92 first_dtype = mgrs_indexers[0][0].blocks[0].dtype 

93 if first_dtype in [np.float64, np.float32]: 

94 # TODO: support more dtypes here. This will be simpler once 

95 # JoinUnit.is_na behavior is deprecated. 

96 # (update 2024-04-13 that deprecation has been enforced) 

97 if ( 

98 all(_is_homogeneous_mgr(mgr, first_dtype) for mgr, _ in mgrs_indexers) 

99 and len(mgrs_indexers) > 1 

100 ): 

101 # Fastpath! 

102 # Length restriction is just to avoid having to worry about 'copy' 

103 shape = tuple(len(x) for x in axes) 

104 nb = _concat_homogeneous_fastpath(mgrs_indexers, shape, first_dtype) 

105 return BlockManager((nb,), axes) 

106 

107 mgrs = _maybe_reindex_columns_na_proxy(axes, mgrs_indexers, needs_copy) 

108 

109 if len(mgrs) == 1: 

110 mgr = mgrs[0] 

111 out = mgr.copy(deep=False) 

112 out.axes = axes 

113 return out 

114 

115 blocks = [] 

116 values: ArrayLike 

117 

118 for placement, join_units in _get_combined_plan(mgrs): 

119 unit = join_units[0] 

120 blk = unit.block 

121 

122 if _is_uniform_join_units(join_units): 

123 vals = [ju.block.values for ju in join_units] 

124 

125 if not blk.is_extension: 

126 # _is_uniform_join_units ensures a single dtype, so 

127 # we can use np.concatenate, which is more performant 

128 # than concat_compat 

129 # error: Argument 1 to "concatenate" has incompatible type 

130 # "List[Union[ndarray[Any, Any], ExtensionArray]]"; 

131 # expected "Union[_SupportsArray[dtype[Any]], 

132 # _NestedSequence[_SupportsArray[dtype[Any]]]]" 

133 values = np.concatenate(vals, axis=1) # type: ignore[arg-type] 

134 elif is_1d_only_ea_dtype(blk.dtype): 

135 # TODO(EA2D): special-casing not needed with 2D EAs 

136 values = concat_compat(vals, axis=0, ea_compat_axis=True) 

137 values = ensure_block_shape(values, ndim=2) 

138 else: 

139 values = concat_compat(vals, axis=1) 

140 

141 values = ensure_wrapped_if_datetimelike(values) 

142 

143 fastpath = blk.values.dtype == values.dtype 

144 else: 

145 values = _concatenate_join_units(join_units, copy=copy) 

146 fastpath = False 

147 

148 if fastpath: 

149 b = blk.make_block_same_class(values, placement=placement) 

150 else: 

151 b = new_block_2d(values, placement=placement) 

152 

153 blocks.append(b) 

154 

155 return BlockManager(tuple(blocks), axes) 

156 

157 

158def _maybe_reindex_columns_na_proxy( 

159 axes: list[Index], 

160 mgrs_indexers: list[tuple[BlockManager, dict[int, np.ndarray]]], 

161 needs_copy: bool, 

162) -> list[BlockManager]: 

163 """ 

164 Reindex along columns so that all of the BlockManagers being concatenated 

165 have matching columns. 

166 

167 Columns added in this reindexing have dtype=np.void, indicating they 

168 should be ignored when choosing a column's final dtype. 

169 """ 

170 new_mgrs = [] 

171 

172 for mgr, indexers in mgrs_indexers: 

173 # For axis=0 (i.e. columns) we use_na_proxy and only_slice, so this 

174 # is a cheap reindexing. 

175 for i, indexer in indexers.items(): 

176 mgr = mgr.reindex_indexer( 

177 axes[i], 

178 indexer, 

179 axis=i, 

180 only_slice=True, # only relevant for i==0 

181 allow_dups=True, 

182 use_na_proxy=True, # only relevant for i==0 

183 ) 

184 if needs_copy and not indexers: 

185 mgr = mgr.copy(deep=True) 

186 

187 new_mgrs.append(mgr) 

188 return new_mgrs 

189 

190 

191def _is_homogeneous_mgr(mgr: BlockManager, first_dtype: DtypeObj) -> bool: 

192 """ 

193 Check if this Manager can be treated as a single ndarray. 

194 """ 

195 if mgr.nblocks != 1: 

196 return False 

197 blk = mgr.blocks[0] 

198 if not (blk.mgr_locs.is_slice_like and blk.mgr_locs.as_slice.step == 1): 

199 return False 

200 

201 return blk.dtype == first_dtype 

202 

203 

204def _concat_homogeneous_fastpath( 

205 mgrs_indexers, shape: Shape, first_dtype: np.dtype 

206) -> Block: 

207 """ 

208 With single-Block managers with homogeneous dtypes (that can already hold nan), 

209 we avoid [...] 

210 """ 

211 # assumes 

212 # all(_is_homogeneous_mgr(mgr, first_dtype) for mgr, _ in in mgrs_indexers) 

213 

214 if all(not indexers for _, indexers in mgrs_indexers): 

215 # https://github.com/pandas-dev/pandas/pull/52685#issuecomment-1523287739 

216 arrs = [mgr.blocks[0].values.T for mgr, _ in mgrs_indexers] 

217 arr = np.concatenate(arrs).T 

218 bp = libinternals.BlockPlacement(slice(shape[0])) 

219 nb = new_block_2d(arr, bp) 

220 return nb 

221 

222 arr = np.empty(shape, dtype=first_dtype) 

223 

224 if first_dtype == np.float64: 

225 take_func = libalgos.take_2d_axis0_float64_float64 

226 else: 

227 take_func = libalgos.take_2d_axis0_float32_float32 

228 

229 start = 0 

230 for mgr, indexers in mgrs_indexers: 

231 mgr_len = mgr.shape[1] 

232 end = start + mgr_len 

233 

234 if 0 in indexers: 

235 take_func( 

236 mgr.blocks[0].values, 

237 indexers[0], 

238 arr[:, start:end], 

239 ) 

240 else: 

241 # No reindexing necessary, we can copy values directly 

242 arr[:, start:end] = mgr.blocks[0].values 

243 

244 start += mgr_len 

245 

246 bp = libinternals.BlockPlacement(slice(shape[0])) 

247 nb = new_block_2d(arr, bp) 

248 return nb 

249 

250 

251def _get_combined_plan( 

252 mgrs: list[BlockManager], 

253) -> Generator[tuple[BlockPlacement, list[JoinUnit]]]: 

254 max_len = mgrs[0].shape[0] 

255 

256 blknos_list = [mgr.blknos for mgr in mgrs] 

257 pairs = libinternals.get_concat_blkno_indexers(blknos_list) 

258 for blknos, bp in pairs: 

259 # assert bp.is_slice_like 

260 # assert len(bp) > 0 

261 

262 units_for_bp = [] 

263 for k, mgr in enumerate(mgrs): 

264 blkno = blknos[k] 

265 

266 nb = _get_block_for_concat_plan(mgr, bp, blkno, max_len=max_len) 

267 unit = JoinUnit(nb) 

268 units_for_bp.append(unit) 

269 

270 yield bp, units_for_bp 

271 

272 

273def _get_block_for_concat_plan( 

274 mgr: BlockManager, bp: BlockPlacement, blkno: int, *, max_len: int 

275) -> Block: 

276 blk = mgr.blocks[blkno] 

277 # Assertions disabled for performance: 

278 # assert bp.is_slice_like 

279 # assert blkno != -1 

280 # assert (mgr.blknos[bp] == blkno).all() 

281 

282 if len(bp) == len(blk.mgr_locs) and ( 

283 blk.mgr_locs.is_slice_like and blk.mgr_locs.as_slice.step == 1 

284 ): 

285 nb = blk 

286 else: 

287 ax0_blk_indexer = mgr.blklocs[bp.indexer] 

288 

289 slc = lib.maybe_indices_to_slice(ax0_blk_indexer, max_len) 

290 # TODO: in all extant test cases 2023-04-08 we have a slice here. 

291 # Will this always be the case? 

292 if isinstance(slc, slice): 

293 nb = blk.slice_block_columns(slc) 

294 else: 

295 nb = blk.take_block_columns(slc) 

296 

297 # assert nb.shape == (len(bp), mgr.shape[1]) 

298 return nb 

299 

300 

301class JoinUnit: 

302 def __init__(self, block: Block) -> None: 

303 self.block = block 

304 

305 def __repr__(self) -> str: 

306 return f"{type(self).__name__}({self.block!r})" 

307 

308 def _is_valid_na_for(self, dtype: DtypeObj) -> bool: 

309 """ 

310 Check that we are all-NA of a type/dtype that is compatible with this dtype. 

311 Augments `self.is_na` with an additional check of the type of NA values. 

312 """ 

313 if not self.is_na: 

314 return False 

315 

316 blk = self.block 

317 if blk.dtype.kind == "V": 

318 return True 

319 

320 if blk.dtype == object: 

321 values = blk.values 

322 return all(is_valid_na_for_dtype(x, dtype) for x in values.ravel(order="K")) 

323 

324 na_value = blk.fill_value 

325 if na_value is NaT and blk.dtype != dtype: 

326 # e.g. we are dt64 and other is td64 

327 # fill_values match but we should not cast blk.values to dtype 

328 # TODO: this will need updating if we ever have non-nano dt64/td64 

329 return False 

330 

331 if na_value is NA and needs_i8_conversion(dtype): 

332 # FIXME: kludge; test_append_empty_frame_with_timedelta64ns_nat 

333 # e.g. blk.dtype == "Int64" and dtype is td64, we dont want 

334 # to consider these as matching 

335 return False 

336 

337 # TODO: better to use can_hold_element? 

338 return is_valid_na_for_dtype(na_value, dtype) 

339 

340 @cache_readonly 

341 def is_na(self) -> bool: 

342 blk = self.block 

343 if blk.dtype.kind == "V": 

344 return True 

345 return False 

346 

347 def get_reindexed_values(self, empty_dtype: DtypeObj, upcasted_na) -> ArrayLike: 

348 values: ArrayLike 

349 

350 if upcasted_na is None and self.block.dtype.kind != "V": 

351 # No upcasting is necessary 

352 return self.block.values 

353 else: 

354 fill_value = upcasted_na 

355 

356 if self._is_valid_na_for(empty_dtype): 

357 # note: always holds when self.block.dtype.kind == "V" 

358 blk_dtype = self.block.dtype 

359 

360 if blk_dtype == np.dtype("object"): 

361 # we want to avoid filling with np.nan if we are 

362 # using None; we already know that we are all 

363 # nulls 

364 values = cast(np.ndarray, self.block.values) 

365 if values.size and values[0, 0] is None: 

366 fill_value = None 

367 

368 return make_na_array(empty_dtype, self.block.shape, fill_value) 

369 

370 return self.block.values 

371 

372 

373def _concatenate_join_units(join_units: list[JoinUnit], copy: bool) -> ArrayLike: 

374 """ 

375 Concatenate values from several join units along axis=1. 

376 """ 

377 empty_dtype = _get_empty_dtype(join_units) 

378 

379 has_none_blocks = any(unit.block.dtype.kind == "V" for unit in join_units) 

380 upcasted_na = _dtype_to_na_value(empty_dtype, has_none_blocks) 

381 

382 to_concat = [ 

383 ju.get_reindexed_values(empty_dtype=empty_dtype, upcasted_na=upcasted_na) 

384 for ju in join_units 

385 ] 

386 

387 if any(is_1d_only_ea_dtype(t.dtype) for t in to_concat): 

388 # TODO(EA2D): special case not needed if all EAs used HybridBlocks 

389 

390 # error: No overload variant of "__getitem__" of "ExtensionArray" matches 

391 # argument type "Tuple[int, slice]" 

392 to_concat = [ 

393 t if is_1d_only_ea_dtype(t.dtype) else t[0, :] # type: ignore[call-overload] 

394 for t in to_concat 

395 ] 

396 concat_values = concat_compat(to_concat, axis=0, ea_compat_axis=True) 

397 concat_values = ensure_block_shape(concat_values, 2) 

398 

399 else: 

400 concat_values = concat_compat(to_concat, axis=1) 

401 

402 return concat_values 

403 

404 

405def _dtype_to_na_value(dtype: DtypeObj, has_none_blocks: bool): 

406 """ 

407 Find the NA value to go with this dtype. 

408 """ 

409 if isinstance(dtype, ExtensionDtype): 

410 return dtype.na_value 

411 elif dtype.kind in "mM": 

412 return dtype.type("NaT", np.datetime_data(dtype)[0]) 

413 elif dtype.kind in "fc": 

414 return dtype.type("NaN") 

415 elif dtype.kind == "b": 

416 # different from missing.na_value_for_dtype 

417 return None 

418 elif dtype.kind in "iu": 

419 if not has_none_blocks: 

420 # different from missing.na_value_for_dtype 

421 return None 

422 return np.nan 

423 elif dtype.kind == "O": 

424 return np.nan 

425 raise NotImplementedError 

426 

427 

428def _get_empty_dtype(join_units: Sequence[JoinUnit]) -> DtypeObj: 

429 """ 

430 Return dtype and N/A values to use when concatenating specified units. 

431 

432 Returned N/A value may be None which means there was no casting involved. 

433 

434 Returns 

435 ------- 

436 dtype 

437 """ 

438 if lib.dtypes_all_equal([ju.block.dtype for ju in join_units]): 

439 empty_dtype = join_units[0].block.dtype 

440 return empty_dtype 

441 

442 has_none_blocks = any(unit.block.dtype.kind == "V" for unit in join_units) 

443 

444 dtypes = [unit.block.dtype for unit in join_units if not unit.is_na] 

445 

446 dtype = find_common_type(dtypes) 

447 if has_none_blocks: 

448 dtype = ensure_dtype_can_hold_na(dtype) 

449 

450 return dtype 

451 

452 

453def _is_uniform_join_units(join_units: list[JoinUnit]) -> bool: 

454 """ 

455 Check if the join units consist of blocks of uniform type that can 

456 be concatenated using Block.concat_same_type instead of the generic 

457 _concatenate_join_units (which uses `concat_compat`). 

458 

459 """ 

460 first = join_units[0].block 

461 if first.dtype.kind == "V": 

462 return False 

463 return ( 

464 # exclude cases where a) ju.block is None or b) we have e.g. Int64+int64 

465 all(type(ju.block) is type(first) for ju in join_units) 

466 and 

467 # e.g. DatetimeLikeBlock can be dt64 or td64, but these are not uniform 

468 all( 

469 ju.block.dtype == first.dtype 

470 # GH#42092 we only want the dtype_equal check for non-numeric blocks 

471 # (for now, may change but that would need a deprecation) 

472 or ju.block.dtype.kind in "iub" 

473 for ju in join_units 

474 ) 

475 and 

476 # no blocks that would get missing values (can lead to type upcasts) 

477 # unless we're an extension dtype. 

478 all(not ju.is_na or ju.block.is_extension for ju in join_units) 

479 )