Repository: pandas-dev/pandas

Issue:
Merge DataFrame and Series using `on`
Now that we support merging `on` combination of column names and index levels, this should work

```python
In [40]: a = pd.DataFrame({"A": [1, 2, 3, 4]}, index=pd.MultiIndex.from_product([['a', 'b'], [0, 1]], names=['outer', 'inner']))

In [41]: b = pd.Series([1, 2, 3, 4], index=pd.MultiIndex.from_product([['a', 'b'], [1, 2]], names=['outer', 'inner']), name='B')

In [42]: pd.merge(a, b, on=['outer', 'inner'])
---------------------------------------------------------------------------
ValueError                                Traceback (most recent call last)
<ipython-input-42-337c5a9e9f8f> in <module>()
----> 1 pd.merge(a, b, on=['outer', 'inner'])

~/Envs/dask-dev/lib/python3.6/site-packages/pandas/core/reshape/merge.py in merge(left, right, how, on, left_on, right_on, left_index, right_index, sort, suffixes, copy, indicator, validate)
     58                          right_index=right_index, sort=sort, suffixes=suffixes,
     59                          copy=copy, indicator=indicator,
---> 60                          validate=validate)
     61     return op.get_result()
     62

~/Envs/dask-dev/lib/python3.6/site-packages/pandas/core/reshape/merge.py in __init__(self, left, right, how, on, left_on, right_on, axis, left_index, right_index, sort, suffixes, copy, indicator, validate)
    524         if not isinstance(right, DataFrame):
    525             raise ValueError('can not merge DataFrame with instance of '
--> 526                              'type {right}'.format(right=type(right)))
    527
    528         if not is_bool(left_index):

ValueError: can not merge DataFrame with instance of type <class 'pandas.core.series.Series'>
```

Should be the same as

```python
In [39]: pd.merge(a, b.to_frame(), on=['outer', 'inner'])
Out[39]:
             A  B
outer inner
a     1      2  1
b     1      4  3
```


Discussion:

Resolving patch:
diff --git a/pandas/core/frame.py b/pandas/core/frame.py
--- a/pandas/core/frame.py
+++ b/pandas/core/frame.py
@@ -137,8 +137,8 @@
 """
 
 _merge_doc = """
-Merge DataFrame objects by performing a database-style join operation by
-columns or indexes.
+Merge DataFrame or named Series objects by performing a database-style join
+operation by columns or indexes.
 
 If joining columns on columns, the DataFrame indexes *will be
 ignored*. Otherwise if joining indexes on indexes or indexes on a column or
@@ -146,7 +146,7 @@
 
 Parameters
 ----------%s
-right : DataFrame, Series or dict
+right : DataFrame or named Series
     Object to merge with.
 how : {'left', 'right', 'outer', 'inner'}, default 'inner'
     Type of merge to be performed.
@@ -217,6 +217,7 @@
 -----
 Support for specifying index levels as the `on`, `left_on`, and
 `right_on` parameters was added in version 0.23.0
+Support for merging named Series objects was added in version 0.24.0
 
 See Also
 --------
diff --git a/pandas/core/reshape/merge.py b/pandas/core/reshape/merge.py
--- a/pandas/core/reshape/merge.py
+++ b/pandas/core/reshape/merge.py
@@ -11,7 +11,7 @@
 import pandas.compat as compat
 
 from pandas import (Categorical, DataFrame,
-                    Index, MultiIndex, Timedelta)
+                    Index, MultiIndex, Timedelta, Series)
 from pandas.core.arrays.categorical import _recode_for_categories
 from pandas.core.frame import _merge_doc
 from pandas.core.dtypes.common import (
@@ -493,6 +493,8 @@ def __init__(self, left, right, how='inner', on=None,
                  left_index=False, right_index=False, sort=True,
                  suffixes=('_x', '_y'), copy=True, indicator=False,
                  validate=None):
+        left = validate_operand(left)
+        right = validate_operand(right)
         self.left = self.orig_left = left
         self.right = self.orig_right = right
         self.how = how
@@ -519,13 +521,6 @@ def __init__(self, left, right, how='inner', on=None,
             raise ValueError(
                 'indicator option can only accept boolean or string arguments')
 
-        if not isinstance(left, DataFrame):
-            raise ValueError('can not merge DataFrame with instance of '
-                             'type {left}'.format(left=type(left)))
-        if not isinstance(right, DataFrame):
-            raise ValueError('can not merge DataFrame with instance of '
-                             'type {right}'.format(right=type(right)))
-
         if not is_bool(left_index):
             raise ValueError(
                 'left_index parameter must be of type bool, not '
@@ -1645,3 +1640,16 @@ def _should_fill(lname, rname):
 
 def _any(x):
     return x is not None and com._any_not_none(*x)
+
+
+def validate_operand(obj):
+    if isinstance(obj, DataFrame):
+        return obj
+    elif isinstance(obj, Series):
+        if obj.name is None:
+            raise ValueError('Cannot merge a Series without a name')
+        else:
+            return obj.to_frame()
+    else:
+        raise TypeError('Can only merge Series or DataFrame objects, '
+                        'a {obj} was passed'.format(obj=type(obj)))


Test patch:
diff --git a/pandas/tests/reshape/merge/test_join.py b/pandas/tests/reshape/merge/test_join.py
--- a/pandas/tests/reshape/merge/test_join.py
+++ b/pandas/tests/reshape/merge/test_join.py
@@ -228,16 +228,18 @@ def test_join_on_fails_with_different_column_counts(self):
                             index=tm.makeCustomIndex(10, 2))
             merge(df, df2, right_on='a', left_on=['a', 'b'])
 
-    def test_join_on_fails_with_wrong_object_type(self):
-        # GH12081
-        wrongly_typed = [Series([0, 1]), 2, 'str', None, np.array([0, 1])]
-        df = DataFrame({'a': [1, 1]})
+    @pytest.mark.parametrize("wrong_type", [2, 'str', None, np.array([0, 1])])
+    def test_join_on_fails_with_wrong_object_type(self, wrong_type):
+        # GH12081 - original issue
+
+        # GH21220 - merging of Series and DataFrame is now allowed
+        # Edited test to remove the Series object from test parameters
 
-        for obj in wrongly_typed:
-            with tm.assert_raises_regex(ValueError, str(type(obj))):
-                merge(obj, df, left_on='a', right_on='a')
-            with tm.assert_raises_regex(ValueError, str(type(obj))):
-                merge(df, obj, left_on='a', right_on='a')
+        df = DataFrame({'a': [1, 1]})
+        with tm.assert_raises_regex(TypeError, str(type(wrong_type))):
+            merge(wrong_type, df, left_on='a', right_on='a')
+        with tm.assert_raises_regex(TypeError, str(type(wrong_type))):
+            merge(df, wrong_type, left_on='a', right_on='a')
 
     def test_join_on_pass_vector(self):
         expected = self.target.join(self.source, on='C')
diff --git a/pandas/tests/reshape/merge/test_merge.py b/pandas/tests/reshape/merge/test_merge.py
--- a/pandas/tests/reshape/merge/test_merge.py
+++ b/pandas/tests/reshape/merge/test_merge.py
@@ -1887,3 +1887,33 @@ def test_merge_index_types(index):
         OrderedDict([('left_data', [1, 2]), ('right_data', [1.0, 2.0])]),
         index=index)
     assert_frame_equal(result, expected)
+
+
+@pytest.mark.parametrize("on,left_on,right_on,left_index,right_index,nms,nm", [
+    (['outer', 'inner'], None, None, False, False, ['outer', 'inner'], 'B'),
+    (None, None, None, True, True, ['outer', 'inner'], 'B'),
+    (None, ['outer', 'inner'], None, False, True, None, 'B'),
+    (None, None, ['outer', 'inner'], True, False, None, 'B'),
+    (['outer', 'inner'], None, None, False, False, ['outer', 'inner'], None),
+    (None, None, None, True, True, ['outer', 'inner'], None),
+    (None, ['outer', 'inner'], None, False, True, None, None),
+    (None, None, ['outer', 'inner'], True, False, None, None)])
+def test_merge_series(on, left_on, right_on, left_index, right_index, nms, nm):
+    # GH 21220
+    a = pd.DataFrame({"A": [1, 2, 3, 4]},
+                     index=pd.MultiIndex.from_product([['a', 'b'], [0, 1]],
+                     names=['outer', 'inner']))
+    b = pd.Series([1, 2, 3, 4],
+                  index=pd.MultiIndex.from_product([['a', 'b'], [1, 2]],
+                  names=['outer', 'inner']), name=nm)
+    expected = pd.DataFrame({"A": [2, 4], "B": [1, 3]},
+                            index=pd.MultiIndex.from_product([['a', 'b'], [1]],
+                            names=nms))
+    if nm is not None:
+        result = pd.merge(a, b, on=on, left_on=left_on, right_on=right_on,
+                          left_index=left_index, right_index=right_index)
+        tm.assert_frame_equal(result, expected)
+    else:
+        with tm.assert_raises_regex(ValueError, 'a Series without a name'):
+            result = pd.merge(a, b, on=on, left_on=left_on, right_on=right_on,
+                              left_index=left_index, right_index=right_index)