Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.11/site-packages/pandas/io/formats/format.py: 17%

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

838 statements  

1""" 

2Internal module for formatting output data in csv, html, xml, 

3and latex files. This module also applies to display formatting. 

4""" 

5 

6from __future__ import annotations 

7 

8from collections.abc import ( 

9 Callable, 

10 Generator, 

11 Hashable, 

12 Mapping, 

13 Sequence, 

14) 

15from contextlib import contextmanager 

16from csv import QUOTE_NONE 

17from decimal import Decimal 

18from functools import partial 

19from io import StringIO 

20import math 

21import re 

22from shutil import get_terminal_size 

23from typing import ( 

24 TYPE_CHECKING, 

25 Any, 

26 Final, 

27 cast, 

28) 

29 

30import numpy as np 

31 

32from pandas._config.config import ( 

33 get_option, 

34 set_option, 

35) 

36 

37from pandas._libs import lib 

38from pandas._libs.missing import NA 

39from pandas._libs.tslibs import ( 

40 NaT, 

41 Timedelta, 

42 Timestamp, 

43) 

44from pandas._libs.tslibs.nattype import NaTType 

45from pandas.util._decorators import set_module 

46 

47from pandas.core.dtypes.common import ( 

48 is_complex_dtype, 

49 is_float, 

50 is_integer, 

51 is_list_like, 

52 is_numeric_dtype, 

53 is_scalar, 

54) 

55from pandas.core.dtypes.dtypes import ( 

56 CategoricalDtype, 

57 DatetimeTZDtype, 

58 ExtensionDtype, 

59) 

60from pandas.core.dtypes.missing import ( 

61 isna, 

62 notna, 

63) 

64 

65from pandas.core.arrays import ( 

66 Categorical, 

67 DatetimeArray, 

68 ExtensionArray, 

69 TimedeltaArray, 

70) 

71from pandas.core.base import PandasObject 

72import pandas.core.common as com 

73from pandas.core.indexes.api import ( 

74 Index, 

75 MultiIndex, 

76 PeriodIndex, 

77 ensure_index, 

78) 

79from pandas.core.indexes.datetimes import DatetimeIndex 

80from pandas.core.indexes.timedeltas import TimedeltaIndex 

81 

82from pandas.io.common import ( 

83 check_parent_directory, 

84 stringify_path, 

85) 

86from pandas.io.formats import printing 

87 

88if TYPE_CHECKING: 

89 from pandas._typing import ( 

90 ArrayLike, 

91 Axes, 

92 ColspaceArgType, 

93 ColspaceType, 

94 CompressionOptions, 

95 FilePath, 

96 FloatFormatType, 

97 FormattersType, 

98 IndexLabel, 

99 SequenceNotStr, 

100 StorageOptions, 

101 WriteBuffer, 

102 ) 

103 

104 from pandas import ( 

105 DataFrame, 

106 Series, 

107 ) 

108 

109 

110common_docstring: Final = """ 

111 Parameters 

112 ---------- 

113 buf : str, Path or StringIO-like, optional, default None 

114 Buffer to write to. If None, the output is returned as a string. 

115 columns : array-like, optional, default None 

116 The subset of columns to write. Writes all columns by default. 

117 col_space : %(col_space_type)s, optional 

118 %(col_space)s 

119 header : %(header_type)s, optional 

120 %(header)s. 

121 index : bool, optional, default True 

122 Whether to print index (row) labels. 

123 na_rep : str, optional, default 'NaN' 

124 String representation of ``NaN`` to use. 

125 formatters : list, tuple or dict of one-param. functions, optional 

126 Formatter functions to apply to columns' elements by position or 

127 name. 

128 The result of each function must be a unicode string. 

129 List/tuple must be of length equal to the number of columns. 

130 float_format : one-parameter function, optional, default None 

131 Formatter function to apply to columns' elements if they are 

132 floats. This function must return a unicode string and will be 

133 applied only to the non-``NaN`` elements, with ``NaN`` being 

134 handled by ``na_rep``. 

135 sparsify : bool, optional, default True 

136 Set to False for a DataFrame with a hierarchical index to print 

137 every multiindex key at each row. 

138 index_names : bool, optional, default True 

139 Prints the names of the indexes. 

140 justify : str, default None 

141 How to justify the column labels. If None uses the option from 

142 the print configuration (controlled by set_option), 'right' out 

143 of the box. Valid values are 

144 

145 * left 

146 * right 

147 * center 

148 * justify 

149 * justify-all 

150 * start 

151 * end 

152 * inherit 

153 * match-parent 

154 * initial 

155 * unset. 

156 max_rows : int, optional 

157 Maximum number of rows to display in the console. 

158 max_cols : int, optional 

159 Maximum number of columns to display in the console. 

160 show_dimensions : bool, default False 

161 Display DataFrame dimensions (number of rows by number of columns). 

162 decimal : str, default '.' 

163 Character recognized as decimal separator, e.g. ',' in Europe. 

164 """ 

165 

166VALID_JUSTIFY_PARAMETERS = ( 

167 "left", 

168 "right", 

169 "center", 

170 "justify", 

171 "justify-all", 

172 "start", 

173 "end", 

174 "inherit", 

175 "match-parent", 

176 "initial", 

177 "unset", 

178) 

179 

180return_docstring: Final = """ 

181 Returns 

182 ------- 

183 str or None 

184 If buf is None, returns the result as a string. Otherwise returns 

185 None. 

186 """ 

187 

188 

189class SeriesFormatter: 

190 """ 

191 Implement the main logic of Series.to_string, which underlies 

192 Series.__repr__. 

193 """ 

194 

195 def __init__( 

196 self, 

197 series: Series, 

198 *, 

199 length: bool | str = True, 

200 header: bool = True, 

201 index: bool = True, 

202 na_rep: str = "NaN", 

203 name: bool = False, 

204 float_format: str | None = None, 

205 dtype: bool = True, 

206 max_rows: int | None = None, 

207 min_rows: int | None = None, 

208 ) -> None: 

209 self.series = series 

210 self.buf = StringIO() 

211 self.name = name 

212 self.na_rep = na_rep 

213 self.header = header 

214 self.length = length 

215 self.index = index 

216 self.max_rows = max_rows 

217 self.min_rows = min_rows 

218 

219 if float_format is None: 

220 float_format = get_option("display.float_format") 

221 self.float_format = float_format 

222 self.dtype = dtype 

223 self.adj = printing.get_adjustment() 

224 

225 self._chk_truncate() 

226 

227 def _chk_truncate(self) -> None: 

228 self.tr_row_num: int | None 

229 

230 min_rows = self.min_rows 

231 max_rows = self.max_rows 

232 # truncation determined by max_rows, actual truncated number of rows 

233 # used below by min_rows 

234 is_truncated_vertically = max_rows and (len(self.series) > max_rows) 

235 series = self.series 

236 if is_truncated_vertically: 

237 max_rows = cast(int, max_rows) 

238 if min_rows: 

239 # if min_rows is set (not None or 0), set max_rows to minimum 

240 # of both 

241 max_rows = min(min_rows, max_rows) 

242 if max_rows == 1: 

243 row_num = max_rows 

244 series = series.iloc[:max_rows] 

245 else: 

246 row_num = max_rows // 2 

247 _len = len(series) 

248 _slice = np.hstack( 

249 [np.arange(row_num), np.arange(_len - row_num, _len)] 

250 ) 

251 series = series.iloc[_slice] 

252 self.tr_row_num = row_num 

253 else: 

254 self.tr_row_num = None 

255 self.tr_series = series 

256 self.is_truncated_vertically = is_truncated_vertically 

257 

258 def _get_footer(self) -> str: 

259 name = self.series.name 

260 footer = "" 

261 

262 index = self.series.index 

263 if ( 

264 isinstance(index, (DatetimeIndex, PeriodIndex, TimedeltaIndex)) 

265 and index.freq is not None 

266 ): 

267 footer += f"Freq: {index.freqstr}" 

268 

269 if self.name is not False and name is not None: 

270 if footer: 

271 footer += ", " 

272 

273 series_name = printing.pprint_thing(name, escape_chars=("\t", "\r", "\n")) 

274 footer += f"Name: {series_name}" 

275 

276 if self.length is True or ( 

277 self.length == "truncate" and self.is_truncated_vertically 

278 ): 

279 if footer: 

280 footer += ", " 

281 footer += f"Length: {len(self.series)}" 

282 

283 if self.dtype is not False and self.dtype is not None: 

284 dtype_name = getattr(self.tr_series.dtype, "name", None) 

285 if dtype_name: 

286 if footer: 

287 footer += ", " 

288 footer += f"dtype: {printing.pprint_thing(dtype_name)}" 

289 

290 # level infos are added to the end and in a new line, like it is done 

291 # for Categoricals 

292 if isinstance(self.tr_series.dtype, CategoricalDtype): 

293 level_info = self.tr_series._values._get_repr_footer() 

294 if footer: 

295 footer += "\n" 

296 footer += level_info 

297 

298 return str(footer) 

299 

300 def _get_formatted_values(self) -> list[str]: 

301 return format_array( 

302 self.tr_series._values, 

303 None, 

304 float_format=self.float_format, 

305 na_rep=self.na_rep, 

306 leading_space=self.index, 

307 ) 

308 

309 def to_string(self) -> str: 

310 series = self.tr_series 

311 footer = self._get_footer() 

312 

313 if len(series) == 0: 

314 return f"{type(self.series).__name__}([], {footer})" 

315 

316 index = series.index 

317 have_header = _has_names(index) 

318 if isinstance(index, MultiIndex): 

319 fmt_index = index._format_multi(include_names=True, sparsify=None) 

320 adj = printing.get_adjustment() 

321 fmt_index = adj.adjoin(2, *fmt_index).split("\n") 

322 else: 

323 fmt_index = index._format_flat(include_name=True) 

324 fmt_values = self._get_formatted_values() 

325 

326 if self.is_truncated_vertically: 

327 n_header_rows = 0 

328 row_num = self.tr_row_num 

329 row_num = cast(int, row_num) 

330 width = self.adj.len(fmt_values[row_num - 1]) 

331 if width > 3: 

332 dot_str = "..." 

333 else: 

334 dot_str = ".." 

335 # Series uses mode=center because it has single value columns 

336 # DataFrame uses mode=left 

337 dot_str = self.adj.justify([dot_str], width, mode="center")[0] 

338 fmt_values.insert(row_num + n_header_rows, dot_str) 

339 fmt_index.insert(row_num + 1, "") 

340 

341 if self.index: 

342 result = self.adj.adjoin(3, *[fmt_index[1:], fmt_values]) 

343 else: 

344 result = self.adj.adjoin(3, fmt_values) 

345 

346 if self.header and have_header: 

347 result = fmt_index[0] + "\n" + result 

348 

349 if footer: 

350 result += "\n" + footer 

351 

352 return str("".join(result)) 

353 

354 

355def get_dataframe_repr_params() -> dict[str, Any]: 

356 """Get the parameters used to repr(dataFrame) calls using DataFrame.to_string. 

357 

358 Supplying these parameters to DataFrame.to_string is equivalent to calling 

359 ``repr(DataFrame)``. This is useful if you want to adjust the repr output. 

360 

361 Example 

362 ------- 

363 >>> import pandas as pd 

364 >>> 

365 >>> df = pd.DataFrame([[1, 2], [3, 4]]) 

366 >>> repr_params = pd.io.formats.format.get_dataframe_repr_params() 

367 >>> repr(df) == df.to_string(**repr_params) 

368 True 

369 """ 

370 from pandas.io.formats import console 

371 

372 if get_option("display.expand_frame_repr"): 

373 line_width, _ = console.get_console_size() 

374 else: 

375 line_width = None 

376 return { 

377 "max_rows": get_option("display.max_rows"), 

378 "min_rows": get_option("display.min_rows"), 

379 "max_cols": get_option("display.max_columns"), 

380 "max_colwidth": get_option("display.max_colwidth"), 

381 "show_dimensions": get_option("display.show_dimensions"), 

382 "line_width": line_width, 

383 } 

384 

385 

386def get_series_repr_params() -> dict[str, Any]: 

387 """Get the parameters used to repr(Series) calls using Series.to_string. 

388 

389 Supplying these parameters to Series.to_string is equivalent to calling 

390 ``repr(series)``. This is useful if you want to adjust the series repr output. 

391 

392 Example 

393 ------- 

394 >>> import pandas as pd 

395 >>> 

396 >>> ser = pd.Series([1, 2, 3, 4]) 

397 >>> repr_params = pd.io.formats.format.get_series_repr_params() 

398 >>> repr(ser) == ser.to_string(**repr_params) 

399 True 

400 """ 

401 width, height = get_terminal_size() 

402 max_rows_opt = get_option("display.max_rows") 

403 max_rows = height if max_rows_opt == 0 else max_rows_opt 

404 min_rows = height if max_rows_opt == 0 else get_option("display.min_rows") 

405 

406 return { 

407 "name": True, 

408 "dtype": True, 

409 "min_rows": min_rows, 

410 "max_rows": max_rows, 

411 "length": get_option("display.show_dimensions"), 

412 } 

413 

414 

415class DataFrameFormatter: 

416 """ 

417 Class for processing dataframe formatting options and data. 

418 

419 Used by DataFrame.to_string, which backs DataFrame.__repr__. 

420 """ 

421 

422 __doc__ = __doc__ if __doc__ else "" 

423 __doc__ += common_docstring + return_docstring 

424 

425 def __init__( 

426 self, 

427 frame: DataFrame, 

428 columns: Axes | None = None, 

429 col_space: ColspaceArgType | None = None, 

430 header: bool | SequenceNotStr[str] = True, 

431 index: bool = True, 

432 na_rep: str = "NaN", 

433 formatters: FormattersType | None = None, 

434 justify: str | None = None, 

435 float_format: FloatFormatType | None = None, 

436 sparsify: bool | None = None, 

437 index_names: bool = True, 

438 max_rows: int | None = None, 

439 min_rows: int | None = None, 

440 max_cols: int | None = None, 

441 show_dimensions: bool | str = False, 

442 decimal: str = ".", 

443 bold_rows: bool = False, 

444 escape: bool = True, 

445 ) -> None: 

446 self.frame = frame 

447 self.columns = self._initialize_columns(columns) 

448 self.col_space = self._initialize_colspace(col_space) 

449 self.header = header 

450 self.index = index 

451 self.na_rep = na_rep 

452 self.formatters = self._initialize_formatters(formatters) 

453 self.justify = self._initialize_justify(justify) 

454 self.float_format = self._validate_float_format(float_format) 

455 self.sparsify = self._initialize_sparsify(sparsify) 

456 self.show_index_names = index_names 

457 self.decimal = decimal 

458 self.bold_rows = bold_rows 

459 self.escape = escape 

460 self.max_rows = max_rows 

461 self.min_rows = min_rows 

462 self.max_cols = max_cols 

463 self.show_dimensions = show_dimensions 

464 

465 self.max_cols_fitted = self._calc_max_cols_fitted() 

466 self.max_rows_fitted = self._calc_max_rows_fitted() 

467 

468 self.tr_frame = self.frame 

469 self.truncate() 

470 self.adj = printing.get_adjustment() 

471 

472 def get_strcols(self) -> list[list[str]]: 

473 """ 

474 Render a DataFrame to a list of columns (as lists of strings). 

475 """ 

476 strcols = self._get_strcols_without_index() 

477 

478 if self.index: 

479 str_index = self._get_formatted_index(self.tr_frame) 

480 strcols.insert(0, str_index) 

481 

482 return strcols 

483 

484 @property 

485 def should_show_dimensions(self) -> bool: 

486 return self.show_dimensions is True or ( 

487 self.show_dimensions == "truncate" and self.is_truncated 

488 ) 

489 

490 @property 

491 def is_truncated(self) -> bool: 

492 return bool(self.is_truncated_horizontally or self.is_truncated_vertically) 

493 

494 @property 

495 def is_truncated_horizontally(self) -> bool: 

496 return bool(self.max_cols_fitted and (len(self.columns) > self.max_cols_fitted)) 

497 

498 @property 

499 def is_truncated_vertically(self) -> bool: 

500 return bool(self.max_rows_fitted and (len(self.frame) > self.max_rows_fitted)) 

501 

502 @property 

503 def dimensions_info(self) -> str: 

504 return f"\n\n[{len(self.frame)} rows x {len(self.frame.columns)} columns]" 

505 

506 @property 

507 def has_index_names(self) -> bool: 

508 return _has_names(self.frame.index) 

509 

510 @property 

511 def has_column_names(self) -> bool: 

512 return _has_names(self.frame.columns) 

513 

514 @property 

515 def show_row_idx_names(self) -> bool: 

516 return all((self.has_index_names, self.index, self.show_index_names)) 

517 

518 @property 

519 def show_col_idx_names(self) -> bool: 

520 return all((self.has_column_names, self.show_index_names, self.header)) 

521 

522 @property 

523 def max_rows_displayed(self) -> int: 

524 return min(self.max_rows or len(self.frame), len(self.frame)) 

525 

526 def _initialize_sparsify(self, sparsify: bool | None) -> bool: 

527 if sparsify is None: 

528 return get_option("display.multi_sparse") 

529 return sparsify 

530 

531 def _initialize_formatters( 

532 self, formatters: FormattersType | None 

533 ) -> FormattersType: 

534 if formatters is None: 

535 return {} 

536 elif len(self.frame.columns) == len(formatters) or isinstance(formatters, dict): 

537 return formatters 

538 else: 

539 raise ValueError( 

540 f"Formatters length({len(formatters)}) should match " 

541 f"DataFrame number of columns({len(self.frame.columns)})" 

542 ) 

543 

544 def _initialize_justify(self, justify: str | None) -> str: 

545 if justify is None: 

546 return get_option("display.colheader_justify") 

547 else: 

548 return justify 

549 

550 def _initialize_columns(self, columns: Axes | None) -> Index: 

551 if columns is not None: 

552 cols = ensure_index(columns) 

553 self.frame = self.frame[cols] 

554 return cols 

555 else: 

556 return self.frame.columns 

557 

558 def _initialize_colspace(self, col_space: ColspaceArgType | None) -> ColspaceType: 

559 result: ColspaceType 

560 

561 if col_space is None: 

562 result = {} 

563 elif isinstance(col_space, (int, str)): 

564 result = {"": col_space} 

565 result.update(dict.fromkeys(self.frame.columns, col_space)) 

566 elif isinstance(col_space, Mapping): 

567 for column in col_space.keys(): 

568 if column not in self.frame.columns and column != "": 

569 raise ValueError( 

570 f"Col_space is defined for an unknown column: {column}" 

571 ) 

572 result = col_space 

573 else: 

574 if len(self.frame.columns) != len(col_space): 

575 raise ValueError( 

576 f"Col_space length({len(col_space)}) should match " 

577 f"DataFrame number of columns({len(self.frame.columns)})" 

578 ) 

579 result = dict(zip(self.frame.columns, col_space, strict=True)) 

580 return result 

581 

582 def _calc_max_cols_fitted(self) -> int | None: 

583 """Number of columns fitting the screen.""" 

584 if not self._is_in_terminal(): 

585 return self.max_cols 

586 

587 width, _ = get_terminal_size() 

588 if self._is_screen_narrow(width): 

589 return width 

590 else: 

591 return self.max_cols 

592 

593 def _calc_max_rows_fitted(self) -> int | None: 

594 """Number of rows with data fitting the screen.""" 

595 max_rows: int | None 

596 

597 if self._is_in_terminal(): 

598 _, height = get_terminal_size() 

599 if self.max_rows == 0: 

600 # rows available to fill with actual data 

601 return height - self._get_number_of_auxiliary_rows() 

602 

603 if self._is_screen_short(height): 

604 max_rows = height 

605 else: 

606 max_rows = self.max_rows 

607 else: 

608 max_rows = self.max_rows 

609 

610 return self._adjust_max_rows(max_rows) 

611 

612 def _adjust_max_rows(self, max_rows: int | None) -> int | None: 

613 """Adjust max_rows using display logic. 

614 

615 See description here: 

616 https://pandas.pydata.org/docs/dev/user_guide/options.html#frequently-used-options 

617 

618 GH #37359 

619 """ 

620 if max_rows: 

621 if (len(self.frame) > max_rows) and self.min_rows: 

622 # if truncated, set max_rows showed to min_rows 

623 max_rows = min(self.min_rows, max_rows) 

624 return max_rows 

625 

626 def _is_in_terminal(self) -> bool: 

627 """Check if the output is to be shown in terminal.""" 

628 return bool(self.max_cols == 0 or self.max_rows == 0) 

629 

630 def _is_screen_narrow(self, max_width) -> bool: 

631 return bool(self.max_cols == 0 and len(self.frame.columns) > max_width) 

632 

633 def _is_screen_short(self, max_height) -> bool: 

634 return bool(self.max_rows == 0 and len(self.frame) > max_height) 

635 

636 def _get_number_of_auxiliary_rows(self) -> int: 

637 """Get number of rows occupied by prompt, dots and dimension info.""" 

638 dot_row = 1 

639 prompt_row = 1 

640 num_rows = dot_row + prompt_row 

641 

642 if self.show_dimensions: 

643 num_rows += len(self.dimensions_info.splitlines()) 

644 

645 if self.header: 

646 num_rows += 1 

647 

648 return num_rows 

649 

650 def truncate(self) -> None: 

651 """ 

652 Check whether the frame should be truncated. If so, slice the frame up. 

653 """ 

654 if self.is_truncated_horizontally: 

655 self._truncate_horizontally() 

656 

657 if self.is_truncated_vertically: 

658 self._truncate_vertically() 

659 

660 def _truncate_horizontally(self) -> None: 

661 """Remove columns, which are not to be displayed and adjust formatters. 

662 

663 Attributes affected: 

664 - tr_frame 

665 - formatters 

666 - tr_col_num 

667 """ 

668 assert self.max_cols_fitted is not None 

669 col_num = self.max_cols_fitted // 2 

670 if col_num >= 1: 

671 _len = len(self.tr_frame.columns) 

672 _slice = np.hstack([np.arange(col_num), np.arange(_len - col_num, _len)]) 

673 self.tr_frame = self.tr_frame.iloc[:, _slice] 

674 

675 # truncate formatter 

676 if isinstance(self.formatters, (list, tuple)): 

677 self.formatters = [ 

678 *self.formatters[:col_num], 

679 *self.formatters[-col_num:], 

680 ] 

681 else: 

682 col_num = cast(int, self.max_cols) 

683 self.tr_frame = self.tr_frame.iloc[:, :col_num] 

684 self.tr_col_num: int = col_num 

685 

686 def _truncate_vertically(self) -> None: 

687 """Remove rows, which are not to be displayed. 

688 

689 Attributes affected: 

690 - tr_frame 

691 - tr_row_num 

692 """ 

693 assert self.max_rows_fitted is not None 

694 row_num = self.max_rows_fitted // 2 

695 if row_num >= 1: 

696 _len = len(self.tr_frame) 

697 _slice = np.hstack([np.arange(row_num), np.arange(_len - row_num, _len)]) 

698 self.tr_frame = self.tr_frame.iloc[_slice] 

699 else: 

700 row_num = cast(int, self.max_rows) 

701 self.tr_frame = self.tr_frame.iloc[:row_num, :] 

702 self.tr_row_num = row_num 

703 

704 def _get_strcols_without_index(self) -> list[list[str]]: 

705 strcols: list[list[str]] = [] 

706 

707 if not is_list_like(self.header) and not self.header: 

708 for i, c in enumerate(self.tr_frame): 

709 fmt_values = self.format_col(i) 

710 fmt_values = _make_fixed_width( 

711 strings=fmt_values, 

712 justify=self.justify, 

713 minimum=int(self.col_space.get(c, 0)), 

714 adj=self.adj, 

715 ) 

716 strcols.append(fmt_values) 

717 return strcols 

718 

719 if is_list_like(self.header): 

720 # cast here since can't be bool if is_list_like 

721 self.header = cast(list[str], self.header) 

722 if len(self.header) != len(self.columns): 

723 raise ValueError( 

724 f"Writing {len(self.columns)} cols " 

725 f"but got {len(self.header)} aliases" 

726 ) 

727 str_columns = [[label] for label in self.header] 

728 else: 

729 str_columns = self._get_formatted_column_labels(self.tr_frame) 

730 

731 if self.show_row_idx_names: 

732 for x in str_columns: 

733 x.append("") 

734 

735 for i, c in enumerate(self.tr_frame): 

736 cheader = str_columns[i] 

737 header_colwidth = max( 

738 int(self.col_space.get(c, 0)), *(self.adj.len(x) for x in cheader) 

739 ) 

740 fmt_values = self.format_col(i) 

741 fmt_values = _make_fixed_width( 

742 fmt_values, self.justify, minimum=header_colwidth, adj=self.adj 

743 ) 

744 

745 max_len = max(*(self.adj.len(x) for x in fmt_values), header_colwidth) 

746 cheader = self.adj.justify(cheader, max_len, mode=self.justify) 

747 strcols.append(cheader + fmt_values) 

748 

749 return strcols 

750 

751 def format_col(self, i: int) -> list[str]: 

752 frame = self.tr_frame 

753 formatter = self._get_formatter(i) 

754 return format_array( 

755 frame.iloc[:, i]._values, 

756 formatter, 

757 float_format=self.float_format, 

758 na_rep=self.na_rep, 

759 space=self.col_space.get(frame.columns[i]), 

760 decimal=self.decimal, 

761 leading_space=self.index, 

762 ) 

763 

764 def _get_formatter(self, i: str | int) -> Callable | None: 

765 if isinstance(self.formatters, (list, tuple)): 

766 if is_integer(i): 

767 i = cast(int, i) 

768 return self.formatters[i] 

769 else: 

770 return None 

771 else: 

772 if is_integer(i) and i not in self.columns: 

773 i = self.columns[i] 

774 return self.formatters.get(i, None) 

775 

776 def _get_formatted_column_labels(self, frame: DataFrame) -> list[list[str]]: 

777 from pandas.core.indexes.multi import sparsify_labels 

778 

779 columns = frame.columns 

780 

781 if isinstance(columns, MultiIndex): 

782 fmt_columns = columns._format_multi(sparsify=False, include_names=False) 

783 if self.sparsify and len(fmt_columns): 

784 fmt_columns = sparsify_labels(fmt_columns) 

785 

786 str_columns = [list(x) for x in zip(*fmt_columns, strict=True)] 

787 else: 

788 fmt_columns = columns._format_flat(include_name=False) 

789 str_columns = [ 

790 [ 

791 " " + x 

792 if not self._get_formatter(i) and is_numeric_dtype(dtype) 

793 else x 

794 ] 

795 for i, (x, dtype) in enumerate( 

796 zip(fmt_columns, self.frame.dtypes, strict=False) 

797 ) 

798 ] 

799 return str_columns 

800 

801 def _get_formatted_index(self, frame: DataFrame) -> list[str]: 

802 # Note: this is only used by to_string() and to_latex(), not by 

803 # to_html(). so safe to cast col_space here. 

804 col_space = {k: cast(int, v) for k, v in self.col_space.items()} 

805 index = frame.index 

806 columns = frame.columns 

807 fmt = self._get_formatter("__index__") 

808 

809 if isinstance(index, MultiIndex): 

810 fmt_index = index._format_multi( 

811 sparsify=self.sparsify, 

812 include_names=self.show_row_idx_names, 

813 formatter=fmt, 

814 ) 

815 else: 

816 fmt_index = [ 

817 index._format_flat(include_name=self.show_row_idx_names, formatter=fmt) 

818 ] 

819 

820 fmt_index = [ 

821 tuple( 

822 _make_fixed_width( 

823 list(x), justify="left", minimum=col_space.get("", 0), adj=self.adj 

824 ) 

825 ) 

826 for x in fmt_index 

827 ] 

828 

829 adjoined = self.adj.adjoin(1, *fmt_index).split("\n") 

830 

831 # empty space for columns 

832 if self.show_col_idx_names: 

833 col_header = [str(x) for x in self._get_column_name_list()] 

834 else: 

835 col_header = [""] * columns.nlevels 

836 

837 if self.header: 

838 return col_header + adjoined 

839 else: 

840 return adjoined 

841 

842 def _get_column_name_list(self) -> list[Hashable]: 

843 names: list[Hashable] = [] 

844 columns = self.frame.columns 

845 if isinstance(columns, MultiIndex): 

846 names.extend("" if name is None else name for name in columns.names) 

847 else: 

848 names.append("" if columns.name is None else columns.name) 

849 return names 

850 

851 def _validate_float_format( 

852 self, fmt: FloatFormatType | None 

853 ) -> FloatFormatType | None: 

854 """ 

855 Validates and processes the float_format argument. 

856 Converts new-style format strings to callables. 

857 """ 

858 if fmt is None or callable(fmt): 

859 return fmt 

860 

861 if isinstance(fmt, str): 

862 if "%" in fmt: 

863 # Keeps old-style format strings as they are (C code handles them) 

864 return fmt 

865 else: 

866 try: 

867 _ = fmt.format(1.0) # Test with an arbitrary float 

868 return fmt.format 

869 except (ValueError, KeyError, IndexError) as e: 

870 raise ValueError(f"Invalid new-style format string {fmt!r}") from e 

871 

872 raise ValueError("float_format must be a string or callable") 

873 

874 

875class DataFrameRenderer: 

876 """Class for creating dataframe output in multiple formats. 

877 

878 Called in pandas.core.generic.NDFrame: 

879 - to_csv 

880 - to_latex 

881 

882 Called in pandas.DataFrame: 

883 - to_html 

884 - to_string 

885 

886 Parameters 

887 ---------- 

888 fmt : DataFrameFormatter 

889 Formatter with the formatting options. 

890 """ 

891 

892 def __init__(self, fmt: DataFrameFormatter) -> None: 

893 self.fmt = fmt 

894 

895 def to_html( 

896 self, 

897 buf: FilePath | WriteBuffer[str] | None = None, 

898 encoding: str | None = None, 

899 classes: str | list | tuple | None = None, 

900 notebook: bool = False, 

901 border: int | bool | None = None, 

902 table_id: str | None = None, 

903 render_links: bool = False, 

904 ) -> str | None: 

905 """ 

906 Render a DataFrame to an html table. 

907 

908 Parameters 

909 ---------- 

910 buf : str, path object, file-like object, or None, default None 

911 String, path object (implementing ``os.PathLike[str]``), or file-like 

912 object implementing a string ``write()`` function. If None, the result is 

913 returned as a string. 

914 encoding : str, default “utf-8” 

915 Set character encoding. 

916 classes : str or list-like 

917 classes to include in the `class` attribute of the opening 

918 ``<table>`` tag, in addition to the default "dataframe". 

919 notebook : {True, False}, optional, default False 

920 Whether the generated HTML is for IPython Notebook. 

921 border : int or bool 

922 When an integer value is provided, it sets the border attribute in 

923 the opening tag, specifying the thickness of the border. 

924 If ``False`` or ``0`` is passed, the border attribute will not 

925 be present in the ``<table>`` tag. 

926 The default value for this parameter is governed by 

927 ``pd.options.display.html.border``. 

928 table_id : str, optional 

929 A css id is included in the opening `<table>` tag if specified. 

930 render_links : bool, default False 

931 Convert URLs to HTML links. 

932 """ 

933 from pandas.io.formats.html import ( 

934 HTMLFormatter, 

935 NotebookFormatter, 

936 ) 

937 

938 Klass = NotebookFormatter if notebook else HTMLFormatter 

939 

940 html_formatter = Klass( 

941 self.fmt, 

942 classes=classes, 

943 border=border, 

944 table_id=table_id, 

945 render_links=render_links, 

946 ) 

947 string = html_formatter.to_string() 

948 return save_to_buffer(string, buf=buf, encoding=encoding) 

949 

950 def to_string( 

951 self, 

952 buf: FilePath | WriteBuffer[str] | None = None, 

953 encoding: str | None = None, 

954 line_width: int | None = None, 

955 ) -> str | None: 

956 """ 

957 Render a DataFrame to a console-friendly tabular output. 

958 

959 Parameters 

960 ---------- 

961 buf : str, path object, file-like object, or None, default None 

962 String, path object (implementing ``os.PathLike[str]``), or file-like 

963 object implementing a string ``write()`` function. If None, the result is 

964 returned as a string. 

965 encoding: str, default “utf-8” 

966 Set character encoding. 

967 line_width : int, optional 

968 Width to wrap a line in characters. 

969 """ 

970 from pandas.io.formats.string import StringFormatter 

971 

972 string_formatter = StringFormatter(self.fmt, line_width=line_width) 

973 string = string_formatter.to_string() 

974 return save_to_buffer(string, buf=buf, encoding=encoding) 

975 

976 def to_csv( 

977 self, 

978 path_or_buf: FilePath | WriteBuffer[bytes] | WriteBuffer[str] | None = None, 

979 encoding: str | None = None, 

980 sep: str = ",", 

981 columns: Sequence[Hashable] | None = None, 

982 index_label: IndexLabel | None = None, 

983 mode: str = "w", 

984 compression: CompressionOptions = "infer", 

985 quoting: int | None = None, 

986 quotechar: str = '"', 

987 lineterminator: str | None = None, 

988 chunksize: int | None = None, 

989 date_format: str | None = None, 

990 doublequote: bool = True, 

991 escapechar: str | None = None, 

992 errors: str = "strict", 

993 storage_options: StorageOptions | None = None, 

994 ) -> str | None: 

995 """ 

996 Render dataframe as comma-separated file. 

997 """ 

998 from pandas.io.formats.csvs import CSVFormatter 

999 

1000 if path_or_buf is None: 

1001 created_buffer = True 

1002 path_or_buf = StringIO() 

1003 else: 

1004 created_buffer = False 

1005 

1006 csv_formatter = CSVFormatter( 

1007 path_or_buf=path_or_buf, 

1008 lineterminator=lineterminator, 

1009 sep=sep, 

1010 encoding=encoding, 

1011 errors=errors, 

1012 compression=compression, 

1013 quoting=quoting, 

1014 cols=columns, 

1015 index_label=index_label, 

1016 mode=mode, 

1017 chunksize=chunksize, 

1018 quotechar=quotechar, 

1019 date_format=date_format, 

1020 doublequote=doublequote, 

1021 escapechar=escapechar, 

1022 storage_options=storage_options, 

1023 formatter=self.fmt, 

1024 ) 

1025 csv_formatter.save() 

1026 

1027 if created_buffer: 

1028 assert isinstance(path_or_buf, StringIO) 

1029 content = path_or_buf.getvalue() 

1030 path_or_buf.close() 

1031 return content 

1032 

1033 return None 

1034 

1035 

1036def save_to_buffer( 

1037 string: str, 

1038 buf: FilePath | WriteBuffer[str] | None = None, 

1039 encoding: str | None = None, 

1040) -> str | None: 

1041 """ 

1042 Perform serialization. Write to buf or return as string if buf is None. 

1043 """ 

1044 with _get_buffer(buf, encoding=encoding) as fd: 

1045 fd.write(string) 

1046 if buf is None: 

1047 # error: "WriteBuffer[str]" has no attribute "getvalue" 

1048 return fd.getvalue() # type: ignore[attr-defined] 

1049 return None 

1050 

1051 

1052@contextmanager 

1053def _get_buffer( 

1054 buf: FilePath | WriteBuffer[str] | None, encoding: str | None = None 

1055) -> Generator[WriteBuffer[str]] | Generator[StringIO]: 

1056 """ 

1057 Context manager to open, yield and close buffer for filenames or Path-like 

1058 objects, otherwise yield buf unchanged. 

1059 """ 

1060 if buf is not None: 

1061 buf = stringify_path(buf) 

1062 else: 

1063 buf = StringIO() 

1064 

1065 if encoding is None: 

1066 encoding = "utf-8" 

1067 elif not isinstance(buf, str): 

1068 raise ValueError("buf is not a file name and encoding is specified.") 

1069 

1070 if hasattr(buf, "write"): 

1071 # Incompatible types in "yield" (actual type "Union[str, WriteBuffer[str], 

1072 # StringIO]", expected type "Union[WriteBuffer[str], StringIO]") 

1073 yield buf # type: ignore[misc] 

1074 elif isinstance(buf, str): 

1075 check_parent_directory(str(buf)) 

1076 with open(buf, "w", encoding=encoding, newline="") as f: 

1077 # GH#30034 open instead of codecs.open prevents a file leak 

1078 # if we have an invalid encoding argument. 

1079 # newline="" is needed to roundtrip correctly on 

1080 # windows test_to_latex_filename 

1081 yield f 

1082 else: 

1083 raise TypeError("buf is not a file name and it has no write method") 

1084 

1085 

1086# ---------------------------------------------------------------------- 

1087# Array formatters 

1088 

1089 

1090def format_array( 

1091 values: ArrayLike, 

1092 formatter: Callable | None, 

1093 float_format: FloatFormatType | None = None, 

1094 na_rep: str = "NaN", 

1095 digits: int | None = None, 

1096 space: str | int | None = None, 

1097 justify: str = "right", 

1098 decimal: str = ".", 

1099 leading_space: bool | None = True, 

1100 quoting: int | None = None, 

1101 fallback_formatter: Callable | None = None, 

1102) -> list[str]: 

1103 """ 

1104 Format an array for printing. 

1105 

1106 Parameters 

1107 ---------- 

1108 values : np.ndarray or ExtensionArray 

1109 formatter 

1110 float_format 

1111 na_rep 

1112 digits 

1113 space 

1114 justify 

1115 decimal 

1116 leading_space : bool, optional, default True 

1117 Whether the array should be formatted with a leading space. 

1118 When an array as a column of a Series or DataFrame, we do want 

1119 the leading space to pad between columns. 

1120 

1121 When formatting an Index subclass 

1122 (e.g. IntervalIndex._get_values_for_csv), we don't want the 

1123 leading space since it should be left-aligned. 

1124 fallback_formatter 

1125 

1126 Returns 

1127 ------- 

1128 List[str] 

1129 """ 

1130 fmt_klass: type[_GenericArrayFormatter] 

1131 if lib.is_np_dtype(values.dtype, "M"): 

1132 fmt_klass = _Datetime64Formatter 

1133 values = cast(DatetimeArray, values) 

1134 elif isinstance(values.dtype, DatetimeTZDtype): 

1135 fmt_klass = _Datetime64TZFormatter 

1136 values = cast(DatetimeArray, values) 

1137 elif lib.is_np_dtype(values.dtype, "m"): 

1138 fmt_klass = _Timedelta64Formatter 

1139 values = cast(TimedeltaArray, values) 

1140 elif isinstance(values.dtype, ExtensionDtype): 

1141 fmt_klass = _ExtensionArrayFormatter 

1142 elif lib.is_np_dtype(values.dtype, "fc"): 

1143 fmt_klass = FloatArrayFormatter 

1144 elif lib.is_np_dtype(values.dtype, "iu"): 

1145 fmt_klass = _IntArrayFormatter 

1146 else: 

1147 fmt_klass = _GenericArrayFormatter 

1148 

1149 if space is None: 

1150 space = 12 

1151 

1152 if float_format is None: 

1153 float_format = get_option("display.float_format") 

1154 

1155 if digits is None: 

1156 digits = get_option("display.precision") 

1157 

1158 fmt_obj = fmt_klass( 

1159 values, 

1160 digits=digits, 

1161 na_rep=na_rep, 

1162 float_format=float_format, 

1163 formatter=formatter, 

1164 space=space, 

1165 justify=justify, 

1166 decimal=decimal, 

1167 leading_space=leading_space, 

1168 quoting=quoting, 

1169 fallback_formatter=fallback_formatter, 

1170 ) 

1171 

1172 return fmt_obj.get_result() 

1173 

1174 

1175class _GenericArrayFormatter: 

1176 def __init__( 

1177 self, 

1178 values: ArrayLike, 

1179 digits: int = 7, 

1180 formatter: Callable | None = None, 

1181 na_rep: str = "NaN", 

1182 space: str | int = 12, 

1183 float_format: FloatFormatType | None = None, 

1184 justify: str = "right", 

1185 decimal: str = ".", 

1186 quoting: int | None = None, 

1187 fixed_width: bool = True, 

1188 leading_space: bool | None = True, 

1189 fallback_formatter: Callable | None = None, 

1190 ) -> None: 

1191 self.values = values 

1192 self.digits = digits 

1193 self.na_rep = na_rep 

1194 self.space = space 

1195 self.formatter = formatter 

1196 self.float_format = float_format 

1197 self.justify = justify 

1198 self.decimal = decimal 

1199 self.quoting = quoting 

1200 self.fixed_width = fixed_width 

1201 self.leading_space = leading_space 

1202 self.fallback_formatter = fallback_formatter 

1203 

1204 def get_result(self) -> list[str]: 

1205 fmt_values = self._format_strings() 

1206 return _make_fixed_width(fmt_values, self.justify) 

1207 

1208 def _format_strings(self) -> list[str]: 

1209 if self.float_format is None: 

1210 float_format = get_option("display.float_format") 

1211 if float_format is None: 

1212 precision = get_option("display.precision") 

1213 float_format = lambda x: _trim_zeros_single_float( 

1214 f"{x: .{precision:d}f}" 

1215 ) 

1216 else: 

1217 float_format = self.float_format 

1218 

1219 if self.formatter is not None: 

1220 formatter = self.formatter 

1221 elif self.fallback_formatter is not None: 

1222 formatter = self.fallback_formatter 

1223 else: 

1224 quote_strings = self.quoting is not None and self.quoting != QUOTE_NONE 

1225 formatter = partial( 

1226 printing.pprint_thing, 

1227 escape_chars=("\t", "\r", "\n"), 

1228 quote_strings=quote_strings, 

1229 ) 

1230 

1231 def _format(x): 

1232 if self.na_rep is not None and is_scalar(x) and isna(x): 

1233 if x is None: 

1234 return "None" 

1235 elif x is NA: 

1236 return str(NA) 

1237 elif x is NaT or isinstance(x, (np.datetime64, np.timedelta64)): 

1238 return "NaT" 

1239 return self.na_rep 

1240 elif isinstance(x, PandasObject): 

1241 return str(x) 

1242 else: 

1243 # object dtype 

1244 return str(formatter(x)) 

1245 

1246 vals = self.values 

1247 if not isinstance(vals, np.ndarray): 

1248 raise TypeError( 

1249 "ExtensionArray formatting should use _ExtensionArrayFormatter" 

1250 ) 

1251 inferred = lib.map_infer(vals, is_float) 

1252 is_float_type = ( 

1253 inferred 

1254 # vals may have 2 or more dimensions 

1255 & np.all(notna(vals), axis=tuple(range(1, len(vals.shape)))) 

1256 ) 

1257 leading_space = self.leading_space 

1258 if leading_space is None: 

1259 leading_space = is_float_type.any() 

1260 

1261 fmt_values = [] 

1262 for i, v in enumerate(vals): 

1263 if (not is_float_type[i] or self.formatter is not None) and leading_space: 

1264 fmt_values.append(f" {_format(v)}") 

1265 elif is_float_type[i]: 

1266 fmt_values.append(float_format(v)) 

1267 else: 

1268 if leading_space is False: 

1269 # False specifically, so that the default is 

1270 # to include a space if we get here. 

1271 tpl = "{v}" 

1272 else: 

1273 tpl = " {v}" 

1274 fmt_values.append(tpl.format(v=_format(v))) 

1275 

1276 return fmt_values 

1277 

1278 

1279class FloatArrayFormatter(_GenericArrayFormatter): 

1280 def __init__(self, *args, **kwargs) -> None: 

1281 super().__init__(*args, **kwargs) 

1282 

1283 # float_format is expected to be a string 

1284 # formatter should be used to pass a function 

1285 if self.float_format is not None and self.formatter is None: 

1286 # GH21625, GH22270 

1287 self.fixed_width = False 

1288 if callable(self.float_format): 

1289 self.formatter = self.float_format 

1290 self.float_format = None 

1291 

1292 def _value_formatter( 

1293 self, 

1294 float_format: FloatFormatType | None = None, 

1295 threshold: float | None = None, 

1296 ) -> Callable: 

1297 """Returns a function to be applied on each value to format it""" 

1298 # the float_format parameter supersedes self.float_format 

1299 if float_format is None: 

1300 float_format = self.float_format 

1301 

1302 # we are going to compose different functions, to first convert to 

1303 # a string, then replace the decimal symbol, and finally chop according 

1304 # to the threshold 

1305 

1306 # when there is no float_format, we use str instead of '%g' 

1307 # because str(0.0) = '0.0' while '%g' % 0.0 = '0' 

1308 if float_format: 

1309 

1310 def base_formatter(v): 

1311 assert float_format is not None # for mypy 

1312 # error: "str" not callable 

1313 # error: Unexpected keyword argument "value" for "__call__" of 

1314 # "EngFormatter" 

1315 return ( 

1316 float_format(value=v) # type: ignore[operator,call-arg] 

1317 if notna(v) 

1318 else self.na_rep 

1319 ) 

1320 

1321 else: 

1322 

1323 def base_formatter(v): 

1324 return str(v) if notna(v) else self.na_rep 

1325 

1326 if self.decimal != ".": 

1327 

1328 def decimal_formatter(v): 

1329 return base_formatter(v).replace(".", self.decimal, 1) 

1330 

1331 else: 

1332 decimal_formatter = base_formatter 

1333 

1334 if threshold is None: 

1335 return decimal_formatter 

1336 

1337 def formatter(value): 

1338 if notna(value): 

1339 if abs(value) > threshold: 

1340 return decimal_formatter(value) 

1341 else: 

1342 return decimal_formatter(0.0) 

1343 else: 

1344 return self.na_rep 

1345 

1346 return formatter 

1347 

1348 def get_result_as_array(self) -> np.ndarray: 

1349 """ 

1350 Returns the float values converted into strings using 

1351 the parameters given at initialisation, as a numpy array 

1352 """ 

1353 

1354 def format_with_na_rep( 

1355 values: ArrayLike, formatter: Callable, na_rep: str 

1356 ) -> np.ndarray: 

1357 mask = isna(values) 

1358 formatted = np.array( 

1359 [ 

1360 formatter(val) if not m else na_rep 

1361 for val, m in zip(values.ravel(), mask.ravel(), strict=True) 

1362 ] 

1363 ).reshape(values.shape) 

1364 return formatted 

1365 

1366 def format_complex_with_na_rep( 

1367 values: ArrayLike, formatter: Callable, na_rep: str 

1368 ) -> np.ndarray: 

1369 real_values = np.real(values).ravel() # type: ignore[arg-type] 

1370 imag_values = np.imag(values).ravel() # type: ignore[arg-type] 

1371 real_mask, imag_mask = isna(real_values), isna(imag_values) 

1372 formatted_lst = [] 

1373 for val, real_val, imag_val, re_isna, im_isna in zip( 

1374 values.ravel(), 

1375 real_values, 

1376 imag_values, 

1377 real_mask, 

1378 imag_mask, 

1379 strict=True, 

1380 ): 

1381 if not re_isna and not im_isna: 

1382 formatted_lst.append(formatter(val)) 

1383 elif not re_isna: # xxx+nanj 

1384 formatted_lst.append(f"{formatter(real_val)}+{na_rep}j") 

1385 elif not im_isna: # nan[+/-]xxxj 

1386 # The imaginary part may either start with a "-" or a space 

1387 imag_formatted = formatter(imag_val).strip() 

1388 if imag_formatted.startswith("-"): 

1389 formatted_lst.append(f"{na_rep}{imag_formatted}j") 

1390 else: 

1391 formatted_lst.append(f"{na_rep}+{imag_formatted}j") 

1392 else: # nan+nanj 

1393 formatted_lst.append(f"{na_rep}+{na_rep}j") 

1394 return np.array(formatted_lst).reshape(values.shape) 

1395 

1396 if self.formatter is not None: 

1397 return format_with_na_rep(self.values, self.formatter, self.na_rep) 

1398 

1399 if self.fixed_width: 

1400 threshold = get_option("display.chop_threshold") 

1401 else: 

1402 threshold = None 

1403 

1404 # if we have a fixed_width, we'll need to try different float_format 

1405 def format_values_with(float_format): 

1406 formatter = self._value_formatter(float_format, threshold) 

1407 

1408 # default formatter leaves a space to the left when formatting 

1409 # floats, must be consistent for left-justifying NaNs (GH #25061) 

1410 na_rep = " " + self.na_rep if self.justify == "left" else self.na_rep 

1411 

1412 # different formatting strategies for complex and non-complex data 

1413 # need to distinguish complex and float NaNs (GH #53762) 

1414 values = self.values 

1415 is_complex = is_complex_dtype(values) 

1416 

1417 # separate the wheat from the chaff 

1418 if is_complex: 

1419 values = format_complex_with_na_rep(values, formatter, na_rep) 

1420 else: 

1421 values = format_with_na_rep(values, formatter, na_rep) 

1422 

1423 if self.fixed_width: 

1424 if is_complex: 

1425 result = _trim_zeros_complex(values, self.decimal) 

1426 else: 

1427 result = _trim_zeros_float(values, self.decimal) 

1428 return np.asarray(result, dtype="object") 

1429 

1430 return values 

1431 

1432 # There is a special default string when we are fixed-width 

1433 # The default is otherwise to use str instead of a formatting string 

1434 float_format: FloatFormatType | None 

1435 if self.float_format is None: 

1436 if self.fixed_width: 

1437 if self.leading_space is True: 

1438 fmt_str = "{value: .{digits:d}f}" 

1439 else: 

1440 fmt_str = "{value:.{digits:d}f}" 

1441 float_format = partial(fmt_str.format, digits=self.digits) 

1442 else: 

1443 float_format = self.float_format 

1444 else: 

1445 float_format = lambda value: self.float_format % value 

1446 

1447 formatted_values = format_values_with(float_format) 

1448 

1449 if not self.fixed_width: 

1450 return formatted_values 

1451 

1452 # we need do convert to engineering format if some values are too small 

1453 # and would appear as 0, or if some values are too big and take too 

1454 # much space 

1455 

1456 if len(formatted_values) > 0: 

1457 maxlen = max(len(x) for x in formatted_values) 

1458 too_long = maxlen > self.digits + 6 

1459 else: 

1460 too_long = False 

1461 

1462 abs_vals = np.abs(self.values) 

1463 # this is pretty arbitrary for now 

1464 # large values: more that 8 characters including decimal symbol 

1465 # and first digit, hence > 1e6 

1466 has_large_values = (abs_vals > 1e6).any() 

1467 has_small_values = ((abs_vals < 10 ** (-self.digits)) & (abs_vals > 0)).any() 

1468 

1469 if has_small_values or (too_long and has_large_values): 

1470 if self.leading_space is True: 

1471 fmt_str = "{value: .{digits:d}e}" 

1472 else: 

1473 fmt_str = "{value:.{digits:d}e}" 

1474 float_format = partial(fmt_str.format, digits=self.digits) 

1475 formatted_values = format_values_with(float_format) 

1476 

1477 return formatted_values 

1478 

1479 def _format_strings(self) -> list[str]: 

1480 return list(self.get_result_as_array()) 

1481 

1482 

1483class _IntArrayFormatter(_GenericArrayFormatter): 

1484 def _format_strings(self) -> list[str]: 

1485 if self.leading_space is False: 

1486 formatter_str = lambda x: f"{x:d}".format(x=x) 

1487 else: 

1488 formatter_str = lambda x: f"{x: d}".format(x=x) 

1489 formatter = self.formatter or formatter_str 

1490 fmt_values = [formatter(x) for x in self.values] 

1491 return fmt_values 

1492 

1493 

1494class _Datetime64Formatter(_GenericArrayFormatter): 

1495 values: DatetimeArray 

1496 

1497 def __init__( 

1498 self, 

1499 values: DatetimeArray, 

1500 nat_rep: str = "NaT", 

1501 date_format: None = None, 

1502 **kwargs, 

1503 ) -> None: 

1504 super().__init__(values, **kwargs) 

1505 self.nat_rep = nat_rep 

1506 self.date_format = date_format 

1507 

1508 def _format_strings(self) -> list[str]: 

1509 """we by definition have DO NOT have a TZ""" 

1510 values = self.values 

1511 

1512 if self.formatter is not None: 

1513 return [self.formatter(x) for x in values] 

1514 

1515 fmt_values = values._format_native_types( 

1516 na_rep=self.nat_rep, date_format=self.date_format 

1517 ) 

1518 return fmt_values.tolist() 

1519 

1520 

1521class _ExtensionArrayFormatter(_GenericArrayFormatter): 

1522 values: ExtensionArray 

1523 

1524 def _format_strings(self) -> list[str]: 

1525 values = self.values 

1526 

1527 formatter = self.formatter 

1528 fallback_formatter = None 

1529 if formatter is None: 

1530 fallback_formatter = values._formatter(boxed=True) 

1531 

1532 if isinstance(values, Categorical): 

1533 # Categorical is special for now, so that we can preserve tzinfo 

1534 array = values._internal_get_values() 

1535 else: 

1536 array = np.asarray(values, dtype=object) 

1537 

1538 fmt_values = format_array( 

1539 array, 

1540 formatter, 

1541 float_format=self.float_format, 

1542 na_rep=self.na_rep, 

1543 digits=self.digits, 

1544 space=self.space, 

1545 justify=self.justify, 

1546 decimal=self.decimal, 

1547 leading_space=self.leading_space, 

1548 quoting=self.quoting, 

1549 fallback_formatter=fallback_formatter, 

1550 ) 

1551 return fmt_values 

1552 

1553 

1554def format_percentiles( 

1555 percentiles: np.ndarray | Sequence[float], 

1556) -> list[str]: 

1557 """ 

1558 Outputs rounded and formatted percentiles. 

1559 

1560 Parameters 

1561 ---------- 

1562 percentiles : list-like, containing floats from interval [0,1] 

1563 

1564 Returns 

1565 ------- 

1566 formatted : list of strings 

1567 

1568 Notes 

1569 ----- 

1570 Rounding precision is chosen so that: (1) if any two elements of 

1571 ``percentiles`` differ, they remain different after rounding 

1572 (2) no entry is *rounded* to 0% or 100%. 

1573 Any non-integer is always rounded to at least 1 decimal place. 

1574 

1575 Examples 

1576 -------- 

1577 Keeps all entries different after rounding: 

1578 

1579 >>> format_percentiles([0.01999, 0.02001, 0.5, 0.666666, 0.9999]) 

1580 ['1.999%', '2.001%', '50%', '66.667%', '99.99%'] 

1581 

1582 No element is rounded to 0% or 100% (unless already equal to it). 

1583 Duplicates are allowed: 

1584 

1585 >>> format_percentiles([0, 0.5, 0.02001, 0.5, 0.666666, 0.9999]) 

1586 ['0%', '50%', '2.0%', '50%', '66.67%', '99.99%'] 

1587 """ 

1588 if len(percentiles) == 0: 

1589 return [] 

1590 

1591 percentiles = np.asarray(percentiles) 

1592 

1593 # It checks for np.nan as well 

1594 if ( 

1595 not is_numeric_dtype(percentiles) 

1596 or not np.all(percentiles >= 0) 

1597 or not np.all(percentiles <= 1) 

1598 ): 

1599 raise ValueError("percentiles should all be in the interval [0,1]") 

1600 

1601 percentiles = 100 * percentiles 

1602 prec = get_precision(percentiles) 

1603 percentiles_round_type = percentiles.round(prec).astype(int) 

1604 

1605 int_idx = np.isclose(percentiles_round_type, percentiles) 

1606 

1607 if np.all(int_idx): 

1608 out = percentiles_round_type.astype(str) 

1609 return [i + "%" for i in out] 

1610 

1611 unique_pcts = np.unique(percentiles) 

1612 prec = get_precision(unique_pcts) 

1613 out = np.empty_like(percentiles, dtype=object) 

1614 out[int_idx] = percentiles[int_idx].round().astype(int).astype(str) 

1615 

1616 out[~int_idx] = percentiles[~int_idx].round(prec).astype(str) 

1617 return [i + "%" for i in out] 

1618 

1619 

1620def get_precision(array: np.ndarray | Sequence[float]) -> int: 

1621 to_begin = array[0] if array[0] > 0 else None 

1622 to_end = 100 - array[-1] if array[-1] < 100 else None 

1623 diff = np.ediff1d(array, to_begin=to_begin, to_end=to_end) 

1624 diff = abs(diff) 

1625 prec = -np.floor(np.log10(np.min(diff))).astype(int) 

1626 prec = max(1, prec) 

1627 return prec 

1628 

1629 

1630def _format_datetime64(x: NaTType | Timestamp, nat_rep: str = "NaT") -> str: 

1631 if x is NaT: 

1632 return nat_rep 

1633 

1634 # Timestamp.__str__ falls back to datetime.datetime.__str__ = isoformat(sep=' ') 

1635 # so it already uses string formatting rather than strftime (faster). 

1636 return str(x) 

1637 

1638 

1639def _format_datetime64_dateonly( 

1640 x: NaTType | Timestamp, 

1641 nat_rep: str = "NaT", 

1642 date_format: str | None = None, 

1643) -> str: 

1644 if isinstance(x, NaTType): 

1645 return nat_rep 

1646 

1647 if date_format: 

1648 return x.strftime(date_format) 

1649 else: 

1650 # Timestamp._date_repr relies on string formatting (faster than strftime) 

1651 return x._date_repr 

1652 

1653 

1654def get_format_datetime64( 

1655 is_dates_only: bool, nat_rep: str = "NaT", date_format: str | None = None 

1656) -> Callable: 

1657 """Return a formatter callable taking a datetime64 as input and providing 

1658 a string as output""" 

1659 

1660 if is_dates_only: 

1661 return lambda x: _format_datetime64_dateonly( 

1662 x, nat_rep=nat_rep, date_format=date_format 

1663 ) 

1664 else: 

1665 return lambda x: _format_datetime64(x, nat_rep=nat_rep) 

1666 

1667 

1668class _Datetime64TZFormatter(_Datetime64Formatter): 

1669 values: DatetimeArray 

1670 

1671 def _format_strings(self) -> list[str]: 

1672 """we by definition have a TZ""" 

1673 ido = self.values._is_dates_only 

1674 values = self.values.astype(object) 

1675 formatter = self.formatter or get_format_datetime64( 

1676 ido, date_format=self.date_format 

1677 ) 

1678 fmt_values = [formatter(x) for x in values] 

1679 

1680 return fmt_values 

1681 

1682 

1683class _Timedelta64Formatter(_GenericArrayFormatter): 

1684 values: TimedeltaArray 

1685 

1686 def __init__( 

1687 self, 

1688 values: TimedeltaArray, 

1689 nat_rep: str = "NaT", 

1690 **kwargs, 

1691 ) -> None: 

1692 # TODO: nat_rep is never passed, na_rep is. 

1693 super().__init__(values, **kwargs) 

1694 self.nat_rep = nat_rep 

1695 

1696 def _format_strings(self) -> list[str]: 

1697 formatter = self.formatter or get_format_timedelta64( 

1698 self.values, nat_rep=self.nat_rep, box=False 

1699 ) 

1700 return [formatter(x) for x in self.values] 

1701 

1702 

1703def get_format_timedelta64( 

1704 values: TimedeltaArray, 

1705 nat_rep: str | float = "NaT", 

1706 box: bool = False, 

1707) -> Callable: 

1708 """ 

1709 Return a formatter function for a range of timedeltas. 

1710 These will all have the same format argument 

1711 

1712 If box, then show the return in quotes 

1713 """ 

1714 even_days = values._is_dates_only 

1715 

1716 if even_days: 

1717 format = None 

1718 else: 

1719 format = "long" 

1720 

1721 def _formatter(x): 

1722 if x is None or (is_scalar(x) and isna(x)): 

1723 return nat_rep 

1724 

1725 if not isinstance(x, Timedelta): 

1726 x = Timedelta(x) 

1727 

1728 # Timedelta._repr_base uses string formatting (faster than strftime) 

1729 result = x._repr_base(format=format) 

1730 if box: 

1731 result = f"'{result}'" 

1732 return result 

1733 

1734 return _formatter 

1735 

1736 

1737def _make_fixed_width( 

1738 strings: list[str], 

1739 justify: str = "right", 

1740 minimum: int | None = None, 

1741 adj: printing._TextAdjustment | None = None, 

1742) -> list[str]: 

1743 if len(strings) == 0 or justify == "all": 

1744 return strings 

1745 

1746 if adj is None: 

1747 adjustment = printing.get_adjustment() 

1748 else: 

1749 adjustment = adj 

1750 

1751 max_len = max(adjustment.len(x) for x in strings) 

1752 

1753 if minimum is not None: 

1754 max_len = max(minimum, max_len) 

1755 

1756 conf_max = get_option("display.max_colwidth") 

1757 if conf_max is not None and max_len > conf_max: 

1758 max_len = conf_max 

1759 

1760 def just(x: str) -> str: 

1761 if conf_max is not None: 

1762 if (conf_max > 3) & (adjustment.len(x) > max_len): 

1763 x = x[: max_len - 3] + "..." 

1764 return x 

1765 

1766 strings = [just(x) for x in strings] 

1767 result = adjustment.justify(strings, max_len, mode=justify) 

1768 return result 

1769 

1770 

1771def _trim_zeros_complex(str_complexes: ArrayLike, decimal: str = ".") -> list[str]: 

1772 """ 

1773 Separates the real and imaginary parts from the complex number, and 

1774 executes the _trim_zeros_float method on each of those. 

1775 """ 

1776 real_part, imag_part = [], [] 

1777 for x in str_complexes: 

1778 # Complex numbers are represented as "(-)xxx(+/-)xxxj" 

1779 # The split will give [{"", "-"}, "xxx", "+/-", "xxx", "j", ""] 

1780 # Therefore, the imaginary part is the 4th and 3rd last elements, 

1781 # and the real part is everything before the imaginary part 

1782 trimmed = re.split(r"(?<!e)([j+-])", x) 

1783 real_part.append("".join(trimmed[:-4])) 

1784 imag_part.append("".join(trimmed[-4:-2])) 

1785 

1786 # We want to align the lengths of the real and imaginary parts of each complex 

1787 # number, as well as the lengths the real (resp. complex) parts of all numbers 

1788 # in the array 

1789 n = len(str_complexes) 

1790 padded_parts = _trim_zeros_float(real_part + imag_part, decimal) 

1791 if len(padded_parts) == 0: 

1792 return [] 

1793 padded_length = max(len(part) for part in padded_parts) - 1 

1794 padded = [ 

1795 real_pt # real part, possibly NaN 

1796 + imag_pt[0] # +/- 

1797 + f"{imag_pt[1:]:>{padded_length}}" # complex part (no sign), possibly nan 

1798 + "j" 

1799 for real_pt, imag_pt in zip(padded_parts[:n], padded_parts[n:], strict=True) 

1800 ] 

1801 return padded 

1802 

1803 

1804def _trim_zeros_single_float(str_float: str) -> str: 

1805 """ 

1806 Trims trailing zeros after a decimal point, 

1807 leaving just one if necessary. 

1808 """ 

1809 str_float = str_float.rstrip("0") 

1810 if str_float.endswith("."): 

1811 str_float += "0" 

1812 

1813 return str_float 

1814 

1815 

1816def _trim_zeros_float( 

1817 str_floats: ArrayLike | list[str], decimal: str = "." 

1818) -> list[str]: 

1819 """ 

1820 Trims the maximum number of trailing zeros equally from 

1821 all numbers containing decimals, leaving just one if 

1822 necessary. 

1823 """ 

1824 trimmed = str_floats 

1825 number_regex = re.compile(rf"^\s*[\+-]?[0-9]+\{decimal}[0-9]*$") 

1826 

1827 def is_number_with_decimal(x) -> bool: 

1828 return re.match(number_regex, x) is not None 

1829 

1830 def should_trim(values: ArrayLike | list[str]) -> bool: 

1831 """ 

1832 Determine if an array of strings should be trimmed. 

1833 

1834 Returns True if all numbers containing decimals (defined by the 

1835 above regular expression) within the array end in a zero, otherwise 

1836 returns False. 

1837 """ 

1838 numbers = [x for x in values if is_number_with_decimal(x)] 

1839 return len(numbers) > 0 and all(x.endswith("0") for x in numbers) 

1840 

1841 while should_trim(trimmed): 

1842 trimmed = [x[:-1] if is_number_with_decimal(x) else x for x in trimmed] 

1843 

1844 # leave one 0 after the decimal points if need be. 

1845 result = [ 

1846 x + "0" if is_number_with_decimal(x) and x.endswith(decimal) else x 

1847 for x in trimmed 

1848 ] 

1849 return result 

1850 

1851 

1852def _has_names(index: Index) -> bool: 

1853 if isinstance(index, MultiIndex): 

1854 return com.any_not_none(*index.names) 

1855 else: 

1856 return index.name is not None 

1857 

1858 

1859class EngFormatter: 

1860 """ 

1861 Formats float values according to engineering format. 

1862 

1863 Based on matplotlib.ticker.EngFormatter 

1864 """ 

1865 

1866 # The SI engineering prefixes 

1867 ENG_PREFIXES = { 

1868 -24: "y", 

1869 -21: "z", 

1870 -18: "a", 

1871 -15: "f", 

1872 -12: "p", 

1873 -9: "n", 

1874 -6: "u", 

1875 -3: "m", 

1876 0: "", 

1877 3: "k", 

1878 6: "M", 

1879 9: "G", 

1880 12: "T", 

1881 15: "P", 

1882 18: "E", 

1883 21: "Z", 

1884 24: "Y", 

1885 } 

1886 

1887 def __init__( 

1888 self, accuracy: int | None = None, use_eng_prefix: bool = False 

1889 ) -> None: 

1890 self.accuracy = accuracy 

1891 self.use_eng_prefix = use_eng_prefix 

1892 

1893 def __call__(self, num: float) -> str: 

1894 """ 

1895 Formats a number in engineering notation, appending a letter 

1896 representing the power of 1000 of the original number. Some examples: 

1897 >>> format_eng = EngFormatter(accuracy=0, use_eng_prefix=True) 

1898 >>> format_eng(0) 

1899 ' 0' 

1900 >>> format_eng = EngFormatter(accuracy=1, use_eng_prefix=True) 

1901 >>> format_eng(1_000_000) 

1902 ' 1.0M' 

1903 >>> format_eng = EngFormatter(accuracy=2, use_eng_prefix=False) 

1904 >>> format_eng("-1e-6") 

1905 '-1.00E-06' 

1906 

1907 @param num: the value to represent 

1908 @type num: either a numeric value or a string that can be converted to 

1909 a numeric value (as per decimal.Decimal constructor) 

1910 

1911 @return: engineering formatted string 

1912 """ 

1913 dnum = Decimal(str(num)) 

1914 

1915 if Decimal.is_nan(dnum): 

1916 return "NaN" 

1917 

1918 if Decimal.is_infinite(dnum): 

1919 return "inf" 

1920 

1921 sign = 1 

1922 

1923 if dnum < 0: # pragma: no cover 

1924 sign = -1 

1925 dnum = -dnum 

1926 

1927 if dnum != 0: 

1928 pow10 = Decimal(int(math.floor(dnum.log10() / 3) * 3)) 

1929 else: 

1930 pow10 = Decimal(0) 

1931 

1932 pow10 = pow10.min(max(self.ENG_PREFIXES.keys())) 

1933 pow10 = pow10.max(min(self.ENG_PREFIXES.keys())) 

1934 int_pow10 = int(pow10) 

1935 

1936 if self.use_eng_prefix: 

1937 prefix = self.ENG_PREFIXES[int_pow10] 

1938 elif int_pow10 < 0: 

1939 prefix = f"E-{-int_pow10:02d}" 

1940 else: 

1941 prefix = f"E+{int_pow10:02d}" 

1942 

1943 mant = sign * dnum / (10**pow10) 

1944 

1945 if self.accuracy is None: # pragma: no cover 

1946 format_str = "{mant: g}{prefix}" 

1947 else: 

1948 format_str = f"{{mant: .{self.accuracy:d}f}}{{prefix}}" 

1949 

1950 formatted = format_str.format(mant=mant, prefix=prefix) 

1951 

1952 return formatted 

1953 

1954 

1955@set_module("pandas") 

1956def set_eng_float_format(accuracy: int = 3, use_eng_prefix: bool = False) -> None: 

1957 """ 

1958 Format float representation in DataFrame with SI notation. 

1959 

1960 Sets the floating-point display format for ``DataFrame`` objects using engineering 

1961 notation (SI units), allowing easier readability of values across wide ranges. 

1962 

1963 Parameters 

1964 ---------- 

1965 accuracy : int, default 3 

1966 Number of decimal digits after the floating point. 

1967 use_eng_prefix : bool, default False 

1968 Whether to represent a value with SI prefixes. 

1969 

1970 Returns 

1971 ------- 

1972 None 

1973 This method does not return a value. it updates the global display format 

1974 for floats in DataFrames. 

1975 

1976 See Also 

1977 -------- 

1978 set_option : Set the value of the specified option or options. 

1979 reset_option : Reset one or more options to their default value. 

1980 

1981 Examples 

1982 -------- 

1983 >>> df = pd.DataFrame([1e-9, 1e-3, 1, 1e3, 1e6]) 

1984 >>> df 

1985 0 

1986 0 1.000000e-09 

1987 1 1.000000e-03 

1988 2 1.000000e+00 

1989 3 1.000000e+03 

1990 4 1.000000e+06 

1991 

1992 >>> pd.set_eng_float_format(accuracy=1) 

1993 >>> df 

1994 0 

1995 0 1.0E-09 

1996 1 1.0E-03 

1997 2 1.0E+00 

1998 3 1.0E+03 

1999 4 1.0E+06 

2000 

2001 >>> pd.set_eng_float_format(use_eng_prefix=True) 

2002 >>> df 

2003 0 

2004 0 1.000n 

2005 1 1.000m 

2006 2 1.000 

2007 3 1.000k 

2008 4 1.000M 

2009 

2010 >>> pd.set_eng_float_format(accuracy=1, use_eng_prefix=True) 

2011 >>> df 

2012 0 

2013 0 1.0n 

2014 1 1.0m 

2015 2 1.0 

2016 3 1.0k 

2017 4 1.0M 

2018 

2019 >>> pd.set_option("display.float_format", None) # unset option 

2020 """ 

2021 set_option("display.float_format", EngFormatter(accuracy, use_eng_prefix)) 

2022 

2023 

2024def get_level_lengths( 

2025 levels: Any, sentinel: bool | object | str = "" 

2026) -> list[dict[int, int]]: 

2027 """ 

2028 For each index in each level the function returns lengths of indexes. 

2029 

2030 Parameters 

2031 ---------- 

2032 levels : list of lists 

2033 List of values on for level. 

2034 sentinel : string, optional 

2035 Value which states that no new index starts on there. 

2036 

2037 Returns 

2038 ------- 

2039 Returns list of maps. For each level returns map of indexes (key is index 

2040 in row and value is length of index). 

2041 """ 

2042 if len(levels) == 0: 

2043 return [] 

2044 

2045 control = [True] * len(levels[0]) 

2046 

2047 result = [] 

2048 for level in levels: 

2049 last_index = 0 

2050 

2051 lengths = {} 

2052 for i, key in enumerate(level): 

2053 if control[i] and key == sentinel: 

2054 pass 

2055 else: 

2056 control[i] = False 

2057 lengths[last_index] = i - last_index 

2058 last_index = i 

2059 

2060 lengths[last_index] = len(level) - last_index 

2061 

2062 result.append(lengths) 

2063 

2064 return result 

2065 

2066 

2067def buffer_put_lines(buf: WriteBuffer[str], lines: list[str]) -> None: 

2068 """ 

2069 Appends lines to a buffer. 

2070 

2071 Parameters 

2072 ---------- 

2073 buf 

2074 The buffer to write to 

2075 lines 

2076 The lines to append. 

2077 """ 

2078 if any(isinstance(x, str) for x in lines): 

2079 lines = [str(x) for x in lines] 

2080 buf.write("\n".join(lines))