Repository: pandas-dev/pandas

Issue:
(row) Index Name with to_html(header=False) is not displayed
#### Code Sample, a copy-pastable example if possible

```python
import pandas as pd
import numpy as np
df = pd.DataFrame(np.zeros((2, 2), dtype=int))
df.index.name = 'index.name'
df.to_html(header=False)
```
<table border="1" class="dataframe">
  <tbody>
    <tr>
      <th>0</th>
      <td>0</td>
      <td>0</td>
    </tr>
    <tr>
      <th>1</th>
      <td>0</td>
      <td>0</td>
    </tr>
  </tbody>
</table>

```python
import pandas as pd
import numpy as np
df = pd.DataFrame(np.zeros((2, 2), dtype=int))
df.index = pd.MultiIndex.from_product([['a'], ['b', 'c']], names=[
                'index.name.0', 'index.name.1'])
df.to_html(header=False)
```
<table border="1" class="dataframe">
  <tbody>
    <tr>
      <th rowspan="2" valign="top">a</th>
      <th>b</th>
      <td>0</td>
      <td>0</td>
    </tr>
    <tr>
      <th>c</th>
      <td>0</td>
      <td>0</td>
    </tr>
  </tbody>
</table>

#### Problem description

`to_html(header=False)` is not displaying the (row) Index names. The `header` parameter should be analogous to the `index` parameter and hide the columns Index only and leave the (row) Index names displayed.

to hide the display of the row Index names, the `index_names=False` parameter should be used.

the problem is due to an early return in the composition of the HTML header. the `to_html` parameter `header` should not be confused with the HTML header.

https://github.com/pandas-dev/pandas/blob/deb7b4d5003b939f47e525bcdaceeea48622a73a/pandas/io/formats/html.py#L196-L201

#### Expected Output

<table border="1" class="dataframe">
  <thead>
    <tr>
      <th>index.name</th>
      <th></th>
      <th></th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <th>0</th>
      <td>0</td>
      <td>0</td>
    </tr>
    <tr>
      <th>1</th>
      <td>0</td>
      <td>0</td>
    </tr>
  </tbody>
</table>

<table border="1" class="dataframe">
  <thead>
    <tr>
      <th>index.name.0</th>
      <th>index.name.1</th>
      <th></th>
      <th></th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <th rowspan="2" valign="top">a</th>
      <th>b</th>
      <td>0</td>
      <td>0</td>
    </tr>
    <tr>
      <th>c</th>
      <td>0</td>
      <td>0</td>
    </tr>
  </tbody>
</table>

#### Output of ``pd.show_versions()``

<details>

INSTALLED VERSIONS
------------------
commit: None
python: 3.6.5.final.0
python-bits: 64
OS: Windows
OS-release: 10
machine: AMD64
processor: Intel64 Family 6 Model 58 Stepping 9, GenuineIntel
byteorder: little
LC_ALL: None
LANG: None
LOCALE: None.None

pandas: 0.23.0
pytest: 3.5.1
pip: 10.0.1
setuptools: 39.1.0
Cython: 0.28.2
numpy: 1.14.3
scipy: 1.1.0
pyarrow: None
xarray: None
IPython: 6.4.0
sphinx: 1.7.4
patsy: 0.5.0
dateutil: 2.7.3
pytz: 2018.4
blosc: None
bottleneck: 1.2.1
tables: 3.4.3
numexpr: 2.6.5
feather: None
matplotlib: 2.2.2
openpyxl: 2.5.3
xlrd: 1.1.0
xlwt: 1.3.0
xlsxwriter: 1.0.4
lxml: 4.2.1
bs4: 4.6.0
html5lib: 1.0.1
sqlalchemy: 1.2.7
pymysql: None
psycopg2: None
jinja2: 2.10
s3fs: None
fastparquet: None
pandas_gbq: None
pandas_datareader: None

</details>

cc @WillAyd 


Discussion:
This isn't just restricted to HTML.  It's a bug across the board, even with `to_csv`.

Resolving patch:
diff --git a/pandas/io/formats/html.py b/pandas/io/formats/html.py
--- a/pandas/io/formats/html.py
+++ b/pandas/io/formats/html.py
@@ -43,6 +43,12 @@ def __init__(self, formatter, classes=None, notebook=False, border=None,
         self.table_id = table_id
         self.render_links = render_links
 
+    @property
+    def show_row_idx_names(self):
+        return all((self.fmt.has_index_names,
+                    self.fmt.index,
+                    self.fmt.show_index_names))
+
     @property
     def show_col_idx_names(self):
         # see gh-22579
@@ -165,9 +171,7 @@ def write_style(self):
             element_props.append(('thead tr th',
                                   'text-align',
                                   'left'))
-            if all((self.fmt.has_index_names,
-                    self.fmt.index,
-                    self.fmt.show_index_names)):
+            if self.show_row_idx_names:
                 element_props.append(('thead tr:last-of-type th',
                                       'text-align',
                                       'right'))
@@ -228,17 +232,8 @@ def write_result(self, buf):
 
         buffer_put_lines(buf, self.elements)
 
-    def _write_header(self, indent):
+    def _write_col_header(self, indent):
         truncate_h = self.fmt.truncate_h
-
-        if not self.fmt.header:
-            # write nothing
-            return indent
-
-        self.write('<thead>', indent)
-
-        indent += self.indent_delta
-
         if isinstance(self.columns, ABCMultiIndex):
             template = 'colspan="{span:d}" halign="left"'
 
@@ -357,12 +352,25 @@ def _write_header(self, indent):
             self.write_tr(row, indent, self.indent_delta, header=True,
                           align=align)
 
-        if all((self.fmt.has_index_names,
-                self.fmt.index,
-                self.fmt.show_index_names)):
-            row = ([x if x is not None else '' for x in self.frame.index.names]
-                   + [''] * (self.ncols + (1 if truncate_h else 0)))
-            self.write_tr(row, indent, self.indent_delta, header=True)
+    def _write_row_header(self, indent):
+        truncate_h = self.fmt.truncate_h
+        row = ([x if x is not None else '' for x in self.frame.index.names]
+               + [''] * (self.ncols + (1 if truncate_h else 0)))
+        self.write_tr(row, indent, self.indent_delta, header=True)
+
+    def _write_header(self, indent):
+        if not (self.fmt.header or self.show_row_idx_names):
+            # write nothing
+            return indent
+
+        self.write('<thead>', indent)
+        indent += self.indent_delta
+
+        if self.fmt.header:
+            self._write_col_header(indent)
+
+        if self.show_row_idx_names:
+            self._write_row_header(indent)
 
         indent -= self.indent_delta
         self.write('</thead>', indent)


Test patch:
diff --git a/pandas/tests/io/formats/data/html/index_named_multi_columns_none.html b/pandas/tests/io/formats/data/html/index_named_multi_columns_none.html
new file mode 100644
--- /dev/null
+++ b/pandas/tests/io/formats/data/html/index_named_multi_columns_none.html
@@ -0,0 +1,23 @@
+<table border="1" class="dataframe">
+  <thead>
+    <tr>
+      <th>index.name.0</th>
+      <th>index.name.1</th>
+      <th></th>
+      <th></th>
+    </tr>
+  </thead>
+  <tbody>
+    <tr>
+      <th rowspan="2" valign="top">a</th>
+      <th>b</th>
+      <td>0</td>
+      <td>0</td>
+    </tr>
+    <tr>
+      <th>c</th>
+      <td>0</td>
+      <td>0</td>
+    </tr>
+  </tbody>
+</table>
diff --git a/pandas/tests/io/formats/data/html/index_named_standard_columns_none.html b/pandas/tests/io/formats/data/html/index_named_standard_columns_none.html
new file mode 100644
--- /dev/null
+++ b/pandas/tests/io/formats/data/html/index_named_standard_columns_none.html
@@ -0,0 +1,21 @@
+<table border="1" class="dataframe">
+  <thead>
+    <tr>
+      <th>index.name</th>
+      <th></th>
+      <th></th>
+    </tr>
+  </thead>
+  <tbody>
+    <tr>
+      <th>0</th>
+      <td>0</td>
+      <td>0</td>
+    </tr>
+    <tr>
+      <th>1</th>
+      <td>0</td>
+      <td>0</td>
+    </tr>
+  </tbody>
+</table>
diff --git a/pandas/tests/io/formats/data/html/index_unnamed_multi_columns_none.html b/pandas/tests/io/formats/data/html/index_unnamed_multi_columns_none.html
new file mode 100644
--- /dev/null
+++ b/pandas/tests/io/formats/data/html/index_unnamed_multi_columns_none.html
@@ -0,0 +1,15 @@
+<table border="1" class="dataframe">
+  <tbody>
+    <tr>
+      <th rowspan="2" valign="top">a</th>
+      <th>b</th>
+      <td>0</td>
+      <td>0</td>
+    </tr>
+    <tr>
+      <th>c</th>
+      <td>0</td>
+      <td>0</td>
+    </tr>
+  </tbody>
+</table>
diff --git a/pandas/tests/io/formats/data/html/index_unnamed_standard_columns_none.html b/pandas/tests/io/formats/data/html/index_unnamed_standard_columns_none.html
new file mode 100644
--- /dev/null
+++ b/pandas/tests/io/formats/data/html/index_unnamed_standard_columns_none.html
@@ -0,0 +1,14 @@
+<table border="1" class="dataframe">
+  <tbody>
+    <tr>
+      <th>0</th>
+      <td>0</td>
+      <td>0</td>
+    </tr>
+    <tr>
+      <th>1</th>
+      <td>0</td>
+      <td>0</td>
+    </tr>
+  </tbody>
+</table>
diff --git a/pandas/tests/io/formats/data/html/trunc_df_index_named_multi_columns_none.html b/pandas/tests/io/formats/data/html/trunc_df_index_named_multi_columns_none.html
new file mode 100644
--- /dev/null
+++ b/pandas/tests/io/formats/data/html/trunc_df_index_named_multi_columns_none.html
@@ -0,0 +1,62 @@
+<table border="1" class="dataframe">
+  <thead>
+    <tr>
+      <th>foo</th>
+      <th></th>
+      <th>baz</th>
+      <th></th>
+      <th></th>
+      <th></th>
+      <th></th>
+      <th></th>
+    </tr>
+  </thead>
+  <tbody>
+    <tr>
+      <th rowspan="2" valign="top">a</th>
+      <th rowspan="2" valign="top">c</th>
+      <th>e</th>
+      <td>0</td>
+      <td>1</td>
+      <td>...</td>
+      <td>6</td>
+      <td>7</td>
+    </tr>
+    <tr>
+      <th>f</th>
+      <td>8</td>
+      <td>9</td>
+      <td>...</td>
+      <td>14</td>
+      <td>15</td>
+    </tr>
+    <tr>
+      <th>...</th>
+      <th>...</th>
+      <th>...</th>
+      <td>...</td>
+      <td>...</td>
+      <td>...</td>
+      <td>...</td>
+      <td>...</td>
+    </tr>
+    <tr>
+      <th rowspan="2" valign="top">b</th>
+      <th rowspan="2" valign="top">d</th>
+      <th>e</th>
+      <td>48</td>
+      <td>49</td>
+      <td>...</td>
+      <td>54</td>
+      <td>55</td>
+    </tr>
+    <tr>
+      <th>f</th>
+      <td>56</td>
+      <td>57</td>
+      <td>...</td>
+      <td>62</td>
+      <td>63</td>
+    </tr>
+  </tbody>
+</table>
diff --git a/pandas/tests/io/formats/data/html/trunc_df_index_named_standard_columns_none.html b/pandas/tests/io/formats/data/html/trunc_df_index_named_standard_columns_none.html
new file mode 100644
--- /dev/null
+++ b/pandas/tests/io/formats/data/html/trunc_df_index_named_standard_columns_none.html
@@ -0,0 +1,54 @@
+<table border="1" class="dataframe">
+  <thead>
+    <tr>
+      <th>index.name</th>
+      <th></th>
+      <th></th>
+      <th></th>
+      <th></th>
+      <th></th>
+    </tr>
+  </thead>
+  <tbody>
+    <tr>
+      <th>0</th>
+      <td>0</td>
+      <td>1</td>
+      <td>...</td>
+      <td>6</td>
+      <td>7</td>
+    </tr>
+    <tr>
+      <th>1</th>
+      <td>8</td>
+      <td>9</td>
+      <td>...</td>
+      <td>14</td>
+      <td>15</td>
+    </tr>
+    <tr>
+      <th>...</th>
+      <td>...</td>
+      <td>...</td>
+      <td>...</td>
+      <td>...</td>
+      <td>...</td>
+    </tr>
+    <tr>
+      <th>6</th>
+      <td>48</td>
+      <td>49</td>
+      <td>...</td>
+      <td>54</td>
+      <td>55</td>
+    </tr>
+    <tr>
+      <th>7</th>
+      <td>56</td>
+      <td>57</td>
+      <td>...</td>
+      <td>62</td>
+      <td>63</td>
+    </tr>
+  </tbody>
+</table>
diff --git a/pandas/tests/io/formats/test_to_html.py b/pandas/tests/io/formats/test_to_html.py
--- a/pandas/tests/io/formats/test_to_html.py
+++ b/pandas/tests/io/formats/test_to_html.py
@@ -429,6 +429,7 @@ def test_to_html_multi_indexes_index_false(self, datapath):
         assert result == expected
 
     @pytest.mark.parametrize('index_names', [True, False])
+    @pytest.mark.parametrize('header', [True, False])
     @pytest.mark.parametrize('index', [True, False])
     @pytest.mark.parametrize('column_index, column_type', [
         (Index([0, 1]), 'unnamed_standard'),
@@ -448,18 +449,21 @@ def test_to_html_multi_indexes_index_false(self, datapath):
     ])
     def test_to_html_basic_alignment(
             self, datapath, row_index, row_type, column_index, column_type,
-            index, index_names):
+            index, header, index_names):
         # GH 22747, GH 22579
         df = DataFrame(np.zeros((2, 2), dtype=int),
                        index=row_index, columns=column_index)
-        result = df.to_html(index=index, index_names=index_names)
+        result = df.to_html(
+            index=index, header=header, index_names=index_names)
 
         if not index:
             row_type = 'none'
         elif not index_names and row_type.startswith('named'):
             row_type = 'un' + row_type
 
-        if not index_names and column_type.startswith('named'):
+        if not header:
+            column_type = 'none'
+        elif not index_names and column_type.startswith('named'):
             column_type = 'un' + column_type
 
         filename = 'index_' + row_type + '_columns_' + column_type
@@ -467,6 +471,7 @@ def test_to_html_basic_alignment(
         assert result == expected
 
     @pytest.mark.parametrize('index_names', [True, False])
+    @pytest.mark.parametrize('header', [True, False])
     @pytest.mark.parametrize('index', [True, False])
     @pytest.mark.parametrize('column_index, column_type', [
         (Index(np.arange(8)), 'unnamed_standard'),
@@ -488,19 +493,22 @@ def test_to_html_basic_alignment(
     ])
     def test_to_html_alignment_with_truncation(
             self, datapath, row_index, row_type, column_index, column_type,
-            index, index_names):
+            index, header, index_names):
         # GH 22747, GH 22579
         df = DataFrame(np.arange(64).reshape(8, 8),
                        index=row_index, columns=column_index)
-        result = df.to_html(max_rows=4, max_cols=4,
-                            index=index, index_names=index_names)
+        result = df.to_html(
+            max_rows=4, max_cols=4,
+            index=index, header=header, index_names=index_names)
 
         if not index:
             row_type = 'none'
         elif not index_names and row_type.startswith('named'):
             row_type = 'un' + row_type
 
-        if not index_names and column_type.startswith('named'):
+        if not header:
+            column_type = 'none'
+        elif not index_names and column_type.startswith('named'):
             column_type = 'un' + column_type
 
         filename = 'trunc_df_index_' + row_type + '_columns_' + column_type