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))