1from __future__ import annotations
2
3from abc import (
4 ABC,
5 abstractmethod,
6)
7from functools import cached_property
8import sys
9from typing import TYPE_CHECKING
10
11from pandas._config.config import _global_config as config
12
13from pandas.io.formats import format as fmt
14from pandas.io.formats.printing import (
15 get_adjustment,
16 pprint_thing,
17)
18
19if TYPE_CHECKING:
20 from collections.abc import (
21 Iterable,
22 Iterator,
23 Mapping,
24 Sequence,
25 )
26
27 from pandas._typing import (
28 Dtype,
29 WriteBuffer,
30 )
31
32 from pandas import (
33 DataFrame,
34 Index,
35 Series,
36 )
37
38 from pandas.io.formats import printing
39
40
41def _sizeof_fmt(num: float, size_qualifier: str) -> str:
42 """
43 Return size in human readable format.
44
45 Parameters
46 ----------
47 num : int
48 Size in bytes.
49 size_qualifier : str
50 Either empty, or '+' (if lower bound).
51
52 Returns
53 -------
54 str
55 Size in human readable format.
56
57 Examples
58 --------
59 >>> _sizeof_fmt(23028, "")
60 '22.5 KB'
61
62 >>> _sizeof_fmt(23028, "+")
63 '22.5+ KB'
64 """
65 for x in ["bytes", "KB", "MB", "GB", "TB"]:
66 if num < 1024.0:
67 return f"{num:3.1f}{size_qualifier} {x}"
68 num /= 1024.0
69 return f"{num:3.1f}{size_qualifier} PB"
70
71
72def _initialize_memory_usage(
73 memory_usage: bool | str | None = None,
74) -> bool | str:
75 """Get memory usage based on inputs and display options."""
76 if memory_usage is None:
77 memory_usage = config["display"]["memory_usage"]
78 return memory_usage
79
80
81class _BaseInfo(ABC):
82 """
83 Base class for DataFrameInfo and SeriesInfo.
84
85 Parameters
86 ----------
87 data : DataFrame or Series
88 Either dataframe or series.
89 memory_usage : bool or str, optional
90 If "deep", introspect the data deeply by interrogating object dtypes
91 for system-level memory consumption, and include it in the returned
92 values.
93 """
94
95 data: DataFrame | Series
96 memory_usage: bool | str
97
98 @property
99 @abstractmethod
100 def dtypes(self) -> Iterable[Dtype]:
101 """
102 Dtypes.
103
104 Returns
105 -------
106 dtypes : sequence
107 Dtype of each of the DataFrame's columns (or one series column).
108 """
109
110 @property
111 @abstractmethod
112 def dtype_counts(self) -> Mapping[str, int]:
113 """Mapping dtype - number of counts."""
114
115 @property
116 @abstractmethod
117 def non_null_counts(self) -> list[int] | Series:
118 """Sequence of non-null counts for all columns or column (if series)."""
119
120 @property
121 @abstractmethod
122 def memory_usage_bytes(self) -> int:
123 """
124 Memory usage in bytes.
125
126 Returns
127 -------
128 memory_usage_bytes : int
129 Object's total memory usage in bytes.
130 """
131
132 @property
133 def memory_usage_string(self) -> str:
134 """Memory usage in a form of human readable string."""
135 return f"{_sizeof_fmt(self.memory_usage_bytes, self.size_qualifier)}\n"
136
137 @property
138 def size_qualifier(self) -> str:
139 size_qualifier = ""
140 if self.memory_usage:
141 if self.memory_usage != "deep":
142 # size_qualifier is just a best effort; not guaranteed to catch
143 # all cases (e.g., it misses categorical data even with object
144 # categories)
145 if (
146 "object" in self.dtype_counts
147 or self.data.index._is_memory_usage_qualified
148 ):
149 size_qualifier = "+"
150 return size_qualifier
151
152 @abstractmethod
153 def render(
154 self,
155 *,
156 buf: WriteBuffer[str] | None,
157 max_cols: int | None,
158 verbose: bool | None,
159 show_counts: bool | None,
160 ) -> None:
161 pass
162
163
164class DataFrameInfo(_BaseInfo):
165 """
166 Class storing dataframe-specific info.
167 """
168
169 def __init__(
170 self,
171 data: DataFrame,
172 memory_usage: bool | str | None = None,
173 ) -> None:
174 self.data: DataFrame = data
175 self.memory_usage = _initialize_memory_usage(memory_usage)
176
177 @property
178 def dtype_counts(self) -> Mapping[str, int]:
179 return _get_dataframe_dtype_counts(self.data)
180
181 @property
182 def dtypes(self) -> Iterable[Dtype]:
183 """
184 Dtypes.
185
186 Returns
187 -------
188 dtypes
189 Dtype of each of the DataFrame's columns.
190 """
191 return self.data.dtypes
192
193 @property
194 def ids(self) -> Index:
195 """
196 Column names.
197
198 Returns
199 -------
200 ids : Index
201 DataFrame's column names.
202 """
203 return self.data.columns
204
205 @property
206 def col_count(self) -> int:
207 """Number of columns to be summarized."""
208 return len(self.ids)
209
210 @property
211 def non_null_counts(self) -> Series:
212 """Sequence of non-null counts for all columns or column (if series)."""
213 return self.data.count()
214
215 @property
216 def memory_usage_bytes(self) -> int:
217 deep = self.memory_usage == "deep"
218 return self.data.memory_usage(index=True, deep=deep).sum()
219
220 def render(
221 self,
222 *,
223 buf: WriteBuffer[str] | None,
224 max_cols: int | None,
225 verbose: bool | None,
226 show_counts: bool | None,
227 ) -> None:
228 printer = _DataFrameInfoPrinter(
229 info=self,
230 max_cols=max_cols,
231 verbose=verbose,
232 show_counts=show_counts,
233 )
234 printer.to_buffer(buf)
235
236
237class SeriesInfo(_BaseInfo):
238 """
239 Class storing series-specific info.
240 """
241
242 def __init__(
243 self,
244 data: Series,
245 memory_usage: bool | str | None = None,
246 ) -> None:
247 self.data: Series = data
248 self.memory_usage = _initialize_memory_usage(memory_usage)
249
250 def render(
251 self,
252 *,
253 buf: WriteBuffer[str] | None = None,
254 max_cols: int | None = None,
255 verbose: bool | None = None,
256 show_counts: bool | None = None,
257 ) -> None:
258 if max_cols is not None:
259 raise ValueError(
260 "Argument `max_cols` can only be passed "
261 "in DataFrame.info, not Series.info"
262 )
263 printer = _SeriesInfoPrinter(
264 info=self,
265 verbose=verbose,
266 show_counts=show_counts,
267 )
268 printer.to_buffer(buf)
269
270 @property
271 def non_null_counts(self) -> list[int]:
272 return [self.data.count()]
273
274 @property
275 def dtypes(self) -> Iterable[Dtype]:
276 return [self.data.dtypes]
277
278 @property
279 def dtype_counts(self) -> Mapping[str, int]:
280 from pandas.core.frame import DataFrame
281
282 return _get_dataframe_dtype_counts(DataFrame(self.data))
283
284 @property
285 def memory_usage_bytes(self) -> int:
286 """Memory usage in bytes.
287
288 Returns
289 -------
290 memory_usage_bytes : int
291 Object's total memory usage in bytes.
292 """
293 deep = self.memory_usage == "deep"
294 return self.data.memory_usage(index=True, deep=deep)
295
296
297class _InfoPrinterAbstract:
298 """
299 Class for printing dataframe or series info.
300 """
301
302 def to_buffer(self, buf: WriteBuffer[str] | None = None) -> None:
303 """Save dataframe info into buffer."""
304 table_builder = self._create_table_builder()
305 lines = table_builder.get_lines()
306 if buf is None: # pragma: no cover
307 buf = sys.stdout
308 fmt.buffer_put_lines(buf, lines)
309
310 @abstractmethod
311 def _create_table_builder(self) -> _TableBuilderAbstract:
312 """Create instance of table builder."""
313
314
315class _DataFrameInfoPrinter(_InfoPrinterAbstract):
316 """
317 Class for printing dataframe info.
318
319 Parameters
320 ----------
321 info : DataFrameInfo
322 Instance of DataFrameInfo.
323 max_cols : int, optional
324 When to switch from the verbose to the truncated output.
325 verbose : bool, optional
326 Whether to print the full summary.
327 show_counts : bool, optional
328 Whether to show the non-null counts.
329 """
330
331 def __init__(
332 self,
333 info: DataFrameInfo,
334 max_cols: int | None = None,
335 verbose: bool | None = None,
336 show_counts: bool | None = None,
337 ) -> None:
338 self.info = info
339 self.data = info.data
340 self.verbose = verbose
341 self.max_cols = self._initialize_max_cols(max_cols)
342 self.show_counts = self._initialize_show_counts(show_counts)
343
344 @property
345 def max_rows(self) -> int:
346 """Maximum info rows to be displayed."""
347 return config["display"]["max_info_rows"]
348
349 @property
350 def exceeds_info_cols(self) -> bool:
351 """Check if number of columns to be summarized does not exceed maximum."""
352 return bool(self.col_count > self.max_cols)
353
354 @property
355 def exceeds_info_rows(self) -> bool:
356 """Check if number of rows to be summarized does not exceed maximum."""
357 return bool(len(self.data) > self.max_rows)
358
359 @property
360 def col_count(self) -> int:
361 """Number of columns to be summarized."""
362 return self.info.col_count
363
364 def _initialize_max_cols(self, max_cols: int | None) -> int:
365 if max_cols is None:
366 return config["display"]["max_info_columns"]
367 return max_cols
368
369 def _initialize_show_counts(self, show_counts: bool | None) -> bool:
370 if show_counts is None:
371 return bool(not self.exceeds_info_cols and not self.exceeds_info_rows)
372 else:
373 return show_counts
374
375 def _create_table_builder(self) -> _DataFrameTableBuilder:
376 """
377 Create instance of table builder based on verbosity and display settings.
378 """
379 if self.verbose:
380 return _DataFrameTableBuilderVerbose(
381 info=self.info,
382 with_counts=self.show_counts,
383 )
384 elif self.verbose is False: # specifically set to False, not necessarily None
385 return _DataFrameTableBuilderNonVerbose(info=self.info)
386 elif self.exceeds_info_cols:
387 return _DataFrameTableBuilderNonVerbose(info=self.info)
388 else:
389 return _DataFrameTableBuilderVerbose(
390 info=self.info,
391 with_counts=self.show_counts,
392 )
393
394
395class _SeriesInfoPrinter(_InfoPrinterAbstract):
396 """Class for printing series info.
397
398 Parameters
399 ----------
400 info : SeriesInfo
401 Instance of SeriesInfo.
402 verbose : bool, optional
403 Whether to print the full summary.
404 show_counts : bool, optional
405 Whether to show the non-null counts.
406 """
407
408 def __init__(
409 self,
410 info: SeriesInfo,
411 verbose: bool | None = None,
412 show_counts: bool | None = None,
413 ) -> None:
414 self.info = info
415 self.data = info.data
416 self.verbose = verbose
417 self.show_counts = self._initialize_show_counts(show_counts)
418
419 def _create_table_builder(self) -> _SeriesTableBuilder:
420 """
421 Create instance of table builder based on verbosity.
422 """
423 if self.verbose or self.verbose is None:
424 return _SeriesTableBuilderVerbose(
425 info=self.info,
426 with_counts=self.show_counts,
427 )
428 else:
429 return _SeriesTableBuilderNonVerbose(info=self.info)
430
431 def _initialize_show_counts(self, show_counts: bool | None) -> bool:
432 if show_counts is None:
433 return True
434 else:
435 return show_counts
436
437
438class _TableBuilderAbstract(ABC):
439 """
440 Abstract builder for info table.
441 """
442
443 _lines: list[str]
444 info: _BaseInfo
445
446 @abstractmethod
447 def get_lines(self) -> list[str]:
448 """Product in a form of list of lines (strings)."""
449
450 @property
451 def data(self) -> DataFrame | Series:
452 return self.info.data
453
454 @property
455 def dtypes(self) -> Iterable[Dtype]:
456 """Dtypes of each of the DataFrame's columns."""
457 return self.info.dtypes
458
459 @property
460 def dtype_counts(self) -> Mapping[str, int]:
461 """Mapping dtype - number of counts."""
462 return self.info.dtype_counts
463
464 @property
465 def display_memory_usage(self) -> bool:
466 """Whether to display memory usage."""
467 return bool(self.info.memory_usage)
468
469 @property
470 def memory_usage_string(self) -> str:
471 """Memory usage string with proper size qualifier."""
472 return self.info.memory_usage_string
473
474 @property
475 def non_null_counts(self) -> list[int] | Series:
476 return self.info.non_null_counts
477
478 def add_object_type_line(self) -> None:
479 """Add line with string representation of dataframe to the table."""
480 self._lines.append(str(type(self.data)))
481
482 def add_index_range_line(self) -> None:
483 """Add line with range of indices to the table."""
484 self._lines.append(self.data.index._summary())
485
486 def add_dtypes_line(self) -> None:
487 """Add summary line with dtypes present in dataframe."""
488 collected_dtypes = [
489 f"{key}({val:d})" for key, val in sorted(self.dtype_counts.items())
490 ]
491 self._lines.append(f"dtypes: {', '.join(collected_dtypes)}")
492
493
494class _DataFrameTableBuilder(_TableBuilderAbstract):
495 """
496 Abstract builder for dataframe info table.
497
498 Parameters
499 ----------
500 info : DataFrameInfo.
501 Instance of DataFrameInfo.
502 """
503
504 def __init__(self, *, info: DataFrameInfo) -> None:
505 self.info: DataFrameInfo = info
506
507 def get_lines(self) -> list[str]:
508 self._lines = []
509 if self.col_count == 0:
510 self._fill_empty_info()
511 else:
512 self._fill_non_empty_info()
513 return self._lines
514
515 def _fill_empty_info(self) -> None:
516 """Add lines to the info table, pertaining to empty dataframe."""
517 self.add_object_type_line()
518 self.add_index_range_line()
519 self._lines.append(f"Empty {type(self.data).__name__}\n")
520
521 @abstractmethod
522 def _fill_non_empty_info(self) -> None:
523 """Add lines to the info table, pertaining to non-empty dataframe."""
524
525 @property
526 def data(self) -> DataFrame:
527 """DataFrame."""
528 return self.info.data
529
530 @property
531 def ids(self) -> Index:
532 """Dataframe columns."""
533 return self.info.ids
534
535 @property
536 def col_count(self) -> int:
537 """Number of dataframe columns to be summarized."""
538 return self.info.col_count
539
540 def add_memory_usage_line(self) -> None:
541 """Add line containing memory usage."""
542 self._lines.append(f"memory usage: {self.memory_usage_string}")
543
544
545class _DataFrameTableBuilderNonVerbose(_DataFrameTableBuilder):
546 """
547 Dataframe info table builder for non-verbose output.
548 """
549
550 def _fill_non_empty_info(self) -> None:
551 """Add lines to the info table, pertaining to non-empty dataframe."""
552 self.add_object_type_line()
553 self.add_index_range_line()
554 self.add_columns_summary_line()
555 self.add_dtypes_line()
556 if self.display_memory_usage:
557 self.add_memory_usage_line()
558
559 def add_columns_summary_line(self) -> None:
560 self._lines.append(self.ids._summary(name="Columns"))
561
562
563class _TableBuilderVerboseMixin(_TableBuilderAbstract):
564 """
565 Mixin for verbose info output.
566 """
567
568 SPACING: str = " " * 2
569 strrows: Sequence[Sequence[str]]
570 gross_column_widths: Sequence[int]
571 with_counts: bool
572
573 @cached_property
574 def _adj(self) -> printing._TextAdjustment:
575 # respects display.unicode.east_asian_width, like DataFrame.__repr__
576 return get_adjustment()
577
578 def _ljust(self, text: str, width: int) -> str:
579 return self._adj.justify([text], width, mode="left")[0]
580
581 @property
582 @abstractmethod
583 def headers(self) -> Sequence[str]:
584 """Headers names of the columns in verbose table."""
585
586 @property
587 def header_column_widths(self) -> Sequence[int]:
588 """Widths of header columns (only titles)."""
589 return [self._adj.len(col) for col in self.headers]
590
591 def _get_gross_column_widths(self) -> Sequence[int]:
592 """Get widths of columns containing both headers and actual content."""
593 body_column_widths = self._get_body_column_widths()
594 return [
595 max(*widths)
596 for widths in zip(
597 self.header_column_widths, body_column_widths, strict=False
598 )
599 ]
600
601 def _get_body_column_widths(self) -> Sequence[int]:
602 """Get widths of table content columns."""
603 strcols: Sequence[Sequence[str]] = list(zip(*self.strrows, strict=True))
604 return [max(self._adj.len(x) for x in col) for col in strcols]
605
606 def _gen_rows(self) -> Iterator[Sequence[str]]:
607 """
608 Generator function yielding rows content.
609
610 Each element represents a row comprising a sequence of strings.
611 """
612 if self.with_counts:
613 return self._gen_rows_with_counts()
614 else:
615 return self._gen_rows_without_counts()
616
617 @abstractmethod
618 def _gen_rows_with_counts(self) -> Iterator[Sequence[str]]:
619 """Iterator with string representation of body data with counts."""
620
621 @abstractmethod
622 def _gen_rows_without_counts(self) -> Iterator[Sequence[str]]:
623 """Iterator with string representation of body data without counts."""
624
625 def add_header_line(self) -> None:
626 header_line = self.SPACING.join(
627 [
628 self._ljust(header, col_width)
629 for header, col_width in zip(
630 self.headers, self.gross_column_widths, strict=True
631 )
632 ]
633 )
634 self._lines.append(header_line)
635
636 def add_separator_line(self) -> None:
637 separator_line = self.SPACING.join(
638 [
639 self._ljust("-" * header_colwidth, gross_colwidth)
640 for header_colwidth, gross_colwidth in zip(
641 self.header_column_widths, self.gross_column_widths, strict=True
642 )
643 ]
644 )
645 self._lines.append(separator_line)
646
647 def add_body_lines(self) -> None:
648 for row in self.strrows:
649 body_line = self.SPACING.join(
650 [
651 self._ljust(col, gross_colwidth)
652 for col, gross_colwidth in zip(
653 row, self.gross_column_widths, strict=True
654 )
655 ]
656 )
657 self._lines.append(body_line)
658
659 def _gen_non_null_counts(self) -> Iterator[str]:
660 """Iterator with string representation of non-null counts."""
661 for count in self.non_null_counts:
662 yield f"{count} non-null"
663
664 def _gen_dtypes(self) -> Iterator[str]:
665 """Iterator with string representation of column dtypes."""
666 for dtype in self.dtypes:
667 yield pprint_thing(dtype)
668
669
670class _DataFrameTableBuilderVerbose(_DataFrameTableBuilder, _TableBuilderVerboseMixin):
671 """
672 Dataframe info table builder for verbose output.
673 """
674
675 def __init__(
676 self,
677 *,
678 info: DataFrameInfo,
679 with_counts: bool,
680 ) -> None:
681 self.info = info
682 self.with_counts = with_counts
683 self.strrows: Sequence[Sequence[str]] = list(self._gen_rows())
684 self.gross_column_widths: Sequence[int] = self._get_gross_column_widths()
685
686 def _fill_non_empty_info(self) -> None:
687 """Add lines to the info table, pertaining to non-empty dataframe."""
688 self.add_object_type_line()
689 self.add_index_range_line()
690 self.add_columns_summary_line()
691 self.add_header_line()
692 self.add_separator_line()
693 self.add_body_lines()
694 self.add_dtypes_line()
695 if self.display_memory_usage:
696 self.add_memory_usage_line()
697
698 @property
699 def headers(self) -> Sequence[str]:
700 """Headers names of the columns in verbose table."""
701 if self.with_counts:
702 return [" # ", "Column", "Non-Null Count", "Dtype"]
703 return [" # ", "Column", "Dtype"]
704
705 def add_columns_summary_line(self) -> None:
706 self._lines.append(f"Data columns (total {self.col_count} columns):")
707
708 def _gen_rows_without_counts(self) -> Iterator[Sequence[str]]:
709 """Iterator with string representation of body data without counts."""
710 yield from zip(
711 self._gen_line_numbers(),
712 self._gen_columns(),
713 self._gen_dtypes(),
714 strict=True,
715 )
716
717 def _gen_rows_with_counts(self) -> Iterator[Sequence[str]]:
718 """Iterator with string representation of body data with counts."""
719 yield from zip(
720 self._gen_line_numbers(),
721 self._gen_columns(),
722 self._gen_non_null_counts(),
723 self._gen_dtypes(),
724 strict=True,
725 )
726
727 def _gen_line_numbers(self) -> Iterator[str]:
728 """Iterator with string representation of column numbers."""
729 for i, _ in enumerate(self.ids):
730 yield f" {i}"
731
732 def _gen_columns(self) -> Iterator[str]:
733 """Iterator with string representation of column names."""
734 for col in self.ids:
735 yield pprint_thing(col)
736
737
738class _SeriesTableBuilder(_TableBuilderAbstract):
739 """
740 Abstract builder for series info table.
741
742 Parameters
743 ----------
744 info : SeriesInfo.
745 Instance of SeriesInfo.
746 """
747
748 def __init__(self, *, info: SeriesInfo) -> None:
749 self.info: SeriesInfo = info
750
751 def get_lines(self) -> list[str]:
752 self._lines = []
753 self._fill_non_empty_info()
754 return self._lines
755
756 @property
757 def data(self) -> Series:
758 """Series."""
759 return self.info.data
760
761 def add_memory_usage_line(self) -> None:
762 """Add line containing memory usage."""
763 self._lines.append(f"memory usage: {self.memory_usage_string}")
764
765 @abstractmethod
766 def _fill_non_empty_info(self) -> None:
767 """Add lines to the info table, pertaining to non-empty series."""
768
769
770class _SeriesTableBuilderNonVerbose(_SeriesTableBuilder):
771 """
772 Series info table builder for non-verbose output.
773 """
774
775 def _fill_non_empty_info(self) -> None:
776 """Add lines to the info table, pertaining to non-empty series."""
777 self.add_object_type_line()
778 self.add_index_range_line()
779 self.add_dtypes_line()
780 if self.display_memory_usage:
781 self.add_memory_usage_line()
782
783
784class _SeriesTableBuilderVerbose(_SeriesTableBuilder, _TableBuilderVerboseMixin):
785 """
786 Series info table builder for verbose output.
787 """
788
789 def __init__(
790 self,
791 *,
792 info: SeriesInfo,
793 with_counts: bool,
794 ) -> None:
795 self.info = info
796 self.with_counts = with_counts
797 self.strrows: Sequence[Sequence[str]] = list(self._gen_rows())
798 self.gross_column_widths: Sequence[int] = self._get_gross_column_widths()
799
800 def _fill_non_empty_info(self) -> None:
801 """Add lines to the info table, pertaining to non-empty series."""
802 self.add_object_type_line()
803 self.add_index_range_line()
804 self.add_series_name_line()
805 self.add_header_line()
806 self.add_separator_line()
807 self.add_body_lines()
808 self.add_dtypes_line()
809 if self.display_memory_usage:
810 self.add_memory_usage_line()
811
812 def add_series_name_line(self) -> None:
813 self._lines.append(f"Series name: {self.data.name}")
814
815 @property
816 def headers(self) -> Sequence[str]:
817 """Headers names of the columns in verbose table."""
818 if self.with_counts:
819 return ["Non-Null Count", "Dtype"]
820 return ["Dtype"]
821
822 def _gen_rows_without_counts(self) -> Iterator[Sequence[str]]:
823 """Iterator with string representation of body data without counts."""
824 yield from ([dtype] for dtype in self._gen_dtypes())
825
826 def _gen_rows_with_counts(self) -> Iterator[Sequence[str]]:
827 """Iterator with string representation of body data with counts."""
828 yield from zip(self._gen_non_null_counts(), self._gen_dtypes(), strict=True)
829
830
831def _get_dataframe_dtype_counts(df: DataFrame) -> Mapping[str, int]:
832 """
833 Create mapping between datatypes and their number of occurrences.
834 """
835 # groupby dtype.name to collect e.g. Categorical columns
836 return df.dtypes.value_counts().groupby(lambda x: x.name).sum()