Repository: pandas-dev/pandas

Issue:
Can't use same function twice with named aggregation
#### Code Sample, a copy-pastable example if possible

```python
import pandas as pd
from functools import partial

quant50 = partial(np.percentile, q=50)
quant70 = partial(np.percentile, q=70)

test = pd.DataFrame({'col1': ['a', 'a', 'b', 'b', 'b'], 'col2': [1,2,3,4,5]})

test.groupby('col1').agg(
    quantile_50=('col2', quant50),
    quantile_70=('col2', quant70)
)
```
#### Problem description

I'm trying to calculate multiple percentiles, I get the following error: "Function names must be unique, found multiple named percentile"

It's an old problem https://github.com/pandas-dev/pandas/issues/7186 but I assumed that it should've been solved with the named aggregations. Now there is no need to deduce the column name from the function name, the column name was specified explicitly.

#### Expected Output

column | quantile_50 | quantile_70
a | 1.5 | 2
b | 4.0 | 3

#### Output of ``pd.show_versions()``

<details>

INSTALLED VERSIONS
------------------
commit           : None
python           : 3.6.9.final.0
python-bits      : 64
OS               : Linux
OS-release       : 5.1.11-200.fc29.x86_64
machine          : x86_64
processor        : x86_64
byteorder        : little
LC_ALL           : None
LANG             : en_US.UTF-8
LOCALE           : en_US.UTF-8

pandas           : 0.25.1
numpy            : 1.16.4
pytz             : 2017.3
dateutil         : 2.7.2
pip              : 19.1.1
setuptools       : 39.1.0
Cython           : 0.28.1
pytest           : 3.3.1
hypothesis       : None
sphinx           : None
blosc            : None
feather          : 0.4.0
xlsxwriter       : None
lxml.etree       : 4.3.3
html5lib         : 1.0.1
pymysql          : None
psycopg2         : None
jinja2           : 2.10
IPython          : 6.3.1
pandas_datareader: None
bs4              : 4.7.1
bottleneck       : 1.2.1
fastparquet      : None
gcsfs            : None
lxml.etree       : 4.3.3
matplotlib       : 2.2.2
numexpr          : None
odfpy            : None
openpyxl         : None
pandas_gbq       : None
pyarrow          : 0.9.0
pytables         : None
s3fs             : None
scipy            : 1.2.1
sqlalchemy       : 1.1.18
tables           : None
xarray           : None
xlrd             : 1.1.0
xlwt             : None
xlsxwriter       : None

</details>



Discussion:

Resolving patch:
diff --git a/pandas/core/groupby/generic.py b/pandas/core/groupby/generic.py
--- a/pandas/core/groupby/generic.py
+++ b/pandas/core/groupby/generic.py
@@ -299,10 +299,6 @@ def _aggregate_multiple_funcs(self, arg):
         results = OrderedDict()
         for name, func in arg:
             obj = self
-            if name in results:
-                raise SpecificationError(
-                    f"Function names must be unique, found multiple named {name}"
-                )
 
             # reset the cache so that we
             # only include the named selection
@@ -912,6 +908,14 @@ def aggregate(self, func=None, *args, **kwargs):
             func, columns, order = _normalize_keyword_aggregation(kwargs)
 
             kwargs = {}
+        elif isinstance(func, list) and len(func) > len(set(func)):
+
+            # GH 28426 will raise error if duplicated function names are used and
+            # there is no reassigned name
+            raise SpecificationError(
+                "Function names must be unique if there is no new column "
+                "names assigned"
+            )
         elif func is None:
             # nicer error message
             raise TypeError("Must provide 'func' or tuples of '(column, aggfunc).")


Test patch:
diff --git a/pandas/tests/groupby/aggregate/test_aggregate.py b/pandas/tests/groupby/aggregate/test_aggregate.py
--- a/pandas/tests/groupby/aggregate/test_aggregate.py
+++ b/pandas/tests/groupby/aggregate/test_aggregate.py
@@ -353,6 +353,14 @@ def test_uint64_type_handling(dtype, how):
     tm.assert_frame_equal(result, expected, check_exact=True)
 
 
+def test_func_duplicates_raises():
+    # GH28426
+    msg = "Function names"
+    df = pd.DataFrame({"A": [0, 0, 1, 1], "B": [1, 2, 3, 4]})
+    with pytest.raises(SpecificationError, match=msg):
+        df.groupby("A").agg(["min", "min"])
+
+
 class TestNamedAggregationSeries:
     def test_series_named_agg(self):
         df = pd.Series([1, 2, 3, 4])
@@ -377,12 +385,12 @@ def test_no_args_raises(self):
         expected = pd.DataFrame()
         tm.assert_frame_equal(result, expected)
 
-    def test_series_named_agg_duplicates_raises(self):
-        # This is a limitation of the named agg implementation reusing
-        # aggregate_multiple_funcs. It could maybe be lifted in the future.
+    def test_series_named_agg_duplicates_no_raises(self):
+        # GH28426
         gr = pd.Series([1, 2, 3]).groupby([0, 0, 1])
-        with pytest.raises(SpecificationError):
-            gr.agg(a="sum", b="sum")
+        grouped = gr.agg(a="sum", b="sum")
+        expected = pd.DataFrame({"a": [3, 3], "b": [3, 3]})
+        tm.assert_frame_equal(expected, grouped)
 
     def test_mangled(self):
         gr = pd.Series([1, 2, 3]).groupby([0, 0, 1])
@@ -439,12 +447,34 @@ def test_agg_relabel_non_identifier(self):
         )
         tm.assert_frame_equal(result, expected)
 
-    def test_duplicate_raises(self):
-        # TODO: we currently raise on multiple lambdas. We could *maybe*
-        # update com.get_callable_name to append `_i` to each lambda.
+    def test_duplicate_no_raises(self):
+        # GH 28426, if use same input function on same column,
+        # no error should raise
         df = pd.DataFrame({"A": [0, 0, 1, 1], "B": [1, 2, 3, 4]})
-        with pytest.raises(SpecificationError, match="Function names"):
-            df.groupby("A").agg(a=("A", "min"), b=("A", "min"))
+
+        grouped = df.groupby("A").agg(a=("B", "min"), b=("B", "min"))
+        expected = pd.DataFrame(
+            {"a": [1, 3], "b": [1, 3]}, index=pd.Index([0, 1], name="A")
+        )
+        tm.assert_frame_equal(grouped, expected)
+
+        quant50 = functools.partial(np.percentile, q=50)
+        quant70 = functools.partial(np.percentile, q=70)
+        quant50.__name__ = "quant50"
+        quant70.__name__ = "quant70"
+
+        test = pd.DataFrame(
+            {"col1": ["a", "a", "b", "b", "b"], "col2": [1, 2, 3, 4, 5]}
+        )
+
+        grouped = test.groupby("col1").agg(
+            quantile_50=("col2", quant50), quantile_70=("col2", quant70)
+        )
+        expected = pd.DataFrame(
+            {"quantile_50": [1.5, 4.0], "quantile_70": [1.7, 4.4]},
+            index=pd.Index(["a", "b"], name="col1"),
+        )
+        tm.assert_frame_equal(grouped, expected)
 
     def test_agg_relabel_with_level(self):
         df = pd.DataFrame(
@@ -557,15 +587,21 @@ def test_agg_relabel_multiindex_raises_not_exist():
         df.groupby(("x", "group")).agg(a=(("Y", "a"), "max"))
 
 
-def test_agg_relabel_multiindex_raises_duplicate():
+def test_agg_relabel_multiindex_duplicates():
     # GH29422, add test for raises senario when getting duplicates
+    # GH28426, after this change, duplicates should also work if the relabelling is
+    # different
     df = DataFrame(
         {"group": ["a", "a", "b", "b"], "A": [0, 1, 2, 3], "B": [5, 6, 7, 8]}
     )
     df.columns = pd.MultiIndex.from_tuples([("x", "group"), ("y", "A"), ("y", "B")])
 
-    with pytest.raises(SpecificationError, match="Function names"):
-        df.groupby(("x", "group")).agg(a=(("y", "A"), "min"), b=(("y", "A"), "min"))
+    result = df.groupby(("x", "group")).agg(
+        a=(("y", "A"), "min"), b=(("y", "A"), "min")
+    )
+    idx = pd.Index(["a", "b"], name=("x", "group"))
+    expected = DataFrame({"a": [0, 2], "b": [0, 2]}, index=idx)
+    tm.assert_frame_equal(result, expected)
 
 
 def myfunc(s):