def mi(x, y, bins_x=None, bins_y=None, bins_xy=None, method='nearest-neighbors', units='bits'):
    '''
    compute and return the mutual information between x and y

    inputs:
    -------
        x, y:       numpy arrays of shape samples x dimension
        method:     'nearest-neighbors', 'gaussian', or 'bin'
        units:      'bits' or 'nats'

    output:
    -------
        mi:     float

    Notes:
    ------
        if you are trying to mix several symbols together as in mi(x, (y0,y1,...)), try

        info[p] = _info.mi(x, info.combine_symbols(y0, y1, ...) )
    '''
    # dict.values() returns a view object that has to be converted to a list before being
    # converted to an array
    # the following lines will execute properly in python3, but not python2 because there
    # is no zip object
    try:
        if isinstance(x, zip):
            x = list(x)
        if isinstance(y, zip):
            y = list(y)
    except:
        pass

    # wrapped in try bracket because x, y might have no .shape attribute
    try:
        # handling for 1d np arrays
        if len(x.shape) == 1:
            x = np.expand_dims(x, 1)
        if len(y.shape) == 1:
            y = np.expand_dims(y, 1)
    except:
        pass

    HX  = entropy(data=x, bins=bins_x, method=method, units=units)
    HY  = entropy(data=y, bins=bins_y, method=method, units=units)
    HXY = entropy(data=np.concatenate([x, y], axis=1), bins=bins_xy, method=method, units=units)

    return HX + HY - HXY