Skip to content

Commit 7c03a72

Browse files
committed
ENH: DataFrame.style column names
BUG: Fix CSS classes for index names Updates doc2 option honor options
1 parent ecba615 commit 7c03a72

File tree

4 files changed

+211
-34
lines changed

4 files changed

+211
-34
lines changed

doc/source/html-styling.ipynb

+21
Original file line numberDiff line numberDiff line change
@@ -788,6 +788,27 @@
788788
"We hope to collect some useful ones either in pandas, or preferable in a new package that [builds on top](#Extensibility) the tools here."
789789
]
790790
},
791+
{
792+
"cell_type": "markdown",
793+
"metadata": {},
794+
"source": [
795+
"# CSS Classes\n",
796+
"\n",
797+
"Certain CSS classes are attached to cells.\n",
798+
"\n",
799+
"- Index and Column names include `index_name` and `level<k>` where `k` is its level in a MultiIndex\n",
800+
"- Index label cells include\n",
801+
" + `row_heading`\n",
802+
" + `row<n>` where `n` is the numeric position of the row\n",
803+
" + `level<k>` where `k` is the level in a MultiIndex\n",
804+
"- Column label cells include\n",
805+
" + `col_heading`\n",
806+
" + `col<n>` where `n` is the numeric position of the column\n",
807+
" + `level<k>` where `k` is the level in a MultiIndex\n",
808+
"- Blank cells include `blank`\n",
809+
"- Data cells include `data`"
810+
]
811+
},
791812
{
792813
"cell_type": "markdown",
793814
"metadata": {},

doc/source/whatsnew/v0.19.0.txt

+2-1
Original file line numberDiff line numberDiff line change
@@ -315,6 +315,7 @@ Other enhancements
315315
- ``.to_stata()`` and ``StataWriter`` can now write variable labels to Stata dta files using a dictionary to make column names to labels (:issue:`13535`, :issue:`13536`)
316316
- ``.to_stata()`` and ``StataWriter`` will automatically convert ``datetime64[ns]`` columns to Stata format ``%tc``, rather than raising a ``ValueError`` (:issue:`12259`)
317317
- ``DataFrame.style`` will now render sparsified MultiIndexes (:issue:`11655`)
318+
- ``DataFrame.style`` will now show column level names (e.g. ``DataFrame.columns.names``) (:issue:`13775`)
318319
- ``DataFrame`` has gained support to re-order the columns based on the values
319320
in a row using ``df.sort_values(by='...', axis=1)`` (:issue:`10806`)
320321

@@ -770,8 +771,8 @@ Bug Fixes
770771
- Bug in ``groupby`` with ``as_index=False`` returns all NaN's when grouping on multiple columns including a categorical one (:issue:`13204`)
771772
- Bug in ``df.groupby(...)[...]`` where getitem with ``Int64Index`` raised an error (:issue:`13731`)
772773

774+
- Bug in the CSS classes assigned to ``DataFrame.style`` for index names. Previously they were assigned ``"col_heading level<n> col<c>"`` where ``n`` was the number of levels + 1. Now they are assigned ``"index_name level<n>"``, where ``n`` is the correct level for that MultiIndex.
773775
- Bug where ``pd.read_gbq()`` could throw ``ImportError: No module named discovery`` as a result of a naming conflict with another python package called apiclient (:issue:`13454`)
774776
- Bug in ``Index.union`` returns an incorrect result with a named empty index (:issue:`13432`)
775777
- Bugs in ``Index.difference`` and ``DataFrame.join`` raise in Python3 when using mixed-integer indexes (:issue:`13432`, :issue:`12814`)
776-
777778
- Bug in ``.to_excel()`` when DataFrame contains a MultiIndex which contains a label with a NaN value (:issue:`13511`)

pandas/formats/style.py

+52-11
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
import numpy as np
2323
import pandas as pd
2424
from pandas.compat import range
25+
from pandas.core.config import get_option
2526
import pandas.core.common as com
2627
from pandas.core.indexing import _maybe_numeric_slice, _non_reducing_slice
2728
try:
@@ -80,6 +81,24 @@ class Styler(object):
8081
to automatically render itself. Otherwise call Styler.render to get
8182
the genterated HTML.
8283
84+
CSS classes are attached to the generated HTML
85+
86+
* Index and Column names include ``index_name`` and ``level<k>``
87+
where `k` is its level in a MultiIndex
88+
* Index label cells include
89+
90+
* ``row_heading``
91+
* ``row<n>`` where `n` is the numeric position of the row
92+
* ``level<k>`` where `k` is the level in a MultiIndex
93+
94+
* Column label cells include
95+
* ``col_heading``
96+
* ``col<n>`` where `n` is the numeric position of the column
97+
* ``evel<k>`` where `k` is the level in a MultiIndex
98+
99+
* Blank cells include ``blank``
100+
* Data cells include ``data``
101+
83102
See Also
84103
--------
85104
pandas.DataFrame.style
@@ -112,7 +131,8 @@ class Styler(object):
112131
<tr>
113132
{% for c in r %}
114133
{% if c.is_visible != False %}
115-
<{{c.type}} class="{{c.class}}" {{ c.attributes|join(" ") }}>{{c.value}}
134+
<{{c.type}} class="{{c.class}}" {{ c.attributes|join(" ") }}>
135+
{{c.value}}
116136
{% endif %}
117137
{% endfor %}
118138
</tr>
@@ -123,7 +143,8 @@ class Styler(object):
123143
<tr>
124144
{% for c in r %}
125145
{% if c.is_visible != False %}
126-
<{{c.type}} id="T_{{uuid}}{{c.id}}" class="{{c.class}}" {{ c.attributes|join(" ") }}>
146+
<{{c.type}} id="T_{{uuid}}{{c.id}}"
147+
class="{{c.class}}" {{ c.attributes|join(" ") }}>
127148
{{ c.display_value }}
128149
{% endif %}
129150
{% endfor %}
@@ -153,7 +174,7 @@ def __init__(self, data, precision=None, table_styles=None, uuid=None,
153174
self.table_styles = table_styles
154175
self.caption = caption
155176
if precision is None:
156-
precision = pd.options.display.precision
177+
precision = get_option('display.precision')
157178
self.precision = precision
158179
self.table_attributes = table_attributes
159180
# display_funcs maps (row, col) -> formatting function
@@ -182,6 +203,8 @@ def _translate(self):
182203
uuid = self.uuid or str(uuid1()).replace("-", "_")
183204
ROW_HEADING_CLASS = "row_heading"
184205
COL_HEADING_CLASS = "col_heading"
206+
INDEX_NAME_CLASS = "index_name"
207+
185208
DATA_CLASS = "data"
186209
BLANK_CLASS = "blank"
187210
BLANK_VALUE = ""
@@ -210,9 +233,24 @@ def format_attr(pair):
210233
head = []
211234

212235
for r in range(n_clvls):
236+
# Blank for Index columns...
213237
row_es = [{"type": "th",
214238
"value": BLANK_VALUE,
215-
"class": " ".join([BLANK_CLASS])}] * n_rlvls
239+
"display_value": BLANK_VALUE,
240+
"is_visible": True,
241+
"class": " ".join([BLANK_CLASS])}] * (n_rlvls - 1)
242+
243+
# ... except maybe the last for columns.names
244+
name = self.data.columns.names[r]
245+
cs = [BLANK_CLASS if name is None else INDEX_NAME_CLASS,
246+
"level%s" % r]
247+
name = BLANK_VALUE if name is None else name
248+
row_es.append({"type": "th",
249+
"value": name,
250+
"display_value": name,
251+
"class": " ".join(cs),
252+
"is_visible": True})
253+
216254
for c in range(len(clabels[0])):
217255
cs = [COL_HEADING_CLASS, "level%s" % r, "col%s" % c]
218256
cs.extend(cell_context.get(
@@ -230,13 +268,14 @@ def format_attr(pair):
230268
]})
231269
head.append(row_es)
232270

233-
if self.data.index.names and self.data.index.names != [None]:
271+
if self.data.index.names and not all(x is None
272+
for x in self.data.index.names):
234273
index_header_row = []
235274

236275
for c, name in enumerate(self.data.index.names):
237-
cs = [COL_HEADING_CLASS,
238-
"level%s" % (n_clvls + 1),
239-
"col%s" % c]
276+
cs = [INDEX_NAME_CLASS,
277+
"level%s" % c]
278+
name = '' if name is None else name
240279
index_header_row.append({"type": "th", "value": name,
241280
"class": " ".join(cs)})
242281

@@ -920,11 +959,11 @@ def _is_visible(idx_row, idx_col, lengths):
920959

921960

922961
def _get_level_lengths(index):
923-
'''
962+
"""
924963
Given an index, find the level lenght for each element.
925964
926965
Result is a dictionary of (level, inital_position): span
927-
'''
966+
"""
928967
sentinel = com.sentinel_factory()
929968
levels = index.format(sparsify=sentinel, adjoin=False, names=False)
930969

@@ -935,7 +974,9 @@ def _get_level_lengths(index):
935974

936975
for i, lvl in enumerate(levels):
937976
for j, row in enumerate(lvl):
938-
if row != sentinel:
977+
if not get_option('display.multi_sparse'):
978+
lengths[(i, j)] = 1
979+
elif row != sentinel:
939980
last_label = j
940981
lengths[(i, last_label)] = 1
941982
else:

pandas/tests/formats/test_style.py

+136-22
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,11 @@
88
from pandas.util.testing import TestCase
99
import pandas.util.testing as tm
1010

11+
# Getting failures on a python 2.7 build with
12+
# whenever we try to import jinja, whether it's installed or not.
13+
# so we're explicitly skipping that one *before* we try to import
14+
# jinja. We still need to export the imports as globals,
15+
# since importing Styler tries to import jinja2.
1116
job_name = os.environ.get('JOB_NAME', None)
1217
if job_name == '27_slow_nnet_LOCALE':
1318
raise SkipTest("No jinja")
@@ -143,7 +148,8 @@ def test_empty_index_name_doesnt_display(self):
143148
df = pd.DataFrame({'A': [1, 2], 'B': [3, 4], 'C': [5, 6]})
144149
result = df.style._translate()
145150

146-
expected = [[{'class': 'blank', 'type': 'th', 'value': ''},
151+
expected = [[{'class': 'blank level0', 'type': 'th', 'value': '',
152+
'is_visible': True, 'display_value': ''},
147153
{'class': 'col_heading level0 col0',
148154
'display_value': 'A',
149155
'type': 'th',
@@ -173,14 +179,15 @@ def test_index_name(self):
173179
df = pd.DataFrame({'A': [1, 2], 'B': [3, 4], 'C': [5, 6]})
174180
result = df.set_index('A').style._translate()
175181

176-
expected = [[{'class': 'blank', 'type': 'th', 'value': ''},
182+
expected = [[{'class': 'blank level0', 'type': 'th', 'value': '',
183+
'display_value': '', 'is_visible': True},
177184
{'class': 'col_heading level0 col0', 'type': 'th',
178185
'value': 'B', 'display_value': 'B',
179186
'is_visible': True, 'attributes': ['colspan=1']},
180187
{'class': 'col_heading level0 col1', 'type': 'th',
181188
'value': 'C', 'display_value': 'C',
182189
'is_visible': True, 'attributes': ['colspan=1']}],
183-
[{'class': 'col_heading level2 col0', 'type': 'th',
190+
[{'class': 'index_name level0', 'type': 'th',
184191
'value': 'A'},
185192
{'class': 'blank', 'type': 'th', 'value': ''},
186193
{'class': 'blank', 'type': 'th', 'value': ''}]]
@@ -192,17 +199,20 @@ def test_multiindex_name(self):
192199
df = pd.DataFrame({'A': [1, 2], 'B': [3, 4], 'C': [5, 6]})
193200
result = df.set_index(['A', 'B']).style._translate()
194201

195-
expected = [[{'class': 'blank', 'type': 'th', 'value': ''},
196-
{'class': 'blank', 'type': 'th', 'value': ''},
197-
{'class': 'col_heading level0 col0', 'type': 'th',
198-
'value': 'C', 'display_value': 'C',
199-
'is_visible': True, 'attributes': ['colspan=1'],
200-
}],
201-
[{'class': 'col_heading level2 col0', 'type': 'th',
202-
'value': 'A'},
203-
{'class': 'col_heading level2 col1', 'type': 'th',
204-
'value': 'B'},
205-
{'class': 'blank', 'type': 'th', 'value': ''}]]
202+
expected = [[
203+
{'class': 'blank', 'type': 'th', 'value': '',
204+
'display_value': '', 'is_visible': True},
205+
{'class': 'blank level0', 'type': 'th', 'value': '',
206+
'display_value': '', 'is_visible': True},
207+
{'class': 'col_heading level0 col0', 'type': 'th',
208+
'value': 'C', 'display_value': 'C',
209+
'is_visible': True, 'attributes': ['colspan=1'],
210+
}],
211+
[{'class': 'index_name level0', 'type': 'th',
212+
'value': 'A'},
213+
{'class': 'index_name level1', 'type': 'th',
214+
'value': 'B'},
215+
{'class': 'blank', 'type': 'th', 'value': ''}]]
206216

207217
self.assertEqual(result['head'], expected)
208218

@@ -594,7 +604,7 @@ def test_get_level_lengths(self):
594604
expected = {(0, 0): 3, (0, 3): 3, (1, 0): 1, (1, 1): 1, (1, 2): 1,
595605
(1, 3): 1, (1, 4): 1, (1, 5): 1}
596606
result = _get_level_lengths(index)
597-
self.assertDictEqual(result, expected)
607+
tm.assert_dict_equal(result, expected)
598608

599609
def test_get_level_lengths_un_sorted(self):
600610
index = pd.MultiIndex.from_arrays([
@@ -604,16 +614,120 @@ def test_get_level_lengths_un_sorted(self):
604614
expected = {(0, 0): 2, (0, 2): 1, (0, 3): 1,
605615
(1, 0): 1, (1, 1): 1, (1, 2): 1, (1, 3): 1}
606616
result = _get_level_lengths(index)
607-
self.assertDictEqual(result, expected)
617+
tm.assert_dict_equal(result, expected)
608618

609619
def test_mi_sparse(self):
610-
df = pd.DataFrame({'A': [1, 2, 3, 4]},
611-
index=pd.MultiIndex.from_product([['a', 'b'],
620+
df = pd.DataFrame({'A': [1, 2]},
621+
index=pd.MultiIndex.from_arrays([['a', 'a'],
612622
[0, 1]]))
613-
result = df.style.render()
614-
assert 'rowspan' in result
615-
result = df.T.style.render()
616-
assert 'colspan' in result
623+
result = df.style._translate()
624+
body_0 = result['body'][0][0]
625+
expected_0 = {
626+
"value": "a", "display_value": "a", "is_visible": True,
627+
"type": "th", "attributes": ["rowspan=2"],
628+
"class": "row_heading level0 row0",
629+
}
630+
tm.assert_dict_equal(body_0, expected_0)
631+
632+
body_1 = result['body'][0][1]
633+
expected_1 = {
634+
"value": 0, "display_value": 0, "is_visible": True,
635+
"type": "th", "attributes": ["rowspan=1"],
636+
"class": "row_heading level1 row0",
637+
}
638+
tm.assert_dict_equal(body_1, expected_1)
639+
640+
body_10 = result['body'][1][0]
641+
expected_10 = {
642+
"value": 'a', "display_value": 'a', "is_visible": False,
643+
"type": "th", "attributes": ["rowspan=1"],
644+
"class": "row_heading level0 row1",
645+
}
646+
tm.assert_dict_equal(body_10, expected_10)
647+
648+
head = result['head'][0]
649+
expected = [
650+
{'type': 'th', 'class': 'blank', 'value': '',
651+
'is_visible': True, "display_value": ''},
652+
{'type': 'th', 'class': 'blank level0', 'value': '',
653+
'is_visible': True, 'display_value': ''},
654+
{'attributes': ['colspan=1'], 'class': 'col_heading level0 col0',
655+
'is_visible': True, 'type': 'th', 'value': 'A',
656+
'display_value': 'A'}]
657+
self.assertEqual(head, expected)
658+
659+
def test_mi_sparse_disabled(self):
660+
with pd.option_context('display.multi_sparse', False):
661+
df = pd.DataFrame({'A': [1, 2]},
662+
index=pd.MultiIndex.from_arrays([['a', 'a'],
663+
[0, 1]]))
664+
result = df.style._translate()
665+
body = result['body']
666+
for row in body:
667+
self.assertEqual(row[0]['attributes'], ['rowspan=1'])
668+
669+
def test_mi_sparse_index_names(self):
670+
df = pd.DataFrame({'A': [1, 2]}, index=pd.MultiIndex.from_arrays(
671+
[['a', 'a'], [0, 1]],
672+
names=['idx_level_0', 'idx_level_1'])
673+
)
674+
result = df.style._translate()
675+
head = result['head'][1]
676+
expected = [{
677+
'class': 'index_name level0', 'value': 'idx_level_0',
678+
'type': 'th'},
679+
{'class': 'index_name level1', 'value': 'idx_level_1',
680+
'type': 'th'},
681+
{'class': 'blank', 'value': '', 'type': 'th'}]
682+
683+
self.assertEqual(head, expected)
684+
685+
def test_mi_sparse_column_names(self):
686+
df = pd.DataFrame(
687+
np.arange(16).reshape(4, 4),
688+
index=pd.MultiIndex.from_arrays(
689+
[['a', 'a', 'b', 'a'], [0, 1, 1, 2]],
690+
names=['idx_level_0', 'idx_level_1']),
691+
columns=pd.MultiIndex.from_arrays(
692+
[['C1', 'C1', 'C2', 'C2'], [1, 0, 1, 0]],
693+
names=['col_0', 'col_1']
694+
)
695+
)
696+
result = df.style._translate()
697+
head = result['head'][1]
698+
expected = [
699+
{'class': 'blank', 'value': '', 'display_value': '',
700+
'type': 'th', 'is_visible': True},
701+
{'class': 'index_name level1', 'value': 'col_1',
702+
'display_value': 'col_1', 'is_visible': True, 'type': 'th'},
703+
{'attributes': ['colspan=1'],
704+
'class': 'col_heading level1 col0',
705+
'display_value': 1,
706+
'is_visible': True,
707+
'type': 'th',
708+
'value': 1},
709+
{'attributes': ['colspan=1'],
710+
'class': 'col_heading level1 col1',
711+
'display_value': 0,
712+
'is_visible': True,
713+
'type': 'th',
714+
'value': 0},
715+
716+
{'attributes': ['colspan=1'],
717+
'class': 'col_heading level1 col2',
718+
'display_value': 1,
719+
'is_visible': True,
720+
'type': 'th',
721+
'value': 1},
722+
723+
{'attributes': ['colspan=1'],
724+
'class': 'col_heading level1 col3',
725+
'display_value': 0,
726+
'is_visible': True,
727+
'type': 'th',
728+
'value': 0},
729+
]
730+
self.assertEqual(head, expected)
617731

618732

619733
@tm.mplskip

0 commit comments

Comments
 (0)