Repository: dask/dask

Issue:
dask.dataframe groupby doesn work when column
```python
import pytest
import dask.dataframe as dd
import pandas as pd
import numpy as np
def test_group():
    pdf = pd.DataFrame({"group_key":[1,2,1],'B':[4,5,6],'C':[7,8,9]})
    ddf = dd.from_pandas(pd.DataFrame(pdf), npartitions=1)
    group = ddf.groupby("group_key").get_group(1)
    group.compute()

```
Tracestack is as follow:
```python
============================= test session starts ==============================
platform linux -- Python 3.7.2, pytest-5.0.0, py-1.8.0, pluggy-0.12.0
rootdir: /bb/mbigc/mbig6642/dl-dataframe
collected 1 item

t.py F                                                                   [100%]

=================================== FAILURES ===================================
__________________________________ test_group __________________________________

    def test_group():
        pdf = pd.DataFrame({"group_key":[1,2,1],'B':[4,5,6],'C':[7,8,9]})
        ddf = dd.from_pandas(pd.DataFrame(pdf), npartitions=1)
        group = ddf.groupby("group_key").get_group(1)
>       group.compute()

t.py:9: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 
venv/lib/python3.7/site-packages/dask/base.py:175: in compute
    (result,) = compute(self, traverse=False, **kwargs)
venv/lib/python3.7/site-packages/dask/base.py:446: in compute
    results = schedule(dsk, keys, **kwargs)
venv/lib/python3.7/site-packages/dask/threaded.py:82: in get
    **kwargs
venv/lib/python3.7/site-packages/dask/local.py:491: in get_async
    raise_exception(exc, tb)
venv/lib/python3.7/site-packages/dask/compatibility.py:130: in reraise
    raise exc
venv/lib/python3.7/site-packages/dask/local.py:233: in execute_task
    result = _execute_task(task, data)
venv/lib/python3.7/site-packages/dask/core.py:119: in _execute_task
    return func(*args2)
venv/lib/python3.7/site-packages/dask/optimization.py:1059: in __call__
    return core.get(self.dsk, self.outkey, dict(zip(self.inkeys, args)))
venv/lib/python3.7/site-packages/dask/core.py:149: in get
    result = _execute_task(task, cache)
venv/lib/python3.7/site-packages/dask/core.py:119: in _execute_task
    return func(*args2)
venv/lib/python3.7/site-packages/dask/compatibility.py:107: in apply
    return func(*args, **kwargs)
venv/lib/python3.7/site-packages/dask/dataframe/core.py:4826: in apply_and_enforce
    df = func(*args, **kwargs)
venv/lib/python3.7/site-packages/dask/dataframe/groupby.py:178: in _groupby_get_group
    grouped = _groupby_raise_unaligned(df, by=by_key)
venv/lib/python3.7/site-packages/dask/dataframe/groupby.py:163: in _groupby_raise_unaligned
    return df.groupby(**kwargs)
venv/lib/python3.7/site-packages/pandas/core/generic.py:7632: in groupby
    observed=observed, **kwargs)
venv/lib/python3.7/site-packages/pandas/core/groupby/groupby.py:2109: in groupby
    return klass(obj, by, **kwds)
venv/lib/python3.7/site-packages/pandas/core/groupby/groupby.py:359: in __init__
    mutated=self.mutated)
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

obj =    group_key  B  C
0          1  4  7
1          2  5  8
2          1  6  9
key = ['g', 'r', 'o', 'u', 'p', '_', ...], axis = 0, level = None, sort = True
observed = False, mutated = False, validate = True

    def _get_grouper(obj, key=None, axis=0, level=None, sort=True,
                     observed=False, mutated=False, validate=True):
        """
        create and return a BaseGrouper, which is an internal
        mapping of how to create the grouper indexers.
        This may be composed of multiple Grouping objects, indicating
        multiple groupers
    
        Groupers are ultimately index mappings. They can originate as:
        index mappings, keys to columns, functions, or Groupers
    
        Groupers enable local references to axis,level,sort, while
        the passed in axis, level, and sort are 'global'.
    
        This routine tries to figure out what the passing in references
        are and then creates a Grouping for each one, combined into
        a BaseGrouper.
    
        If observed & we have a categorical grouper, only show the observed
        values
    
        If validate, then check for key/level overlaps
    
        """
        group_axis = obj._get_axis(axis)
    
        # validate that the passed single level is compatible with the passed
        # axis of the object
        if level is not None:
            # TODO: These if-block and else-block are almost same.
            # MultiIndex instance check is removable, but it seems that there are
            # some processes only for non-MultiIndex in else-block,
            # eg. `obj.index.name != level`. We have to consider carefully whether
            # these are applicable for MultiIndex. Even if these are applicable,
            # we need to check if it makes no side effect to subsequent processes
            # on the outside of this condition.
            # (GH 17621)
            if isinstance(group_axis, MultiIndex):
                if is_list_like(level) and len(level) == 1:
                    level = level[0]
    
                if key is None and is_scalar(level):
                    # Get the level values from group_axis
                    key = group_axis.get_level_values(level)
                    level = None
    
            else:
                # allow level to be a length-one list-like object
                # (e.g., level=[0])
                # GH 13901
                if is_list_like(level):
                    nlevels = len(level)
                    if nlevels == 1:
                        level = level[0]
                    elif nlevels == 0:
                        raise ValueError('No group keys passed!')
                    else:
                        raise ValueError('multiple levels only valid with '
                                         'MultiIndex')
    
                if isinstance(level, compat.string_types):
                    if obj.index.name != level:
                        raise ValueError('level name {} is not the name of the '
                                         'index'.format(level))
                elif level > 0 or level < -1:
                    raise ValueError(
                        'level > 0 or level < -1 only valid with MultiIndex')
    
                # NOTE: `group_axis` and `group_axis.get_level_values(level)`
                # are same in this section.
                level = None
                key = group_axis
    
        # a passed-in Grouper, directly convert
        if isinstance(key, Grouper):
            binner, grouper, obj = key._get_grouper(obj, validate=False)
            if key.key is None:
                return grouper, [], obj
            else:
                return grouper, {key.key}, obj
    
        # already have a BaseGrouper, just return it
        elif isinstance(key, BaseGrouper):
            return key, [], obj
    
        # In the future, a tuple key will always mean an actual key,
        # not an iterable of keys. In the meantime, we attempt to provide
        # a warning. We can assume that the user wanted a list of keys when
        # the key is not in the index. We just have to be careful with
        # unhashble elements of `key`. Any unhashable elements implies that
        # they wanted a list of keys.
        # https://github.com/pandas-dev/pandas/issues/18314
        is_tuple = isinstance(key, tuple)
        all_hashable = is_tuple and is_hashable(key)
    
        if is_tuple:
            if ((all_hashable and key not in obj and set(key).issubset(obj))
                    or not all_hashable):
                # column names ('a', 'b') -> ['a', 'b']
                # arrays like (a, b) -> [a, b]
                msg = ("Interpreting tuple 'by' as a list of keys, rather than "
                       "a single key. Use 'by=[...]' instead of 'by=(...)'. In "
                       "the future, a tuple will always mean a single key.")
                warnings.warn(msg, FutureWarning, stacklevel=5)
                key = list(key)
    
        if not isinstance(key, list):
            keys = [key]
            match_axis_length = False
        else:
            keys = key
            match_axis_length = len(keys) == len(group_axis)
    
        # what are we after, exactly?
        any_callable = any(callable(g) or isinstance(g, dict) for g in keys)
        any_groupers = any(isinstance(g, Grouper) for g in keys)
        any_arraylike = any(isinstance(g, (list, tuple, Series, Index, np.ndarray))
                            for g in keys)
    
        try:
            if isinstance(obj, DataFrame):
                all_in_columns_index = all(g in obj.columns or g in obj.index.names
                                           for g in keys)
            else:
                all_in_columns_index = False
        except Exception:
            all_in_columns_index = False
    
        if (not any_callable and not all_in_columns_index and
                not any_arraylike and not any_groupers and
                match_axis_length and level is None):
            keys = [com.asarray_tuplesafe(keys)]
    
        if isinstance(level, (tuple, list)):
            if key is None:
                keys = [None] * len(level)
            levels = level
        else:
            levels = [level] * len(keys)
    
        groupings = []
        exclusions = []
    
        # if the actual grouper should be obj[key]
        def is_in_axis(key):
            if not _is_label_like(key):
                try:
                    obj._data.items.get_loc(key)
                except Exception:
                    return False
    
            return True
    
        # if the grouper is obj[name]
        def is_in_obj(gpr):
            try:
                return id(gpr) == id(obj[gpr.name])
            except Exception:
                return False
    
        for i, (gpr, level) in enumerate(zip(keys, levels)):
    
            if is_in_obj(gpr):  # df.groupby(df['name'])
                in_axis, name = True, gpr.name
                exclusions.append(name)
    
            elif is_in_axis(gpr):  # df.groupby('name')
                if gpr in obj:
                    if validate:
                        obj._check_label_or_level_ambiguity(gpr)
                    in_axis, name, gpr = True, gpr, obj[gpr]
                    exclusions.append(name)
                elif obj._is_level_reference(gpr):
                    in_axis, name, level, gpr = False, None, gpr, None
                else:
>                   raise KeyError(gpr)
E                   KeyError: 'g'

venv/lib/python3.7/site-packages/pandas/core/groupby/grouper.py:578: KeyError
=========================== 1 failed in 8.38 seconds ===========================

```


I tried this code with version **2.1.0, 2.0.0 and 1.2.2**
All of them has the same problem.


Discussion:
I was able to reproduce.  However, if you try with the input as a list you should be ok:

```
In [41]: ddf.groupby(['group_key']).get_group(1).compute()
Out[41]:
   group_key  B  C
0          1  4  7
2          1  6  9
```

I suspect there is a small bug in the support of labels.  Docstring says:

> A label or list of labels may be passed..
@quasiben Thank you. Actually I just figured out the same work around you are using. 
Root cause of it seems to be this:
in dask/dataframe/groupby.py line 158.
```python
    elif by is not None and len(by):
        # since we're coming through apply, `by` will be a tuple.
        # Pandas treats tuples as a single key, and lists as multiple keys
        # We want multiple keys
        kwargs.update(by=list(by))
```
It would be helpful that in doc string that we mention this that all labels should be passed in as a list to avoid this error.

I suspect that adding

```python
if isinstance(by, str):
    by = [by]
```

earlier would allow us to support string labels. @RobinRule any interest in submitting a PR? (no obligation though)
@jrbourbeau sure, would love to do that.
Great, thanks! You can find documentation on contributing to Dask at https://docs.dask.org/en/latest/develop.html. Feel free to ask questions if you run into any issues 

Resolving patch:
diff --git a/dask/dataframe/groupby.py b/dask/dataframe/groupby.py
--- a/dask/dataframe/groupby.py
+++ b/dask/dataframe/groupby.py
@@ -156,6 +156,8 @@ def _groupby_raise_unaligned(df, **kwargs):
         # since we're coming through apply, `by` will be a tuple.
         # Pandas treats tuples as a single key, and lists as multiple keys
         # We want multiple keys
+        if isinstance(by, str):
+            by = [by]
         kwargs.update(by=list(by))
     return df.groupby(**kwargs)
 


Test patch:
diff --git a/dask/dataframe/tests/test_groupby.py b/dask/dataframe/tests/test_groupby.py
--- a/dask/dataframe/tests/test_groupby.py
+++ b/dask/dataframe/tests/test_groupby.py
@@ -1496,6 +1496,19 @@ def add1(x):
         assert_eq(res, sol)
 
 
+def test_groupby_string_label():
+    df = pd.DataFrame({"foo": [1, 1, 4], "B": [2, 3, 4], "C": [5, 6, 7]})
+    ddf = dd.from_pandas(pd.DataFrame(df), npartitions=1)
+    ddf_group = ddf.groupby("foo")
+    result = ddf_group.get_group(1).compute()
+
+    expected = pd.DataFrame(
+        {"foo": [1, 1], "B": [2, 3], "C": [5, 6]}, index=pd.Index([0, 1])
+    )
+
+    tm.assert_frame_equal(result, expected)
+
+
 def test_groupby_dataframe_cum_caching():
     """Test caching behavior of cumulative operations on grouped dataframes.