Skip to content

ENH: Add custom descriptors (such as dtype, nunique, etc.) to Styler output #43894

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 96 commits into from
Closed
Show file tree
Hide file tree
Changes from 89 commits
Commits
Show all changes
96 commits
Select commit Hold shift + click to select a range
9743099
ignore hidden rows in loop
attack68 Sep 18, 2021
8a0253e
add latex 43644 test
attack68 Sep 18, 2021
74c418e
add latex 43644 test
attack68 Sep 18, 2021
70535c5
Merge remote-tracking branch 'upstream/master' into bug_styler_multii…
attack68 Sep 21, 2021
2fbe569
clean up code
attack68 Sep 21, 2021
7903723
clean up code
attack68 Sep 21, 2021
6a2793c
row, col and level css
attack68 Sep 21, 2021
d227914
whats new
attack68 Sep 21, 2021
7ca5002
tests and user guide
attack68 Sep 21, 2021
f27f7ed
merge
attack68 Sep 22, 2021
a1000a7
merge
attack68 Sep 22, 2021
c44dcda
move private methods
attack68 Sep 22, 2021
c4c9aaa
Merge branch 'clean_styler_css_classes' into bug_styler_multiindex_hi…
attack68 Sep 22, 2021
c22cf0d
refactor methods
attack68 Sep 22, 2021
0e0b46e
add test for 43703
attack68 Sep 22, 2021
1f3bbec
add test for 43703
attack68 Sep 22, 2021
021bc26
docs
attack68 Sep 22, 2021
4ba3dff
docs
attack68 Sep 22, 2021
7fee05d
more explicit test
attack68 Sep 22, 2021
baa3233
more explicit test
attack68 Sep 22, 2021
f4ad390
fix checks
attack68 Sep 23, 2021
22b03e3
fix checks
attack68 Sep 23, 2021
db214d8
fix checks
attack68 Sep 23, 2021
771d056
Merge remote-tracking branch 'upstream/master' into bug_styler_multii…
attack68 Sep 24, 2021
24952ae
fix checks
attack68 Sep 24, 2021
1d47d0f
Merge remote-tracking branch 'upstream/master' into clean_styler_css_…
attack68 Sep 24, 2021
566738d
fix checks
attack68 Sep 24, 2021
230138a
fix checks
attack68 Sep 24, 2021
b9ba9ea
refactor to get tests to pass
attack68 Sep 25, 2021
ea2bba1
refactor to get tests to pass
attack68 Sep 25, 2021
75de033
Merge branch 'clean_styler_css_classes' into bug_styler_multiindex_hi…
attack68 Sep 25, 2021
4c34bc2
try filter
attack68 Sep 25, 2021
68ee832
docs methods
attack68 Sep 25, 2021
ca70491
correct css classes in set_table_styles
attack68 Sep 25, 2021
7a1994e
Merge remote-tracking branch 'upstream/master' into clean_styler_css_…
attack68 Sep 29, 2021
91320f8
add version added
attack68 Sep 29, 2021
92c1941
add version added
attack68 Sep 29, 2021
aad0e16
checks fix
attack68 Sep 29, 2021
61b24ed
add tests allow none
attack68 Sep 29, 2021
d4c5715
fix checks
attack68 Sep 29, 2021
aa0172f
Merge remote-tracking branch 'upstream/master' into clean_styler_css_…
attack68 Oct 2, 2021
84a814f
rename `css` `css_class_names`
attack68 Oct 2, 2021
f06b727
rename `css` `css_class_names`
attack68 Oct 2, 2021
131070f
rename `css` `css_class_names`
attack68 Oct 2, 2021
a048122
update arg name
attack68 Oct 2, 2021
4931f32
Merge remote-tracking branch 'upstream/master' into clean_styler_css_…
attack68 Oct 3, 2021
e9716f2
fix line length
attack68 Oct 3, 2021
d983464
black
attack68 Oct 3, 2021
c08ef82
add vars
attack68 Oct 5, 2021
0f11a62
Merge branch 'clean_styler_css_classes' into bug_styler_multiindex_hi…
attack68 Oct 5, 2021
5c52957
Merge branch 'bug_styler_multiindex_hiding' into describe_styler
attack68 Oct 5, 2021
17d181c
add descriptor rendering
attack68 Oct 5, 2021
e5b4d3e
mypy
attack68 Oct 5, 2021
f98ba9f
fix merge
attack68 Oct 19, 2021
0cc5502
Merge remote-tracking branch 'upstream/master' into describe_styler
attack68 Oct 23, 2021
c5b75cc
docs
attack68 Oct 23, 2021
ac1384b
Merge remote-tracking branch 'upstream/master' into describe_styler
attack68 Oct 23, 2021
c948f11
tests edit
attack68 Oct 23, 2021
afa8f10
some tests
attack68 Oct 23, 2021
60efbbb
doc string
attack68 Oct 23, 2021
9ef5b44
add direct test
attack68 Oct 23, 2021
a02b4ac
doc fixes
attack68 Oct 24, 2021
6b51d88
update user guide
attack68 Oct 24, 2021
814bace
table styles doc
attack68 Oct 24, 2021
33c35a4
version identifiers
attack68 Oct 24, 2021
91d8680
Merge remote-tracking branch 'upstream/master' into describe_styler
attack68 Oct 31, 2021
260d356
merge master
attack68 Nov 5, 2021
46856fb
add tests
attack68 Nov 5, 2021
a4dce4d
Merge remote-tracking branch 'upstream/master' into describe_styler
attack68 Nov 12, 2021
fefb420
Merge remote-tracking branch 'upstream/master' into describe_styler
attack68 Nov 16, 2021
f2c8f0e
whats new
attack68 Nov 16, 2021
a945da5
Merge remote-tracking branch 'upstream/master' into describe_styler
attack68 Nov 29, 2021
b3c0f96
Merge branch 'master' into describe_styler
attack68 Dec 11, 2021
f36d4fd
formatting
attack68 Dec 11, 2021
e78907f
fdoc string validate
attack68 Dec 13, 2021
cd085df
Merge remote-tracking branch 'upstream/master' into describe_styler
attack68 Dec 15, 2021
45f5879
Merge remote-tracking branch 'upstream/master' into describe_styler
attack68 Jan 4, 2022
6f64e80
Merge remote-tracking branch 'upstream/master' into describe_styler
attack68 Jan 5, 2022
1c5cf53
Merge remote-tracking branch 'upstream/master' into describe_styler
attack68 Jan 6, 2022
58aa894
push to 1.5.0
attack68 Jan 6, 2022
bd2fe7d
Merge remote-tracking branch 'upstream/master' into describe_styler
attack68 Jan 7, 2022
02aeae5
cleaer comment
attack68 Jan 7, 2022
5541396
Merge remote-tracking branch 'upstream/master' into describe_styler
attack68 Jan 9, 2022
beb2bc1
fix formatter
attack68 Jan 13, 2022
7d62bb3
Merge remote-tracking branch 'upstream/main' into describe_styler
attack68 Jan 13, 2022
6bfb5a9
fix formatter
attack68 Jan 14, 2022
9554f89
Merge remote-tracking branch 'upstream/main' into describe_styler
attack68 Jan 25, 2022
d8e11c8
Merge remote-tracking branch 'upstream/main' into describe_styler
attack68 Jan 26, 2022
4f935c4
is_integer replacemnet
attack68 Jan 26, 2022
7031897
Merge remote-tracking branch 'upstream/main' into describe_styler
attack68 Jan 28, 2022
0c034a6
bastage req: refactor _element
attack68 Jan 28, 2022
6128ccb
bashtage req: is_float / complex
attack68 Jan 28, 2022
bb8201e
bashtage req: is_float / complex
attack68 Jan 28, 2022
44204e0
bashtage req: __name__ and typing
attack68 Jan 28, 2022
ebec1d6
bashtage req: doc updates for __name__
attack68 Jan 28, 2022
c205c38
doc fix
attack68 Jan 28, 2022
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Binary file added doc/source/_static/style/des_mean.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions doc/source/reference/style.rst
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ Style application
Styler.set_td_classes
Styler.set_table_styles
Styler.set_table_attributes
Styler.set_descriptors
Styler.set_tooltips
Styler.set_caption
Styler.set_sticky
Expand Down
44 changes: 43 additions & 1 deletion doc/source/user_guide/style.ipynb
Original file line number Diff line number Diff line change
Expand Up @@ -216,6 +216,47 @@
"weather_df.loc[\"2021-01-04\":\"2021-01-08\"].style.pipe(make_pretty)"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"### Describing Data\n",
"\n",
"The data can also be explored with the ability to add header level calculations. The [.set_descriptors()][descriptors] method is used here. We begin with a large DataFrame and reconfigure the `pandas.options` to reduce the rendered size, whilst adding descriptors we wish to calculate on the data.\n",
"\n",
"[descriptors]: ../reference/api/pandas.io.formats.style.Styler.set_descriptors.rst"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"pd.options.styler.render.max_rows = 5\n",
"df_described = pd.DataFrame({\"A\": np.random.randn(1000), \n",
" \"B\": np.random.randint(low=-10, high=10, size=1000, dtype=\"int64\")})\n",
"df_described.style.set_descriptors([\n",
" \"mean\",\n",
" (\"mean 2dp\", lambda s: f\"{s.mean():.2f}\"),\n",
" (\"std\", pd.Series.std),\n",
" \"nunique\",\n",
" lambda s: s.dtype,\n",
"])"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {
"nbsphinx": "hidden"
},
"outputs": [],
"source": [
"# Hidden cell to reset pandas options \n",
"pd.options.styler.render.max_rows = None"
]
},
{
"cell_type": "markdown",
"metadata": {},
Expand Down Expand Up @@ -1661,6 +1702,7 @@
" + `col<n>`, where `n` is the numeric position of the cell.\n",
"- Blank cells include `blank`\n",
"- Trimmed cells include `col_trim` or `row_trim`\n",
"- Descriptor name cells include `descriptor_name`, descriptor value cells include `descriptor_value` and both also include `descriptor<j>`, where `j` is the numeric index of the list of descriptors.\n",
"\n",
"The structure of the `id` is `T_uuid_level<k>_row<m>_col<n>` where `level<k>` is used only on headings, and headings will only have either `row<m>` or `col<n>` whichever is needed. By default we've also prepended each row/column identifier with a UUID unique to each DataFrame so that the style from one doesn't collide with the styling from another within the same notebook or page. You can read more about the use of UUIDs in [Optimization](#Optimization).\n",
"\n",
Expand All @@ -1675,7 +1717,7 @@
"metadata": {},
"outputs": [],
"source": [
"print(pd.DataFrame([[1,2],[3,4]], index=['i1', 'i2'], columns=['c1', 'c2']).style.to_html())"
"print(pd.DataFrame([[1,2],[3,4]], index=['i1', 'i2'], columns=['c1', 'c2']).style.set_descriptors([\"mean\"]).to_html())"
]
},
{
Expand Down
1 change: 1 addition & 0 deletions doc/source/whatsnew/v1.5.0.rst
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ Styler
^^^^^^

- New method :meth:`.Styler.to_string` for alternative customisable output methods (:issue:`44502`)
- Added a new method :meth:`.Styler.set_descriptors` which allows adding customised header rows to explore and make calculations on the data, e.g. totals and counts etc. (:issue:`43875`)
- Various bug fixes, see below.

.. _whatsnew_150.enhancements.enhancement2:
Expand Down
58 changes: 57 additions & 1 deletion pandas/io/formats/style.py
Original file line number Diff line number Diff line change
Expand Up @@ -1435,6 +1435,7 @@ def _copy(self, deepcopy: bool = False) -> Styler:
]
deep = [ # nested lists or dicts
"css",
"descriptors",
"_display_funcs",
"_display_funcs_index",
"_display_funcs_columns",
Expand Down Expand Up @@ -1977,6 +1978,9 @@ def export(self) -> dict[str, Any]:

Can be applied to a second Styler with ``Styler.use``.

.. versionchanged:: 1.5.0
Adds ``descriptors`` to the exported items.

Returns
-------
styles : dict
Expand All @@ -1998,6 +2002,7 @@ def export(self) -> dict[str, Any]:
- Whether axes and names are hidden from the display, if unambiguous.
- Table attributes
- Table styles
- Descriptors

The following attributes are considered data dependent and therefore not
exported:
Expand Down Expand Up @@ -2027,6 +2032,7 @@ def export(self) -> dict[str, Any]:
"hide_index_names": self.hide_index_names,
"hide_column_names": self.hide_column_names,
"css": copy.copy(self.css),
"descriptors": copy.copy(self.descriptors),
}

def use(self, styles: dict[str, Any]) -> Styler:
Expand All @@ -2035,6 +2041,9 @@ def use(self, styles: dict[str, Any]) -> Styler:

Possibly uses styles from ``Styler.export``.

.. versionchanged:: 1.5.0
Adds ``descriptors`` to the used items.

Parameters
----------
styles : dict(str, Any)
Expand All @@ -2052,6 +2061,8 @@ def use(self, styles: dict[str, Any]) -> Styler:
- "hide_index_names": whether index names are hidden.
- "hide_column_names": whether column header names are hidden.
- "css": the css class names used.
- "descriptors": list of descriptors, typically added with
``set_descriptors``.

Returns
-------
Expand Down Expand Up @@ -2094,6 +2105,8 @@ def use(self, styles: dict[str, Any]) -> Styler:
self.hide_column_names = styles.get("hide_column_names", False)
if styles.get("css"):
self.css = styles.get("css") # type: ignore[assignment]
if styles.get("descriptors"):
self.set_descriptors(styles.get("descriptors"))
return self

def set_uuid(self, uuid: str) -> Styler:
Expand Down Expand Up @@ -2352,7 +2365,10 @@ def set_table_styles(
"row_trim": "row_trim",
"level": "level",
"data": "data",
"blank": "blank}
"blank": "blank",
"descriptor": "descriptor",
"descriptor_name": "descriptor_name",
"descriptor_value": "descriptor_value"}

Examples
--------
Expand Down Expand Up @@ -2423,6 +2439,46 @@ def set_table_styles(
self.table_styles = table_styles
return self

def set_descriptors(
self, descriptors: list[str | Callable | tuple[str, Callable]] | None = None
) -> Styler:
"""
Add header-level calculations to the output which describes the data.

.. versionadded:: 1.5.0

Parameters
----------
descriptors : list of str, callables or 2-tuples of str and callable
If a string is given must be a valid Series method, e.g. "mean" invokes
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you some additional details about the callable. Specifically it must (or should) return a scalar quantity. Is the callable allowed to throw and exception? If so, what happens? I would be good to clarify this.

Series.mean().

If a callable is given must accept a Series and return a scalar. No name
will be displayed for the row.

If a 2-tuple, must be a string used as the name of the row and a
callable as above.

Returns
-------
self : Styler

Examples
--------

>>> df = DataFrame([[1, 2], [3, 4]], columns=["A", "B"])
>>> styler = df.style.set_descriptors([
... "mean",
... Series.mean,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The screen show shows no lable on this line? Was it not possible to have .__name__ as a default test?

... ("my-text", Series.mean),
... ("my-func", lambda s: s.sum()/2),
... ]) # doctest: +SKIP

.. figure:: ../../_static/style/des_mean.png
"""
self.descriptors = descriptors if descriptors is not None else []
return self

def set_na_rep(self, na_rep: str) -> StylerRenderer:
"""
Set the missing data representation on a ``Styler``.
Expand Down
123 changes: 119 additions & 4 deletions pandas/io/formats/style_render.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
from pandas._typing import Level
from pandas.compat._optional import import_optional_dependency

from pandas.core.dtypes.common import is_integer
from pandas.core.dtypes.generic import ABCSeries

from pandas import (
Expand Down Expand Up @@ -115,6 +116,9 @@ def __init__(
"level": "level",
"data": "data",
"blank": "blank",
"descriptor": "descriptor",
"descriptor_value": "descriptor_value",
"descriptor_name": "descriptor_name",
}

# add rendering variables
Expand All @@ -124,6 +128,7 @@ def __init__(
self.hide_columns_: list = [False] * self.columns.nlevels
self.hidden_rows: Sequence[int] = [] # sequence for specific hidden rows/cols
self.hidden_columns: Sequence[int] = []
self.descriptors: list[str | Callable | tuple[str, Callable]] = []
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this a common API? I think most places where alternative input are allowed either use a Sequence of some sort or dict[str, str | Callable]. Would be be cleaner if the allowed inputs were Sequence[str | Callable] | dict[str | Callable].

Also, can't the Callable be better described? Isn't is Callable[[Series], float]? It seems that the Callablemust take aSeriesand return some sort of scalar value. Thie question is whether the scalar value can be non-numeric. If it could be any numeric value, than you could you use something likeCallable[[Series], int | float]. So more generally, what are the requirements for the Callable`?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also, can't the Callable be better described? Isn't is Callable[[Series], float]? It seems that the Callablemust take aSeriesand return some sort of scalar value. Thie question is whether the scalar value can be non-numeric. If it could be any numeric value, than you could you use something likeCallable[[Series], int | float]. So more generally, what are the requirements for the Callable`?

The Callable could techincally return anything so long as the returned object has a __str__ representation to populate in the HTML element. Common values might be int, float, string, but yes it does accept only series.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think the sig should then be Callable[[Series],Any]

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  • changed typing. But did not add the dict. List[Tuple[str, str]] is present for some of the Styler functions

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you do stick with the tuple, it should be tuple(str, str | Callable) which would allow someone to pass a custom name for the output along with a common function, e.g., ("average", "mean"). If moving to a dict, then both {"average":"mean"}and{"average":Series.mean}` should be allowed.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes, agreed, having a think if this can be improved.

self.ctx: DefaultDict[tuple[int, int], CSSList] = defaultdict(list)
self.ctx_index: DefaultDict[tuple[int, int], CSSList] = defaultdict(list)
self.ctx_columns: DefaultDict[tuple[int, int], CSSList] = defaultdict(list)
Expand Down Expand Up @@ -329,7 +334,9 @@ def _translate_header(self, sparsify_cols: bool, max_cols: int):
1) | .. | .. | .. |
| index_blanks ... | column_name_n | column_headers (level_n) |
+----------------------------+---------------+---------------------------+
2) | index_names (level_0 to level_n) ... | column_blanks ... |
2) | index_blanks ... | descriptor | value by column |
+----------------------------+---------------+---------------------------+
3) | index_names (level_0 to level_n) ... | column_blanks ... |
+----------------------------+---------------+---------------------------+

Parameters
Expand Down Expand Up @@ -365,7 +372,12 @@ def _translate_header(self, sparsify_cols: bool, max_cols: int):
)
head.append(header_row)

# 2) index names
# 2) Descriptor calcs
for r, descriptor in enumerate(self.descriptors):
descriptor_row = self._generate_descriptor_row((r, descriptor), max_cols)
head.append(descriptor_row)

# 3) index names
if (
self.data.index.names
and com.any_not_none(*self.data.index.names)
Expand Down Expand Up @@ -477,6 +489,109 @@ def _generate_col_header_row(self, iter: tuple, max_cols: int, col_lengths: dict

return index_blanks + column_name + column_headers

def _generate_descriptor_row(self, iter: tuple, max_cols: int):
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What kind of tuples are allowed? Would be better to be as specific as possible to reduce any future refactor risks.

"""
Generate the row containing calculated descriptor values for columns:

+----------------------------+---------------+---------------------------+
| index_blanks ... | descriptor_i | value_i by col |
+----------------------------+---------------+---------------------------+

Parameters
----------
iter : tuple
Looping variables from outer scope
max_cols : int
Permissible number of columns

Returns
-------
list of elements
"""

r, descriptor = iter

# number of index blanks is governed by number of hidden index levels
index_blanks = [
_element("th", self.css["blank"], self.css["blank_value"], True)
] * (self.index.nlevels - sum(self.hide_index_) - 1)

if isinstance(descriptor, str):
name: str | None = descriptor
func: Callable = getattr(Series, descriptor)
elif isinstance(descriptor, tuple):
name, func = descriptor[0], descriptor[1]
else:
name, func = None, descriptor
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When the descriptor is a Callable I think it would be best to try getattr(descriptor, "name", None) for the name rather than committing to a blank name. For example pd.Series.mean.__name__ is mean.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  • chged. note the case of "<lambda>" i thought was worth blanking


display_func: Callable = _maybe_wrap_formatter(
formatter=None, # use _default_formatter
decimal=get_option("styler.format.decimal"),
thousands=get_option("styler.format.thousands"),
precision=get_option("styler.format.precision"),
na_rep=get_option("styler.format.na_rep"),
escape=get_option("styler.format.escape"),
)

base_css = f"{self.css['descriptor_name']} {self.css['descriptor']}{r}"
descriptor_name = [
_element(
"th",
base_css
if (name is not None and not self.hide_column_names)
else f"{self.css['blank']} {base_css}",
name
if (name is not None and not self.hide_column_names)
else self.css["blank_value"],
not all(self.hide_index_),
)
]

descriptor_values, visible_col_count = [], 0
for c, col in enumerate(self.columns):
if c not in self.hidden_columns:
header_element_visible = True
visible_col_count += 1
try:
header_element_value = func(self.data[col])
except Exception:
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why not be strict here and raise?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

in the interest of a simpler initial PR i did not include a subset argument so the function needs to work on all colums. some dtypes might bot be suitable for the function so best approach is silent raise i think. descriptors aim to be more helpful than precise, so i think raising when failing would be annoying

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see. What you think about raising a UserWarning that applying func failed for a column? Would that be too noisy?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I still think so, but even if it wasn't, the way to turn it off is difficult:

  • either a pandas global option is created: I think that's a bit messy for such a small thing.
  • or since the descriptors are calc'ed at render time a show_warnings kwarg would have to be saved in Styler namespace and evaluated upon render: still a bit messy.

However, with you prompting me to think about it, probably means better documentation and better examples could highlight this and make the behaviour (and workarounds) clear.

header_element_value = self.css["blank_value"]
else:
header_element_visible = False
header_element_value = None

if visible_col_count > max_cols:
# add an extra column with `...` value to indicate trimming
descriptor_values.append(
_element(
"th",
(
f"{self.css['descriptor_value']} "
f"{self.css['descriptor']}{r} "
f"{self.css['col_trim']}"
),
"...",
True,
attributes="",
)
)
break

header_element = _element(
"th",
(
f"{self.css['descriptor_value']} {self.css['descriptor']}{r} "
f"{self.css['col']}{c}"
),
header_element_value,
header_element_visible,
display_value=display_func(header_element_value),
attributes="",
)
descriptor_values.append(header_element)

return index_blanks + descriptor_name + descriptor_values

def _generate_index_names_row(self, iter: tuple, max_cols: int, col_lengths: dict):
"""
Generate the row containing index names
Expand Down Expand Up @@ -1418,7 +1533,7 @@ def _default_formatter(x: Any, precision: int, thousands: bool = False) -> Any:
"""
if isinstance(x, (float, complex)):
return f"{x:,.{precision}f}" if thousands else f"{x:.{precision}f}"
elif isinstance(x, int):
elif is_integer(x):
return f"{x:,.0f}" if thousands else f"{x:.0f}"
return x

Expand All @@ -1433,7 +1548,7 @@ def _wrap_decimal_thousands(
"""

def wrapper(x):
if isinstance(x, (float, complex, int)):
if isinstance(x, (float, complex)) or is_integer(x):
if decimal != "." and thousands is not None and thousands != ",":
return (
formatter(x)
Expand Down
Loading