Repository: pandas-dev/pandas

Issue:
wide_to_long mishandles string arg for `stubnames`
wide_to_long should sanitize/format input args BEFORE sanity checking. 

This bug report pertains to wide_to_long() in master/pandas/core/reshape/melt.py

#### Code Sample, a copy-pastable example if possible

```python
# This check (~line 411) needs to move below input sanitization (~line 425).
 if any(col in stubnames for col in df.columns):
        raise ValueError("stubname can't be identical to a column name")
```
#### Problem description

The intention of the sanity checking code is to check if any element of the list stubname is in the the list of column names and generate an exception (line 411). Unfortunately, this check occours before the input sanitization that ensures that stubnames is a list -- it is also permitted to be string. As it is, the sanity check sometimes looks for substrings in a string and sometimes looks for substrings in a list, depending on the arg type for stubnames. 

#### Expected Output
na

#### Output of ``pd.show_versions()``
It is in master branch on github as of 8/22/2018


Discussion:
Can you provide a minimal code sample to reproduce the issue from an end user perspective?
I will work on one that makes an error, but the error is not the issue - it a logic error that arises from assuming that stubnames is always a list BEFORE the code that enforces that assumption has a chance to run.  
The following code sample throws an error when `stubnames='PA'` and works when `stubnames=['PA']`. This illustrates how an end user might be impacted by the bug. 

Notice that when stubnames is a string, line 411 (above) is actually checking the following:
`any( substring in stubnames_str for substring in df.columns)`   
Because the column `A` is a substring of `PA`, it throws an error. The intention is to check if any column name matches the string `PA`, which it also fails to do. 

Lines 415+ (below) need to run *before* the code on line 411:

```python
# ~line 415
    if not is_list_like(stubnames):
        stubnames = [stubnames]
    else:
        stubnames = list(stubnames)
```


 
Example of a spurious error:

```python
import pandas as pd

foo = {
   'node_id': {0: 0, 1: 1, 2: 2, 3: 3, 4: 4}, 
   'A': {0: 0.805236, 1: 0.0, 2: 0.250981, 3: 1.0, 4: 0.812131}, 
   'PA0': {0: 0.775511, 1: 0.566016, 2: 0.5676359999999999, 3: 0.9837879999999999, 4: 0.67783},
   'PA1': {0: 0.775511, 1: 0.64623, 2: 0.525783, 3: 0.9837879999999999, 4: 0.67783},
   'PA3': {0: 0.775511, 1: 0.703248, 2: 0.526626, 3: 0.985926, 4: 0.67783}
}

test_df = pd.DataFrame.from_dict(foo)

pd.wide_to_long(
    test_df,
    stubnames='PA',  #also try as list ['PA'] 
    i = ['node_id', 'A'],
    j = 'time'
)
```
PS: this is a simple fix involving swapping the order of existing code. 

Resolving patch:
diff --git a/pandas/core/reshape/melt.py b/pandas/core/reshape/melt.py
--- a/pandas/core/reshape/melt.py
+++ b/pandas/core/reshape/melt.py
@@ -409,14 +409,14 @@ def melt_stub(df, stub, i, j, value_vars, sep):
 
         return newdf.set_index(i + [j])
 
-    if any(col in stubnames for col in df.columns):
-        raise ValueError("stubname can't be identical to a column name")
-
     if not is_list_like(stubnames):
         stubnames = [stubnames]
     else:
         stubnames = list(stubnames)
 
+    if any(col in stubnames for col in df.columns):
+        raise ValueError("stubname can't be identical to a column name")
+
     if not is_list_like(i):
         i = [i]
     else:


Test patch:
diff --git a/pandas/tests/reshape/test_melt.py b/pandas/tests/reshape/test_melt.py
--- a/pandas/tests/reshape/test_melt.py
+++ b/pandas/tests/reshape/test_melt.py
@@ -640,3 +640,24 @@ def test_float_suffix(self):
         result = wide_to_long(df, ['result', 'treatment'],
                               i='A', j='colname', suffix='[0-9.]+', sep='_')
         tm.assert_frame_equal(result, expected)
+
+    def test_col_substring_of_stubname(self):
+        # GH22468
+        # Don't raise ValueError when a column name is a substring
+        # of a stubname that's been passed as a string
+        wide_data = {'node_id': {0: 0, 1: 1, 2: 2, 3: 3, 4: 4},
+                     'A': {0: 0.80, 1: 0.0, 2: 0.25, 3: 1.0, 4: 0.81},
+                     'PA0': {0: 0.74, 1: 0.56, 2: 0.56, 3: 0.98, 4: 0.6},
+                     'PA1': {0: 0.77, 1: 0.64, 2: 0.52, 3: 0.98, 4: 0.67},
+                     'PA3': {0: 0.34, 1: 0.70, 2: 0.52, 3: 0.98, 4: 0.67}
+                     }
+        wide_df = pd.DataFrame.from_dict(wide_data)
+        expected = pd.wide_to_long(wide_df,
+                                   stubnames=['PA'],
+                                   i=['node_id', 'A'],
+                                   j='time')
+        result = pd.wide_to_long(wide_df,
+                                 stubnames='PA',
+                                 i=['node_id', 'A'],
+                                 j='time')
+        tm.assert_frame_equal(result, expected)