Repository: pandas-dev/pandas

Issue:
bug using `read_excel` and `index_col` option
### Code Sample, a copy-pastable example if possible

**Example 1 (weird column/value assignment)**
```
import pandas as pd

# Writing excel file with no column name in first column
tab = pd.DataFrame([['a', 'x'], ['b', 'y']], columns=['col1', 'col2'])
print('Original:\n', tab)
tab.to_excel('tmp_can_del.xlsx')

# The table read using read_excel assigns values of col1 to col2
new_tab = pd.read_excel('tmp_can_del.xlsx', index_col=2)
print('Read:\n', new_tab)
```

**Example 2 (index_col=None seems not to work)**
```
import pandas as pd

# Writing excel file with no column name in first column
tab = pd.DataFrame([['a', 'x'], ['b', 'y']], columns=['col1', 'col2'], index=['i1', 'i2'])
print('Original:\n', tab)
tab.to_excel('tmp_can_del.xlsx')

# The table read using read_excel should have three columns (`Unnamed: 0`, `col1`, `col2`) but just has two
new_tab = pd.read_excel('tmp_can_del.xlsx', index_col=None)
print('Read:\n', new_tab)
print(new_tab.columns)
```

### Problem description

The `index_col` option in `pd.read_excel` seems not to behave properly when the first column name is missing (for example, when a file is written using `to_excel(..., index=True)`).

**Example 1**
We end up with a DataFrame where the data for `col2` was originally the data below `col1`. This does not seem intuitive.

```
Original:
   col1 col2
0    a    x
1    b    y
Read:
    col1 col2
x     0    a
y     1    b
```

**Example 2**
It seems like the `index_col=None` option does not work. The first column  of the file is treated as the DataFrame index.

```
Original:
    col1 col2
i1    a    x
i2    b    y
Read:
    col1 col2
i1    a    x
i2    b    y
Index(['col1', 'col2'], dtype='object')
```
### Expected Output

**Example 1**
If column names are not specified, I would expect the data assigned to `col1` to be that below `col1` in the original excel file (same for `col2`).  Something like this:

```
Read:
    col1 col2
x     a    x
y     b    y
```

**Example 2**
I would expect the newly read tab to look somewhat like this (as is the case when doing the same operations using `write_csv` and `read_csv`):

```
   Unnamed: 0 col1 col2
0           i1    a    x
1           i2    b    y
```

### Output of ``pd.show_versions()``

INSTALLED VERSIONS
------------------
commit: None
python: 3.5.2.final.0
python-bits: 64
OS: Linux
OS-release: 4.8.0-58-generic
machine: x86_64
processor: x86_64
byteorder: little
LC_ALL: None
LANG: fr_FR.UTF-8
LOCALE: fr_FR.UTF-8

pandas: 0.21.1
pytest: 3.1.2
pip: 9.0.1
setuptools: 37.0.0
Cython: 0.25.2
numpy: 1.13.3
scipy: 0.19.1
pyarrow: None
xarray: None
IPython: 6.1.0
sphinx: 1.6.3
patsy: None
dateutil: 2.6.1
pytz: 2017.3
blosc: None
bottleneck: None
tables: None
numexpr: 2.6.2
feather: None
matplotlib: 2.0.2
openpyxl: None
xlrd: 1.0.0
xlwt: 1.3.0
xlsxwriter: 0.7.3
lxml: None
bs4: None
html5lib: 0.999999999
sqlalchemy: None
pymysql: None
psycopg2: None
jinja2: 2.9.6
s3fs: None
fastparquet: None
pandas_gbq: None
pandas_datareader: None

</details>



Discussion:
Re-read my issue; I believe this should be tagged as a bug.

Cheers!

Resolving patch:
diff --git a/pandas/io/excel.py b/pandas/io/excel.py
--- a/pandas/io/excel.py
+++ b/pandas/io/excel.py
@@ -662,10 +662,14 @@ def _parse_cell(cell_contents, cell_typ):
 
                 output[asheetname] = parser.read(nrows=nrows)
 
-                if ((not squeeze or isinstance(output[asheetname], DataFrame))
-                        and header_names):
-                    output[asheetname].columns = output[
-                        asheetname].columns.set_names(header_names)
+                if not squeeze or isinstance(output[asheetname], DataFrame):
+                    if header_names:
+                        output[asheetname].columns = output[
+                            asheetname].columns.set_names(header_names)
+                    elif compat.PY2:
+                        output[asheetname].columns = _maybe_convert_to_string(
+                            output[asheetname].columns)
+
             except EmptyDataError:
                 # No Data, return an empty DataFrame
                 output[asheetname] = DataFrame()
@@ -810,6 +814,39 @@ def _trim_excel_header(row):
     return row
 
 
+def _maybe_convert_to_string(row):
+    """
+    Convert elements in a row to string from Unicode.
+
+    This is purely a Python 2.x patch and is performed ONLY when all
+    elements of the row are string-like.
+
+    Parameters
+    ----------
+    row : array-like
+        The row of data to convert.
+
+    Returns
+    -------
+    converted : array-like
+    """
+    if compat.PY2:
+        converted = []
+
+        for i in range(len(row)):
+            if isinstance(row[i], compat.string_types):
+                try:
+                    converted.append(str(row[i]))
+                except UnicodeEncodeError:
+                    break
+            else:
+                break
+        else:
+            row = converted
+
+    return row
+
+
 def _fill_mi_header(row, control_row):
     """Forward fills blank entries in row, but only inside the same parent index
 
@@ -838,7 +875,7 @@ def _fill_mi_header(row, control_row):
             control_row[i] = False
             last = row[i]
 
-    return row, control_row
+    return _maybe_convert_to_string(row), control_row
 
 # fill blank if index_col not None
 


Test patch:
diff --git a/pandas/tests/io/data/test1.xls b/pandas/tests/io/data/test1.xls
Binary files a/pandas/tests/io/data/test1.xls and b/pandas/tests/io/data/test1.xls differ
diff --git a/pandas/tests/io/data/test1.xlsm b/pandas/tests/io/data/test1.xlsm
Binary files a/pandas/tests/io/data/test1.xlsm and b/pandas/tests/io/data/test1.xlsm differ
diff --git a/pandas/tests/io/data/test1.xlsx b/pandas/tests/io/data/test1.xlsx
Binary files a/pandas/tests/io/data/test1.xlsx and b/pandas/tests/io/data/test1.xlsx differ
diff --git a/pandas/tests/io/test_excel.py b/pandas/tests/io/test_excel.py
--- a/pandas/tests/io/test_excel.py
+++ b/pandas/tests/io/test_excel.py
@@ -264,6 +264,18 @@ def test_index_col_empty(self, ext):
                                               names=["A", "B", "C"]))
         tm.assert_frame_equal(result, expected)
 
+    @pytest.mark.parametrize("index_col", [None, 2])
+    def test_index_col_with_unnamed(self, ext, index_col):
+        # see gh-18792
+        result = self.get_exceldf("test1", ext, "Sheet4",
+                                  index_col=index_col)
+        expected = DataFrame([["i1", "a", "x"], ["i2", "b", "y"]],
+                             columns=["Unnamed: 0", "col1", "col2"])
+        if index_col:
+            expected = expected.set_index(expected.columns[index_col])
+
+        tm.assert_frame_equal(result, expected)
+
     def test_usecols_pass_non_existent_column(self, ext):
         msg = ("Usecols do not match columns, "
                "columns expected but not found: " + r"\['E'\]")
@@ -923,9 +935,9 @@ def test_read_excel_multiindex_empty_level(self, ext):
             })
 
             expected = DataFrame({
-                ("One", u"x"): {0: 1},
-                ("Two", u"X"): {0: 3},
-                ("Two", u"Y"): {0: 7},
+                ("One", "x"): {0: 1},
+                ("Two", "X"): {0: 3},
+                ("Two", "Y"): {0: 7},
                 ("Zero", "Unnamed: 4_level_1"): {0: 0}
             })
 
@@ -942,9 +954,9 @@ def test_read_excel_multiindex_empty_level(self, ext):
 
             expected = pd.DataFrame({
                 ("Beg", "Unnamed: 1_level_1"): {0: 0},
-                ("Middle", u"x"): {0: 1},
-                ("Tail", u"X"): {0: 3},
-                ("Tail", u"Y"): {0: 7}
+                ("Middle", "x"): {0: 1},
+                ("Tail", "X"): {0: 3},
+                ("Tail", "Y"): {0: 7}
             })
 
             df.to_excel(path)