Repository: ibis-project/ibis

Issue:
ux: count aggregates create column named "count" which isn't super friendly 
### What happened?

All of our count aggregates create a column named "count" in the outputs.  Consider the silly example below:

```python
import pandas as pd
import ibis

[ins] In [11]: df = pd.DataFrame({"x": [1.0, 2.0, 3.0, 1.0], "y": ["a", "b", "c", "a"]})

[ins] In [12]: t = ibis.memtable(df)

[ins] In [13]: t
Out[13]: 
┏━━━━━━━━━┳━━━━━━━━┓
┃ x       ┃ y      ┃
┡━━━━━━━━━╇━━━━━━━━┩
│ float64 │ string │
├─────────┼────────┤
│     1.0 │ a      │
│     2.0 │ b      │
│     3.0 │ c      │
│     1.0 │ a      │
└─────────┴────────┘


[ins] In [14]: t.x.value_counts().join(t, "x")
Out[14]: 
┏━━━━━━━━━┳━━━━━━━┳━━━━━━━━┓
┃ x       ┃ count ┃ y      ┃
┡━━━━━━━━━╇━━━━━━━╇━━━━━━━━┩
│ float64 │ int64 │ string │
├─────────┼───────┼────────┤
│     1.0 │     2 │ a      │
│     2.0 │     1 │ b      │
│     3.0 │     1 │ c      │
│     1.0 │     2 │ a      │
└─────────┴───────┴────────┘


[ins] In [15]: t.x.value_counts().join(t, "x").order_by(ibis._.count)
```

The last line fails because `count` is very much a method on tables.

There are other ways to spell this -- either of

```
t.x.value_counts().join(t, "x").order_by("count")
t.x.value_counts().join(t, "x").order_by(ibis._.["count"])
```

Naming collisions happen, but this one is a little bit aggravating because it's a column name that Ibis creates that conflicts with itself.

@jcrist suggested that we might be able to do some descriptor stuff to at least raise a better error message.  
Altenatively, we could pick a different name for that column (although I admit I'm not sure what that would be).

This may be intractable, but I wanted to raise it.

### What version of ibis are you using?

master

### What backend(s) are you using, if any?

all of them, all the time

### Relevant log output

```sh
---------------------------------------------------------------------------
IbisTypeError                             Traceback (most recent call last)
Cell In[15], line 1
----> 1 t.x.value_counts().join(t, "x").order_by(ibis._.count)

File ~/github.com/ibis-project/ibis/ibis/expr/types/relations.py:477, in Table.order_by(self, by)
    475 else:
    476     by = util.promote_list(by)
--> 477 return self.op().order_by(by).to_expr()

File ~/github.com/ibis-project/ibis/ibis/expr/operations/relations.py:366, in Selection.order_by(self, sort_exprs)
    363 def order_by(self, sort_exprs):
    364     from ibis.expr.analysis import shares_all_roots, sub_immediate_parents
--> 366     keys = rlz.tuple_of(rlz.sort_key_from(rlz.just(self)), sort_exprs)
    368     if not self.selections:
    369         if shares_all_roots(keys, table := self.table):

File /nix/store/crr9ya0n4hnnay9myvffdrlzpkrcrws3-python3-3.10.9-env/lib/python3.10/site-packages/toolz/functoolz.py:304, in curry.__call__(self, *args, **kwargs)
    302 def __call__(self, *args, **kwargs):
    303     try:
--> 304         return self._partial(*args, **kwargs)
    305     except TypeError as exc:
    306         if self._should_curry(args, kwargs, exc):

File ~/github.com/ibis-project/ibis/ibis/common/validators.py:194, in container_of(inner, arg, type, min_length, flatten, **kwargs)
    191 if flatten:
    192     arg = flatten_iterable(arg)
--> 194 return type(inner(item, **kwargs) for item in arg)

File ~/github.com/ibis-project/ibis/ibis/common/validators.py:194, in <genexpr>(.0)
    191 if flatten:
    192     arg = flatten_iterable(arg)
--> 194 return type(inner(item, **kwargs) for item in arg)

File ~/github.com/ibis-project/ibis/ibis/expr/rules.py:84, in rule.__call__(self, *args, **kwargs)
     82 args = map(self._erase_expr, args)
     83 kwargs = {k: self._erase_expr(v) for k, v in kwargs.items()}
---> 84 result = super().__call__(*args, **kwargs)
     85 assert not isinstance(result, ir.Expr)
     86 return result

File /nix/store/crr9ya0n4hnnay9myvffdrlzpkrcrws3-python3-3.10.9-env/lib/python3.10/site-packages/toolz/functoolz.py:304, in curry.__call__(self, *args, **kwargs)
    302 def __call__(self, *args, **kwargs):
    303     try:
--> 304         return self._partial(*args, **kwargs)
    305     except TypeError as exc:
    306         if self._should_curry(args, kwargs, exc):

File ~/github.com/ibis-project/ibis/ibis/expr/rules.py:114, in sort_key_from(table_ref, key, **kwargs)
    102 is_ascending = {
    103     "asc": True,
    104     "ascending": True,
   (...)
    110     True: True,
    111 }
    113 if callable(key):
--> 114     key = function_of(table_ref, key, **kwargs)
    116 if isinstance(key, ops.SortKey):
    117     return key

File ~/github.com/ibis-project/ibis/ibis/expr/rules.py:84, in rule.__call__(self, *args, **kwargs)
     82 args = map(self._erase_expr, args)
     83 kwargs = {k: self._erase_expr(v) for k, v in kwargs.items()}
---> 84 result = super().__call__(*args, **kwargs)
     85 assert not isinstance(result, ir.Expr)
     86 return result

File /nix/store/crr9ya0n4hnnay9myvffdrlzpkrcrws3-python3-3.10.9-env/lib/python3.10/site-packages/toolz/functoolz.py:304, in curry.__call__(self, *args, **kwargs)
    302 def __call__(self, *args, **kwargs):
    303     try:
--> 304         return self._partial(*args, **kwargs)
    305     except TypeError as exc:
    306         if self._should_curry(args, kwargs, exc):

File ~/github.com/ibis-project/ibis/ibis/expr/rules.py:512, in function_of(table_ref, fn, output_rule, this)
    507 else:
    508     raise com.IbisTypeError(
    509         'argument `fn` must be a function, lambda or deferred operation'
    510     )
--> 512 return output_rule(arg, this=this)

File ~/github.com/ibis-project/ibis/ibis/expr/rules.py:84, in rule.__call__(self, *args, **kwargs)
     82 args = map(self._erase_expr, args)
     83 kwargs = {k: self._erase_expr(v) for k, v in kwargs.items()}
---> 84 result = super().__call__(*args, **kwargs)
     85 assert not isinstance(result, ir.Expr)
     86 return result

File /nix/store/crr9ya0n4hnnay9myvffdrlzpkrcrws3-python3-3.10.9-env/lib/python3.10/site-packages/toolz/functoolz.py:304, in curry.__call__(self, *args, **kwargs)
    302 def __call__(self, *args, **kwargs):
    303     try:
--> 304         return self._partial(*args, **kwargs)
    305     except TypeError as exc:
    306         if self._should_curry(args, kwargs, exc):

File ~/github.com/ibis-project/ibis/ibis/expr/rules.py:210, in value(dtype, arg, **kwargs)
    206 import ibis.expr.operations as ops
    208 if not isinstance(arg, ops.Value):
    209     # coerce python literal to ibis literal
--> 210     arg = literal(None, arg)
    212 if dtype is None:
    213     # no datatype restriction
    214     return arg

File ~/github.com/ibis-project/ibis/ibis/expr/rules.py:84, in rule.__call__(self, *args, **kwargs)
     82 args = map(self._erase_expr, args)
     83 kwargs = {k: self._erase_expr(v) for k, v in kwargs.items()}
---> 84 result = super().__call__(*args, **kwargs)
     85 assert not isinstance(result, ir.Expr)
     86 return result

File /nix/store/crr9ya0n4hnnay9myvffdrlzpkrcrws3-python3-3.10.9-env/lib/python3.10/site-packages/toolz/functoolz.py:304, in curry.__call__(self, *args, **kwargs)
    302 def __call__(self, *args, **kwargs):
    303     try:
--> 304         return self._partial(*args, **kwargs)
    305     except TypeError as exc:
    306         if self._should_curry(args, kwargs, exc):

File ~/github.com/ibis-project/ibis/ibis/expr/rules.py:178, in literal(dtype, value, **kwargs)
    176     dtype = inferred_dtype
    177 else:
--> 178     raise com.IbisTypeError(
    179         'The datatype of value {!r} cannot be inferred, try '
    180         'passing it explicitly with the `type` keyword.'.format(value)
    181     )
    183 if dtype.is_null():
    184     return ops.NullLiteral()

IbisTypeError: The datatype of value <bound method Table.count of ┏━━━━━━━━━┳━━━━━━━┳━━━━━━━━┓
┃ x       ┃ count ┃ y      ┃
┡━━━━━━━━━╇━━━━━━━╇━━━━━━━━┩
│ float64 │ int64 │ string │
├─────────┼───────┼────────┤
│     1.0 │     2 │ a      │
│     2.0 │     1 │ b      │
│     3.0 │     1 │ c      │
│     1.0 │     2 │ a      │
└─────────┴───────┴────────┘
> cannot be inferred, try passing it explicitly with the `type` keyword.
```


### Code of Conduct

- [X] I agree to follow this project's Code of Conduct


Discussion:
I recently removed this for specific aggregate calls (so if you wrote value counts manually you won't get the automatic `count` name), but left it in for the sugary methods like value counts and topk mostly because I was tired of fixing tests. I believe those are the main APIs left to "dename".

Resolving patch:
diff --git a/ibis/expr/types/generic.py b/ibis/expr/types/generic.py
--- a/ibis/expr/types/generic.py
+++ b/ibis/expr/types/generic.py
@@ -799,27 +799,54 @@ def count(self, where: ir.BooleanValue | None = None) -> ir.IntegerScalar:
         """
         return ops.Count(self, where).to_expr()
 
-    def value_counts(self, metric_name: str = "count") -> ir.Table:
+    def value_counts(self) -> ir.Table:
         """Compute a frequency table.
 
-        Parameters
-        ----------
-        metric_name
-            Output column name of the `count()` metric
-
         Returns
         -------
         Table
             Frequency table expression
+
+        Examples
+        --------
+        >>> import ibis
+        >>> ibis.options.interactive = True
+        >>> t = ibis.memtable({"chars": char} for char in "aabcddd")
+        >>> t
+        ┏━━━━━━━━┓
+        ┃ chars  ┃
+        ┡━━━━━━━━┩
+        │ string │
+        ├────────┤
+        │ a      │
+        │ a      │
+        │ b      │
+        │ c      │
+        │ d      │
+        │ d      │
+        │ d      │
+        └────────┘
+        >>> t.chars.value_counts()
+        ┏━━━━━━━━┳━━━━━━━━━━━━━┓
+        ┃ chars  ┃ chars_count ┃
+        ┡━━━━━━━━╇━━━━━━━━━━━━━┩
+        │ string │ int64       │
+        ├────────┼─────────────┤
+        │ a      │           2 │
+        │ b      │           1 │
+        │ c      │           1 │
+        │ d      │           3 │
+        └────────┴─────────────┘
         """
         from ibis.expr.analysis import find_first_base_table
 
+        name = self.get_name()
         return (
             find_first_base_table(self.op())
             .to_expr()
             .select(self)
-            .group_by(self.get_name())
-            .agg(**{metric_name: lambda t: t.count()})
+            .group_by(name)
+            .agg(**{f"{name}_count": lambda t: t.count()})
         )
 
     def first(self) -> Column:


Test patch:
diff --git a/ibis/backends/dask/tests/execution/test_operations.py b/ibis/backends/dask/tests/execution/test_operations.py
--- a/ibis/backends/dask/tests/execution/test_operations.py
+++ b/ibis/backends/dask/tests/execution/test_operations.py
@@ -548,7 +548,7 @@ def test_value_counts(t, df):
         df.compute()
         .dup_strings.value_counts()
         .reset_index()
-        .rename(columns={'dup_strings': 'count'})
+        .rename(columns={'dup_strings': 'dup_strings_count'})
         .rename(columns={'index': 'dup_strings'})
         .sort_values(['dup_strings'])
         .reset_index(drop=True)
diff --git a/ibis/backends/pandas/tests/execution/test_operations.py b/ibis/backends/pandas/tests/execution/test_operations.py
--- a/ibis/backends/pandas/tests/execution/test_operations.py
+++ b/ibis/backends/pandas/tests/execution/test_operations.py
@@ -393,7 +393,7 @@ def test_value_counts(t, df):
     expected = (
         df.dup_strings.value_counts()
         .reset_index()
-        .rename(columns={'dup_strings': 'count'})
+        .rename(columns={'dup_strings': 'dup_strings_count'})
         .rename(columns={'index': 'dup_strings'})
         .sort_values(['dup_strings'])
         .reset_index(drop=True)
diff --git a/ibis/tests/expr/test_table.py b/ibis/tests/expr/test_table.py
--- a/ibis/tests/expr/test_table.py
+++ b/ibis/tests/expr/test_table.py
@@ -781,7 +781,7 @@ def test_group_by_column_select_api(table):
 def test_value_counts_convenience(table):
     # #152
     result = table.g.value_counts()
-    expected = table.select('g').group_by('g').aggregate(count=lambda t: t.count())
+    expected = table.select('g').group_by('g').aggregate(g_count=lambda t: t.count())
 
     assert_equal(result, expected)
 
diff --git a/ibis/tests/sql/snapshots/test_select_sql/test_sort_then_group_by_propagates_keys/result1.sql b/ibis/tests/sql/snapshots/test_select_sql/test_sort_then_group_by_propagates_keys/result1.sql
--- a/ibis/tests/sql/snapshots/test_select_sql/test_sort_then_group_by_propagates_keys/result1.sql
+++ b/ibis/tests/sql/snapshots/test_select_sql/test_sort_then_group_by_propagates_keys/result1.sql
@@ -1,4 +1,4 @@
-SELECT t0.`b`, count(1) AS `count`
+SELECT t0.`b`, count(1) AS `b_count`
 FROM (
   SELECT t1.`b`
   FROM (
diff --git a/ibis/tests/sql/snapshots/test_select_sql/test_sort_then_group_by_propagates_keys/result2.sql b/ibis/tests/sql/snapshots/test_select_sql/test_sort_then_group_by_propagates_keys/result2.sql
--- a/ibis/tests/sql/snapshots/test_select_sql/test_sort_then_group_by_propagates_keys/result2.sql
+++ b/ibis/tests/sql/snapshots/test_select_sql/test_sort_then_group_by_propagates_keys/result2.sql
@@ -1,4 +1,4 @@
-SELECT t0.`b`, count(1) AS `count`
+SELECT t0.`b`, count(1) AS `b_count`
 FROM (
   SELECT t1.`b`
   FROM (