# Buggy code
def _apply_full_axis(self, axis, func, new_index=None, new_columns=None, dtypes=None):
    """Perform a function across an entire axis.

    Note: The data shape may change as a result of the function.

    Args:
        axis: The axis to apply over.
        func: The function to apply.
        new_index: (optional) The index of the result. We may know this in advance,
            and if not provided it must be computed.
        new_columns: (optional) The columns of the result. We may know this in
            advance, and if not provided it must be computed.
        dtypes: (optional) The data types of the result. This is an optimization
            because there are functions that always result in a particular data
            type, and allows us to avoid (re)computing it.

    Returns:
        A new dataframe.
    """
    new_partitions = self._frame_mgr_cls.map_axis_partitions(
        axis, self._partitions, func
    )
    # Index objects for new object creation. This is shorter than if..else
    if new_columns is None:
        new_columns = self._frame_mgr_cls.get_indices(
            1, new_partitions, lambda df: df.columns
        )
    if new_index is None:
        new_index = self._frame_mgr_cls.get_indices(
            1, new_partitions, lambda df: df.index
        )
    # Length objects for new object creation. This is shorter than if..else
    # This object determines the lengths and widths based on the given parameters
    # and builds a dictionary used in the constructor below. 0 gives the row lengths
    # and 1 gives the column widths. Since the dimension of `axis` given may have
    # changed, we current just recompute it.
    lengths_objs = {
        axis: None,
        axis ^ 1: [self._row_lengths, self._column_widths][axis ^ 1],
    }
    if dtypes == "copy":
        dtypes = self._dtypes
    elif dtypes is not None:
        dtypes = pandas.Series([np.dtype(dtypes)] * len(new_columns), index=new_columns)
    return self.__constructor__(
        new_partitions,
        new_index,
        new_columns,
        lengths_objs[0],
        lengths_objs[1],
        dtypes,
    )


# Traceback (KeyError)
p0 Index(['data0', 'data1', 'data2'], dtype='object')
---------------------------------------------------------------------------
KeyError                                  Traceback (most recent call last)
<ipython-input-6-704c3a79c97b> in <module>()
----> 1 df_g=csv2.groupby('p0').get_group('A')

~/.local/lib/python3.7/site-packages/modin/pandas/dataframe.py in groupby(self, by, axis, level, as_index, sort, group_keys, squeeze, observed, **kwargs)
353         elif isinstance(by, string_types):
354             idx_name = by
--> 355             by = self.__getitem__(by)._query_compiler
356         elif is_list_like(by):
357             if isinstance(by, Series):

~/.local/lib/python3.7/site-packages/modin/pandas/base.py in __getitem__(self, key)
3220             return self._getitem_slice(indexer)
3221         else:
-> 3222             return self._getitem(key)
3223
3224     def _getitem_slice(self, key):

~/.local/lib/python3.7/site-packages/modin/pandas/dataframe.py in _getitem(self, key)
1835         else:
1836             print(key, self.columns)
-> 1837             return self._getitem_column(key)
1838
1839     def _getitem_column(self, key):

~/.local/lib/python3.7/site-packages/modin/pandas/dataframe.py in _getitem_column(self, key)
1839     def _getitem_column(self, key):
1840         if key not in self.keys():
-> 1841             raise KeyError("{}".format(key))
1842         s = self._reduce_dimension(self._query_compiler.getitem_column_array([key]))
1843         s._parent = self

KeyError: 'p0'

# Fixed code
def _apply_full_axis(self, axis, func, new_index=None, new_columns=None, dtypes=None):
    """Perform a function across an entire axis.

    Note: The data shape may change as a result of the function.

    Args:
        axis: The axis to apply over.
        func: The function to apply.
        new_index: (optional) The index of the result. We may know this in advance,
            and if not provided it must be computed.
        new_columns: (optional) The columns of the result. We may know this in
            advance, and if not provided it must be computed.
        dtypes: (optional) The data types of the result. This is an optimization
            because there are functions that always result in a particular data
            type, and allows us to avoid (re)computing it.

    Returns:
        A new dataframe.
    """
    new_partitions = self._frame_mgr_cls.map_axis_partitions(
        axis, self._partitions, func
    )
    # Index objects for new object creation. This is shorter than if..else
    if new_columns is None:
        new_columns = self._frame_mgr_cls.get_indices(
            1, new_partitions, lambda df: df.columns
        )
    if new_index is None:
        new_index = self._frame_mgr_cls.get_indices(
            0, new_partitions, lambda df: df.index
        )
    # Length objects for new object creation. This is shorter than if..else
    # This object determines the lengths and widths based on the given parameters
    # and builds a dictionary used in the constructor below. 0 gives the row lengths
    # and 1 gives the column widths. Since the dimension of `axis` given may have
    # changed, we current just recompute it.
    lengths_objs = {
        axis: None,
        axis ^ 1: [self._row_lengths, self._column_widths][axis ^ 1],
    }
    if dtypes == "copy":
        dtypes = self._dtypes
    elif dtypes is not None:
        dtypes = pandas.Series([np.dtype(dtypes)] * len(new_columns), index=new_columns)
    return self.__constructor__(
        new_partitions,
        new_index,
        new_columns,
        lengths_objs[0],
        lengths_objs[1],
        dtypes,
    )