Repository: pandas-dev/pandas

Issue:
read_excel in version 0.25.0rc0 treats empty columns differently
I'm using this code to load an Excel file.
```python
df = pandas.read_excel(
    "data.xlsx",
    sheet_name="sheet1",
    usecols=[0, 1], 
    header=None,
    names=["foo", "bar"]
)

print(df.head())
```
The Excel file has the cells `A7`=`1`, `A8`=`2`, `A9`=`3`, everything else is empty.

With pandas 0.24.2 I get this:
```
   foo  bar
0    1  NaN
1    2  NaN
2    3  NaN
```
With pandas 0.25.0rc0 I get:
```
Traceback (most recent call last):
  File "tester.py", line 8, in <module>
    names=["foo", "bar"]
  File "/home/me/.env/lib/python3.7/site-packages/pandas/util/_decorators.py", line 196, in wrapper
    return func(*args, **kwargs)
  File "/home/me/.env/lib/python3.7/site-packages/pandas/io/excel/_base.py", line 334, in read_excel
    **kwds
  File "/home/me/.env/lib/python3.7/site-packages/pandas/io/excel/_base.py", line 877, in parse
    **kwds
  File "/home/me/.env/lib/python3.7/site-packages/pandas/io/excel/_base.py", line 507, in parse
    **kwds
  File "/home/me/.env/lib/python3.7/site-packages/pandas/io/parsers.py", line 2218, in TextParser
    return TextFileReader(*args, **kwds)
  File "/home/me/.env/lib/python3.7/site-packages/pandas/io/parsers.py", line 895, in __init__
    self._make_engine(self.engine)
  File "/home/me/.env/lib/python3.7/site-packages/pandas/io/parsers.py", line 1147, in _make_engine
    self._engine = klass(self.f, **self.options)
  File "/home/me/.env/lib/python3.7/site-packages/pandas/io/parsers.py", line 2305, in __init__
    ) = self._infer_columns()
  File "/home/me/.env/lib/python3.7/site-packages/pandas/io/parsers.py", line 2712, in _infer_columns
    _validate_usecols_names(self.usecols, range(ncols))
  File "/home/me/.env/lib/python3.7/site-packages/pandas/io/parsers.py", line 1255, in _validate_usecols_names
    "columns expected but not found: {missing}".format(missing=missing)
ValueError: Usecols do not match columns, columns expected but not found: [1]
```
The problem happens because the `bar` column does not contain any data. As soon as I put a value into it, both versions do the same thing.
I'm using Python 3.7.3 in Ubuntu 19.04.


Discussion:
I think this is intentional ref #25623 so not really a regression. Do you have a particular use case for this?
@WillAyd Our use case is that we have daily reports and one of the columns only contains data when something unusual happened. Consequently, in some files this column is completely empty and "the column is completely empty" is exactly the information that we are looking for.

The change in #25623 that you referenced mentions CSV files. For CSV files I agree that the change is very useful, since the CSV file really does not contain the column. But for Excel files, there is no such thing as a non-existing column. 
I don't think this is something likely to be reverted as it was a bug in core IO handling before that allowed this not to raise but let's see what others think
shouldn’t just specifying names work?
Seems to work for me locally - @snordhausen how about on your end?
@WillAyd To make sure that we are both testing the same thing, I extended my test program to also create the `data.xlsx` file:
```
import pandas
from openpyxl import Workbook

wb = Workbook()
ws = wb.active
ws['A7'] = 1
ws['A8'] = 2
ws['A9'] = 3
wb.save("data.xlsx")

df = pandas.read_excel(
    "data.xlsx",
    sheet_name="Sheet",
    usecols=[0, 1],
    header=None,
    names=["foo", "bar"]
)

print(df)
```
I also tried this out in a fresh Ubuntu 18.04 docker container and could reproduce the issue.
Try removing usecols from your call

Sent from my iPhone

> On Jul 8, 2019, at 2:41 AM, Stefan Nordhausen <notifications@github.com> wrote:
> 
> @WillAyd To make sure that we are both testing the same thing, I extended my test program to also create the data.xlsx file:
> 
> import pandas
> from openpyxl import Workbook
> 
> wb = Workbook()
> ws = wb.active
> ws['A7'] = 1
> ws['A8'] = 2
> ws['A9'] = 3
> wb.save("data.xlsx")
> 
> df = pandas.read_excel(
>     "data.xlsx",
>     sheet_name="Sheet",
>     usecols=[0, 1],
>     header=None,
>     names=["foo", "bar"]
> )
> 
> print(df)
> I also tried this out in a fresh Ubuntu 18.04 docker container and could reproduce the issue.
> 
> —
> You are receiving this because you were mentioned.
> Reply to this email directly, view it on GitHub, or mute the thread.

Removing `usecols` makes the program work with 0.25.0rc0.

However, that looks inconsistent to me: why can I implicitly load empty columns, but when I explicitly ask for them I get an error? Also, it means I cannot load (potentially) empty columns in the middle of the table, e.g. if I only wanted column 0 and 20.
> However, that looks inconsistent to me: why can I implicitly load empty columns, but when I explicitly ask for them I get an error?

The fact that this worked previously is inconsistent with read_csv. `usecols` is typically validated and missing indexes or labels throws errors. For example:

```python
>>> data = """a,b,c\n1,2,3"""
>>> pd.read_csv(io.StringIO(test), usecols=['x'])
ValueError: Usecols do not match columns, columns expected but not found: ['x']

>>> pd.read_csv(io.StringIO(test), usecols=[10])
ValueError: Usecols do not match columns, columns expected but not found: [10]
```

So I don't think there is any reason to have Excel be excepted from that validation. You can use `names` as suggested above or reindex the output on your own
The biggest issue is using the parser to read multiple sheets from 1 excel file.

Trying to read multiple sheets in 1 IO causes a lot of issues if the column length varies within a range (eg. "AA, AG:BZ") with AA being the index and AG:BZ the potential columns.
This example will throw an error instead of omitting the empty columns, which caused a lot of headaches and let me to revert to 0.24.
@pandas-dev/pandas-core would anyone object to reverting #25623 ? It looks like this is causing confusion in the Excel world as described by users above

To support use cases above with that in place we would need to break Excel `usecols` handling from the CSV one. I'm not sure this is desired but at the same time I don't think the issue we solved to raise for bad `usecols` is that urgent so could defer that if its a hang up for RC users
I have no objections to reverting the original PR.

However, I would meet that issue half-way and issue warnings instead.
A FutureWarning or did you have something else in mind?
I would go with `UserWarning`.

`FutureWarning` to me implies some kind of deprecation, which I don't think will happen at this point (unless we have some really strong feelings about keeping this behavior).
I am fine with reverting to restore the functionality of excel for 0.25.0.

But I also wanted to mention that from a user perspective, I wouldn't mind that some options behave differently between csv and excel (in the end, they are different formats with different capabilities). Whether this is possible/desirable from a code perspective, don't know the parsing code well enough for that.
> I wouldn't mind that some options behave differently between csv and excel (in the end, they are different formats with different capabilities)

> Whether this is possible/desirable from a code perspective, don't know the parsing code well enough for that

It's definitely possible, but I would want more feedback from users, hence why I suggested the warning.  That way we can draw people's attention to it (maybe even reference the two issues).

Resolving patch:
diff --git a/pandas/io/parsers.py b/pandas/io/parsers.py
--- a/pandas/io/parsers.py
+++ b/pandas/io/parsers.py
@@ -1947,12 +1947,6 @@ def __init__(self, src, **kwds):
             ):
                 _validate_usecols_names(usecols, self.orig_names)
 
-            # GH 25623
-            # validate that column indices in usecols are not out of bounds
-            elif self.usecols_dtype == "integer":
-                indices = range(self._reader.table_width)
-                _validate_usecols_names(usecols, indices)
-
             if len(self.names) > len(usecols):
                 self.names = [
                     n
@@ -2258,7 +2252,7 @@ def __init__(self, f, **kwds):
         self.skipinitialspace = kwds["skipinitialspace"]
         self.lineterminator = kwds["lineterminator"]
         self.quoting = kwds["quoting"]
-        self.usecols, self.usecols_dtype = _validate_usecols_arg(kwds["usecols"])
+        self.usecols, _ = _validate_usecols_arg(kwds["usecols"])
         self.skip_blank_lines = kwds["skip_blank_lines"]
 
         self.warn_bad_lines = kwds["warn_bad_lines"]
@@ -2665,13 +2659,6 @@ def _infer_columns(self):
             if clear_buffer:
                 self._clear_buffer()
 
-            # GH 25623
-            # validate that column indices in usecols are not out of bounds
-            if self.usecols_dtype == "integer":
-                for col in columns:
-                    indices = range(len(col))
-                    _validate_usecols_names(self.usecols, indices)
-
             if names is not None:
                 if (self.usecols is not None and len(names) != len(self.usecols)) or (
                     self.usecols is None and len(names) != len(columns[0])
@@ -2706,11 +2693,6 @@ def _infer_columns(self):
             ncols = len(line)
             num_original_columns = ncols
 
-            # GH 25623
-            # validate that column indices in usecols are not out of bounds
-            if self.usecols_dtype == "integer":
-                _validate_usecols_names(self.usecols, range(ncols))
-
             if not names:
                 if self.prefix:
                     columns = [


Test patch:
diff --git a/pandas/tests/io/parser/test_usecols.py b/pandas/tests/io/parser/test_usecols.py
--- a/pandas/tests/io/parser/test_usecols.py
+++ b/pandas/tests/io/parser/test_usecols.py
@@ -22,25 +22,6 @@
 )
 
 
-@pytest.mark.parametrize(
-    "names,usecols,missing",
-    [
-        (None, [0, 3], r"\[3\]"),
-        (["a", "b", "c"], [0, -1, 2], r"\[-1\]"),
-        (None, [3], r"\[3\]"),
-        (["a"], [3], r"\[3\]"),
-    ],
-)
-def test_usecols_out_of_bounds(all_parsers, names, usecols, missing):
-    # See gh-25623
-    data = "a,b,c\n1,2,3\n4,5,6"
-    parser = all_parsers
-
-    mssg = _msg_validate_usecols_names.format(missing)
-    with pytest.raises(ValueError, match=mssg):
-        parser.read_csv(StringIO(data), usecols=usecols, names=names)
-
-
 def test_raise_on_mixed_dtype_usecols(all_parsers):
     # See gh-12678
     data = """a,b,c