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 cast,
27)
28import warnings
29
30import numpy as np
31
32from pandas._config.config import (
33 _global_config as config,
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 iNaT,
44)
45from pandas._libs.tslibs.dtypes import (
46 NpyDatetimeUnit,
47 periods_per_second,
48)
49from pandas._libs.tslibs.nattype import NaTType
50from pandas.errors import Pandas4Warning
51from pandas.util._decorators import set_module
52from pandas.util._exceptions import find_stack_level
53
54from pandas.core.dtypes.common import (
55 is_complex_dtype,
56 is_float,
57 is_integer,
58 is_list_like,
59 is_numeric_dtype,
60 is_scalar,
61)
62from pandas.core.dtypes.dtypes import (
63 CategoricalDtype,
64 DatetimeTZDtype,
65 ExtensionDtype,
66)
67from pandas.core.dtypes.missing import (
68 isna,
69 notna,
70)
71
72from pandas.core.arrays import (
73 Categorical,
74 DatetimeArray,
75 ExtensionArray,
76 TimedeltaArray,
77)
78from pandas.core.base import PandasObject
79import pandas.core.common as com
80from pandas.core.indexes.api import (
81 Index,
82 MultiIndex,
83 PeriodIndex,
84 ensure_index,
85)
86from pandas.core.indexes.datetimes import DatetimeIndex
87from pandas.core.indexes.timedeltas import TimedeltaIndex
88
89from pandas.io.common import (
90 check_parent_directory,
91 stringify_path,
92)
93from pandas.io.formats import printing
94
95if TYPE_CHECKING:
96 from pandas._typing import (
97 ArrayLike,
98 Axes,
99 ColspaceArgType,
100 ColspaceType,
101 CompressionOptions,
102 FilePath,
103 FloatFormatType,
104 FormattersType,
105 IndexLabel,
106 SequenceNotStr,
107 StorageOptions,
108 WriteBuffer,
109 )
110
111 from pandas import (
112 DataFrame,
113 Series,
114 )
115
116
117VALID_JUSTIFY_PARAMETERS = (
118 "left",
119 "right",
120 "center",
121 "justify",
122 "justify-all",
123 "start",
124 "end",
125 "inherit",
126 "match-parent",
127 "initial",
128 "unset",
129)
130
131
132class SeriesFormatter:
133 """
134 Implement the main logic of Series.to_string, which underlies
135 Series.__repr__.
136 """
137
138 def __init__(
139 self,
140 series: Series,
141 *,
142 length: bool | str = True,
143 header: bool = True,
144 index: bool = True,
145 na_rep: str = "NaN",
146 name: bool = False,
147 float_format: str | None = None,
148 dtype: bool = True,
149 max_rows: int | None = None,
150 min_rows: int | None = None,
151 ) -> None:
152 self.series = series
153 self.buf = StringIO()
154 self.name = name
155 self.na_rep = na_rep
156 self.header = header
157 self.length = length
158 self.index = index
159 self.max_rows = max_rows
160 self.min_rows = min_rows
161
162 if float_format is None:
163 float_format = config["display"]["float_format"]
164 self.float_format = float_format
165 self.dtype = dtype
166 self.adj = printing.get_adjustment()
167
168 self._chk_truncate()
169
170 def _chk_truncate(self) -> None:
171 self.tr_row_num: int | None
172
173 min_rows = self.min_rows
174 max_rows = self.max_rows
175 # truncation determined by max_rows, actual truncated number of rows
176 # used below by min_rows
177 is_truncated_vertically = max_rows and (len(self.series) > max_rows)
178 series = self.series
179 if is_truncated_vertically:
180 max_rows = cast("int", max_rows)
181 if min_rows:
182 # if min_rows is set (not None or 0), set max_rows to minimum
183 # of both
184 max_rows = min(min_rows, max_rows)
185 if max_rows == 1:
186 row_num = max_rows
187 series = series.iloc[:max_rows]
188 else:
189 row_num = max_rows // 2
190 _len = len(series)
191 _slice = np.hstack(
192 [np.arange(row_num), np.arange(_len - row_num, _len)]
193 )
194 series = series.iloc[_slice]
195 self.tr_row_num = row_num
196 else:
197 self.tr_row_num = None
198 self.tr_series = series
199 self.is_truncated_vertically = is_truncated_vertically
200
201 def _get_footer(self) -> str:
202 name = self.series.name
203 footer = ""
204
205 index = self.series.index
206 if (
207 isinstance(index, (DatetimeIndex, PeriodIndex, TimedeltaIndex))
208 and index.freq is not None
209 ):
210 footer += f"Freq: {index.freqstr}"
211
212 if self.name is not False and name is not None:
213 if footer:
214 footer += ", "
215
216 footer += f"Name: {format_name(name)}"
217
218 if self.length is True or (
219 self.length == "truncate" and self.is_truncated_vertically
220 ):
221 if footer:
222 footer += ", "
223 footer += f"Length: {len(self.series)}"
224
225 if self.dtype is not False and self.dtype is not None:
226 dtype_name = getattr(self.tr_series.dtype, "name", None)
227 if dtype_name:
228 if footer:
229 footer += ", "
230 footer += f"dtype: {printing.pprint_thing(dtype_name)}"
231
232 # level infos are added to the end and in a new line, like it is done
233 # for Categoricals
234 if isinstance(self.tr_series.dtype, CategoricalDtype):
235 level_info = self.tr_series._values._get_repr_footer()
236 if footer:
237 footer += "\n"
238 footer += level_info
239
240 return str(footer)
241
242 def _get_formatted_values(self) -> list[str]:
243 return format_array(
244 self.tr_series._values,
245 None,
246 float_format=self.float_format,
247 na_rep=self.na_rep,
248 leading_space=self.index,
249 )
250
251 def to_string(self) -> str:
252 series = self.tr_series
253 footer = self._get_footer()
254
255 if len(series) == 0:
256 return f"{type(self.series).__name__}([], {footer})"
257
258 index = series.index
259 have_header = _has_names(index)
260 if isinstance(index, MultiIndex):
261 fmt_index = index._format_multi(include_names=True, sparsify=None)
262 adj = printing.get_adjustment()
263 fmt_index = adj.adjoin(2, *fmt_index).split("\n")
264 else:
265 fmt_index = index._format_flat(include_name=True)
266 fmt_values = self._get_formatted_values()
267
268 if self.is_truncated_vertically:
269 n_header_rows = 0
270 row_num = self.tr_row_num
271 row_num = cast("int", row_num)
272 width = self.adj.len(fmt_values[row_num - 1])
273 if width > 3:
274 dot_str = "..."
275 else:
276 dot_str = ".."
277 # Series uses mode=center because it has single value columns
278 # DataFrame uses mode=left
279 dot_str = self.adj.justify([dot_str], width, mode="center")[0]
280 fmt_values.insert(row_num + n_header_rows, dot_str)
281 fmt_index.insert(row_num + 1, "")
282
283 if self.index:
284 result = self.adj.adjoin(3, *[fmt_index[1:], fmt_values])
285 else:
286 result = self.adj.adjoin(3, fmt_values)
287
288 if self.header and have_header:
289 result = fmt_index[0] + "\n" + result
290
291 if footer:
292 result += "\n" + footer
293
294 return str("".join(result))
295
296
297def get_dataframe_repr_params() -> dict[str, Any]:
298 """Get the parameters used to repr(dataFrame) calls using DataFrame.to_string.
299
300 Supplying these parameters to DataFrame.to_string is equivalent to calling
301 ``repr(DataFrame)``. This is useful if you want to adjust the repr output.
302
303 Example
304 -------
305 >>> import pandas as pd
306 >>>
307 >>> df = pd.DataFrame([[1, 2], [3, 4]])
308 >>> repr_params = pd.io.formats.format.get_dataframe_repr_params()
309 >>> repr(df) == df.to_string(**repr_params)
310 True
311 """
312 from pandas.io.formats import console
313
314 if config["display"]["expand_frame_repr"]:
315 line_width, _ = console.get_console_size()
316 else:
317 line_width = None
318 return {
319 "max_rows": config["display"]["max_rows"],
320 "min_rows": config["display"]["min_rows"],
321 "max_cols": config["display"]["max_columns"],
322 "max_colwidth": config["display"]["max_colwidth"],
323 "show_dimensions": config["display"]["show_dimensions"],
324 "line_width": line_width,
325 }
326
327
328def get_series_repr_params() -> dict[str, Any]:
329 """Get the parameters used to repr(Series) calls using Series.to_string.
330
331 Supplying these parameters to Series.to_string is equivalent to calling
332 ``repr(series)``. This is useful if you want to adjust the series repr output.
333
334 Example
335 -------
336 >>> import pandas as pd
337 >>>
338 >>> ser = pd.Series([1, 2, 3, 4])
339 >>> repr_params = pd.io.formats.format.get_series_repr_params()
340 >>> repr(ser) == ser.to_string(**repr_params)
341 True
342 """
343 width, height = get_terminal_size()
344 max_rows_opt = config["display"]["max_rows"]
345 max_rows = height if max_rows_opt == 0 else max_rows_opt
346 min_rows = height if max_rows_opt == 0 else config["display"]["min_rows"]
347
348 return {
349 "name": True,
350 "dtype": True,
351 "min_rows": min_rows,
352 "max_rows": max_rows,
353 "length": config["display"]["show_dimensions"],
354 }
355
356
357class DataFrameFormatter:
358 """
359 Class for processing dataframe formatting options and data.
360
361 Used by DataFrame.to_string, which backs DataFrame.__repr__.
362
363 Parameters
364 ----------
365 buf : str, Path or StringIO-like, optional, default None
366 Buffer to write to. If None, the output is returned as a string.
367 columns : array-like, optional, default None
368 The subset of columns to write. Writes all columns by default.
369 col_space : int, list or dict of int, optional
370 The minimum width of each column. If a list of ints is given
371 every integers corresponds with one column. If a dict is given,
372 the key references the column, while the value defines the space
373 to use.
374 header : bool or list of str, optional
375 Write out the column names. If a list of columns is given,
376 it is assumed to be aliases for the column names.
377 index : bool, optional, default True
378 Whether to print index (row) labels.
379 na_rep : str, optional, default 'NaN'
380 String representation of ``NaN`` to use.
381 formatters : list, tuple or dict of one-param. functions, optional
382 Formatter functions to apply to columns' elements by position or
383 name.
384 The result of each function must be a unicode string.
385 List/tuple must be of length equal to the number of columns.
386 float_format : one-parameter function, optional, default None
387 Formatter function to apply to columns' elements if they are
388 floats. This function must return a unicode string and will be
389 applied only to the non-``NaN`` elements, with ``NaN`` being
390 handled by ``na_rep``.
391 sparsify : bool, optional, default True
392 Set to False for a DataFrame with a hierarchical index to print
393 every multiindex key at each row.
394 index_names : bool, optional, default True
395 Prints the names of the indexes.
396 justify : str, default None
397 How to justify the column labels. If None uses the option from
398 the print configuration (controlled by set_option), 'right' out
399 of the box. Valid values are
400
401 * left
402 * right
403 * center
404 * justify
405 * justify-all
406 * start
407 * end
408 * inherit
409 * match-parent
410 * initial
411 * unset.
412 max_rows : int, optional
413 Maximum number of rows to display in the console.
414 max_cols : int, optional
415 Maximum number of columns to display in the console.
416 show_dimensions : bool, default False
417 Display DataFrame dimensions (number of rows by number of columns).
418 decimal : str, default '.'
419 Character recognized as decimal separator, e.g. ',' in Europe.
420
421 Returns
422 -------
423 str or None
424 If buf is None, returns the result as a string. Otherwise returns
425 None.
426 """
427
428 def __init__(
429 self,
430 frame: DataFrame,
431 columns: Axes | None = None,
432 col_space: ColspaceArgType | None = None,
433 header: bool | SequenceNotStr[str] = True,
434 index: bool = True,
435 na_rep: str = "NaN",
436 formatters: FormattersType | None = None,
437 justify: str | None = None,
438 float_format: FloatFormatType | None = None,
439 sparsify: bool | None = None,
440 index_names: bool = True,
441 max_rows: int | None = None,
442 min_rows: int | None = None,
443 max_cols: int | None = None,
444 show_dimensions: bool | str = False,
445 decimal: str = ".",
446 bold_rows: bool = False,
447 escape: bool = True,
448 ) -> None:
449 self.frame = frame
450 self.columns = self._initialize_columns(columns)
451 self.col_space = self._initialize_colspace(col_space)
452 self.header = header
453 self.index = index
454 self.na_rep = na_rep
455 self.formatters = self._initialize_formatters(formatters)
456 self.justify = self._initialize_justify(justify)
457 self.float_format = self._validate_float_format(float_format)
458 self.sparsify = self._initialize_sparsify(sparsify)
459 self.show_index_names = index_names
460 self.decimal = decimal
461 self.bold_rows = bold_rows
462 self.escape = escape
463 self.max_rows = max_rows
464 self.min_rows = min_rows
465 self.max_cols = max_cols
466 self.show_dimensions = show_dimensions
467
468 self.max_cols_fitted = self._calc_max_cols_fitted()
469 self.max_rows_fitted = self._calc_max_rows_fitted()
470
471 self.tr_frame = self.frame
472 self.truncate()
473 self.adj = printing.get_adjustment()
474
475 def get_strcols(self) -> list[list[str]]:
476 """
477 Render a DataFrame to a list of columns (as lists of strings).
478 """
479 strcols = self._get_strcols_without_index()
480
481 if self.index:
482 str_index = self._get_formatted_index(self.tr_frame)
483 strcols.insert(0, str_index)
484
485 return strcols
486
487 @property
488 def should_show_dimensions(self) -> bool:
489 return self.show_dimensions is True or (
490 self.show_dimensions == "truncate" and self.is_truncated
491 )
492
493 @property
494 def is_truncated(self) -> bool:
495 return bool(self.is_truncated_horizontally or self.is_truncated_vertically)
496
497 @property
498 def is_truncated_horizontally(self) -> bool:
499 return bool(self.max_cols_fitted and (len(self.columns) > self.max_cols_fitted))
500
501 @property
502 def is_truncated_vertically(self) -> bool:
503 return bool(self.max_rows_fitted and (len(self.frame) > self.max_rows_fitted))
504
505 @property
506 def dimensions_info(self) -> str:
507 return f"\n\n[{len(self.frame)} rows x {len(self.frame.columns)} columns]"
508
509 @property
510 def has_index_names(self) -> bool:
511 return _has_names(self.frame.index)
512
513 @property
514 def has_column_names(self) -> bool:
515 return _has_names(self.frame.columns)
516
517 @property
518 def show_row_idx_names(self) -> bool:
519 return all((self.has_index_names, self.index, self.show_index_names))
520
521 @property
522 def show_col_idx_names(self) -> bool:
523 return all((self.has_column_names, self.show_index_names, self.header))
524
525 @property
526 def max_rows_displayed(self) -> int:
527 return min(self.max_rows or len(self.frame), len(self.frame))
528
529 def _initialize_sparsify(self, sparsify: bool | None) -> bool:
530 if sparsify is None:
531 return config["display"]["multi_sparse"]
532 return sparsify
533
534 def _initialize_formatters(
535 self, formatters: FormattersType | None
536 ) -> FormattersType:
537 if formatters is None:
538 return {}
539 elif len(self.frame.columns) == len(formatters) or isinstance(formatters, dict):
540 return formatters
541 else:
542 raise ValueError(
543 f"Formatters length({len(formatters)}) should match "
544 f"DataFrame number of columns({len(self.frame.columns)})"
545 )
546
547 def _initialize_justify(self, justify: str | None) -> str:
548 if justify is None:
549 return config["display"]["colheader_justify"]
550 else:
551 return justify
552
553 def _initialize_columns(self, columns: Axes | None) -> Index:
554 if columns is not None:
555 cols = ensure_index(columns)
556 self.frame = self.frame[cols]
557 return cols
558 else:
559 return self.frame.columns
560
561 def _initialize_colspace(self, col_space: ColspaceArgType | None) -> ColspaceType:
562 result: ColspaceType
563
564 if col_space is None:
565 result = {}
566 elif isinstance(col_space, (int, str)):
567 result = {"": col_space}
568 result.update(dict.fromkeys(self.frame.columns, col_space))
569 elif isinstance(col_space, Mapping):
570 for column in col_space.keys():
571 if column not in self.frame.columns and column != "":
572 raise ValueError(
573 f"Col_space is defined for an unknown column: {column}"
574 )
575 result = col_space
576 else:
577 if len(self.frame.columns) != len(col_space):
578 raise ValueError(
579 f"Col_space length({len(col_space)}) should match "
580 f"DataFrame number of columns({len(self.frame.columns)})"
581 )
582 result = dict(zip(self.frame.columns, col_space, strict=True))
583 return result
584
585 def _calc_max_cols_fitted(self) -> int | None:
586 """Number of columns fitting the screen."""
587 if not self._is_in_terminal():
588 return self.max_cols
589
590 width, _ = get_terminal_size()
591 if self._is_screen_narrow(width):
592 return width
593 else:
594 return self.max_cols
595
596 def _calc_max_rows_fitted(self) -> int | None:
597 """Number of rows with data fitting the screen."""
598 max_rows: int | None
599
600 if self._is_in_terminal():
601 _, height = get_terminal_size()
602 if self.max_rows == 0:
603 # rows available to fill with actual data
604 return height - self._get_number_of_auxiliary_rows()
605
606 if self._is_screen_short(height):
607 max_rows = height
608 else:
609 max_rows = self.max_rows
610 else:
611 max_rows = self.max_rows
612
613 return self._adjust_max_rows(max_rows)
614
615 def _adjust_max_rows(self, max_rows: int | None) -> int | None:
616 """Adjust max_rows using display logic.
617
618 See description here:
619 https://pandas.pydata.org/docs/dev/user_guide/options.html#frequently-used-options
620
621 GH #37359
622 """
623 if max_rows:
624 if (len(self.frame) > max_rows) and self.min_rows:
625 # if truncated, set max_rows showed to min_rows
626 max_rows = min(self.min_rows, max_rows)
627 return max_rows
628
629 def _is_in_terminal(self) -> bool:
630 """Check if the output is to be shown in terminal."""
631 return bool(self.max_cols == 0 or self.max_rows == 0)
632
633 def _is_screen_narrow(self, max_width) -> bool:
634 return bool(self.max_cols == 0 and len(self.frame.columns) > max_width)
635
636 def _is_screen_short(self, max_height) -> bool:
637 return bool(self.max_rows == 0 and len(self.frame) > max_height)
638
639 def _get_number_of_auxiliary_rows(self) -> int:
640 """Get number of rows occupied by prompt, dots and dimension info."""
641 dot_row = 1
642 prompt_row = 1
643 num_rows = dot_row + prompt_row
644
645 if self.show_dimensions:
646 num_rows += len(self.dimensions_info.splitlines())
647
648 if self.header:
649 num_rows += 1
650
651 return num_rows
652
653 def truncate(self) -> None:
654 """
655 Check whether the frame should be truncated. If so, slice the frame up.
656 """
657 if self.is_truncated_horizontally:
658 self._truncate_horizontally()
659
660 if self.is_truncated_vertically:
661 self._truncate_vertically()
662
663 def _truncate_horizontally(self) -> None:
664 """Remove columns, which are not to be displayed and adjust formatters.
665
666 Attributes affected:
667 - tr_frame
668 - formatters
669 - tr_col_num
670 """
671 assert self.max_cols_fitted is not None
672 col_num = self.max_cols_fitted // 2
673 if col_num >= 1:
674 _len = len(self.tr_frame.columns)
675 _slice = np.hstack([np.arange(col_num), np.arange(_len - col_num, _len)])
676 self.tr_frame = self.tr_frame.iloc[:, _slice]
677
678 # truncate formatter
679 if isinstance(self.formatters, (list, tuple)):
680 self.formatters = [
681 *self.formatters[:col_num],
682 *self.formatters[-col_num:],
683 ]
684 else:
685 col_num = cast("int", self.max_cols)
686 self.tr_frame = self.tr_frame.iloc[:, :col_num]
687 self.tr_col_num: int = col_num
688
689 def _truncate_vertically(self) -> None:
690 """Remove rows, which are not to be displayed.
691
692 Attributes affected:
693 - tr_frame
694 - tr_row_num
695 """
696 assert self.max_rows_fitted is not None
697 row_num = self.max_rows_fitted // 2
698 if row_num >= 1:
699 _len = len(self.tr_frame)
700 _slice = np.hstack([np.arange(row_num), np.arange(_len - row_num, _len)])
701 self.tr_frame = self.tr_frame.iloc[_slice]
702 else:
703 row_num = cast("int", self.max_rows)
704 self.tr_frame = self.tr_frame.iloc[:row_num, :]
705 self.tr_row_num = row_num
706
707 def _get_strcols_without_index(self) -> list[list[str]]:
708 strcols: list[list[str]] = []
709
710 if not is_list_like(self.header) and not self.header:
711 for i, c in enumerate(self.tr_frame):
712 fmt_values = self.format_col(i)
713 fmt_values, _ = _make_fixed_width(
714 strings=fmt_values,
715 justify=self.justify,
716 minimum=int(self.col_space.get(c, 0)),
717 adj=self.adj,
718 )
719 strcols.append(fmt_values)
720 return strcols
721
722 if is_list_like(self.header):
723 # cast here since can't be bool if is_list_like
724 self.header = cast("list[str]", self.header)
725 if len(self.header) != len(self.columns):
726 raise ValueError(
727 f"Writing {len(self.columns)} cols "
728 f"but got {len(self.header)} aliases"
729 )
730 str_columns = [[label] for label in self.header]
731 else:
732 str_columns = self._get_formatted_column_labels(self.tr_frame)
733
734 if self.show_row_idx_names:
735 for x in str_columns:
736 x.append("")
737
738 for i, c in enumerate(self.tr_frame):
739 cheader = str_columns[i]
740 header_colwidth = max(
741 int(self.col_space.get(c, 0)), *(self.adj.len(x) for x in cheader)
742 )
743 fmt_values = self.format_col(i)
744 fmt_values, max_len = _make_fixed_width(
745 fmt_values, self.justify, minimum=header_colwidth, adj=self.adj
746 )
747 max_len = max(max_len, header_colwidth)
748 cheader = self.adj.justify(cheader, max_len, mode=self.justify)
749 strcols.append(cheader + fmt_values)
750
751 return strcols
752
753 def format_col(self, i: int) -> list[str]:
754 frame = self.tr_frame
755 formatter = self._get_formatter(i)
756 return format_array(
757 frame._get_column_array(i),
758 formatter,
759 float_format=self.float_format,
760 na_rep=self.na_rep,
761 space=self.col_space.get(frame.columns[i]),
762 decimal=self.decimal,
763 leading_space=self.index,
764 )
765
766 def _get_formatter(self, i: str | int) -> Callable | None:
767 if isinstance(self.formatters, (list, tuple)):
768 if is_integer(i):
769 i = cast("int", i)
770 return self.formatters[i]
771 else:
772 return None
773 else:
774 if is_integer(i):
775 i = self.tr_frame.columns[i]
776 return self.formatters.get(i, None)
777
778 def _get_formatted_column_labels(self, frame: DataFrame) -> list[list[str]]:
779 from pandas.core.indexes.multi import sparsify_labels
780
781 columns = frame.columns
782
783 if isinstance(columns, MultiIndex):
784 fmt_columns = columns._format_multi(sparsify=False, include_names=False)
785 if self.sparsify and len(fmt_columns):
786 fmt_columns = sparsify_labels(fmt_columns)
787
788 str_columns = [list(x) for x in zip(*fmt_columns, strict=True)]
789 else:
790 fmt_columns = columns._format_flat(include_name=False)
791 str_columns = [
792 [
793 " " + x
794 if not self._get_formatter(i) and is_numeric_dtype(dtype)
795 else x
796 ]
797 for i, (x, dtype) in enumerate(
798 zip(fmt_columns, self.frame.dtypes, strict=False)
799 )
800 ]
801 return str_columns
802
803 def _get_formatted_index(self, frame: DataFrame) -> list[str]:
804 # Note: this is only used by to_string() and to_latex(), not by
805 # to_html(). so safe to cast col_space here.
806 col_space = {k: cast("int", v) for k, v in self.col_space.items()}
807 index = frame.index
808 columns = frame.columns
809 fmt = self._get_formatter("__index__")
810
811 if isinstance(index, MultiIndex):
812 fmt_index = index._format_multi(
813 sparsify=self.sparsify,
814 include_names=self.show_row_idx_names,
815 formatter=fmt,
816 )
817 else:
818 fmt_index = [
819 index._format_flat(include_name=self.show_row_idx_names, formatter=fmt)
820 ]
821
822 fmt_index = [
823 tuple(
824 _make_fixed_width(
825 list(x), justify="left", minimum=col_space.get("", 0), adj=self.adj
826 )[0]
827 )
828 for x in fmt_index
829 ]
830
831 adjoined = self.adj.adjoin(1, *fmt_index).split("\n")
832
833 # empty space for columns
834 if self.show_col_idx_names:
835 col_header = [format_name(x) for x in self._get_column_name_list()]
836 else:
837 col_header = [""] * columns.nlevels
838
839 if self.header:
840 return col_header + adjoined
841 else:
842 return adjoined
843
844 def _get_column_name_list(self) -> list[Hashable]:
845 names: list[Hashable] = []
846 columns = self.frame.columns
847 if isinstance(columns, MultiIndex):
848 names.extend(columns.names)
849 else:
850 names.append(columns.name)
851 return names
852
853 def _validate_float_format(
854 self, fmt: FloatFormatType | None
855 ) -> FloatFormatType | None:
856 """
857 Validates and processes the float_format argument.
858 Converts new-style format strings to callables.
859 """
860 if fmt is None or callable(fmt):
861 return fmt
862
863 if isinstance(fmt, str):
864 if "%" in fmt:
865 # Keeps old-style format strings as they are (C code handles them)
866 return fmt
867 else:
868 try:
869 _ = fmt.format(1.0) # Test with an arbitrary float
870 return fmt.format
871 except (ValueError, KeyError, IndexError) as e:
872 raise ValueError(f"Invalid new-style format string {fmt!r}") from e
873
874 raise ValueError("float_format must be a string or callable")
875
876
877class DataFrameRenderer:
878 """Class for creating dataframe output in multiple formats.
879
880 Called in pandas.core.generic.NDFrame:
881 - to_csv
882 - to_latex
883
884 Called in pandas.DataFrame:
885 - to_html
886 - to_string
887
888 Parameters
889 ----------
890 fmt : DataFrameFormatter
891 Formatter with the formatting options.
892 """
893
894 def __init__(self, fmt: DataFrameFormatter) -> None:
895 self.fmt = fmt
896
897 def to_html(
898 self,
899 buf: FilePath | WriteBuffer[str] | None = None,
900 encoding: str | None = None,
901 classes: str | list | tuple | None = None,
902 notebook: bool = False,
903 border: int | bool | None = None,
904 table_id: str | None = None,
905 render_links: bool = False,
906 ) -> str | None:
907 """
908 Render a DataFrame to an html table.
909
910 Parameters
911 ----------
912 buf : str, path object, file-like object, or None, default None
913 String, path object (implementing ``os.PathLike[str]``), or file-like
914 object implementing a string ``write()`` function. If None, the result is
915 returned as a string.
916 encoding : str, default “utf-8”
917 Set character encoding.
918 classes : str or list-like
919 classes to include in the `class` attribute of the opening
920 ``<table>`` tag, in addition to the default "dataframe".
921 notebook : {True, False}, optional, default False
922 Whether the generated HTML is for IPython Notebook.
923 border : int or bool
924 When an integer value is provided, it sets the border attribute in
925 the opening tag, specifying the thickness of the border.
926 If ``False`` or ``0`` is passed, the border attribute will not
927 be present in the ``<table>`` tag.
928 The default value for this parameter is governed by
929 ``pd.options.display.html.border``.
930 table_id : str, optional
931 A css id is included in the opening `<table>` tag if specified.
932 render_links : bool, default False
933 Convert URLs to HTML links.
934 """
935 from pandas.io.formats.html import (
936 HTMLFormatter,
937 NotebookFormatter,
938 )
939
940 Klass = NotebookFormatter if notebook else HTMLFormatter
941
942 html_formatter = Klass(
943 self.fmt,
944 classes=classes,
945 border=border,
946 table_id=table_id,
947 render_links=render_links,
948 )
949 string = html_formatter.to_string()
950 return save_to_buffer(string, buf=buf, encoding=encoding)
951
952 def to_string(
953 self,
954 buf: FilePath | WriteBuffer[str] | None = None,
955 encoding: str | None = None,
956 line_width: int | None = None,
957 ) -> str | None:
958 """
959 Render a DataFrame to a console-friendly tabular output.
960
961 Parameters
962 ----------
963 buf : str, path object, file-like object, or None, default None
964 String, path object (implementing ``os.PathLike[str]``), or file-like
965 object implementing a string ``write()`` function. If None, the result is
966 returned as a string.
967 encoding: str, default “utf-8”
968 Set character encoding.
969 line_width : int, optional
970 Width to wrap a line in characters.
971 """
972 from pandas.io.formats.string import StringFormatter
973
974 string_formatter = StringFormatter(self.fmt, line_width=line_width)
975 string = string_formatter.to_string()
976 return save_to_buffer(string, buf=buf, encoding=encoding)
977
978 def to_csv(
979 self,
980 path_or_buf: FilePath | WriteBuffer[bytes] | WriteBuffer[str] | None = None,
981 encoding: str | None = None,
982 sep: str = ",",
983 columns: Sequence[Hashable] | None = None,
984 index_label: IndexLabel | None = None,
985 mode: str = "w",
986 compression: CompressionOptions = "infer",
987 quoting: int | None = None,
988 quotechar: str = '"',
989 lineterminator: str | None = None,
990 chunksize: int | None = None,
991 date_format: str | None = None,
992 doublequote: bool = True,
993 escapechar: str | None = None,
994 errors: str = "strict",
995 storage_options: StorageOptions | None = None,
996 ) -> str | None:
997 """
998 Render dataframe as comma-separated file.
999 """
1000 from pandas.io.formats.csvs import CSVFormatter
1001
1002 if path_or_buf is None:
1003 created_buffer = True
1004 path_or_buf = StringIO()
1005 else:
1006 created_buffer = False
1007
1008 csv_formatter = CSVFormatter(
1009 path_or_buf=path_or_buf,
1010 lineterminator=lineterminator,
1011 sep=sep,
1012 encoding=encoding,
1013 errors=errors,
1014 compression=compression,
1015 quoting=quoting,
1016 cols=columns,
1017 index_label=index_label,
1018 mode=mode,
1019 chunksize=chunksize,
1020 quotechar=quotechar,
1021 date_format=date_format,
1022 doublequote=doublequote,
1023 escapechar=escapechar,
1024 storage_options=storage_options,
1025 formatter=self.fmt,
1026 )
1027 csv_formatter.save()
1028
1029 if created_buffer:
1030 assert isinstance(path_or_buf, StringIO)
1031 content = path_or_buf.getvalue()
1032 path_or_buf.close()
1033 return content
1034
1035 return None
1036
1037
1038def save_to_buffer(
1039 string: str,
1040 buf: FilePath | WriteBuffer[str] | None = None,
1041 encoding: str | None = None,
1042) -> str | None:
1043 """
1044 Perform serialization. Write to buf or return as string if buf is None.
1045 """
1046 with _get_buffer(buf, encoding=encoding) as fd:
1047 fd.write(string)
1048 if buf is None:
1049 # error: "WriteBuffer[str]" has no attribute "getvalue"
1050 return fd.getvalue() # type: ignore[attr-defined]
1051 return None
1052
1053
1054@contextmanager
1055def _get_buffer(
1056 buf: FilePath | WriteBuffer[str] | None, encoding: str | None = None
1057) -> Generator[WriteBuffer[str]] | Generator[StringIO]:
1058 """
1059 Context manager to open, yield and close buffer for filenames or Path-like
1060 objects, otherwise yield buf unchanged.
1061 """
1062 if buf is not None:
1063 buf = stringify_path(buf)
1064 else:
1065 buf = StringIO()
1066
1067 if encoding is None:
1068 encoding = "utf-8"
1069 elif not isinstance(buf, str):
1070 raise ValueError("buf is not a file name and encoding is specified.")
1071
1072 if hasattr(buf, "write"):
1073 # Incompatible types in "yield" (actual type "Union[str, WriteBuffer[str],
1074 # StringIO]", expected type "Union[WriteBuffer[str], StringIO]")
1075 yield buf # type: ignore[misc]
1076 elif isinstance(buf, str):
1077 check_parent_directory(str(buf))
1078 with open(buf, "w", encoding=encoding, newline="") as f:
1079 # GH#30034 open instead of codecs.open prevents a file leak
1080 # if we have an invalid encoding argument.
1081 # newline="" is needed to roundtrip correctly on
1082 # windows test_to_latex_filename
1083 yield f
1084 else:
1085 raise TypeError("buf is not a file name and it has no write method")
1086
1087
1088# ----------------------------------------------------------------------
1089# Array formatters
1090
1091
1092def format_name(name: Hashable) -> str:
1093 """
1094 Render an Index or Series name for display.
1095
1096 Float names follow ``display.precision`` like float values do (GH#25917).
1097 """
1098 if name is None:
1099 return ""
1100 if lib.is_float(name) and not isna(name):
1101 return format_array(np.array([name]), None, leading_space=False)[0]
1102 return printing.pprint_thing(name, escape_chars=("\t", "\r", "\n"))
1103
1104
1105def format_array(
1106 values: ArrayLike,
1107 formatter: Callable | None,
1108 float_format: FloatFormatType | None = None,
1109 na_rep: str = "NaN",
1110 digits: int | None = None,
1111 space: str | int | None = None,
1112 justify: str = "right",
1113 decimal: str = ".",
1114 leading_space: bool | None = True,
1115 quoting: int | None = None,
1116 fallback_formatter: Callable | None = None,
1117) -> list[str]:
1118 """
1119 Format an array for printing.
1120
1121 Parameters
1122 ----------
1123 values : np.ndarray or ExtensionArray
1124 formatter
1125 float_format
1126 na_rep
1127 digits
1128 space
1129 justify
1130 decimal
1131 leading_space : bool, optional, default True
1132 Whether the array should be formatted with a leading space.
1133 When an array as a column of a Series or DataFrame, we do want
1134 the leading space to pad between columns.
1135
1136 When formatting an Index subclass
1137 (e.g. IntervalIndex._get_values_for_csv), we don't want the
1138 leading space since it should be left-aligned.
1139 fallback_formatter
1140
1141 Returns
1142 -------
1143 List[str]
1144 """
1145 fmt_klass: type[_GenericArrayFormatter]
1146 if lib.is_np_dtype(values.dtype, "M") or isinstance(values.dtype, DatetimeTZDtype):
1147 fmt_klass = _Datetime64Formatter
1148 values = cast("DatetimeArray", values)
1149 if na_rep == "NaN":
1150 na_rep = "NaT"
1151 elif lib.is_np_dtype(values.dtype, "m"):
1152 fmt_klass = _Timedelta64Formatter
1153 values = cast("TimedeltaArray", values)
1154 if na_rep == "NaN":
1155 na_rep = "NaT"
1156 elif isinstance(values.dtype, ExtensionDtype):
1157 fmt_klass = _ExtensionArrayFormatter
1158 elif lib.is_np_dtype(values.dtype, "fc"):
1159 fmt_klass = FloatArrayFormatter
1160 elif lib.is_np_dtype(values.dtype, "iu"):
1161 fmt_klass = _IntArrayFormatter
1162 else:
1163 fmt_klass = _GenericArrayFormatter
1164
1165 if space is None:
1166 space = 12
1167
1168 if float_format is None:
1169 float_format = config["display"]["float_format"]
1170
1171 if digits is None:
1172 digits = config["display"]["precision"]
1173
1174 fmt_obj = fmt_klass(
1175 values,
1176 digits=digits,
1177 na_rep=na_rep,
1178 float_format=float_format,
1179 formatter=formatter,
1180 space=space,
1181 justify=justify,
1182 decimal=decimal,
1183 leading_space=leading_space,
1184 quoting=quoting,
1185 fallback_formatter=fallback_formatter,
1186 )
1187
1188 return fmt_obj.get_result()
1189
1190
1191class _GenericArrayFormatter:
1192 def __init__(
1193 self,
1194 values: ArrayLike,
1195 digits: int = 7,
1196 formatter: Callable | None = None,
1197 na_rep: str = "NaN",
1198 space: str | int = 12,
1199 float_format: FloatFormatType | None = None,
1200 justify: str = "right",
1201 decimal: str = ".",
1202 quoting: int | None = None,
1203 fixed_width: bool = True,
1204 leading_space: bool | None = True,
1205 fallback_formatter: Callable | None = None,
1206 ) -> None:
1207 self.values = values
1208 self.digits = digits
1209 self.na_rep = na_rep
1210 self.space = space
1211 self.formatter = formatter
1212 self.float_format = float_format
1213 self.justify = justify
1214 self.decimal = decimal
1215 self.quoting = quoting
1216 self.fixed_width = fixed_width
1217 self.leading_space = leading_space
1218 self.fallback_formatter = fallback_formatter
1219
1220 def get_result(self) -> list[str]:
1221 fmt_values = self._format_strings()
1222 result, _ = _make_fixed_width(fmt_values, self.justify)
1223 return result
1224
1225 def _format_strings(self) -> list[str]:
1226 if self.float_format is None:
1227 float_format = config["display"]["float_format"]
1228 if float_format is None:
1229 precision = config["display"]["precision"]
1230 float_format = lambda x: _trim_zeros_single_float(
1231 f"{x: .{precision:d}f}"
1232 )
1233 else:
1234 float_format = self.float_format
1235
1236 if self.formatter is not None:
1237 formatter = self.formatter
1238 elif self.fallback_formatter is not None:
1239 formatter = self.fallback_formatter
1240 else:
1241 quote_strings = self.quoting is not None and self.quoting != QUOTE_NONE
1242 formatter = partial(
1243 printing.pprint_thing,
1244 escape_chars=("\t", "\r", "\n"),
1245 quote_strings=quote_strings,
1246 )
1247
1248 def _format(x):
1249 if self.na_rep is not None and is_scalar(x) and isna(x):
1250 if x is None:
1251 return "None"
1252 elif x is NA:
1253 return str(NA)
1254 elif x is NaT or isinstance(x, (np.datetime64, np.timedelta64)):
1255 return "NaT"
1256 return self.na_rep
1257 elif isinstance(x, PandasObject):
1258 return str(x)
1259 else:
1260 # object dtype
1261 return str(formatter(x))
1262
1263 vals = self.values
1264 if not isinstance(vals, np.ndarray):
1265 raise TypeError(
1266 "ExtensionArray formatting should use _ExtensionArrayFormatter"
1267 )
1268 inferred = lib.map_infer(vals, is_float)
1269 is_float_type = (
1270 inferred # type: ignore[operator]
1271 # vals may have 2 or more dimensions
1272 & np.all(notna(vals), axis=tuple(range(1, len(vals.shape))))
1273 )
1274 leading_space = self.leading_space
1275 if leading_space is None:
1276 leading_space = is_float_type.any() # type: ignore[assignment]
1277
1278 fmt_values = []
1279 for i, v in enumerate(vals):
1280 if self.formatter is not None:
1281 # match the int/float/datetime formatters, GH#26002
1282 fmt_values.append(_format(v))
1283 elif not is_float_type[i] and leading_space:
1284 fmt_values.append(f" {_format(v)}")
1285 elif is_float_type[i]:
1286 fmt_values.append(float_format(v))
1287 else:
1288 if leading_space is False:
1289 # False specifically, so that the default is
1290 # to include a space if we get here.
1291 tpl = "{v}"
1292 else:
1293 tpl = " {v}"
1294 fmt_values.append(tpl.format(v=_format(v)))
1295
1296 return fmt_values
1297
1298
1299class FloatArrayFormatter(_GenericArrayFormatter):
1300 def __init__(self, *args, **kwargs) -> None:
1301 super().__init__(*args, **kwargs)
1302
1303 # float_format is expected to be a string
1304 # formatter should be used to pass a function
1305 if self.float_format is not None and self.formatter is None:
1306 # GH21625, GH22270
1307 self.fixed_width = False
1308 if callable(self.float_format):
1309 self.formatter = self.float_format
1310 self.float_format = None
1311
1312 def _value_formatter(
1313 self,
1314 float_format: FloatFormatType | None = None,
1315 threshold: float | None = None,
1316 ) -> Callable:
1317 """Returns a function to be applied on each value to format it"""
1318 # the float_format parameter supersedes self.float_format
1319 if float_format is None:
1320 float_format = self.float_format
1321
1322 # we are going to compose different functions, to first convert to
1323 # a string, then replace the decimal symbol, and finally chop according
1324 # to the threshold
1325
1326 # when there is no float_format, we use str instead of '%g'
1327 # because str(0.0) = '0.0' while '%g' % 0.0 = '0'
1328 if float_format:
1329
1330 def base_formatter(v): # pyright: ignore[reportRedeclaration]
1331 assert float_format is not None # for mypy
1332 # error: "str" not callable
1333 # error: Unexpected keyword argument "value" for "__call__" of
1334 # "EngFormatter"
1335 return (
1336 float_format(value=v) # type: ignore[operator,call-arg]
1337 if notna(v)
1338 else self.na_rep
1339 )
1340
1341 else:
1342
1343 def base_formatter(v):
1344 return str(v) if notna(v) else self.na_rep
1345
1346 if self.decimal != ".":
1347
1348 def decimal_formatter(v):
1349 return base_formatter(v).replace(".", self.decimal, 1)
1350
1351 else:
1352 decimal_formatter = base_formatter
1353
1354 if threshold is None:
1355 return decimal_formatter
1356
1357 def formatter(value):
1358 if notna(value):
1359 if abs(value) > threshold:
1360 return decimal_formatter(value)
1361 else:
1362 return decimal_formatter(0.0)
1363 else:
1364 return self.na_rep
1365
1366 return formatter
1367
1368 def get_result_as_array(self) -> np.ndarray:
1369 """
1370 Returns the float values converted into strings using
1371 the parameters given at initialisation, as a numpy array
1372 """
1373
1374 def format_with_na_rep(
1375 values: ArrayLike, formatter: Callable, na_rep: str
1376 ) -> np.ndarray:
1377 mask = isna(values)
1378 formatted = np.array(
1379 [
1380 formatter(val) if not m else na_rep
1381 for val, m in zip(values.ravel(), mask.ravel(), strict=True)
1382 ]
1383 ).reshape(values.shape)
1384 return formatted
1385
1386 def format_complex_with_na_rep(
1387 values: ArrayLike, formatter: Callable, na_rep: str
1388 ) -> np.ndarray:
1389 real_values = np.real(values).ravel() # type: ignore[arg-type]
1390 imag_values = np.imag(values).ravel() # type: ignore[arg-type]
1391 real_mask, imag_mask = isna(real_values), isna(imag_values)
1392 formatted_lst = []
1393 for val, real_val, imag_val, re_isna, im_isna in zip(
1394 values.ravel(),
1395 real_values,
1396 imag_values,
1397 real_mask,
1398 imag_mask,
1399 strict=True,
1400 ):
1401 if not re_isna and not im_isna:
1402 formatted_lst.append(formatter(val))
1403 elif not re_isna: # xxx+nanj
1404 formatted_lst.append(f"{formatter(real_val)}+{na_rep}j")
1405 elif not im_isna: # nan[+/-]xxxj
1406 # The imaginary part may either start with a "-" or a space
1407 imag_formatted = formatter(imag_val).strip()
1408 if imag_formatted.startswith("-"):
1409 formatted_lst.append(f"{na_rep}{imag_formatted}j")
1410 else:
1411 formatted_lst.append(f"{na_rep}+{imag_formatted}j")
1412 else: # nan+nanj
1413 formatted_lst.append(f"{na_rep}+{na_rep}j")
1414 return np.array(formatted_lst).reshape(values.shape)
1415
1416 if self.formatter is not None:
1417 return format_with_na_rep(self.values, self.formatter, self.na_rep)
1418
1419 if self.fixed_width:
1420 threshold = config["display"]["chop_threshold"]
1421 else:
1422 threshold = None
1423
1424 # if we have a fixed_width, we'll need to try different float_format
1425 def format_values_with(float_format):
1426 formatter = self._value_formatter(float_format, threshold)
1427
1428 # default formatter leaves a space to the left when formatting
1429 # floats, must be consistent for left-justifying NaNs (GH #25061)
1430 na_rep = " " + self.na_rep if self.justify == "left" else self.na_rep
1431
1432 # different formatting strategies for complex and non-complex data
1433 # need to distinguish complex and float NaNs (GH #53762)
1434 values = self.values
1435 is_complex = is_complex_dtype(values)
1436
1437 # separate the wheat from the chaff
1438 if is_complex:
1439 values = format_complex_with_na_rep(values, formatter, na_rep)
1440 else:
1441 values = format_with_na_rep(values, formatter, na_rep)
1442
1443 if self.fixed_width:
1444 if is_complex:
1445 result = _trim_zeros_complex(values, self.decimal)
1446 else:
1447 result = _trim_zeros_float(values, self.decimal)
1448 return np.asarray(result, dtype="object")
1449
1450 return values
1451
1452 # There is a special default string when we are fixed-width
1453 # The default is otherwise to use str instead of a formatting string
1454 float_format: FloatFormatType | None
1455 if self.float_format is None:
1456 if self.fixed_width:
1457 if self.leading_space is True:
1458 fmt_str = "{value: .{digits:d}f}"
1459 else:
1460 fmt_str = "{value:.{digits:d}f}"
1461 float_format = partial(fmt_str.format, digits=self.digits)
1462 else:
1463 float_format = self.float_format
1464 else:
1465 float_format = lambda value: self.float_format % value
1466
1467 formatted_values = format_values_with(float_format)
1468
1469 if not self.fixed_width:
1470 return formatted_values
1471
1472 # we need do convert to engineering format if some values are too small
1473 # and would appear as 0, or if some values are too big and take too
1474 # much space
1475
1476 if len(formatted_values) > 0:
1477 maxlen = max(len(x) for x in formatted_values)
1478 too_long = maxlen > self.digits + 6
1479 else:
1480 too_long = False
1481
1482 abs_vals = np.abs(self.values)
1483 # this is pretty arbitrary for now
1484 # large values: more that 8 characters including decimal symbol
1485 # and first digit, hence > 1e6
1486 has_large_values = (abs_vals > 1e6).any()
1487 has_small_values = ((abs_vals < 10 ** (-self.digits)) & (abs_vals > 0)).any()
1488
1489 if has_small_values or (too_long and has_large_values):
1490 if self.leading_space is True:
1491 fmt_str = "{value: .{digits:d}e}"
1492 else:
1493 fmt_str = "{value:.{digits:d}e}"
1494 float_format = partial(fmt_str.format, digits=self.digits)
1495 formatted_values = format_values_with(float_format)
1496
1497 return formatted_values
1498
1499 def _format_strings(self) -> list[str]:
1500 return list(self.get_result_as_array())
1501
1502
1503class _IntArrayFormatter(_GenericArrayFormatter):
1504 def _format_strings(self) -> list[str]:
1505 if self.leading_space is False:
1506 formatter_str = lambda x: f"{x:d}"
1507 else:
1508 formatter_str = lambda x: f"{x: d}"
1509 formatter = self.formatter or formatter_str
1510 fmt_values = [formatter(x) for x in self.values]
1511 return fmt_values
1512
1513
1514class _Datetime64Formatter(_GenericArrayFormatter):
1515 values: DatetimeArray
1516
1517 def __init__(
1518 self,
1519 values: DatetimeArray,
1520 na_rep: str = "NaT",
1521 date_format: None = None,
1522 **kwargs,
1523 ) -> None:
1524 super().__init__(values, na_rep=na_rep, **kwargs)
1525 self.date_format = date_format
1526
1527 def _format_strings(self) -> list[str]:
1528 values = self.values
1529
1530 if self.formatter is not None:
1531 return [self.formatter(x) for x in values]
1532
1533 fmt_values = values._format_native_types(
1534 na_rep=self.na_rep, date_format=self.date_format
1535 )
1536 return fmt_values.tolist()
1537
1538
1539class _ExtensionArrayFormatter(_GenericArrayFormatter):
1540 values: ExtensionArray
1541
1542 def _format_strings(self) -> list[str]:
1543 values = self.values
1544
1545 formatter = self.formatter
1546 fallback_formatter = None
1547 if formatter is None:
1548 fallback_formatter = values._formatter(boxed=True)
1549
1550 if isinstance(values, Categorical):
1551 # Categorical is special for now, so that we can preserve tzinfo
1552 array = values._internal_get_values()
1553 else:
1554 array = np.asarray(values, dtype=object)
1555
1556 fmt_values = format_array(
1557 array,
1558 formatter,
1559 float_format=self.float_format,
1560 na_rep=self.na_rep,
1561 digits=self.digits,
1562 space=self.space,
1563 justify=self.justify,
1564 decimal=self.decimal,
1565 leading_space=self.leading_space,
1566 quoting=self.quoting,
1567 fallback_formatter=fallback_formatter,
1568 )
1569 return fmt_values
1570
1571
1572def format_percentiles(
1573 percentiles: np.ndarray | Sequence[float],
1574) -> list[str]:
1575 """
1576 Outputs rounded and formatted percentiles.
1577
1578 Parameters
1579 ----------
1580 percentiles : list-like, containing floats from interval [0,1]
1581
1582 Returns
1583 -------
1584 formatted : list of strings
1585
1586 Notes
1587 -----
1588 Rounding precision is chosen so that: (1) if any two elements of
1589 ``percentiles`` differ, they remain different after rounding
1590 (2) no entry is *rounded* to 0% or 100%.
1591 Any non-integer is always rounded to at least 1 decimal place.
1592
1593 Examples
1594 --------
1595 Keeps all entries different after rounding:
1596
1597 >>> format_percentiles([0.01999, 0.02001, 0.5, 0.666666, 0.9999])
1598 ['1.999%', '2.001%', '50%', '66.667%', '99.99%']
1599
1600 No element is rounded to 0% or 100% (unless already equal to it).
1601 Duplicates are allowed:
1602
1603 >>> format_percentiles([0, 0.5, 0.02001, 0.5, 0.666666, 0.9999])
1604 ['0%', '50%', '2.0%', '50%', '66.67%', '99.99%']
1605 """
1606 if len(percentiles) == 0:
1607 return []
1608
1609 percentiles = np.asarray(percentiles)
1610
1611 # It checks for np.nan as well
1612 if (
1613 not is_numeric_dtype(percentiles)
1614 or not np.all(percentiles >= 0)
1615 or not np.all(percentiles <= 1)
1616 ):
1617 raise ValueError("percentiles should all be in the interval [0,1]")
1618
1619 percentiles = 100 * percentiles
1620 prec = get_precision(percentiles)
1621 percentiles_round_type = percentiles.round(prec).astype(int)
1622
1623 int_idx = np.isclose(percentiles_round_type, percentiles)
1624
1625 if np.all(int_idx):
1626 out = percentiles_round_type.astype(str)
1627 return [i + "%" for i in out]
1628
1629 unique_pcts = np.unique(percentiles)
1630 prec = get_precision(unique_pcts)
1631 out = np.empty_like(percentiles, dtype=object)
1632 out[int_idx] = percentiles[int_idx].round().astype(int).astype(str)
1633
1634 out[~int_idx] = percentiles[~int_idx].round(prec).astype(str)
1635 return [i + "%" for i in out]
1636
1637
1638def get_precision(array: np.ndarray | Sequence[float]) -> int:
1639 to_begin = array[0] if array[0] > 0 else None
1640 to_end = 100 - array[-1] if array[-1] < 100 else None
1641 diff = np.ediff1d(array, to_begin=to_begin, to_end=to_end)
1642 diff = abs(diff)
1643 prec = -np.floor(np.log10(np.min(diff))).astype(int)
1644 prec = max(1, prec)
1645 return prec
1646
1647
1648def _format_datetime64(x: NaTType | Timestamp, na_rep: str = "NaT") -> str:
1649 if x is NaT:
1650 return na_rep
1651
1652 # Timestamp.__str__ falls back to datetime.datetime.__str__ = isoformat(sep=' ')
1653 # so it already uses string formatting rather than strftime (faster).
1654 return str(x)
1655
1656
1657def _format_datetime64_dateonly(
1658 x: NaTType | Timestamp,
1659 na_rep: str = "NaT",
1660 date_format: str | None = None,
1661) -> str:
1662 if isinstance(x, NaTType):
1663 return na_rep
1664
1665 if date_format:
1666 return x.strftime(date_format)
1667 else:
1668 # Timestamp._date_repr relies on string formatting (faster than strftime)
1669 return x._date_repr
1670
1671
1672def get_format_datetime64(
1673 is_dates_only: bool, na_rep: str = "NaT", date_format: str | None = None
1674) -> Callable:
1675 """Return a formatter callable taking a datetime64 as input and providing
1676 a string as output"""
1677
1678 if is_dates_only:
1679 return lambda x: _format_datetime64_dateonly(
1680 x, na_rep=na_rep, date_format=date_format
1681 )
1682 else:
1683 return lambda x: _format_datetime64(x, na_rep=na_rep)
1684
1685
1686class _Timedelta64Formatter(_GenericArrayFormatter):
1687 values: TimedeltaArray
1688
1689 def __init__(
1690 self,
1691 values: TimedeltaArray,
1692 na_rep: str = "NaT",
1693 **kwargs,
1694 ) -> None:
1695 super().__init__(values, na_rep=na_rep, **kwargs)
1696
1697 def _format_strings(self) -> list[str]:
1698 formatter = self.formatter or get_format_timedelta64(
1699 self.values, na_rep=self.na_rep, box=False
1700 )
1701 return [formatter(x) for x in self.values]
1702
1703
1704def get_format_timedelta64(
1705 values: TimedeltaArray,
1706 na_rep: str | float = "NaT",
1707 box: bool = False,
1708) -> Callable:
1709 """
1710 Return a formatter function for a range of timedeltas.
1711 These will all have the same format argument
1712
1713 If box, then show the return in quotes
1714 """
1715 if values._is_dates_only:
1716 format = None
1717 else:
1718 i8 = values.asi8
1719 vals = i8[i8 != iNaT]
1720 if vals.size == 0 or not (vals % periods_per_second(values._creso)).any():
1721 format = "long"
1722 elif NpyDatetimeUnit.NPY_FR_ns.value == values._creso and (vals % 1_000).any():
1723 format = "all" # nanosecond precision → 9 digits
1724 else:
1725 format = "us" # microsecond precision → 6 digits
1726
1727 def _formatter(x):
1728 if x is None or (is_scalar(x) and isna(x)):
1729 return na_rep
1730
1731 if not isinstance(x, Timedelta):
1732 x = Timedelta(x)
1733
1734 # Timedelta._repr_base uses string formatting (faster than strftime)
1735 result = x._repr_base(format=format)
1736 if box:
1737 result = f"'{result}'"
1738 return result
1739
1740 return _formatter
1741
1742
1743def _make_fixed_width(
1744 strings: list[str],
1745 justify: str = "right",
1746 minimum: int | None = None,
1747 adj: printing._TextAdjustment | None = None,
1748) -> tuple[list[str], int]:
1749 if adj is None:
1750 adjustment = printing.get_adjustment()
1751 else:
1752 adjustment = adj
1753
1754 if len(strings) == 0 or justify == "all":
1755 max_len = max(adjustment.len(x) for x in strings) if strings else 0
1756 return strings, max_len
1757
1758 max_len = max(adjustment.len(x) for x in strings)
1759
1760 if minimum is not None:
1761 max_len = max(minimum, max_len)
1762
1763 conf_max = config["display"]["max_colwidth"]
1764 if conf_max is not None and max_len > conf_max:
1765 max_len = conf_max
1766
1767 if conf_max is not None:
1768 # When the column is too narrow to hold any text alongside the "..."
1769 # placeholder, show as much of the placeholder as fits (GH#16097).
1770 keep = max(max_len - 3, 0)
1771 placeholder = "..."[: max_len - keep]
1772 strings = [
1773 x[:keep] + placeholder if adjustment.len(x) > max_len else x
1774 for x in strings
1775 ]
1776
1777 result = adjustment.justify(strings, max_len, mode=justify)
1778 return result, max_len
1779
1780
1781def _trim_zeros_complex(str_complexes: ArrayLike, decimal: str = ".") -> list[str]:
1782 """
1783 Separates the real and imaginary parts from the complex number, and
1784 executes the _trim_zeros_float method on each of those.
1785 """
1786 real_part, imag_part = [], []
1787 for x in str_complexes:
1788 # Complex numbers are represented as "(-)xxx(+/-)xxxj"
1789 # The split will give [{"", "-"}, "xxx", "+/-", "xxx", "j", ""]
1790 # Therefore, the imaginary part is the 4th and 3rd last elements,
1791 # and the real part is everything before the imaginary part
1792 trimmed = re.split(r"(?<!e)([j+-])", x)
1793 real_part.append("".join(trimmed[:-4]))
1794 imag_part.append("".join(trimmed[-4:-2]))
1795
1796 # We want to align the lengths of the real and imaginary parts of each complex
1797 # number, as well as the lengths the real (resp. complex) parts of all numbers
1798 # in the array
1799 n = len(str_complexes)
1800 padded_parts = _trim_zeros_float(real_part + imag_part, decimal)
1801 if len(padded_parts) == 0:
1802 return []
1803 padded_length = max(len(part) for part in padded_parts) - 1
1804 padded = [
1805 real_pt # real part, possibly NaN
1806 + imag_pt[0] # +/-
1807 + f"{imag_pt[1:]:>{padded_length}}" # complex part (no sign), possibly nan
1808 + "j"
1809 for real_pt, imag_pt in zip(padded_parts[:n], padded_parts[n:], strict=True)
1810 ]
1811 return padded
1812
1813
1814def _trim_zeros_single_float(str_float: str) -> str:
1815 """
1816 Trims trailing zeros after a decimal point,
1817 leaving just one if necessary.
1818 """
1819 str_float = str_float.rstrip("0")
1820 if str_float.endswith("."):
1821 str_float += "0"
1822
1823 return str_float
1824
1825
1826_NUMBER_WITH_DECIMAL_RE = re.compile(r"^\s*[+-]?[0-9]+\.[0-9]*$")
1827
1828
1829def _trim_zeros_float(
1830 str_floats: ArrayLike | list[str], decimal: str = "."
1831) -> list[str]:
1832 """
1833 Trims the maximum number of trailing zeros equally from
1834 all numbers containing decimals, leaving just one if
1835 necessary.
1836 """
1837 if decimal != ".":
1838 number_regex = re.compile(rf"^\s*[+-]?[0-9]+\{decimal}[0-9]*$")
1839 else:
1840 number_regex = _NUMBER_WITH_DECIMAL_RE
1841
1842 # Identify which elements are decimal numbers once, up front
1843 is_number = [number_regex.match(x) is not None for x in str_floats]
1844
1845 if not any(is_number):
1846 return list(str_floats)
1847
1848 trimmed = list(str_floats)
1849
1850 # Trim trailing zeros from all decimal numbers equally
1851 while True:
1852 # Check if all decimal numbers end in "0"
1853 if not all(
1854 x.endswith("0")
1855 for x, is_num in zip(trimmed, is_number, strict=True)
1856 if is_num
1857 ):
1858 break
1859 trimmed = [
1860 x[:-1] if is_num else x
1861 for x, is_num in zip(trimmed, is_number, strict=True)
1862 ]
1863
1864 # Leave one 0 after the decimal point if needed
1865 result = [
1866 x + "0" if is_num and x.endswith(decimal) else x
1867 for x, is_num in zip(trimmed, is_number, strict=True)
1868 ]
1869 return result
1870
1871
1872def _has_names(index: Index) -> bool:
1873 if isinstance(index, MultiIndex):
1874 return com.any_not_none(*index.names)
1875 else:
1876 return index.name is not None
1877
1878
1879class EngFormatter:
1880 """
1881 Formats float values according to engineering format.
1882
1883 Based on matplotlib.ticker.EngFormatter
1884 """
1885
1886 # The SI engineering prefixes
1887 ENG_PREFIXES = {
1888 -24: "y",
1889 -21: "z",
1890 -18: "a",
1891 -15: "f",
1892 -12: "p",
1893 -9: "n",
1894 -6: "u",
1895 -3: "m",
1896 0: "",
1897 3: "k",
1898 6: "M",
1899 9: "G",
1900 12: "T",
1901 15: "P",
1902 18: "E",
1903 21: "Z",
1904 24: "Y",
1905 }
1906
1907 def __init__(
1908 self, accuracy: int | None = None, use_eng_prefix: bool = False
1909 ) -> None:
1910 self.accuracy = accuracy
1911 self.use_eng_prefix = use_eng_prefix
1912
1913 def __call__(self, num: float) -> str:
1914 """
1915 Formats a number in engineering notation, appending a letter
1916 representing the power of 1000 of the original number. Some examples:
1917 >>> format_eng = EngFormatter(accuracy=0, use_eng_prefix=True)
1918 >>> format_eng(0)
1919 ' 0'
1920 >>> format_eng = EngFormatter(accuracy=1, use_eng_prefix=True)
1921 >>> format_eng(1_000_000)
1922 ' 1.0M'
1923 >>> format_eng = EngFormatter(accuracy=2, use_eng_prefix=False)
1924 >>> format_eng("-1e-6")
1925 '-1.00E-06'
1926
1927 @param num: the value to represent
1928 @type num: either a numeric value or a string that can be converted to
1929 a numeric value (as per decimal.Decimal constructor)
1930
1931 @return: engineering formatted string
1932 """
1933 dnum = Decimal(str(num))
1934
1935 if Decimal.is_nan(dnum):
1936 return "NaN"
1937
1938 if Decimal.is_infinite(dnum):
1939 return "inf"
1940
1941 sign = 1
1942
1943 if dnum < 0: # pragma: no cover
1944 sign = -1
1945 dnum = -dnum
1946
1947 if dnum != 0:
1948 pow10 = Decimal(int(math.floor(dnum.log10() / 3) * 3))
1949 else:
1950 pow10 = Decimal(0)
1951
1952 pow10 = pow10.min(max(self.ENG_PREFIXES.keys()))
1953 pow10 = pow10.max(min(self.ENG_PREFIXES.keys()))
1954 int_pow10 = int(pow10)
1955
1956 if self.use_eng_prefix:
1957 prefix = self.ENG_PREFIXES[int_pow10]
1958 elif int_pow10 < 0:
1959 prefix = f"E-{-int_pow10:02d}"
1960 else:
1961 prefix = f"E+{int_pow10:02d}"
1962
1963 mant = sign * dnum / (10**pow10)
1964
1965 if self.accuracy is None: # pragma: no cover
1966 format_str = "{mant: g}{prefix}"
1967 else:
1968 format_str = f"{{mant: .{self.accuracy:d}f}}{{prefix}}"
1969
1970 formatted = format_str.format(mant=mant, prefix=prefix)
1971
1972 return formatted
1973
1974
1975@set_module("pandas")
1976def set_eng_float_format(accuracy: int = 3, use_eng_prefix: bool = False) -> None:
1977 """
1978 Format float representation in DataFrame with SI notation.
1979
1980 .. deprecated:: 3.1.0
1981 Use ``pd.set_option("display.precision", N)`` to control decimal
1982 precision, or pass a custom callable to
1983 ``pd.set_option("display.float_format", func)``.
1984
1985 Sets the floating-point display format for ``DataFrame`` objects using engineering
1986 notation (SI units), allowing easier readability of values across wide ranges.
1987
1988 Parameters
1989 ----------
1990 accuracy : int, default 3
1991 Number of decimal digits after the floating point.
1992 use_eng_prefix : bool, default False
1993 Whether to represent a value with SI prefixes.
1994
1995 Returns
1996 -------
1997 None
1998 This method does not return a value. it updates the global display format
1999 for floats in DataFrames.
2000
2001 See Also
2002 --------
2003 set_option : Set the value of the specified option or options.
2004 reset_option : Reset one or more options to their default value.
2005
2006 Examples
2007 --------
2008 Use ``pd.set_option("display.precision", N)`` to control decimal
2009 precision instead:
2010
2011 >>> with pd.option_context("display.precision", 3):
2012 ... print(pd.DataFrame([1e-9, 1e-3, 1, 1e3, 1e6]))
2013 0
2014 0 1.000e-09
2015 1 1.000e-03
2016 2 1.000e+00
2017 3 1.000e+03
2018 4 1.000e+06
2019 """
2020 warnings.warn(
2021 "set_eng_float_format is deprecated and will be removed in a future "
2022 "version. Use pd.set_option('display.precision', N) to control decimal "
2023 "precision, or pass a custom callable to "
2024 "pd.set_option('display.float_format', func).",
2025 Pandas4Warning,
2026 stacklevel=find_stack_level(),
2027 )
2028 set_option("display.float_format", EngFormatter(accuracy, use_eng_prefix))
2029
2030
2031def get_level_lengths(
2032 levels: Any, sentinel: bool | object | str = ""
2033) -> list[dict[int, int]]:
2034 """
2035 For each index in each level the function returns lengths of indexes.
2036
2037 Parameters
2038 ----------
2039 levels : list of lists
2040 List of values on for level.
2041 sentinel : string, optional
2042 Value which states that no new index starts on there.
2043
2044 Returns
2045 -------
2046 Returns list of maps. For each level returns map of indexes (key is index
2047 in row and value is length of index).
2048 """
2049 if len(levels) == 0:
2050 return []
2051
2052 control = [True] * len(levels[0])
2053
2054 result = []
2055 for level in levels:
2056 last_index = 0
2057
2058 lengths = {}
2059 for i, key in enumerate(level):
2060 if control[i] and key == sentinel:
2061 pass
2062 else:
2063 control[i] = False
2064 lengths[last_index] = i - last_index
2065 last_index = i
2066
2067 lengths[last_index] = len(level) - last_index
2068
2069 result.append(lengths)
2070
2071 return result
2072
2073
2074def buffer_put_lines(buf: WriteBuffer[str], lines: list[str]) -> None:
2075 """
2076 Appends lines to a buffer.
2077
2078 Parameters
2079 ----------
2080 buf
2081 The buffer to write to
2082 lines
2083 The lines to append.
2084 """
2085 if any(isinstance(x, str) for x in lines):
2086 lines = [str(x) for x in lines]
2087 buf.write("\n".join(lines))