# Buggy code
def _gen_reshape_rechunk_nsplits(old_shape, new_shape, nsplits):
    old_idx = len(old_shape) - 1
    new_idx = len(new_shape) - 1
    rechunk_nsplists = [None for _ in old_shape]
    reshape_nsplists = [None for _ in new_shape]

    while old_idx >= 0 or new_idx >= 0:
        old_dim_size = old_shape[old_idx]
        new_dim_size = new_shape[new_idx]

        if old_dim_size == new_dim_size:
            # nothing need to do
            rechunk_nsplists[old_idx] = nsplits[old_idx]
            reshape_nsplists[new_idx] = nsplits[old_idx]
            old_idx -= 1
            new_idx -= 1
            continue

        if old_dim_size == 1:
            rechunk_nsplists[old_idx] = (1,)
            old_idx -= 1
        elif new_dim_size == 1:
            reshape_nsplists[new_idx] = (1,)
            new_idx -= 1
        elif old_dim_size < new_dim_size:
            left_old_idx = old_idx - 1
            while (
                left_old_idx >= 0
                and np.prod(old_shape[left_old_idx : old_idx + 1]) < new_dim_size
            ):
                left_old_idx -= 1
            if np.prod(old_shape[left_old_idx : old_idx + 1]) != new_dim_size:
                raise ValueError("shapes not compatible")

            for i in range(left_old_idx + 1, old_idx + 1):
                # rechunk the higher dimension into 1 chunk
                # e.g. ((2, 2, 2), [(3, 3), (4, 4))] -> [6, 8]
                rechunk_nsplists[i] = (old_shape[i],)

            chunk_reduce = np.prod(
                [len(c) for c in nsplits[left_old_idx + 1 : old_idx + 1]]
            )
            # cause the higher dimension has been concatenated,
            # the lowest dimension should be expanded to reduce size
            rechunk_nsplists[left_old_idx] = TensorReshape._expand_nsplit_by_reduce(
                nsplits[left_old_idx], chunk_reduce
            )

            size_reduce = np.prod(old_shape[left_old_idx + 1 : old_idx + 1])
            reshape_nsplists[new_idx] = tuple(
                size_reduce * c for c in rechunk_nsplists[left_old_idx]
            )

            old_idx = left_old_idx - 1
            new_idx -= 1
        else:
            assert old_dim_size > new_dim_size
            lef_new_idx = new_idx - 1
            while (
                lef_new_idx >= 0
                and np.prod(new_shape[lef_new_idx : new_idx + 1]) < old_dim_size
            ):
                lef_new_idx -= 1
            if np.prod(new_shape[lef_new_idx : new_idx + 1]) != old_dim_size:
                raise ValueError("shapes not compatible")

            chunk_expand = np.prod(new_shape[lef_new_idx + 1 : new_idx + 1])
            rechunk_nsplists[old_idx] = TensorReshape._reduce_nsplit_by_expand(
                nsplits[old_idx], chunk_expand
            )

            for i in range(lef_new_idx + 1, new_idx + 1):
                reshape_nsplists[i] = (new_shape[i],)
            reshape_nsplists[lef_new_idx] = tuple(
                c // chunk_expand for c in rechunk_nsplists[old_idx]
            )

            old_idx -= 1
            new_idx = lef_new_idx - 1

    assert np.prod([len(s) for s in rechunk_nsplists]) == np.prod(
        [len(s) for s in reshape_nsplists]
    )
    return rechunk_nsplists, reshape_nsplists


# Traceback (AttributeError)
In [3]: import pandas as pd

In [4]: data = pd.DataFrame(np.arange(20).reshape((4, 5)) + 1, columns=['a', 'b', 'c', 'd', 'e'])

In [6]: df = md.DataFrame(data)

In [7]: df.groupby(['a','b']).size().execute()
Unexpected exception occurred in enter_build_mode.<locals>.inner.
Traceback (most recent call last):
File "/Users/hekaisheng/Documents/mars_dev/mars/mars/utils.py", line 353, in _wrapped
return func(*args, **kwargs)
File "/Users/hekaisheng/Documents/mars_dev/mars/mars/utils.py", line 493, in inner
return func(*args, **kwargs)
File "/Users/hekaisheng/Documents/mars_dev/mars/mars/scheduler/graph.py", line 633, in prepare_graph
self._target_tileable_datas + fetch_tileables, tileable_graph)
File "/Users/hekaisheng/Documents/mars_dev/mars/mars/utils.py", line 399, in _wrapped
return func(*args, **kwargs)
File "/Users/hekaisheng/Documents/mars_dev/mars/mars/utils.py", line 493, in inner
return func(*args, **kwargs)
File "/Users/hekaisheng/Documents/mars_dev/mars/mars/tiles.py", line 350, in build
tileables, tileable_graph=tileable_graph)
File "/Users/hekaisheng/Documents/mars_dev/mars/mars/utils.py", line 399, in _wrapped
return func(*args, **kwargs)
File "/Users/hekaisheng/Documents/mars_dev/mars/mars/utils.py", line 493, in inner
return func(*args, **kwargs)
File "/Users/hekaisheng/Documents/mars_dev/mars/mars/tiles.py", line 263, in build
self._on_tile_failure(tileable_data.op, exc_info)
File "/Users/hekaisheng/Documents/mars_dev/mars/mars/tiles.py", line 302, in inner
raise exc_info[1].with_traceback(exc_info[2]) from None
File "/Users/hekaisheng/Documents/mars_dev/mars/mars/tiles.py", line 243, in build
tiled = self._tile(tileable_data, tileable_graph)
File "/Users/hekaisheng/Documents/mars_dev/mars/mars/tiles.py", line 338, in _tile
return super()._tile(tileable_data, tileable_graph)
File "/Users/hekaisheng/Documents/mars_dev/mars/mars/tiles.py", line 203, in _tile
tds = on_tile(tileable_data.op.outputs, tds)
File "/Users/hekaisheng/Documents/mars_dev/mars/mars/scheduler/graph.py", line 615, in on_tile
return self.context.wraps(handler.dispatch)(first.op)
File "/Users/hekaisheng/Documents/mars_dev/mars/mars/context.py", line 69, in h
return func(*args, **kwargs)
File "/Users/hekaisheng/Documents/mars_dev/mars/mars/utils.py", line 399, in _wrapped
return func(*args, **kwargs)
File "/Users/hekaisheng/Documents/mars_dev/mars/mars/tiles.py", line 119, in dispatch
tiled = op_cls.tile(op)
File "/Users/hekaisheng/Documents/mars_dev/mars/mars/dataframe/groupby/aggregation.py", line 481, in tile
return cls._tile_with_tree(op)
File "/Users/hekaisheng/Documents/mars_dev/mars/mars/dataframe/groupby/aggregation.py", line 412, in _tile_with_tree
index = out_df.index_value.to_pandas()
File "/Users/hekaisheng/Documents/mars_dev/mars/mars/dataframe/core.py", line 283, in to_pandas
return self._index_value.to_pandas()
File "/Users/hekaisheng/Documents/mars_dev/mars/mars/dataframe/core.py", line 197, in to_pandas
sortorder=self._sortorder, names=self._names)
AttributeError: _sortorder

# Fixed code
def _gen_reshape_rechunk_nsplits(old_shape, new_shape, nsplits):
    old_idx = len(old_shape) - 1
    new_idx = len(new_shape) - 1
    rechunk_nsplists = [None for _ in old_shape]
    reshape_nsplists = [None for _ in new_shape]

    while old_idx >= 0 or new_idx >= 0:
        old_dim_size = old_shape[old_idx]
        new_dim_size = new_shape[new_idx]

        if old_dim_size == new_dim_size:
            # nothing need to do
            rechunk_nsplists[old_idx] = nsplits[old_idx]
            reshape_nsplists[new_idx] = nsplits[old_idx]
            old_idx -= 1
            new_idx -= 1
            continue

        if old_dim_size == 1:
            rechunk_nsplists[old_idx] = (1,)
            old_idx -= 1
        elif new_dim_size == 1:
            reshape_nsplists[new_idx] = (1,)
            new_idx -= 1
        elif old_dim_size < new_dim_size:
            left_old_idx = old_idx - 1
            while (
                left_old_idx >= 0
                and np.prod(old_shape[left_old_idx : old_idx + 1]) < new_dim_size
            ):
                left_old_idx -= 1
            if np.prod(old_shape[left_old_idx : old_idx + 1]) != new_dim_size:
                raise ValueError("shapes not compatible")

            for i in range(left_old_idx + 1, old_idx + 1):
                # rechunk the higher dimension into 1 chunk
                # e.g. ((2, 2, 2), [(3, 3), (4, 4))] -> [6, 8]
                rechunk_nsplists[i] = (old_shape[i],)

            chunk_reduce = np.prod(
                [len(c) for c in nsplits[left_old_idx + 1 : old_idx + 1]]
            ).item()
            # cause the higher dimension has been concatenated,
            # the lowest dimension should be expanded to reduce size
            rechunk_nsplists[left_old_idx] = TensorReshape._expand_nsplit_by_reduce(
                nsplits[left_old_idx], chunk_reduce
            )

            size_reduce = np.prod(old_shape[left_old_idx + 1 : old_idx + 1]).item()
            reshape_nsplists[new_idx] = tuple(
                size_reduce * c for c in rechunk_nsplists[left_old_idx]
            )

            old_idx = left_old_idx - 1
            new_idx -= 1
        else:
            assert old_dim_size > new_dim_size
            lef_new_idx = new_idx - 1
            while (
                lef_new_idx >= 0
                and np.prod(new_shape[lef_new_idx : new_idx + 1]) < old_dim_size
            ):
                lef_new_idx -= 1
            if np.prod(new_shape[lef_new_idx : new_idx + 1]) != old_dim_size:
                raise ValueError("shapes not compatible")

            chunk_expand = np.prod(new_shape[lef_new_idx + 1 : new_idx + 1]).item()
            rechunk_nsplists[old_idx] = TensorReshape._reduce_nsplit_by_expand(
                nsplits[old_idx], chunk_expand
            )

            for i in range(lef_new_idx + 1, new_idx + 1):
                reshape_nsplists[i] = (new_shape[i],)
            reshape_nsplists[lef_new_idx] = tuple(
                c // chunk_expand for c in rechunk_nsplists[old_idx]
            )

            old_idx -= 1
            new_idx = lef_new_idx - 1

    assert np.prod([len(s) for s in rechunk_nsplists]) == np.prod(
        [len(s) for s in reshape_nsplists]
    )
    return rechunk_nsplists, reshape_nsplists