Compute a unique numeric id for each unique row in
    a data frame. The ids start at 1 -- in the spirit
    of `plyr::id`

    Parameters
    ----------
    df : dataframe
        Rows
    drop : bool
        If true, drop unused categorical levels leaving no
        gaps in the assignments.

    Returns
    -------
    out : list
        Row asssignments.

    Notes
    -----
    So far there has been no need not to drop unused levels
    of categorical variables.

def ninteraction(df, drop=False):
    """
    Compute a unique numeric id for each unique row in
    a data frame. The ids start at 1 -- in the spirit
    of `plyr::id`

    Parameters
    ----------
    df : dataframe
        Rows
    drop : bool
        If true, drop unused categorical levels leaving no
        gaps in the assignments.

    Returns
    -------
    out : list
        Row asssignments.

    Notes
    -----
    So far there has been no need not to drop unused levels
    of categorical variables.
    """
    if len(df) == 0:
        return []

    # Special case for single variable
    if len(df.columns) == 1:
        return _id_var(df[df.columns[0]], drop)

    # Calculate individual ids
    ids = df.apply(_id_var, axis=0)
    ids = ids.reindex(columns=reversed(ids.columns))

    # Calculate dimensions
    def len_unique(x):
        return len(np.unique(x))
    ndistinct = ids.apply(len_unique, axis=0).values

    combs = np.array(
        np.hstack([1, np.cumprod(ndistinct[:-1])]))
    mat = np.array(ids)
    res = (mat - 1) @ combs.T + 1
    res = np.array(res).flatten().tolist()

    if drop:
        return _id_var(res, drop)
    else:
        return res