Calculates univariate O-ring for a species

    Parameters
    ----------
    {0}
    bin_edges : iterable
        List of edges of distance classes to bin histogram of distances
    spp : str
        String corresponding to focal species code
    density : bool
        If True, return densities (counts divided by area of torus defined
        by bin edges) instead of counts. Default True.
    full : bool
        If True, return a separate column giving density at distance x for
        every individual, rather than mean density. Default False.

    Returns
    -------
    {1} Result has two columns, x and y, that give the distance to the center
    of a torus and the number or density of individuals found in that torus.

    Notes
    -----
    If density is False, counts are raw counts, non-edge corrected, within
    rings.

    Pairwise distances are directional, giving n(n-1) total distances for a
    species with n individuals, as edge correction is inherently directional.

    Bins include the lower edge and exclude the upper edge, except for the
    final bin which includes both the lower and upper edge. Floating point
    arithmetic may cause points located "exactly" on edges to be allocated
    contrary to this rule, however.

    If there are no records for a species, result table will be a dataframe
    with no records. If there are records but a species has only one
    individual, dataframe will have zero count at all torus areas.

    When using density, the maximum distance used for edge correction, given by
    the mean of the last two bin_edge values, should ideally be set to no
    greater than one half the diagonal distance across the plot. This ensures
    that it is not possible for an entire edge correction buffer to be outside
    of the plot.

    {2}

    For the 0-ring analysis, cols must also contain x_col and y_col, giving the
    x and y dimensions along which to analyze spatial pattern.

    {3}

def o_ring(patch, cols, splits, spp, bin_edges, density=True, full=False):
    """
    Calculates univariate O-ring for a species

    Parameters
    ----------
    {0}
    bin_edges : iterable
        List of edges of distance classes to bin histogram of distances
    spp : str
        String corresponding to focal species code
    density : bool
        If True, return densities (counts divided by area of torus defined
        by bin edges) instead of counts. Default True.
    full : bool
        If True, return a separate column giving density at distance x for
        every individual, rather than mean density. Default False.

    Returns
    -------
    {1} Result has two columns, x and y, that give the distance to the center
    of a torus and the number or density of individuals found in that torus.

    Notes
    -----
    If density is False, counts are raw counts, non-edge corrected, within
    rings.

    Pairwise distances are directional, giving n(n-1) total distances for a
    species with n individuals, as edge correction is inherently directional.

    Bins include the lower edge and exclude the upper edge, except for the
    final bin which includes both the lower and upper edge. Floating point
    arithmetic may cause points located "exactly" on edges to be allocated
    contrary to this rule, however.

    If there are no records for a species, result table will be a dataframe
    with no records. If there are records but a species has only one
    individual, dataframe will have zero count at all torus areas.

    When using density, the maximum distance used for edge correction, given by
    the mean of the last two bin_edge values, should ideally be set to no
    greater than one half the diagonal distance across the plot. This ensures
    that it is not possible for an entire edge correction buffer to be outside
    of the plot.

    {2}

    For the 0-ring analysis, cols must also contain x_col and y_col, giving the
    x and y dimensions along which to analyze spatial pattern.

    {3}

    """

    try:
        geo.box(0, 0, 1, 1)
    except:
        raise ImportError, "O-ring analysis requires shapely package"

    (spp_col, count_col, x_col, y_col), patch = \
        _get_cols(['spp_col', 'count_col', 'x_col', 'y_col'], cols, patch)

    # Loop through each split
    result_list = []
    for substring, subpatch in _yield_subpatches(patch, splits):

        # Get table for just this species
        spp_table = subpatch.table[subpatch.table[spp_col] == spp]

        # If spp not present, continue
        if (len(spp_table) == 0):
            result_list.append((substring, pd.DataFrame(columns=['x','y'])))
            continue

        # Set up plot geometry
        plot_poly, radii, torus_areas = \
            _get_plot_geometry(subpatch, bin_edges, x_col, y_col)

        # Get lists of all points and counts in spp_table
        x = spp_table[x_col]
        y = spp_table[y_col]
        points = zip(x,y)
        counts = list(spp_table[count_col])

        # Arrays to hold summed areas and distance histograms for all points

        if full:
            hists = []  # Vectors of len(radii) appended for each point
            areas = []
        else:
            hists = np.zeros(len(radii))
            areas = np.zeros(len(radii))

        # Go through each point and associated count
        for i, (point, count) in enumerate(zip(points, counts)):

            # Create list of all other points and counts except this
            other_points = points[0:i] + points[i+1:]
            other_counts = counts[0:i] + counts[i+1:]

            # Get dist from this point to all other points
            if other_points:
                other_dists = dist.cdist(np.array([point]),
                                         np.array(other_points))
            else:
                other_dists = np.array(())

            # Repeat other point distances to acccount for their counts
            other_dists = np.repeat(other_dists, other_counts)

            # Repeat entire other_dist array to account for count here
            other_dists = np.tile(other_dists, count)

            # Add 0's for count at this point to account for count here
            if count > 1:
                other_dists = np.concatenate((other_dists,
                                              np.zeros(count*(count-1))))

            # Calculate histogram of distances to other points
            hist, _ = np.histogram(other_dists, bin_edges)

            # Convert histogram to density if desired
            corr_factor = np.ones(len(radii))  # Frac length in plot
            for j, r in enumerate(radii):
                circ = geo.Point(*point).buffer(r, resolution=64)
                outside_len = circ.boundary.difference(plot_poly).length
                corr_factor[j] = ((circ.boundary.length - outside_len) /
                                   circ.boundary.length)

            # Add hist and corrected area for this point to running totals
            if full:
                hists.append(hist)
                areas.append(torus_areas * corr_factor * count)
            else:
                hists += hist
                areas += torus_areas * corr_factor * count

        # If density, divide summed torus counts by summed areas
        if density:
            hists = np.array(hists) / np.array(areas)

        # Append subset result
        subresult = pd.DataFrame({'x': radii})
        if full:
            for i in range(len(hists)):
                subresult[i] = hists[i]
        else:
            subresult['y'] = hists
        result_list.append((substring, subresult))

    # Return all results
    return result_list