Apply function to each step object in the index

    Args:
        fn: function to apply. If a list then each function is applied
        pairwise: whether to apply the function to pairs of steps
        symmetric, diagonal, block: passed to apply_pairwise when pairwise=True
        kwargs: a keyword arguments to pass to each function. Arguments
            with list value are grid searched using util.dict_product.

    Returns: a StepFrame or StepSeries

def dapply(self, fn, pairwise=False, symmetric=True, diagonal=False, block=None, **kwargs):
    """
    Apply function to each step object in the index

    Args:
        fn: function to apply. If a list then each function is applied
        pairwise: whether to apply the function to pairs of steps
        symmetric, diagonal, block: passed to apply_pairwise when pairwise=True
        kwargs: a keyword arguments to pass to each function. Arguments
            with list value are grid searched using util.dict_product.

    Returns: a StepFrame or StepSeries
    """
    search_keys = [k for k, v in kwargs.items() if isinstance(v, list) and len(v) > 1]
    functions = util.make_list(fn)
    search = list(product(functions, util.dict_product(kwargs)))

    results = []
    for fn, kw in search:
        if not pairwise:
            r = self.index.to_series().apply(lambda step: fn(step, **kw))
        else:
            r = apply_pairwise(self, fn,
                               symmetric=symmetric, diagonal=diagonal, block=block,
                               **kw)

        name = [] if len(functions) == 1 else [fn.__name__]
        name += util.dict_subset(kw, search_keys).values()

        if isinstance(r, pd.DataFrame):
            columns = pd.MultiIndex.from_tuples(
                    [tuple(name + util.make_list(c)) for c in r.columns])
            r.columns = columns
        else:
            r.name = tuple(name)
        results.append(r)

    if len(results) > 1:
        result = pd.concat(results, axis=1)
        # get subset of parameters that were searched over
        column_names = [] if len(functions) == 1 else [None]
        column_names += search_keys
        column_names += [None]*(len(result.columns.names)-len(column_names))
        result.columns.names = column_names

        return StepFrame(result)
    else:
        result = results[0]
        if isinstance(result, pd.DataFrame):
            return StepFrame(result)
        else:
            result.name = functions[0].__name__
            return StepSeries(result)