Coverage for /var/srv/projects/api.amasfac.comuna18.com/tmp/venv/lib/python3.9/site-packages/pandas/core/ops/__init__.py: 21%

181 statements  

« prev     ^ index     » next       coverage.py v6.4.4, created at 2023-07-17 14:22 -0600

1""" 

2Arithmetic operations for PandasObjects 

3 

4This is not a public API. 

5""" 

6from __future__ import annotations 

7 

8import operator 

9from typing import TYPE_CHECKING 

10import warnings 

11 

12import numpy as np 

13 

14from pandas._libs.ops_dispatch import maybe_dispatch_ufunc_to_dunder_op 

15from pandas._typing import Level 

16from pandas.util._decorators import Appender 

17from pandas.util._exceptions import find_stack_level 

18 

19from pandas.core.dtypes.common import ( 

20 is_array_like, 

21 is_list_like, 

22) 

23from pandas.core.dtypes.generic import ( 

24 ABCDataFrame, 

25 ABCSeries, 

26) 

27from pandas.core.dtypes.missing import isna 

28 

29from pandas.core import ( 

30 algorithms, 

31 roperator, 

32) 

33from pandas.core.ops.array_ops import ( 

34 arithmetic_op, 

35 comp_method_OBJECT_ARRAY, 

36 comparison_op, 

37 get_array_op, 

38 logical_op, 

39 maybe_prepare_scalar_for_op, 

40) 

41from pandas.core.ops.common import ( 

42 get_op_result_name, 

43 unpack_zerodim_and_defer, 

44) 

45from pandas.core.ops.docstrings import ( 

46 _flex_comp_doc_FRAME, 

47 _op_descriptions, 

48 make_flex_doc, 

49) 

50from pandas.core.ops.invalid import invalid_comparison 

51from pandas.core.ops.mask_ops import ( 

52 kleene_and, 

53 kleene_or, 

54 kleene_xor, 

55) 

56from pandas.core.ops.methods import add_flex_arithmetic_methods 

57from pandas.core.roperator import ( 

58 radd, 

59 rand_, 

60 rdiv, 

61 rdivmod, 

62 rfloordiv, 

63 rmod, 

64 rmul, 

65 ror_, 

66 rpow, 

67 rsub, 

68 rtruediv, 

69 rxor, 

70) 

71 

72if TYPE_CHECKING: 72 ↛ 73line 72 didn't jump to line 73, because the condition on line 72 was never true

73 from pandas import ( 

74 DataFrame, 

75 Series, 

76 ) 

77 

78# ----------------------------------------------------------------------------- 

79# constants 

80ARITHMETIC_BINOPS: set[str] = { 

81 "add", 

82 "sub", 

83 "mul", 

84 "pow", 

85 "mod", 

86 "floordiv", 

87 "truediv", 

88 "divmod", 

89 "radd", 

90 "rsub", 

91 "rmul", 

92 "rpow", 

93 "rmod", 

94 "rfloordiv", 

95 "rtruediv", 

96 "rdivmod", 

97} 

98 

99 

100COMPARISON_BINOPS: set[str] = {"eq", "ne", "lt", "gt", "le", "ge"} 

101 

102 

103# ----------------------------------------------------------------------------- 

104# Masking NA values and fallbacks for operations numpy does not support 

105 

106 

107def fill_binop(left, right, fill_value): 

108 """ 

109 If a non-None fill_value is given, replace null entries in left and right 

110 with this value, but only in positions where _one_ of left/right is null, 

111 not both. 

112 

113 Parameters 

114 ---------- 

115 left : array-like 

116 right : array-like 

117 fill_value : object 

118 

119 Returns 

120 ------- 

121 left : array-like 

122 right : array-like 

123 

124 Notes 

125 ----- 

126 Makes copies if fill_value is not None and NAs are present. 

127 """ 

128 if fill_value is not None: 

129 left_mask = isna(left) 

130 right_mask = isna(right) 

131 

132 # one but not both 

133 mask = left_mask ^ right_mask 

134 

135 if left_mask.any(): 

136 # Avoid making a copy if we can 

137 left = left.copy() 

138 left[left_mask & mask] = fill_value 

139 

140 if right_mask.any(): 

141 # Avoid making a copy if we can 

142 right = right.copy() 

143 right[right_mask & mask] = fill_value 

144 

145 return left, right 

146 

147 

148# ----------------------------------------------------------------------------- 

149# Series 

150 

151 

152def align_method_SERIES(left: Series, right, align_asobject: bool = False): 

153 """align lhs and rhs Series""" 

154 # ToDo: Different from align_method_FRAME, list, tuple and ndarray 

155 # are not coerced here 

156 # because Series has inconsistencies described in #13637 

157 

158 if isinstance(right, ABCSeries): 

159 # avoid repeated alignment 

160 if not left.index.equals(right.index): 

161 

162 if align_asobject: 

163 # to keep original value's dtype for bool ops 

164 left = left.astype(object) 

165 right = right.astype(object) 

166 

167 left, right = left.align(right, copy=False) 

168 

169 return left, right 

170 

171 

172def flex_method_SERIES(op): 

173 name = op.__name__.strip("_") 

174 doc = make_flex_doc(name, "series") 

175 

176 @Appender(doc) 

177 def flex_wrapper(self, other, level=None, fill_value=None, axis=0): 

178 # validate axis 

179 if axis is not None: 

180 self._get_axis_number(axis) 

181 

182 res_name = get_op_result_name(self, other) 

183 

184 if isinstance(other, ABCSeries): 

185 return self._binop(other, op, level=level, fill_value=fill_value) 

186 elif isinstance(other, (np.ndarray, list, tuple)): 

187 if len(other) != len(self): 

188 raise ValueError("Lengths must be equal") 

189 other = self._constructor(other, self.index) 

190 result = self._binop(other, op, level=level, fill_value=fill_value) 

191 result.name = res_name 

192 return result 

193 else: 

194 if fill_value is not None: 

195 self = self.fillna(fill_value) 

196 

197 return op(self, other) 

198 

199 flex_wrapper.__name__ = name 

200 return flex_wrapper 

201 

202 

203# ----------------------------------------------------------------------------- 

204# DataFrame 

205 

206 

207def align_method_FRAME( 

208 left, right, axis, flex: bool | None = False, level: Level = None 

209): 

210 """ 

211 Convert rhs to meet lhs dims if input is list, tuple or np.ndarray. 

212 

213 Parameters 

214 ---------- 

215 left : DataFrame 

216 right : Any 

217 axis : int, str, or None 

218 flex : bool or None, default False 

219 Whether this is a flex op, in which case we reindex. 

220 None indicates not to check for alignment. 

221 level : int or level name, default None 

222 

223 Returns 

224 ------- 

225 left : DataFrame 

226 right : Any 

227 """ 

228 

229 def to_series(right): 

230 msg = "Unable to coerce to Series, length must be {req_len}: given {given_len}" 

231 if axis is not None and left._get_axis_name(axis) == "index": 

232 if len(left.index) != len(right): 

233 raise ValueError( 

234 msg.format(req_len=len(left.index), given_len=len(right)) 

235 ) 

236 right = left._constructor_sliced(right, index=left.index) 

237 else: 

238 if len(left.columns) != len(right): 

239 raise ValueError( 

240 msg.format(req_len=len(left.columns), given_len=len(right)) 

241 ) 

242 right = left._constructor_sliced(right, index=left.columns) 

243 return right 

244 

245 if isinstance(right, np.ndarray): 

246 

247 if right.ndim == 1: 

248 right = to_series(right) 

249 

250 elif right.ndim == 2: 

251 if right.shape == left.shape: 

252 right = left._constructor(right, index=left.index, columns=left.columns) 

253 

254 elif right.shape[0] == left.shape[0] and right.shape[1] == 1: 

255 # Broadcast across columns 

256 right = np.broadcast_to(right, left.shape) 

257 right = left._constructor(right, index=left.index, columns=left.columns) 

258 

259 elif right.shape[1] == left.shape[1] and right.shape[0] == 1: 

260 # Broadcast along rows 

261 right = to_series(right[0, :]) 

262 

263 else: 

264 raise ValueError( 

265 "Unable to coerce to DataFrame, shape " 

266 f"must be {left.shape}: given {right.shape}" 

267 ) 

268 

269 elif right.ndim > 2: 

270 raise ValueError( 

271 "Unable to coerce to Series/DataFrame, " 

272 f"dimension must be <= 2: {right.shape}" 

273 ) 

274 

275 elif is_list_like(right) and not isinstance(right, (ABCSeries, ABCDataFrame)): 

276 # GH 36702. Raise when attempting arithmetic with list of array-like. 

277 if any(is_array_like(el) for el in right): 

278 raise ValueError( 

279 f"Unable to coerce list of {type(right[0])} to Series/DataFrame" 

280 ) 

281 # GH17901 

282 right = to_series(right) 

283 

284 if flex is not None and isinstance(right, ABCDataFrame): 

285 if not left._indexed_same(right): 

286 if flex: 

287 left, right = left.align(right, join="outer", level=level, copy=False) 

288 else: 

289 raise ValueError( 

290 "Can only compare identically-labeled DataFrame objects" 

291 ) 

292 elif isinstance(right, ABCSeries): 

293 # axis=1 is default for DataFrame-with-Series op 

294 axis = left._get_axis_number(axis) if axis is not None else 1 

295 

296 if not flex: 

297 if not left.axes[axis].equals(right.index): 

298 warnings.warn( 

299 "Automatic reindexing on DataFrame vs Series comparisons " 

300 "is deprecated and will raise ValueError in a future version. " 

301 "Do `left, right = left.align(right, axis=1, copy=False)` " 

302 "before e.g. `left == right`", 

303 FutureWarning, 

304 stacklevel=find_stack_level(), 

305 ) 

306 

307 left, right = left.align( 

308 right, join="outer", axis=axis, level=level, copy=False 

309 ) 

310 right = _maybe_align_series_as_frame(left, right, axis) 

311 

312 return left, right 

313 

314 

315def should_reindex_frame_op( 

316 left: DataFrame, right, op, axis, default_axis, fill_value, level 

317) -> bool: 

318 """ 

319 Check if this is an operation between DataFrames that will need to reindex. 

320 """ 

321 assert isinstance(left, ABCDataFrame) 

322 

323 if op is operator.pow or op is roperator.rpow: 

324 # GH#32685 pow has special semantics for operating with null values 

325 return False 

326 

327 if not isinstance(right, ABCDataFrame): 

328 return False 

329 

330 if fill_value is None and level is None and axis is default_axis: 

331 # TODO: any other cases we should handle here? 

332 

333 # Intersection is always unique so we have to check the unique columns 

334 left_uniques = left.columns.unique() 

335 right_uniques = right.columns.unique() 

336 cols = left_uniques.intersection(right_uniques) 

337 if len(cols) and not ( 

338 len(cols) == len(left_uniques) and len(cols) == len(right_uniques) 

339 ): 

340 # TODO: is there a shortcut available when len(cols) == 0? 

341 return True 

342 

343 return False 

344 

345 

346def frame_arith_method_with_reindex(left: DataFrame, right: DataFrame, op) -> DataFrame: 

347 """ 

348 For DataFrame-with-DataFrame operations that require reindexing, 

349 operate only on shared columns, then reindex. 

350 

351 Parameters 

352 ---------- 

353 left : DataFrame 

354 right : DataFrame 

355 op : binary operator 

356 

357 Returns 

358 ------- 

359 DataFrame 

360 """ 

361 # GH#31623, only operate on shared columns 

362 cols, lcols, rcols = left.columns.join( 

363 right.columns, how="inner", level=None, return_indexers=True 

364 ) 

365 

366 new_left = left.iloc[:, lcols] 

367 new_right = right.iloc[:, rcols] 

368 result = op(new_left, new_right) 

369 

370 # Do the join on the columns instead of using align_method_FRAME 

371 # to avoid constructing two potentially large/sparse DataFrames 

372 join_columns, _, _ = left.columns.join( 

373 right.columns, how="outer", level=None, return_indexers=True 

374 ) 

375 

376 if result.columns.has_duplicates: 

377 # Avoid reindexing with a duplicate axis. 

378 # https://github.com/pandas-dev/pandas/issues/35194 

379 indexer, _ = result.columns.get_indexer_non_unique(join_columns) 

380 indexer = algorithms.unique1d(indexer) 

381 result = result._reindex_with_indexers( 

382 {1: [join_columns, indexer]}, allow_dups=True 

383 ) 

384 else: 

385 result = result.reindex(join_columns, axis=1) 

386 

387 return result 

388 

389 

390def _maybe_align_series_as_frame(frame: DataFrame, series: Series, axis: int): 

391 """ 

392 If the Series operand is not EA-dtype, we can broadcast to 2D and operate 

393 blockwise. 

394 """ 

395 rvalues = series._values 

396 if not isinstance(rvalues, np.ndarray): 

397 # TODO(EA2D): no need to special-case with 2D EAs 

398 if rvalues.dtype == "datetime64[ns]" or rvalues.dtype == "timedelta64[ns]": 

399 # We can losslessly+cheaply cast to ndarray 

400 rvalues = np.asarray(rvalues) 

401 else: 

402 return series 

403 

404 if axis == 0: 

405 rvalues = rvalues.reshape(-1, 1) 

406 else: 

407 rvalues = rvalues.reshape(1, -1) 

408 

409 rvalues = np.broadcast_to(rvalues, frame.shape) 

410 return type(frame)(rvalues, index=frame.index, columns=frame.columns) 

411 

412 

413def flex_arith_method_FRAME(op): 

414 op_name = op.__name__.strip("_") 

415 default_axis = "columns" 

416 

417 na_op = get_array_op(op) 

418 doc = make_flex_doc(op_name, "dataframe") 

419 

420 @Appender(doc) 

421 def f(self, other, axis=default_axis, level=None, fill_value=None): 

422 

423 if should_reindex_frame_op( 

424 self, other, op, axis, default_axis, fill_value, level 

425 ): 

426 return frame_arith_method_with_reindex(self, other, op) 

427 

428 if isinstance(other, ABCSeries) and fill_value is not None: 

429 # TODO: We could allow this in cases where we end up going 

430 # through the DataFrame path 

431 raise NotImplementedError(f"fill_value {fill_value} not supported.") 

432 

433 axis = self._get_axis_number(axis) if axis is not None else 1 

434 

435 other = maybe_prepare_scalar_for_op(other, self.shape) 

436 self, other = align_method_FRAME(self, other, axis, flex=True, level=level) 

437 

438 if isinstance(other, ABCDataFrame): 

439 # Another DataFrame 

440 new_data = self._combine_frame(other, na_op, fill_value) 

441 

442 elif isinstance(other, ABCSeries): 

443 new_data = self._dispatch_frame_op(other, op, axis=axis) 

444 else: 

445 # in this case we always have `np.ndim(other) == 0` 

446 if fill_value is not None: 

447 self = self.fillna(fill_value) 

448 

449 new_data = self._dispatch_frame_op(other, op) 

450 

451 return self._construct_result(new_data) 

452 

453 f.__name__ = op_name 

454 

455 return f 

456 

457 

458def flex_comp_method_FRAME(op): 

459 op_name = op.__name__.strip("_") 

460 default_axis = "columns" # because we are "flex" 

461 

462 doc = _flex_comp_doc_FRAME.format( 

463 op_name=op_name, desc=_op_descriptions[op_name]["desc"] 

464 ) 

465 

466 @Appender(doc) 

467 def f(self, other, axis=default_axis, level=None): 

468 axis = self._get_axis_number(axis) if axis is not None else 1 

469 

470 self, other = align_method_FRAME(self, other, axis, flex=True, level=level) 

471 

472 new_data = self._dispatch_frame_op(other, op, axis=axis) 

473 return self._construct_result(new_data) 

474 

475 f.__name__ = op_name 

476 

477 return f 

478 

479 

480__all__ = [ 

481 "add_flex_arithmetic_methods", 

482 "align_method_FRAME", 

483 "align_method_SERIES", 

484 "ARITHMETIC_BINOPS", 

485 "arithmetic_op", 

486 "COMPARISON_BINOPS", 

487 "comparison_op", 

488 "comp_method_OBJECT_ARRAY", 

489 "fill_binop", 

490 "flex_arith_method_FRAME", 

491 "flex_comp_method_FRAME", 

492 "flex_method_SERIES", 

493 "frame_arith_method_with_reindex", 

494 "invalid_comparison", 

495 "kleene_and", 

496 "kleene_or", 

497 "kleene_xor", 

498 "logical_op", 

499 "maybe_dispatch_ufunc_to_dunder_op", 

500 "radd", 

501 "rand_", 

502 "rdiv", 

503 "rdivmod", 

504 "rfloordiv", 

505 "rmod", 

506 "rmul", 

507 "ror_", 

508 "rpow", 

509 "rsub", 

510 "rtruediv", 

511 "rxor", 

512 "should_reindex_frame_op", 

513 "unpack_zerodim_and_defer", 

514]