Skip to content

Commit ecba615

Browse files
committed
ENH: MultiIndex Structure for DataFrame.style
BUG: Fix index class level row MVP Columns too tests
1 parent 4dd734c commit ecba615

File tree

3 files changed

+110
-24
lines changed

3 files changed

+110
-24
lines changed

doc/source/whatsnew/v0.19.0.txt

+1
Original file line numberDiff line numberDiff line change
@@ -314,6 +314,7 @@ Other enhancements
314314
- ``Series.append`` now supports the ``ignore_index`` option (:issue:`13677`)
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`)
317+
- ``DataFrame.style`` will now render sparsified MultiIndexes (:issue:`11655`)
317318
- ``DataFrame`` has gained support to re-order the columns based on the values
318319
in a row using ``df.sort_values(by='...', axis=1)`` (:issue:`10806`)
319320

pandas/formats/style.py

+63-12
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,8 @@
2121

2222
import numpy as np
2323
import pandas as pd
24-
from pandas.compat import lzip, range
24+
from pandas.compat import range
25+
import pandas.core.common as com
2526
from pandas.core.indexing import _maybe_numeric_slice, _non_reducing_slice
2627
try:
2728
import matplotlib.pyplot as plt
@@ -110,7 +111,9 @@ class Styler(object):
110111
{% for r in head %}
111112
<tr>
112113
{% for c in r %}
113-
<{{c.type}} class="{{c.class}}">{{c.value}}
114+
{% if c.is_visible != False %}
115+
<{{c.type}} class="{{c.class}}" {{ c.attributes|join(" ") }}>{{c.value}}
116+
{% endif %}
114117
{% endfor %}
115118
</tr>
116119
{% endfor %}
@@ -119,8 +122,10 @@ class Styler(object):
119122
{% for r in body %}
120123
<tr>
121124
{% for c in r %}
122-
<{{c.type}} id="T_{{uuid}}{{c.id}}" class="{{c.class}}">
125+
{% if c.is_visible != False %}
126+
<{{c.type}} id="T_{{uuid}}{{c.id}}" class="{{c.class}}" {{ c.attributes|join(" ") }}>
123127
{{ c.display_value }}
128+
{% endif %}
124129
{% endfor %}
125130
</tr>
126131
{% endfor %}
@@ -181,17 +186,20 @@ def _translate(self):
181186
BLANK_CLASS = "blank"
182187
BLANK_VALUE = ""
183188

189+
def format_attr(pair):
190+
return "{key}={value}".format(**pair)
191+
192+
# for sparsifying a MultiIndex
193+
idx_lengths = _get_level_lengths(self.index)
194+
col_lengths = _get_level_lengths(self.columns)
195+
184196
cell_context = dict()
185197

186198
n_rlvls = self.data.index.nlevels
187199
n_clvls = self.data.columns.nlevels
188200
rlabels = self.data.index.tolist()
189201
clabels = self.data.columns.tolist()
190202

191-
idx_values = self.data.index.format(sparsify=False, adjoin=False,
192-
names=False)
193-
idx_values = lzip(*idx_values)
194-
195203
if n_rlvls == 1:
196204
rlabels = [[x] for x in rlabels]
197205
if n_clvls == 1:
@@ -213,7 +221,13 @@ def _translate(self):
213221
row_es.append({"type": "th",
214222
"value": value,
215223
"display_value": value,
216-
"class": " ".join(cs)})
224+
"class": " ".join(cs),
225+
"is_visible": _is_visible(c, r, col_lengths),
226+
"attributes": [
227+
format_attr({"key": "colspan",
228+
"value": col_lengths.get(
229+
(r, c), 1)})
230+
]})
217231
head.append(row_es)
218232

219233
if self.data.index.names and self.data.index.names != [None]:
@@ -236,12 +250,17 @@ def _translate(self):
236250

237251
body = []
238252
for r, idx in enumerate(self.data.index):
239-
cs = [ROW_HEADING_CLASS, "level%s" % c, "row%s" % r]
240-
cs.extend(
241-
cell_context.get("row_headings", {}).get(r, {}).get(c, []))
253+
# cs.extend(
254+
# cell_context.get("row_headings", {}).get(r, {}).get(c, []))
242255
row_es = [{"type": "th",
256+
"is_visible": _is_visible(r, c, idx_lengths),
257+
"attributes": [
258+
format_attr({"key": "rowspan",
259+
"value": idx_lengths.get((c, r), 1)})
260+
],
243261
"value": rlabels[r][c],
244-
"class": " ".join(cs),
262+
"class": " ".join([ROW_HEADING_CLASS, "level%s" % c,
263+
"row%s" % r]),
245264
"display_value": rlabels[r][c]}
246265
for c in range(len(rlabels[r]))]
247266

@@ -893,6 +912,38 @@ def _highlight_extrema(data, color='yellow', max_=True):
893912
index=data.index, columns=data.columns)
894913

895914

915+
def _is_visible(idx_row, idx_col, lengths):
916+
"""
917+
Index -> {(idx_row, idx_col): bool})
918+
"""
919+
return (idx_col, idx_row) in lengths
920+
921+
922+
def _get_level_lengths(index):
923+
'''
924+
Given an index, find the level lenght for each element.
925+
926+
Result is a dictionary of (level, inital_position): span
927+
'''
928+
sentinel = com.sentinel_factory()
929+
levels = index.format(sparsify=sentinel, adjoin=False, names=False)
930+
931+
if index.nlevels == 1:
932+
return {(0, i): 1 for i, value in enumerate(levels)}
933+
934+
lengths = {}
935+
936+
for i, lvl in enumerate(levels):
937+
for j, row in enumerate(lvl):
938+
if row != sentinel:
939+
last_label = j
940+
lengths[(i, last_label)] = 1
941+
else:
942+
lengths[(i, last_label)] += 1
943+
944+
return lengths
945+
946+
896947
def _maybe_wrap_formatter(formatter):
897948
if is_string_like(formatter):
898949
return lambda x: formatter.format(x)

pandas/tests/formats/test_style.py

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

11-
# this is a mess. 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.
1611
job_name = os.environ.get('JOB_NAME', None)
1712
if job_name == '27_slow_nnet_LOCALE':
1813
raise SkipTest("No jinja")
@@ -22,7 +17,7 @@
2217
import jinja2 # noqa
2318
except ImportError:
2419
raise SkipTest("No Jinja2")
25-
from pandas.formats.style import Styler # noqa
20+
from pandas.formats.style import Styler, _get_level_lengths # noqa
2621

2722

2823
class TestStyler(TestCase):
@@ -152,15 +147,24 @@ def test_empty_index_name_doesnt_display(self):
152147
{'class': 'col_heading level0 col0',
153148
'display_value': 'A',
154149
'type': 'th',
155-
'value': 'A'},
150+
'value': 'A',
151+
'is_visible': True,
152+
'attributes': ["colspan=1"],
153+
},
156154
{'class': 'col_heading level0 col1',
157155
'display_value': 'B',
158156
'type': 'th',
159-
'value': 'B'},
157+
'value': 'B',
158+
'is_visible': True,
159+
'attributes': ["colspan=1"],
160+
},
160161
{'class': 'col_heading level0 col2',
161162
'display_value': 'C',
162163
'type': 'th',
163-
'value': 'C'}]]
164+
'value': 'C',
165+
'is_visible': True,
166+
'attributes': ["colspan=1"],
167+
}]]
164168

165169
self.assertEqual(result['head'], expected)
166170

@@ -171,9 +175,11 @@ def test_index_name(self):
171175

172176
expected = [[{'class': 'blank', 'type': 'th', 'value': ''},
173177
{'class': 'col_heading level0 col0', 'type': 'th',
174-
'value': 'B', 'display_value': 'B'},
178+
'value': 'B', 'display_value': 'B',
179+
'is_visible': True, 'attributes': ['colspan=1']},
175180
{'class': 'col_heading level0 col1', 'type': 'th',
176-
'value': 'C', 'display_value': 'C'}],
181+
'value': 'C', 'display_value': 'C',
182+
'is_visible': True, 'attributes': ['colspan=1']}],
177183
[{'class': 'col_heading level2 col0', 'type': 'th',
178184
'value': 'A'},
179185
{'class': 'blank', 'type': 'th', 'value': ''},
@@ -189,7 +195,9 @@ def test_multiindex_name(self):
189195
expected = [[{'class': 'blank', 'type': 'th', 'value': ''},
190196
{'class': 'blank', 'type': 'th', 'value': ''},
191197
{'class': 'col_heading level0 col0', 'type': 'th',
192-
'value': 'C', 'display_value': 'C'}],
198+
'value': 'C', 'display_value': 'C',
199+
'is_visible': True, 'attributes': ['colspan=1'],
200+
}],
193201
[{'class': 'col_heading level2 col0', 'type': 'th',
194202
'value': 'A'},
195203
{'class': 'col_heading level2 col1', 'type': 'th',
@@ -581,6 +589,32 @@ def f(x):
581589
with tm.assertRaises(ValueError):
582590
df.style._apply(f, axis=None)
583591

592+
def test_get_level_lengths(self):
593+
index = pd.MultiIndex.from_product([['a', 'b'], [0, 1, 2]])
594+
expected = {(0, 0): 3, (0, 3): 3, (1, 0): 1, (1, 1): 1, (1, 2): 1,
595+
(1, 3): 1, (1, 4): 1, (1, 5): 1}
596+
result = _get_level_lengths(index)
597+
self.assertDictEqual(result, expected)
598+
599+
def test_get_level_lengths_un_sorted(self):
600+
index = pd.MultiIndex.from_arrays([
601+
[1, 1, 2, 1],
602+
['a', 'b', 'b', 'd']
603+
])
604+
expected = {(0, 0): 2, (0, 2): 1, (0, 3): 1,
605+
(1, 0): 1, (1, 1): 1, (1, 2): 1, (1, 3): 1}
606+
result = _get_level_lengths(index)
607+
self.assertDictEqual(result, expected)
608+
609+
def test_mi_sparse(self):
610+
df = pd.DataFrame({'A': [1, 2, 3, 4]},
611+
index=pd.MultiIndex.from_product([['a', 'b'],
612+
[0, 1]]))
613+
result = df.style.render()
614+
assert 'rowspan' in result
615+
result = df.T.style.render()
616+
assert 'colspan' in result
617+
584618

585619
@tm.mplskip
586620
class TestStylerMatplotlibDep(TestCase):

0 commit comments

Comments
 (0)