Repository: pandas-dev/pandas

Issue:
MultiIndex notebook repr is incorrect when number of columns is > max_columns
See this comment: https://github.com/pydata/pandas/issues/7146#issuecomment-43518940

notebook http://goo.gl/qLUUx8



Discussion:
cc @bjonen can you have a look?

in the second case i think the `...` should be in the first level of the multindex as well as all the others

if the frame is transposed (and use max_rows) is it same issue?

yes, well, sort of. it looks like there might be an off-by-one error some where in the repring code for html .. i'll put a pr together if @bjonen doesn't have time.

@cpcloud I can have a look tonight. 

ok sounds good

@bjonen i've almost got this finished

Ok excellent

@bjonen want to take a look at this? i thought i had it, but i don't think i'll be able to get to this in time for 0.14.0 ... here's a notebook with where i left off: http://nbviewer.ipython.org/gist/cpcloud/61910750733823892bd1 ... i think i'm pretty close but there's an issue with  the central `...` an a multiindex ... i think you might have a better feel for this code since you just changed it :)

you can cherry pick my changes from https://github.com/cpcloud/pandas/tree/fix-mi-format-repr-7174 if you want

@cpcloud No problem, I'll have a look. 


Resolving patch:
diff --git a/pandas/core/format.py b/pandas/core/format.py
--- a/pandas/core/format.py
+++ b/pandas/core/format.py
@@ -780,6 +780,7 @@ def write_result(self, buf):
 
     def _write_header(self, indent):
         truncate_h = self.fmt.truncate_h
+        row_levels = self.frame.index.nlevels
         if not self.fmt.header:
             # write nothing
             return indent
@@ -819,13 +820,49 @@ def _column_header():
                 sentinel = None
             levels = self.columns.format(sparsify=sentinel,
                                          adjoin=False, names=False)
-
             level_lengths = _get_level_lengths(levels, sentinel)
-
-            row_levels = self.frame.index.nlevels
-
+            inner_lvl = len(level_lengths) - 1
             for lnum, (records, values) in enumerate(zip(level_lengths,
                                                          levels)):
+                if truncate_h:
+                    # modify the header lines
+                    ins_col = self.fmt.tr_col_num
+                    if self.fmt.sparsify:
+                        recs_new = {}
+                        # Increment tags after ... col. 
+                        for tag,span in list(records.items()):
+                            if tag >= ins_col:
+                                recs_new[tag + 1] = span
+                            elif tag + span > ins_col:
+                                recs_new[tag] = span + 1
+                                if lnum == inner_lvl:
+                                    values = values[:ins_col] + (u('...'),) + \
+                                        values[ins_col:]
+                                else:  # sparse col headers do not receive a ...
+                                    values = values[:ins_col] + \
+                                        (values[ins_col - 1],) + values[ins_col:]
+                            else:
+                                recs_new[tag] = span
+                            # if ins_col lies between tags, all col headers get ...
+                            if tag + span == ins_col:  
+                                recs_new[ins_col] = 1
+                                values = values[:ins_col] + (u('...'),) + \
+                                    values[ins_col:]
+                        records = recs_new
+                        inner_lvl = len(level_lengths) - 1
+                        if lnum == inner_lvl:
+                            records[ins_col] = 1
+                    else:
+                        recs_new = {}
+                        for tag,span in list(records.items()):
+                            if tag >= ins_col:
+                                recs_new[tag + 1] = span
+                            else:
+                                recs_new[tag] = span
+                        recs_new[ins_col] = 1
+                        records = recs_new
+                        values = values[:ins_col] + [u('...')] + values[ins_col:]
+
                 name = self.columns.names[lnum]
                 row = [''] * (row_levels - 1) + ['' if name is None
                                                  else com.pprint_thing(name)]
@@ -839,17 +876,6 @@ def _column_header():
                         continue
                     j += 1
                     row.append(v)
-                if truncate_h:
-                    if self.fmt.sparsify and lnum == 0:
-                        ins_col = row_levels + self.fmt.tr_col_num - 1
-                        row.insert(ins_col, '...')
-
-                        for tag in list(tags.keys()):
-                            if tag >= ins_col:
-                                tags[tag+1] = tags.pop(tag)
-                    else:
-                        row.insert(row_levels + self.fmt.tr_col_num, '...')
-
                 self.write_tr(row, indent, self.indent_delta, tags=tags,
                               header=True)
         else:
@@ -857,7 +883,8 @@ def _column_header():
             align = self.fmt.justify
 
             if truncate_h:
-                col_row.insert(self.fmt.tr_col_num + 1, '...')
+                ins_col = row_levels + self.fmt.tr_col_num
+                col_row.insert(ins_col, '...')
 
             self.write_tr(col_row, indent, self.indent_delta, header=True,
                           align=align)
@@ -866,6 +893,9 @@ def _column_header():
             row = [
                 x if x is not None else '' for x in self.frame.index.names
             ] + [''] * min(len(self.columns), self.max_cols)
+            if truncate_h:
+                ins_col = row_levels + self.fmt.tr_col_num
+                row.insert(ins_col, '')                
             self.write_tr(row, indent, self.indent_delta, header=True)
 
         indent -= self.indent_delta
@@ -948,12 +978,36 @@ def _write_hierarchical_rows(self, fmt_values, indent):
                                                 adjoin=False, names=False)
 
             level_lengths = _get_level_lengths(levels, sentinel)
+            inner_lvl = len(level_lengths) - 1
+            if truncate_v:
+                # Insert ... row and adjust idx_values and
+                # level_lengths to take this into account. 
+                ins_row = self.fmt.tr_row_num
+                for lnum,records in enumerate(level_lengths):
+                    rec_new = {}
+                    for tag,span in list(records.items()):
+                        if tag >= ins_row:
+                            rec_new[tag + 1] = span
+                        elif tag + span > ins_row:
+                            rec_new[tag] = span + 1
+                            dot_row = list(idx_values[ins_row - 1])
+                            dot_row[-1] = u('...')
+                            idx_values.insert(ins_row,tuple(dot_row))
+                        else:
+                            rec_new[tag] = span
+                        # If ins_row lies between tags, all cols idx cols receive ...
+                        if tag + span == ins_row:
+                            rec_new[ins_row] = 1
+                            if lnum == 0:
+                                idx_values.insert(ins_row,tuple([u('...')]*len(level_lengths)))                            
+                    level_lengths[lnum] = rec_new
+
+                level_lengths[inner_lvl][ins_row] = 1
+                for ix_col in range(len(fmt_values)):
+                    fmt_values[ix_col].insert(ins_row,'...')
+                nrows += 1
 
             for i in range(nrows):
-                if truncate_v and i == (self.fmt.tr_row_num):
-                    str_sep_row = [ '...' ]  * (len(row) + sparse_offset)
-                    self.write_tr(str_sep_row, indent, self.indent_delta, tags=None)
-
                 row = []
                 tags = {}
 


Test patch:
diff --git a/pandas/tests/test_format.py b/pandas/tests/test_format.py
--- a/pandas/tests/test_format.py
+++ b/pandas/tests/test_format.py
@@ -953,8 +953,8 @@ def test_to_html_truncate_multi_index(self):
       <td> NaN</td>
     </tr>
     <tr>
-      <td>...</td>
-      <td>...</td>
+      <th>...</th>
+      <th>...</th>
       <td>...</td>
       <td>...</td>
       <td>...</td>