This function is a member of StepFrame and StepSeries. It is used to
    expand the kwargs of the steps either into the index (index=True) or
    as columns (index=False). By default (diff=True) only the kwargs which
    differ among steps are expanded.

    Note that index objects in pandas must be hashable so any unhashable
    argument values are converted to string representations (using pprint)
    when index=True.

    If "inputs" is an argument those steps' kwargs are also expanded (and
    their inputs recursively). If there are multiple steps with the same
    argument names they are prefixed by their names or if those are not set
    then by their class names. To enable prefixing for all args set
    prefix=True.

    Sometimes the difference between pipelines is that a step exists or it
    doesn't. When diff=True and existence=True, instead of expanding all
    the kwargs for that step, we expand a single column whose name is the
    step name and whose value is a boolean indicating whether the step exists
    in the given tree.

    Args:
        prefix: whether to always use step name prefix for kwarg name.
            Default False, which uses prefixes when necessary, i.e. for
            keywords that are shared by multiple step names.
        index: If True expand args into index. Otherwise expand into
            columns
        diff: whether to only expand keywords whose values that are
            non-constant
        existence: whether to check for existence of a step in the tree
            instead of a full diff. Only applicable when diff=True. See
            note above.

    Returns: a DatFrame with the arguments of the steps expanded.

def expand(self, prefix=False, index=True, diff=True, existence=True):
    """
    This function is a member of StepFrame and StepSeries. It is used to
    expand the kwargs of the steps either into the index (index=True) or
    as columns (index=False). By default (diff=True) only the kwargs which
    differ among steps are expanded.

    Note that index objects in pandas must be hashable so any unhashable
    argument values are converted to string representations (using pprint)
    when index=True.

    If "inputs" is an argument those steps' kwargs are also expanded (and
    their inputs recursively). If there are multiple steps with the same
    argument names they are prefixed by their names or if those are not set
    then by their class names. To enable prefixing for all args set
    prefix=True.

    Sometimes the difference between pipelines is that a step exists or it
    doesn't. When diff=True and existence=True, instead of expanding all
    the kwargs for that step, we expand a single column whose name is the
    step name and whose value is a boolean indicating whether the step exists
    in the given tree.

    Args:
        prefix: whether to always use step name prefix for kwarg name.
            Default False, which uses prefixes when necessary, i.e. for
            keywords that are shared by multiple step names.
        index: If True expand args into index. Otherwise expand into
            columns
        diff: whether to only expand keywords whose values that are
            non-constant
        existence: whether to check for existence of a step in the tree
            instead of a full diff. Only applicable when diff=True. See
            note above.

    Returns: a DatFrame with the arguments of the steps expanded.
    """
    # collect kwargs resulting in a list of {name: kwargs} dicts
    dicts = [step._collect_kwargs(s, drop_duplicate_names=True) for s in self.index]
    # if any of the kwargs are themselves dicts, expand them
    dicts = [{k: util.dict_expand(v) for k, v in s.items()} for s in dicts]

    if diff:
        diff_dicts = [{} for d in dicts]  # the desired list of dicts

        names = util.union([set(d.keys()) for d in dicts])  # all names among these steps
        for name in names:
            if existence:
                ndicts = [d[name] for d in dicts if name in d.keys()]  # all dicts for this name
            else:
                ndicts = [d[name] if name in d.keys() else {} for d in dicts]

            ndiffs = util.dict_diff(ndicts)  # diffs for this name

            if sum(map(len, ndiffs)) == 0:  # if they're all the same
                # but not all had the key and existence=True
                if existence and len(ndicts) < len(self):
                    for m, d in zip(diff_dicts, dicts):
                        m[name] = {tuple(): name in d.keys()}
            else:  # if there was a diff
                diff_iter = iter(ndiffs)
                for m, d in zip(diff_dicts, dicts):
                    if name in d.keys() or not existence:
                        m[name] = diff_iter.next()  # get the corresponding diff

        dicts = diff_dicts

    # restructure so name is in the key
    merged_dicts = []
    for dd in dicts:
        merged_dicts.append(util.dict_merge(*({tuple([name] + list(util.make_tuple(k))): v
                            for k, v in d.items()} for name, d in dd.items())))

    # prefix_keys are the keys that will keep their prefix
    keys = [list((k[1:] for k in d.keys())) for d in merged_dicts]
    if not prefix:
        key_count = [Counter(kk) for kk in keys]
        prefix_keys = util.union({k for k in c if c[k] > 1} for c in key_count)
    else:
        prefix_keys = util.union((set(kk) for kk in keys))

    merged_dicts = [{str.join('_', map(str, k if k[1:] in prefix_keys else k[1:])): v
                    for k, v in d.items()} for d in merged_dicts]

    expanded = pd.DataFrame(merged_dicts, index=self.index)

    if index:
        columns = list(expanded.columns)
        try:
            if len(columns) > 0:
                expanded.set_index(columns, inplace=True)
            else:
                expanded.index = [None]*len(expanded)
        except TypeError:
            _print_unhashable(expanded, columns)
            expanded.set_index(columns, inplace=True)

        df = self.__class__.__bases__[0](self, copy=True)
        df.index = expanded.index

    else:
        df = pd.concat((expanded, self), axis=1)
        # When index=False, the index is still a Step collection
        df = StepFrame(expanded)

    return df