Transforms a "long" DataFrame into a "wide" format using a key and value
    column.

    If you have a mixed datatype column in your long-format DataFrame then the
    default behavior is for the spread columns to be of type `object`, or
    string. If you want to try to convert dtypes when spreading, you can set
    the convert keyword argument in spread to True.

    Args:
        key (str, int, or symbolic): Label for the key column.
        values (str, int, or symbolic): Label for the values column.

    Kwargs:
        convert (bool): Boolean indicating whether or not to try and convert
            the spread columns to more appropriate data types.


    Example:
        widened = elongated >> spread(X.variable, X.value)
        widened >> head(5)

            _ID carat clarity color        cut depth price table     x     y     z
        0     0  0.23     SI2     E      Ideal  61.5   326    55  3.95  3.98  2.43
        1     1  0.21     SI1     E    Premium  59.8   326    61  3.89  3.84  2.31
        2    10   0.3     SI1     J       Good    64   339    55  4.25  4.28  2.73
        3   100  0.75     SI1     D  Very Good  63.2  2760    56   5.8  5.75  3.65
        4  1000  0.75     SI1     D      Ideal  62.3  2898    55  5.83   5.8  3.62

def spread(df, key, values, convert=False):
    """
    Transforms a "long" DataFrame into a "wide" format using a key and value
    column.

    If you have a mixed datatype column in your long-format DataFrame then the
    default behavior is for the spread columns to be of type `object`, or
    string. If you want to try to convert dtypes when spreading, you can set
    the convert keyword argument in spread to True.

    Args:
        key (str, int, or symbolic): Label for the key column.
        values (str, int, or symbolic): Label for the values column.

    Kwargs:
        convert (bool): Boolean indicating whether or not to try and convert
            the spread columns to more appropriate data types.


    Example:
        widened = elongated >> spread(X.variable, X.value)
        widened >> head(5)

            _ID carat clarity color        cut depth price table     x     y     z
        0     0  0.23     SI2     E      Ideal  61.5   326    55  3.95  3.98  2.43
        1     1  0.21     SI1     E    Premium  59.8   326    61  3.89  3.84  2.31
        2    10   0.3     SI1     J       Good    64   339    55  4.25  4.28  2.73
        3   100  0.75     SI1     D  Very Good  63.2  2760    56   5.8  5.75  3.65
        4  1000  0.75     SI1     D      Ideal  62.3  2898    55  5.83   5.8  3.62
    """

    # Taken mostly from dplython package
    columns = df.columns.tolist()
    id_cols = [col for col in columns if not col in [key, values]]

    temp_index = ['' for i in range(len(df))]
    for id_col in id_cols:
        temp_index += df[id_col].map(str)

    out_df = df.assign(temp_index=temp_index)
    out_df = out_df.set_index('temp_index')
    spread_data = out_df[[key, values]]

    if not all(spread_data.groupby([spread_data.index, key]).agg(
            'count').reset_index()[values] < 2):
        raise ValueError('Duplicate identifiers')

    spread_data = spread_data.pivot(columns=key, values=values)

    if convert and (out_df[values].dtype.kind in 'OSaU'):
        columns_to_convert = [col for col in spread_data if col not in columns]
        spread_data = convert_type(spread_data, columns_to_convert)

    out_df = out_df[id_cols].drop_duplicates()
    out_df = out_df.merge(spread_data, left_index=True, right_index=True).reset_index(drop=True)

    out_df = (out_df >> arrange(id_cols)).reset_index(drop=True)

    return out_df