From 1c638afec973fae90f3bf2dbf939482e2cdb41ac Mon Sep 17 00:00:00 2001 From: Pankaj Date: Wed, 2 Oct 2024 20:00:42 -0400 Subject: [PATCH 1/6] created evaluate.py to inculde boyce_index function --- elapid/evaluate.py | 180 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 180 insertions(+) create mode 100644 elapid/evaluate.py diff --git a/elapid/evaluate.py b/elapid/evaluate.py new file mode 100644 index 0000000..c1b9d66 --- /dev/null +++ b/elapid/evaluate.py @@ -0,0 +1,180 @@ +import geopandas as gpd +import numpy as np +import pandas as pd +import matplotlib.pyplot as plt +from scipy.stats import spearmanr + + +# implement Boyce index as describe in https://www.whoi.edu/cms/files/hirzel_etal_2006_53457.pdf (Eq.4) + + +def boycei(interval, obs, fit): + """ + Calculate the Boyce index for a given interval. + + Args: + interval (tuple or list): Two elements representing the lower and upper bounds of the interval. + obs (numpy.ndarray): Observed suitability values (i.e., predictions at presence points). + fit (numpy.ndarray): Suitability values (e.g., from a raster), i.e., predictions at presence + background points. + + Returns: + float: The ratio of observed to expected frequencies, representing the Boyce index for the given interval. + """ + # Boolean arrays for classification + fit_bin = (fit >= interval[0]) & (fit <= interval[1]) + obs_bin = (obs >= interval[0]) & (obs <= interval[1]) + + # Compute pi and ei + pi = np.sum(obs_bin) / len(obs_bin) + ei = np.sum(fit_bin) / len(fit_bin) + + if ei == 0: + fi = np.nan # Avoid division by zero + else: + fi = pi / ei + + return fi + + +def boyce_index(fit, obs, nclass=0, window="default", res=100, PEplot=False): + """ + Compute the Boyce index to evaluate habitat suitability models. + + The Boyce index evaluates how well a model predicts species presence by comparing its predictions + to a random distribution of observed presences along the prediction gradients. It is specifically + designed for presence-only models and serves as an appropriate metric in such cases. + + It divides the probability of species presence into ranges and, for each range, calculates the predicted-to-expected ratio (F ratio). + The final output is given by the Spearman correlation between the mid-point of the probability interval and the F ratio. + + Index ranges from -1 to +1: + - Positive values: Model predictions align with actual species presence distribution. + - Values near zero: Model performs similarly to random predictions. + - Negative values: Model incorrectly predicts low-quality areas where species are more frequently found. + + This calculation is based on the continuous Boyce index (Eq. 4) as defined in Hirzel et al. 2006. + + Args: + fit (numpy.ndarray | pd.Series | gpd.GeoSeries): Suitability values (e.g., predictions at presence + background points). + obs (numpy.ndarray | pd.Series | gpd.GeoSeries): Observed suitability values, i.e., predictions at presence points. + nclass (int | list, optional): Number of classes or list of class thresholds. Defaults to 0. + window (float | str, optional): Width of the moving window. Defaults to 'default' which sets window as 1/10th of the fit range. + res (int, optional): Resolution, i.e., number of steps if nclass=0. Defaults to 100. + PEplot (bool, optional): Whether to plot the predicted-to-expected (P/E) curve. Defaults to False. + + Returns: + dict: A dictionary with the following keys: + - 'F.ratio' (numpy.ndarray): The P/E ratio for each bin. + - 'Spearman.cor' (float): The Spearman's rank correlation coefficient between interval midpoints and F ratios. + - 'HS' (numpy.ndarray): The habitat suitability intervals. + + Example: + # Predicted suitability scores (e.g., predictions at presence + background points) + predicted = np.array([0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 1.0]) + + # Observed presence suitability scores (e.g., predictions at presence points) + observed = np.array([0.3, 0.7, 0.8, 0.9]) + + # Call the boyce_index function to calculate the Boyce index and Spearman correlation + results = boyce_index(fit=predicted, obs=observed, nclass=3) + print(results) + + # Output: + # {'F.ratio': array([0.625, 0.625, 1.875]), + # 'Spearman.cor': 0.866, + # 'HS': array([[0.1 , 0.4 ], + # [0.4 , 0.7 ], + # [0.7 , 1. ]])} + """ + + + # Check types of fit and obs + acceptable_types = (np.ndarray, pd.Series, gpd.GeoSeries) + if not isinstance(fit, acceptable_types): + raise TypeError("The 'fit' parameter must be a NumPy array, Pandas Series, or GeoPandas GeoSeries.") + if not isinstance(obs, acceptable_types): + raise TypeError("The 'obs' parameter must be a NumPy array, Pandas Series, or GeoPandas GeoSeries.") + + + # Convert inputs to NumPy arrays + fit = np.asarray(fit) + obs = np.asarray(obs) + + + # Ensure fit and obs are one-dimensional arrays + if fit.ndim != 1 or obs.ndim != 1: + raise ValueError("Both 'fit' and 'obs' must be one-dimensional arrays.") + + + # Remove NaNs from fit and obs + fit = fit[~np.isnan(fit)] + obs = obs[~np.isnan(obs)] + + if len(fit) == 0 or len(obs) == 0: + raise ValueError("After removing NaNs, 'fit' or 'obs' arrays cannot be empty.") + + + # Remove NaNs from fit + fit = fit[~np.isnan(fit)] + + if window == "default": + window = (np.max(fit) - np.min(fit)) / 10.0 + + mini = np.min(fit) + maxi = np.max(fit) + + if nclass == 0: + vec_mov = np.linspace(mini, maxi - window, num=res+1) + intervals = np.column_stack((vec_mov, vec_mov + window)) + elif isinstance(nclass, (list, np.ndarray)) and len(nclass) > 1: + nclass.sort() + if mini > nclass[0] or maxi < nclass[-1]: + raise ValueError(f"The range provided via nclass is: ({nclass[0], nclass[-1]}). The range computed via fit is: ({mini, maxi}). Provided range via nclass should be in range computed via (max(fit), min(fit)).") + vec_mov = np.concatenate(([mini], nclass)) + intervals = np.column_stack((vec_mov[:-1], vec_mov[1:])) + print(vec_mov) + print(intervals) + elif nclass > 0: + vec_mov = np.linspace(mini, maxi, num=nclass + 1) + intervals = np.column_stack((vec_mov[:-1], vec_mov[1:])) + else: + raise ValueError("Invalid nclass value.") + + + # Apply boycei function to each interval + f_list = [] + for inter in intervals: + fi = boycei(inter, obs, fit) + f_list.append(fi) + f = np.array(f_list) + + + # Remove NaNs + valid = ~np.isnan(f) + + # use interval midpoints to calculate the spearmanr coeff. + intervals_mid = np.mean(intervals[valid], axis=1) + if np.sum(valid) <= 2: + corr = np.nan + else: + f_valid = f[valid] + corr, _ = spearmanr(f_valid, intervals_mid) + + + if PEplot: + plt.figure() + plt.plot(intervals_mid, f[valid], marker='o') + plt.xlabel('Habitat suitability') + plt.ylabel('Predicted/Expected ratio') + plt.title('Boyce Index') + plt.show() + + + results = { + 'F.ratio': f, + 'Spearman.cor': round(corr, 3) if not np.isnan(corr) else np.nan, + 'HS': intervals, + } + + return results + From 91b8d25e9c5cda979ecd62b98989ce6f4a9c29ac Mon Sep 17 00:00:00 2001 From: Pankaj Date: Wed, 2 Oct 2024 20:01:23 -0400 Subject: [PATCH 2/6] added test case for evaluate.py --- tests/test_evaluate.py | 150 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 150 insertions(+) create mode 100644 tests/test_evaluate.py diff --git a/tests/test_evaluate.py b/tests/test_evaluate.py new file mode 100644 index 0000000..e687ea3 --- /dev/null +++ b/tests/test_evaluate.py @@ -0,0 +1,150 @@ +import numpy as np +import pytest +import matplotlib.pyplot as plt +from elapid.evaluate import boycei, boyce_index +import pandas as pd +import geopandas as gpd +from shapely.geometry import Point + + + +# Test Case 1: Normal case with random data +def test_normal_case(): + np.random.seed(0) + fit = np.random.rand(1000) + obs = np.random.choice(fit, size=100, replace=False) + results = boyce_index(fit, obs, nclass=10, PEplot=False) + assert 'Spearman.cor' in results + assert 'F.ratio' in results + spearman_cor = results['Spearman.cor'] + f_ratio = results['F.ratio'] + assert not np.isnan(spearman_cor) + assert -1 <= spearman_cor <= 1 + assert len(f_ratio) == 10 + assert not np.any(np.isnan(f_ratio)) + assert np.all(f_ratio >= 0) + +# Test Case 2: Edge case with empty 'fit' array +def test_empty_fit(): + fit = np.array([]) + obs = np.array([0.5, 0.6, 0.7]) + with pytest.raises(ValueError): + boyce_index(fit, obs, nclass=10, PEplot=False) + +# Test Case 3: Edge case with empty 'obs' array +def test_empty_obs(): + fit = np.random.rand(1000) + obs = np.array([]) + with pytest.raises(ValueError) as exc_info: + boyce_index(fit, obs, nclass=10, PEplot=False) + assert "After removing NaNs, 'fit' or 'obs' arrays cannot be empty." in str(exc_info.value) + +# Test Case 4: 'obs' containing NaNs +def test_obs_with_nans(): + fit = np.random.rand(1000) + obs = np.random.choice(fit, size=100, replace=False) + obs[::10] = np.nan # Introduce NaNs into 'obs' + results = boyce_index(fit, obs, nclass=10, PEplot=False) + spearman_cor = results['Spearman.cor'] + assert 'Spearman.cor' in results + if not np.isnan(spearman_cor): + assert -1 <= spearman_cor <= 1 + f_ratio = results['F.ratio'] + assert len(f_ratio) == 10 + +# Test Case 5: Invalid 'nclass' value (negative number) +def test_invalid_nclass(): + fit = np.random.rand(1000) + obs = np.random.choice(fit, size=100, replace=False) + with pytest.raises(ValueError): + boyce_index(fit, obs, nclass=-5, PEplot=False) + +# Test Case 6: Custom 'window' value +def test_custom_window(): + fit = np.random.rand(1000) + obs = np.random.choice(fit, size=100, replace=False) + results = boyce_index(fit, obs, window=0.1, PEplot=False) + assert 'Spearman.cor' in results + spearman_cor = results['Spearman.cor'] + assert not np.isnan(spearman_cor) + assert -1 <= spearman_cor <= 1 + f_ratio = results['F.ratio'] + assert len(f_ratio) > 0 + +# Test Case 7: 'PEplot' set to True +def test_peplot_true(): + fit = np.random.rand(1000) + obs = np.random.choice(fit, size=100, replace=False) + results = boyce_index(fit, obs, nclass=10, PEplot=True) + assert 'Spearman.cor' in results + spearman_cor = results['Spearman.cor'] + assert not np.isnan(spearman_cor) + assert -1 <= spearman_cor <= 1 + plt.close('all') # Close the plot to avoid display during testing + +# Test Case 8: 'fit' containing NaNs +def test_fit_with_nans(): +# In this code snippet: + fit = np.random.rand(1000) + fit[::50] = np.nan # Introduce NaNs into 'fit' + obs = np.random.choice(fit[~np.isnan(fit)], size=100, replace=False) + results = boyce_index(fit, obs, nclass=10, PEplot=False) + assert 'Spearman.cor' in results + spearman_cor = results['Spearman.cor'] + assert not np.isnan(spearman_cor) + assert -1 <= spearman_cor <= 1 + f_ratio = results['F.ratio'] + assert len(f_ratio) == 10 + +# Test Case 9: 'obs' values outside the range of 'fit' +def test_obs_outside_fit_range(): + fit = np.random.rand(1000) + obs = np.array([1.5, 2.0, 2.5]) # Values outside the range [0, 1] + results = boyce_index(fit, obs, nclass=10, PEplot=False) + spearman_cor = results['Spearman.cor'] + assert 'Spearman.cor' in results + assert np.isnan(spearman_cor) or -1 <= spearman_cor <= 1 + f_ratio = results['F.ratio'] + assert len(f_ratio) == 10 + +# Test Case 10: Large dataset +def test_large_dataset(): + fit = np.random.rand(1000000) + obs = np.random.choice(fit, size=10000, replace=False) + results = boyce_index(fit, obs, nclass=20, PEplot=False) + assert 'Spearman.cor' in results + spearman_cor = results['Spearman.cor'] + assert not np.isnan(spearman_cor) + assert -1 <= spearman_cor <= 1 + f_ratio = results['F.ratio'] + assert len(f_ratio) == 20 + + +# Test Case 11: Using Pandas Series +def test_with_pandas_series(): + np.random.seed(0) + fit = pd.Series(np.random.rand(1000)) + obs = fit.sample(n=100, replace=False) + results = boyce_index(fit, obs, nclass=10, PEplot=False) + assert 'Spearman.cor' in results + spearman_cor = results['Spearman.cor'] + assert not np.isnan(spearman_cor) + assert -1 <= spearman_cor <= 1 + +# Test Case 12: Using GeoPandas GeoSeries +def test_with_geopandas_geoseries(): + np.random.seed(0) + num_points = 1000 + x = np.random.uniform(-180, 180, num_points) + y = np.random.uniform(-90, 90, num_points) + suitability = np.random.rand(num_points) + geometry = [Point(xy) for xy in zip(x, y)] + gdf = gpd.GeoDataFrame({'suitability': suitability}, geometry=geometry) + + fit = gdf['suitability'] # This is a Pandas Series + obs = fit.sample(n=100, replace=False) + results = boyce_index(fit, obs, nclass=10, PEplot=False) + assert 'Spearman.cor' in results + spearman_cor = results['Spearman.cor'] + assert not np.isnan(spearman_cor) + assert -1 <= spearman_cor <= 1 \ No newline at end of file From be80087ff3b34831209ba1a4476ed84655374f18 Mon Sep 17 00:00:00 2001 From: Pankaj Date: Wed, 2 Oct 2024 20:02:39 -0400 Subject: [PATCH 3/6] added Boyce index calculation in WorkingWithGeospatialData.ipynb, updated gitignore, and updated __init__.py --- .gitignore | 4 ++ docs/examples/WorkingWithGeospatialData.ipynb | 58 ++++++++++++++++++- elapid/__init__.py | 1 + 3 files changed, 62 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index c69f22d..34d3369 100644 --- a/.gitignore +++ b/.gitignore @@ -131,3 +131,7 @@ dmypy.json # conda smithy build_artifacts package/ + + +#poetry lock file +poetry.lock \ No newline at end of file diff --git a/docs/examples/WorkingWithGeospatialData.ipynb b/docs/examples/WorkingWithGeospatialData.ipynb index 7ab4627..55b08b1 100644 --- a/docs/examples/WorkingWithGeospatialData.ipynb +++ b/docs/examples/WorkingWithGeospatialData.ipynb @@ -1682,6 +1682,62 @@ "\n", "This example is simply demonstrative and will not work as-is because this group label doesn't exist for this dataset." ] + }, + { + "cell_type": "markdown", + "id": "94a148c1", + "metadata": {}, + "source": [ + "# Boyce Index\n", + "\n", + "The Boyce index measures the reliability of habitat suitability models by evaluating how predicted suitability values correlate with observed presence data. It focuses on whether areas predicted to have high suitability also contain a proportionately higher number of observed presences, making it useful for evaluating model performance over specific suitability ranges.\n", + "\n", + "AUC (Area Under the Curve) alone is not sufficient in MaxEnt modeling because it evaluates the overall discrimination between presence and absence but does not assess how well the model predicts relative suitability across different areas. It can be misleading, especially when the model performs well in distinguishing presence and background but poorly in identifying areas of high suitability. The Boyce index complements AUC by focusing on this aspect." + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "id": "f2b5b7f3", + "metadata": {}, + "outputs": [ + { + "data": { + "image/png": "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", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/plain": [ + "{'F.ratio': array([0.625, 0.625, 1.875]),\n", + " 'Spearman.cor': 0.866,\n", + " 'HS': array([[0.1, 0.4],\n", + " [0.4, 0.7],\n", + " [0.7, 1. ]])}" + ] + }, + "execution_count": 10, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# Predicted suitability scores (e.g., predictions at presence + background points)\n", + "predicted = np.array([0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 1.0])\n", + "\n", + "# Observed presence suitability scores (e.g predictions at presence points)\n", + "observed = np.array([0.3, 0.7, 0.8, 0.9])\n", + "\n", + "# Call the boyce_index function to calculate the Boyce index and Spearman correlation\n", + "results = ela.boyce_index(fit=predicted, obs=observed, nclass=3, PEplot=True)\n", + "\n", + "results" + ] } ], "metadata": { @@ -1700,7 +1756,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.8.16" + "version": "3.10.14" } }, "nbformat": 4, diff --git a/elapid/__init__.py b/elapid/__init__.py index cf4968f..2ecacbf 100644 --- a/elapid/__init__.py +++ b/elapid/__init__.py @@ -26,4 +26,5 @@ from elapid.stats import normalize_sample_probabilities from elapid.train_test_split import BufferedLeaveOneOut, GeographicKFold, checkerboard_split from elapid.utils import download_sample_data, load_object, load_sample_data, save_object +from elapid.evaluate import boycei, boyce_index from elapid.version import __version__ From 7608ee57d745c10f8152acb0438d624be169bd53 Mon Sep 17 00:00:00 2001 From: Pankaj Date: Mon, 7 Oct 2024 13:07:24 -0400 Subject: [PATCH 4/6] Refactored code significantly. Fixed the bug to ensure predictions are made at presence and background points, not presence and combined presence + background (Thanks for pointing that out, I misinterpreted the paper). Removed redundant NaN checks. Updated test cases, and example notebook. --- .gitignore | 2 +- docs/examples/WorkingWithGeospatialData.ipynb | 24 +- elapid/__init__.py | 2 +- elapid/evaluate.py | 277 +++++++++--------- tests/test_evaluate.py | 232 ++++++++------- 5 files changed, 291 insertions(+), 246 deletions(-) diff --git a/.gitignore b/.gitignore index 34d3369..32cbf20 100644 --- a/.gitignore +++ b/.gitignore @@ -134,4 +134,4 @@ package/ #poetry lock file -poetry.lock \ No newline at end of file +poetry.lock diff --git a/docs/examples/WorkingWithGeospatialData.ipynb b/docs/examples/WorkingWithGeospatialData.ipynb index 55b08b1..604191d 100644 --- a/docs/examples/WorkingWithGeospatialData.ipynb +++ b/docs/examples/WorkingWithGeospatialData.ipynb @@ -1692,18 +1692,18 @@ "\n", "The Boyce index measures the reliability of habitat suitability models by evaluating how predicted suitability values correlate with observed presence data. It focuses on whether areas predicted to have high suitability also contain a proportionately higher number of observed presences, making it useful for evaluating model performance over specific suitability ranges.\n", "\n", - "AUC (Area Under the Curve) alone is not sufficient in MaxEnt modeling because it evaluates the overall discrimination between presence and absence but does not assess how well the model predicts relative suitability across different areas. It can be misleading, especially when the model performs well in distinguishing presence and background but poorly in identifying areas of high suitability. The Boyce index complements AUC by focusing on this aspect." + "AUC (Area Under the Curve) alone is not sufficient in MaxEnt modeling because it evaluates the overall discrimination between presence and absence but does not assess how well the model predicts relative suitability across different areas. It can be misleading, especially when the model performs well in distinguishing presence and background but poorly in identifying areas of high suitability. The Boyce index complements AUC by focusing on this aspect. Based on work of [Hirzel et al. 2006](https://www.whoi.edu/cms/files/hirzel_etal_2006_53457.pdf) (Eq.4)" ] }, { "cell_type": "code", - "execution_count": 10, + "execution_count": 3, "id": "f2b5b7f3", "metadata": {}, "outputs": [ { "data": { - "image/png": "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", + "image/png": "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", "text/plain": [ "
" ] @@ -1715,26 +1715,26 @@ "data": { "text/plain": [ "{'F.ratio': array([0.625, 0.625, 1.875]),\n", - " 'Spearman.cor': 0.866,\n", + " 'Spearman.cor': 0.8660254037844387,\n", " 'HS': array([[0.1, 0.4],\n", " [0.4, 0.7],\n", " [0.7, 1. ]])}" ] }, - "execution_count": 10, + "execution_count": 3, "metadata": {}, "output_type": "execute_result" } ], "source": [ - "# Predicted suitability scores (e.g., predictions at presence + background points)\n", - "predicted = np.array([0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 1.0])\n", + "# presence suitability scores (e.g., predictions at presences)\n", + "suitability_presence = np.array([0.3, 0.7, 0.8, 0.9])\n", "\n", - "# Observed presence suitability scores (e.g predictions at presence points)\n", - "observed = np.array([0.3, 0.7, 0.8, 0.9])\n", + "# background suitability scores (e.g., predictions at backgrounds)\n", + "suitability_background = np.array([0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 1.0])\n", "\n", - "# Call the boyce_index function to calculate the Boyce index and Spearman correlation\n", - "results = ela.boyce_index(fit=predicted, obs=observed, nclass=3, PEplot=True)\n", + "# Call the continuous_boyce_index function to calculate the Boyce index and Spearman correlation\n", + "results = ela.continuous_boyce_index(yobs=suitability_presence, ypred=suitability_background, nbins=3, to_plot=True)\n", "\n", "results" ] @@ -1756,7 +1756,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.10.14" + "version": "3.11.10" } }, "nbformat": 4, diff --git a/elapid/__init__.py b/elapid/__init__.py index 2ecacbf..8e00fd1 100644 --- a/elapid/__init__.py +++ b/elapid/__init__.py @@ -1,5 +1,6 @@ """User entrypoint to elapid""" +from elapid.evaluate import boyce_index, continuous_boyce_index, get_intervals, plot_PE_curve from elapid.features import ( CategoricalTransformer, HingeTransformer, @@ -26,5 +27,4 @@ from elapid.stats import normalize_sample_probabilities from elapid.train_test_split import BufferedLeaveOneOut, GeographicKFold, checkerboard_split from elapid.utils import download_sample_data, load_object, load_sample_data, save_object -from elapid.evaluate import boycei, boyce_index from elapid.version import __version__ diff --git a/elapid/evaluate.py b/elapid/evaluate.py index c1b9d66..1d233ae 100644 --- a/elapid/evaluate.py +++ b/elapid/evaluate.py @@ -1,32 +1,103 @@ +import math +import warnings +from typing import Dict, List, Tuple, Union + import geopandas as gpd +import matplotlib.pyplot as plt import numpy as np import pandas as pd -import matplotlib.pyplot as plt from scipy.stats import spearmanr +# implement continuous Boyce index as describe in https://www.whoi.edu/cms/files/hirzel_etal_2006_53457.pdf (Eq.4) -# implement Boyce index as describe in https://www.whoi.edu/cms/files/hirzel_etal_2006_53457.pdf (Eq.4) +def plot_PE_curve(x: Union[np.ndarray, List[float]], y: Union[np.ndarray, List[float]]) -> None: + """ + Plots the Predicted/Expected (P/E) curve for the Boyce Index. -def boycei(interval, obs, fit): + Args: + x (array-like): Habitat suitability values (e.g., interval midpoints). + y (array-like): Predicted/Expected ratios corresponding to the habitat suitability intervals. + + Returns: + None """ - Calculate the Boyce index for a given interval. - + plt.figure() + plt.plot(x, y, marker="o") + plt.xlabel("Habitat suitability") + plt.ylabel("Predicted/Expected ratio") + plt.title("Boyce Index") + plt.show() + + +def get_intervals( + nbins: Union[int, List[float], np.ndarray] = 0, + bin_size: Union[float, str] = "default", + range: Tuple[float, float] = (0, 1), +) -> np.ndarray: + """ + Generates habitat suitability intervals for the Boyce Index calculation. + + Calculates intervals based on the provided range and either the number of bins or bin size. + Args: - interval (tuple or list): Two elements representing the lower and upper bounds of the interval. - obs (numpy.ndarray): Observed suitability values (i.e., predictions at presence points). - fit (numpy.ndarray): Suitability values (e.g., from a raster), i.e., predictions at presence + background points. - + nbins (int or list or np.ndarray, optional): Number of classes or a list of class thresholds. Defaults to 0. + bin_size (float or str, optional): Width of the bins. Defaults to 'default', which sets the width as 1/10th of the range. + range (tuple or list): Two elements representing the minimum and maximum values of habitat suitability. Default : [0, 1] + Returns: - float: The ratio of observed to expected frequencies, representing the Boyce index for the given interval. + np.ndarray: An array of intervals, each represented by a pair of lower and upper bounds. + + Raises: + ValueError: If invalid values are provided for nbins or bin_size. """ - # Boolean arrays for classification - fit_bin = (fit >= interval[0]) & (fit <= interval[1]) - obs_bin = (obs >= interval[0]) & (obs <= interval[1]) + mini, maxi = range + + if isinstance(bin_size, float): + nbins = (maxi - mini) / bin_size + if not nbins.is_integer(): + warnings.warn( + f"bin_size has been adjusted to nearest appropriate size using ceil, as range/bin_size : {(maxi - mini)} / {bin_size} is not an integer.", + UserWarning, + ) + nbins = math.ceil(nbins) + elif nbins == 0 and bin_size == "default": + nbins = 10 + + if isinstance(nbins, (list, np.ndarray)): + if len(nbins) == 1: + raise ValueError("Invalid nbins value. len(nbins) must be > 1") + nbins.sort() + intervals = np.column_stack((nbins[:-1], nbins[1:])) + elif nbins > 1: + boundary = np.linspace(mini, maxi, num=nbins + 1) + intervals = np.column_stack((boundary[:-1], boundary[1:])) + else: + raise ValueError("Invalid nbins value. nbins > 1") + + return intervals - # Compute pi and ei - pi = np.sum(obs_bin) / len(obs_bin) - ei = np.sum(fit_bin) / len(fit_bin) + +def boyce_index(yobs: np.ndarray, ypred: np.ndarray, interval: Union[Tuple[float, float], List[float]]) -> float: + """ + Calculates the Boyce index for a given interval. + + Uses the convention as defined in Hirzel et al. 2006 to compute the ratio of observed to expected frequencies. + + Args: + yobs (np.ndarray): Suitability values at observed locations (e.g., predictions at presence points). + ypred (np.ndarray): Suitability values at random locations (e.g., predictions at background points). + interval (tuple or list): Two elements representing the lower and upper bounds of the interval (i.e., habitat suitability). + + Returns: + float: The ratio of observed to expected frequencies for the given interval. + + """ + yobs_bin = (yobs >= interval[0]) & (yobs <= interval[1]) + ypred_bin = (ypred >= interval[0]) & (ypred <= interval[1]) + + pi = np.sum(yobs_bin) / len(yobs_bin) + ei = np.sum(ypred_bin) / len(ypred_bin) if ei == 0: fi = np.nan # Avoid division by zero @@ -36,145 +107,83 @@ def boycei(interval, obs, fit): return fi -def boyce_index(fit, obs, nclass=0, window="default", res=100, PEplot=False): +def continuous_boyce_index( + yobs: Union[np.ndarray, pd.Series, gpd.GeoSeries], + ypred: Union[np.ndarray, pd.Series, gpd.GeoSeries], + nbins: Union[int, List[float], np.ndarray] = 0, + bin_size: Union[float, str] = "default", + to_plot: bool = False, +) -> Dict[str, Union[np.ndarray, float]]: """ - Compute the Boyce index to evaluate habitat suitability models. - - The Boyce index evaluates how well a model predicts species presence by comparing its predictions - to a random distribution of observed presences along the prediction gradients. It is specifically - designed for presence-only models and serves as an appropriate metric in such cases. - - It divides the probability of species presence into ranges and, for each range, calculates the predicted-to-expected ratio (F ratio). - The final output is given by the Spearman correlation between the mid-point of the probability interval and the F ratio. - - Index ranges from -1 to +1: - - Positive values: Model predictions align with actual species presence distribution. - - Values near zero: Model performs similarly to random predictions. - - Negative values: Model incorrectly predicts low-quality areas where species are more frequently found. - - This calculation is based on the continuous Boyce index (Eq. 4) as defined in Hirzel et al. 2006. + Compute the continuous Boyce index to evaluate habitat suitability models. + + Uses the convention as defined in Hirzel et al. 2006 to compute the ratio of observed to expected frequencies. Args: - fit (numpy.ndarray | pd.Series | gpd.GeoSeries): Suitability values (e.g., predictions at presence + background points). - obs (numpy.ndarray | pd.Series | gpd.GeoSeries): Observed suitability values, i.e., predictions at presence points. - nclass (int | list, optional): Number of classes or list of class thresholds. Defaults to 0. - window (float | str, optional): Width of the moving window. Defaults to 'default' which sets window as 1/10th of the fit range. - res (int, optional): Resolution, i.e., number of steps if nclass=0. Defaults to 100. - PEplot (bool, optional): Whether to plot the predicted-to-expected (P/E) curve. Defaults to False. - + yobs (numpy.ndarray | pd.Series | gpd.GeoSeries): Suitability values at observed location (i.e., predictions at presence points). + ypred (numpy.ndarray | pd.Series | gpd.GeoSeries): Suitability values at random location (i.e., predictions at background points). + nbins (int | list, optional): Number of classes or a list of class thresholds. Defaults to 0. + bin_size (float | str, optional): Width of the the bin. Defaults to 'default' which sets width as 1/10th of the fit range. + to_plot (bool, optional): Whether to plot the predicted-to-expected (P/E) curve. Defaults to False. + Returns: dict: A dictionary with the following keys: - 'F.ratio' (numpy.ndarray): The P/E ratio for each bin. - 'Spearman.cor' (float): The Spearman's rank correlation coefficient between interval midpoints and F ratios. - 'HS' (numpy.ndarray): The habitat suitability intervals. - - Example: - # Predicted suitability scores (e.g., predictions at presence + background points) - predicted = np.array([0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 1.0]) - - # Observed presence suitability scores (e.g., predictions at presence points) - observed = np.array([0.3, 0.7, 0.8, 0.9]) - - # Call the boyce_index function to calculate the Boyce index and Spearman correlation - results = boyce_index(fit=predicted, obs=observed, nclass=3) - print(results) - - # Output: - # {'F.ratio': array([0.625, 0.625, 1.875]), - # 'Spearman.cor': 0.866, - # 'HS': array([[0.1 , 0.4 ], - # [0.4 , 0.7 ], - # [0.7 , 1. ]])} """ - - - # Check types of fit and obs - acceptable_types = (np.ndarray, pd.Series, gpd.GeoSeries) - if not isinstance(fit, acceptable_types): - raise TypeError("The 'fit' parameter must be a NumPy array, Pandas Series, or GeoPandas GeoSeries.") - if not isinstance(obs, acceptable_types): - raise TypeError("The 'obs' parameter must be a NumPy array, Pandas Series, or GeoPandas GeoSeries.") - - - # Convert inputs to NumPy arrays - fit = np.asarray(fit) - obs = np.asarray(obs) - - - # Ensure fit and obs are one-dimensional arrays - if fit.ndim != 1 or obs.ndim != 1: - raise ValueError("Both 'fit' and 'obs' must be one-dimensional arrays.") - - - # Remove NaNs from fit and obs - fit = fit[~np.isnan(fit)] - obs = obs[~np.isnan(obs)] - - if len(fit) == 0 or len(obs) == 0: - raise ValueError("After removing NaNs, 'fit' or 'obs' arrays cannot be empty.") - - - # Remove NaNs from fit - fit = fit[~np.isnan(fit)] - - if window == "default": - window = (np.max(fit) - np.min(fit)) / 10.0 - - mini = np.min(fit) - maxi = np.max(fit) - - if nclass == 0: - vec_mov = np.linspace(mini, maxi - window, num=res+1) - intervals = np.column_stack((vec_mov, vec_mov + window)) - elif isinstance(nclass, (list, np.ndarray)) and len(nclass) > 1: - nclass.sort() - if mini > nclass[0] or maxi < nclass[-1]: - raise ValueError(f"The range provided via nclass is: ({nclass[0], nclass[-1]}). The range computed via fit is: ({mini, maxi}). Provided range via nclass should be in range computed via (max(fit), min(fit)).") - vec_mov = np.concatenate(([mini], nclass)) - intervals = np.column_stack((vec_mov[:-1], vec_mov[1:])) - print(vec_mov) - print(intervals) - elif nclass > 0: - vec_mov = np.linspace(mini, maxi, num=nclass + 1) - intervals = np.column_stack((vec_mov[:-1], vec_mov[1:])) - else: - raise ValueError("Invalid nclass value.") + if not isinstance(ypred, (np.ndarray, pd.Series, gpd.GeoSeries)): + raise TypeError("The 'ypred' parameter must be a NumPy array, Pandas Series, or GeoPandas GeoSeries.") + if not isinstance(yobs, (np.ndarray, pd.Series, gpd.GeoSeries)): + raise TypeError("The 'yobs' parameter must be a NumPy array, Pandas Series, or GeoPandas GeoSeries.") + if not isinstance(nbins, (int, list, np.ndarray)): + raise TypeError("The 'nbins' parameter must be a int, list, or 1-d NumPy array.") + if not isinstance(bin_size, (float, str)): + raise TypeError("The 'bin_size' parameter must be a float, or str ('default').") + if isinstance(bin_size, float) and (isinstance(nbins, (list, np.ndarray)) or nbins > 0): + raise ValueError( + f"Ambiguous value provided. Provide either nbins or bin_size. Cannot provide both. Provided values for nbins, bin_size are: ({nbins, bin_size})" + ) - # Apply boycei function to each interval - f_list = [] - for inter in intervals: - fi = boycei(inter, obs, fit) - f_list.append(fi) - f = np.array(f_list) + # Check for NaN values and issue warnings + if np.isnan(ypred).any(): + warnings.warn("'ypred' contains NaN values, which will be ignored.", UserWarning) + ypred = ypred[~np.isnan(ypred)] + if np.isnan(yobs).any(): + warnings.warn("'yobs' contains NaN values, which will be ignored.", UserWarning) + yobs = yobs[~np.isnan(yobs)] + ypred = np.asarray(ypred) + yobs = np.asarray(yobs) + + if ypred.ndim != 1 or yobs.ndim != 1: + raise ValueError("Both 'ypred' and 'yobs' must be one-dimensional arrays.") + + if len(ypred) == 0 or len(yobs) == 0: + raise ValueError("'ypred' or 'yobs' arrays cannot be empty.") + + mini, maxi = np.min(ypred), np.max(ypred) + + intervals = get_intervals(nbins, bin_size, range=[mini, maxi]) + f_scores = np.array([boyce_index(yobs, ypred, interval) for interval in intervals]) + + valid = ~np.isnan(f_scores) + f_valid = f_scores[valid] - # Remove NaNs - valid = ~np.isnan(f) - - # use interval midpoints to calculate the spearmanr coeff. intervals_mid = np.mean(intervals[valid], axis=1) if np.sum(valid) <= 2: corr = np.nan else: - f_valid = f[valid] corr, _ = spearmanr(f_valid, intervals_mid) - - if PEplot: - plt.figure() - plt.plot(intervals_mid, f[valid], marker='o') - plt.xlabel('Habitat suitability') - plt.ylabel('Predicted/Expected ratio') - plt.title('Boyce Index') - plt.show() - + if to_plot: + plot_PE_curve(x=intervals_mid, y=f_valid) results = { - 'F.ratio': f, - 'Spearman.cor': round(corr, 3) if not np.isnan(corr) else np.nan, - 'HS': intervals, + "F.ratio": f_scores, + "Spearman.cor": corr, + "HS": intervals, } return results - diff --git a/tests/test_evaluate.py b/tests/test_evaluate.py index e687ea3..69e481c 100644 --- a/tests/test_evaluate.py +++ b/tests/test_evaluate.py @@ -1,137 +1,144 @@ -import numpy as np -import pytest +import geopandas as gpd import matplotlib.pyplot as plt -from elapid.evaluate import boycei, boyce_index +import numpy as np import pandas as pd -import geopandas as gpd +import pytest from shapely.geometry import Point +from elapid.evaluate import boyce_index, continuous_boyce_index # Test Case 1: Normal case with random data def test_normal_case(): np.random.seed(0) - fit = np.random.rand(1000) - obs = np.random.choice(fit, size=100, replace=False) - results = boyce_index(fit, obs, nclass=10, PEplot=False) - assert 'Spearman.cor' in results - assert 'F.ratio' in results - spearman_cor = results['Spearman.cor'] - f_ratio = results['F.ratio'] + ypred = np.random.rand(1000) + yobs = np.random.choice(ypred, size=100, replace=False) + results = continuous_boyce_index(yobs, ypred, nbins=10, to_plot=False) + assert "Spearman.cor" in results + assert "F.ratio" in results + spearman_cor = results["Spearman.cor"] + f_ratio = results["F.ratio"] assert not np.isnan(spearman_cor) assert -1 <= spearman_cor <= 1 assert len(f_ratio) == 10 assert not np.any(np.isnan(f_ratio)) assert np.all(f_ratio >= 0) -# Test Case 2: Edge case with empty 'fit' array -def test_empty_fit(): - fit = np.array([]) - obs = np.array([0.5, 0.6, 0.7]) - with pytest.raises(ValueError): - boyce_index(fit, obs, nclass=10, PEplot=False) -# Test Case 3: Edge case with empty 'obs' array -def test_empty_obs(): - fit = np.random.rand(1000) - obs = np.array([]) +# Test Case 2: Edge case with empty 'ypred' array +def test_empty_ypred(): + ypred = np.array([]) + yobs = np.array([0.5, 0.6, 0.7]) + with pytest.raises(ValueError) as exc_info: + continuous_boyce_index(yobs, ypred, nbins=10, to_plot=False) + assert "'ypred' or 'yobs' arrays cannot be empty." in str(exc_info.value) + + +# Test Case 3: Edge case with empty 'yobs' array +def test_empty_yobs(): + ypred = np.random.rand(1000) + yobs = np.array([]) with pytest.raises(ValueError) as exc_info: - boyce_index(fit, obs, nclass=10, PEplot=False) - assert "After removing NaNs, 'fit' or 'obs' arrays cannot be empty." in str(exc_info.value) - -# Test Case 4: 'obs' containing NaNs -def test_obs_with_nans(): - fit = np.random.rand(1000) - obs = np.random.choice(fit, size=100, replace=False) - obs[::10] = np.nan # Introduce NaNs into 'obs' - results = boyce_index(fit, obs, nclass=10, PEplot=False) - spearman_cor = results['Spearman.cor'] - assert 'Spearman.cor' in results + continuous_boyce_index(yobs, ypred, nbins=10, to_plot=False) + assert "'ypred' or 'yobs' arrays cannot be empty." in str(exc_info.value) + + +# Test Case 4: 'yobs' containing NaNs +def test_yobs_with_nans(recwarn): + ypred = np.random.rand(1000) + yobs = np.random.choice(ypred, size=100, replace=False) + yobs[::10] = np.nan # Introduce NaNs into 'yobs' + results = continuous_boyce_index(yobs, ypred, nbins=10, to_plot=False) + # Check for warnings + w = recwarn.pop(UserWarning) + assert "'yobs' contains NaN values, which will be ignored." in str(w.message) + # Ensure function outputs are as expected + assert "Spearman.cor" in results + spearman_cor = results["Spearman.cor"] if not np.isnan(spearman_cor): assert -1 <= spearman_cor <= 1 - f_ratio = results['F.ratio'] + f_ratio = results["F.ratio"] assert len(f_ratio) == 10 -# Test Case 5: Invalid 'nclass' value (negative number) -def test_invalid_nclass(): - fit = np.random.rand(1000) - obs = np.random.choice(fit, size=100, replace=False) + +# Test Case 5: Invalid 'nbins' value (negative number) +def test_invalid_nbins(): + ypred = np.random.rand(1000) + yobs = np.random.choice(ypred, size=100, replace=False) with pytest.raises(ValueError): - boyce_index(fit, obs, nclass=-5, PEplot=False) - -# Test Case 6: Custom 'window' value -def test_custom_window(): - fit = np.random.rand(1000) - obs = np.random.choice(fit, size=100, replace=False) - results = boyce_index(fit, obs, window=0.1, PEplot=False) - assert 'Spearman.cor' in results - spearman_cor = results['Spearman.cor'] + continuous_boyce_index(yobs, ypred, nbins=-5, to_plot=False) + + +# Test Case 6: Custom 'bin_size' value +def test_custom_bin_size(): + ypred = np.random.rand(1000) + yobs = np.random.choice(ypred, size=100, replace=False) + results = continuous_boyce_index(yobs, ypred, bin_size=0.1, to_plot=False) + assert "Spearman.cor" in results + spearman_cor = results["Spearman.cor"] assert not np.isnan(spearman_cor) assert -1 <= spearman_cor <= 1 - f_ratio = results['F.ratio'] + f_ratio = results["F.ratio"] assert len(f_ratio) > 0 -# Test Case 7: 'PEplot' set to True -def test_peplot_true(): - fit = np.random.rand(1000) - obs = np.random.choice(fit, size=100, replace=False) - results = boyce_index(fit, obs, nclass=10, PEplot=True) - assert 'Spearman.cor' in results - spearman_cor = results['Spearman.cor'] - assert not np.isnan(spearman_cor) - assert -1 <= spearman_cor <= 1 - plt.close('all') # Close the plot to avoid display during testing - -# Test Case 8: 'fit' containing NaNs -def test_fit_with_nans(): -# In this code snippet: - fit = np.random.rand(1000) - fit[::50] = np.nan # Introduce NaNs into 'fit' - obs = np.random.choice(fit[~np.isnan(fit)], size=100, replace=False) - results = boyce_index(fit, obs, nclass=10, PEplot=False) - assert 'Spearman.cor' in results - spearman_cor = results['Spearman.cor'] + +# Test Case 7: 'ypred' containing NaNs +def test_ypred_with_nans(recwarn): + ypred = np.random.rand(1000) + ypred[::50] = np.nan # Introduce NaNs into 'ypred' + yobs = np.random.choice(ypred[~np.isnan(ypred)], size=100, replace=False) + results = continuous_boyce_index(yobs, ypred, nbins=10, to_plot=False) + # Check for warnings + w = recwarn.pop(UserWarning) + assert "'ypred' contains NaN values, which will be ignored." in str(w.message) + # Ensure function outputs are as expected + assert "Spearman.cor" in results + spearman_cor = results["Spearman.cor"] assert not np.isnan(spearman_cor) assert -1 <= spearman_cor <= 1 - f_ratio = results['F.ratio'] + f_ratio = results["F.ratio"] assert len(f_ratio) == 10 -# Test Case 9: 'obs' values outside the range of 'fit' -def test_obs_outside_fit_range(): - fit = np.random.rand(1000) - obs = np.array([1.5, 2.0, 2.5]) # Values outside the range [0, 1] - results = boyce_index(fit, obs, nclass=10, PEplot=False) - spearman_cor = results['Spearman.cor'] - assert 'Spearman.cor' in results + +# Test Case 8: 'yobs' values outside the range of 'ypred' +def test_yobs_outside_ypred_range(): + ypred = np.random.rand(1000) + yobs = np.array([1.5, 2.0, 2.5]) # Values outside the range [0, 1] + results = continuous_boyce_index(yobs, ypred, nbins=10, to_plot=False) + assert "Spearman.cor" in results + spearman_cor = results["Spearman.cor"] + # Spearman correlation may be NaN due to insufficient valid data assert np.isnan(spearman_cor) or -1 <= spearman_cor <= 1 - f_ratio = results['F.ratio'] + f_ratio = results["F.ratio"] assert len(f_ratio) == 10 -# Test Case 10: Large dataset + +# Test Case 9: Large dataset def test_large_dataset(): - fit = np.random.rand(1000000) - obs = np.random.choice(fit, size=10000, replace=False) - results = boyce_index(fit, obs, nclass=20, PEplot=False) - assert 'Spearman.cor' in results - spearman_cor = results['Spearman.cor'] + ypred = np.random.rand(1000000) + yobs = np.random.choice(ypred, size=10000, replace=False) + results = continuous_boyce_index(yobs, ypred, nbins=20, to_plot=False) + assert "Spearman.cor" in results + spearman_cor = results["Spearman.cor"] assert not np.isnan(spearman_cor) assert -1 <= spearman_cor <= 1 - f_ratio = results['F.ratio'] + f_ratio = results["F.ratio"] assert len(f_ratio) == 20 -# Test Case 11: Using Pandas Series +# Test Case 10: Using Pandas Series def test_with_pandas_series(): np.random.seed(0) - fit = pd.Series(np.random.rand(1000)) - obs = fit.sample(n=100, replace=False) - results = boyce_index(fit, obs, nclass=10, PEplot=False) - assert 'Spearman.cor' in results - spearman_cor = results['Spearman.cor'] + ypred = pd.Series(np.random.rand(1000)) + yobs = ypred.sample(n=100, replace=False) + results = continuous_boyce_index(yobs, ypred, nbins=10, to_plot=False) + assert "Spearman.cor" in results + spearman_cor = results["Spearman.cor"] assert not np.isnan(spearman_cor) assert -1 <= spearman_cor <= 1 -# Test Case 12: Using GeoPandas GeoSeries + +# Test Case 11: Using GeoPandas GeoSeries def test_with_geopandas_geoseries(): np.random.seed(0) num_points = 1000 @@ -139,12 +146,41 @@ def test_with_geopandas_geoseries(): y = np.random.uniform(-90, 90, num_points) suitability = np.random.rand(num_points) geometry = [Point(xy) for xy in zip(x, y)] - gdf = gpd.GeoDataFrame({'suitability': suitability}, geometry=geometry) - - fit = gdf['suitability'] # This is a Pandas Series - obs = fit.sample(n=100, replace=False) - results = boyce_index(fit, obs, nclass=10, PEplot=False) - assert 'Spearman.cor' in results - spearman_cor = results['Spearman.cor'] + gdf = gpd.GeoDataFrame({"suitability": suitability}, geometry=geometry) + + ypred = gdf["suitability"] # This is a Pandas Series + yobs = ypred.sample(n=100, replace=False) + results = continuous_boyce_index(yobs, ypred, nbins=10, to_plot=False) + assert "Spearman.cor" in results + spearman_cor = results["Spearman.cor"] assert not np.isnan(spearman_cor) - assert -1 <= spearman_cor <= 1 \ No newline at end of file + assert -1 <= spearman_cor <= 1 + + +# Test Case 12: Both 'ypred' and 'yobs' containing NaNs +def test_both_ypred_yobs_with_nans(recwarn): + ypred = np.random.rand(1000) + ypred[::50] = np.nan # Introduce NaNs into 'ypred' + yobs = np.random.choice(ypred, size=100, replace=False) + yobs[::10] = np.nan # Introduce NaNs into 'yobs' + results = continuous_boyce_index(yobs, ypred, nbins=10, to_plot=False) + # Check for multiple warnings + warnings_list = [str(w.message) for w in recwarn.list] + assert "'ypred' contains NaN values, which will be ignored." in warnings_list + assert "'yobs' contains NaN values, which will be ignored." in warnings_list + # Ensure function outputs are as expected + assert "Spearman.cor" in results + spearman_cor = results["Spearman.cor"] + if not np.isnan(spearman_cor): + assert -1 <= spearman_cor <= 1 + f_ratio = results["F.ratio"] + assert len(f_ratio) == 10 + + +# Test Case 13: Empty arrays after removing NaNs +def test_empty_arrays_after_nan_removal(): + ypred = np.array([np.nan, np.nan]) + yobs = np.array([np.nan, np.nan]) + with pytest.raises(ValueError) as exc_info: + continuous_boyce_index(yobs, ypred, nbins=10, to_plot=False) + assert "'ypred' or 'yobs' arrays cannot be empty." in str(exc_info.value) From 6634ca32f5e7232b3a1dd16adf13d60c75cc26d6 Mon Sep 17 00:00:00 2001 From: Pankaj Date: Sat, 10 May 2025 14:03:44 -0400 Subject: [PATCH 5/6] updated evaluate.py as per comments asked --- elapid/evaluate.py | 181 +++++++++++++++++++++------------------------ 1 file changed, 84 insertions(+), 97 deletions(-) diff --git a/elapid/evaluate.py b/elapid/evaluate.py index 1d233ae..7d67326 100644 --- a/elapid/evaluate.py +++ b/elapid/evaluate.py @@ -8,8 +8,6 @@ import pandas as pd from scipy.stats import spearmanr -# implement continuous Boyce index as describe in https://www.whoi.edu/cms/files/hirzel_etal_2006_53457.pdf (Eq.4) - def plot_PE_curve(x: Union[np.ndarray, List[float]], y: Union[np.ndarray, List[float]]) -> None: """ @@ -31,148 +29,143 @@ def plot_PE_curve(x: Union[np.ndarray, List[float]], y: Union[np.ndarray, List[f def get_intervals( - nbins: Union[int, List[float], np.ndarray] = 0, - bin_size: Union[float, str] = "default", - range: Tuple[float, float] = (0, 1), + bins: Union[int, float, List[float], np.ndarray, str] = "default", + value_range: Tuple[float, float] = (0, 1), ) -> np.ndarray: """ - Generates habitat suitability intervals for the Boyce Index calculation. - - Calculates intervals based on the provided range and either the number of bins or bin size. - + Generates habitat suitability intervals based on a 'bins' parameter. Args: - nbins (int or list or np.ndarray, optional): Number of classes or a list of class thresholds. Defaults to 0. - bin_size (float or str, optional): Width of the bins. Defaults to 'default', which sets the width as 1/10th of the range. - range (tuple or list): Two elements representing the minimum and maximum values of habitat suitability. Default : [0, 1] + bins (int | float | list | np.ndarray | str, optional): Defines the binning strategy. + - float: Bin width. Number of bins will be computed as ceil(range / width). + - int: Number of bins. + - list or ndarray: Custom bin edges. Must contain at least 2 values. + - "default": Uses 10 equally spaced bins. Default is "default". + value_range (tuple): The (min, max) range over which to create intervals. Default is (0, 1). Returns: - np.ndarray: An array of intervals, each represented by a pair of lower and upper bounds. - - Raises: - ValueError: If invalid values are provided for nbins or bin_size. + np.ndarray: An (N, 2) array of bin intervals, where each row is (lower_bound, upper_bound). N = total number of bins. """ - mini, maxi = range + mini, maxi = value_range - if isinstance(bin_size, float): - nbins = (maxi - mini) / bin_size - if not nbins.is_integer(): + if isinstance(bins, float): + div_result = (maxi - mini) / bins + nbins = int(np.ceil(div_result)) + if not np.isclose(div_result, nbins): warnings.warn( - f"bin_size has been adjusted to nearest appropriate size using ceil, as range/bin_size : {(maxi - mini)} / {bin_size} is not an integer.", + f"Bin size results in a non-integer number of bins. Using ceil: {div_result} -> {nbins}", UserWarning, ) - nbins = math.ceil(nbins) - elif nbins == 0 and bin_size == "default": - nbins = 10 - - if isinstance(nbins, (list, np.ndarray)): - if len(nbins) == 1: - raise ValueError("Invalid nbins value. len(nbins) must be > 1") - nbins.sort() - intervals = np.column_stack((nbins[:-1], nbins[1:])) - elif nbins > 1: - boundary = np.linspace(mini, maxi, num=nbins + 1) - intervals = np.column_stack((boundary[:-1], boundary[1:])) + boundaries = np.linspace(mini, maxi, nbins + 1) + intervals = np.column_stack((boundaries[:-1], boundaries[1:])) + elif isinstance(bins, (list, np.ndarray)): + bins = np.sort(np.array(bins)) + if len(bins) < 2: + raise ValueError("bins list must have at least two elements.") + intervals = np.column_stack((bins[:-1], bins[1:])) + elif isinstance(bins, int) and bins > 1: + boundaries = np.linspace(mini, maxi, bins + 1) + intervals = np.column_stack((boundaries[:-1], boundaries[1:])) + elif bins == "default": + boundaries = np.linspace(mini, maxi, 11) + intervals = np.column_stack((boundaries[:-1], boundaries[1:])) else: - raise ValueError("Invalid nbins value. nbins > 1") + raise ValueError("Invalid `bins` value. Must be a float (width), int (count), or list of edges.") return intervals -def boyce_index(yobs: np.ndarray, ypred: np.ndarray, interval: Union[Tuple[float, float], List[float]]) -> float: +def boyce_index( + ypred_observed: np.ndarray, ypred_background: np.ndarray, interval: Union[Tuple[float, float], List[float]] +) -> float: """ Calculates the Boyce index for a given interval. Uses the convention as defined in Hirzel et al. 2006 to compute the ratio of observed to expected frequencies. Args: - yobs (np.ndarray): Suitability values at observed locations (e.g., predictions at presence points). - ypred (np.ndarray): Suitability values at random locations (e.g., predictions at background points). + ypred_observed (np.ndarray): Suitability values at observed locations (e.g., predictions at presence points). + ypred_background (np.ndarray): Suitability values at random locations (e.g., predictions at background points). interval (tuple or list): Two elements representing the lower and upper bounds of the interval (i.e., habitat suitability). Returns: float: The ratio of observed to expected frequencies for the given interval. - """ - yobs_bin = (yobs >= interval[0]) & (yobs <= interval[1]) - ypred_bin = (ypred >= interval[0]) & (ypred <= interval[1]) + lower, upper = interval + yobs_bin = (ypred_observed >= lower) & (ypred_observed < upper) + ypred_bin = (ypred_background >= lower) & (ypred_background < upper) - pi = np.sum(yobs_bin) / len(yobs_bin) - ei = np.sum(ypred_bin) / len(ypred_bin) + # Include upper edge for last interval + if np.isclose(upper, np.max(ypred_background)): + yobs_bin |= ypred_observed == upper + ypred_bin |= ypred_background == upper - if ei == 0: - fi = np.nan # Avoid division by zero - else: - fi = pi / ei + pi = np.sum(yobs_bin) / len(ypred_observed) + ei = np.sum(ypred_bin) / len(ypred_background) - return fi + return np.nan if ei == 0 else pi / ei def continuous_boyce_index( - yobs: Union[np.ndarray, pd.Series, gpd.GeoSeries], - ypred: Union[np.ndarray, pd.Series, gpd.GeoSeries], - nbins: Union[int, List[float], np.ndarray] = 0, - bin_size: Union[float, str] = "default", + ypred_observed: Union[np.ndarray, pd.Series, gpd.GeoSeries], + ypred_background: Union[np.ndarray, pd.Series, gpd.GeoSeries], + bins: Union[int, float, List[float], np.ndarray, str] = "default", to_plot: bool = False, -) -> Dict[str, Union[np.ndarray, float]]: +) -> Tuple[np.ndarray, float, np.ndarray]: """ Compute the continuous Boyce index to evaluate habitat suitability models. Uses the convention as defined in Hirzel et al. 2006 to compute the ratio of observed to expected frequencies. Args: - yobs (numpy.ndarray | pd.Series | gpd.GeoSeries): Suitability values at observed location (i.e., predictions at presence points). - ypred (numpy.ndarray | pd.Series | gpd.GeoSeries): Suitability values at random location (i.e., predictions at background points). - nbins (int | list, optional): Number of classes or a list of class thresholds. Defaults to 0. - bin_size (float | str, optional): Width of the the bin. Defaults to 'default' which sets width as 1/10th of the fit range. + ypred_observed (numpy.ndarray | pd.Series | gpd.GeoSeries): Suitability values at observed locations (i.e., presence points). + ypred_background (numpy.ndarray | pd.Series | gpd.GeoSeries): Suitability values at random/background locations. + bins (int | float | list | np.ndarray | str, optional): Defines the binning strategy: + - int: number of bins + - float: bin width + - list/ndarray: custom bin edges + - 'default': 10 equally spaced bins over the prediction range to_plot (bool, optional): Whether to plot the predicted-to-expected (P/E) curve. Defaults to False. Returns: - dict: A dictionary with the following keys: - - 'F.ratio' (numpy.ndarray): The P/E ratio for each bin. - - 'Spearman.cor' (float): The Spearman's rank correlation coefficient between interval midpoints and F ratios. - - 'HS' (numpy.ndarray): The habitat suitability intervals. + Tuple: + - f_scores (numpy.ndarray): The Boyce index scores for each interval. + - corr (float): Spearman correlation coefficient between the P/E ratios and the midpoints of the intervals. + - intervals (numpy.ndarray): The intervals used for the Boyce index calculation. """ - if not isinstance(ypred, (np.ndarray, pd.Series, gpd.GeoSeries)): - raise TypeError("The 'ypred' parameter must be a NumPy array, Pandas Series, or GeoPandas GeoSeries.") - if not isinstance(yobs, (np.ndarray, pd.Series, gpd.GeoSeries)): - raise TypeError("The 'yobs' parameter must be a NumPy array, Pandas Series, or GeoPandas GeoSeries.") - if not isinstance(nbins, (int, list, np.ndarray)): - raise TypeError("The 'nbins' parameter must be a int, list, or 1-d NumPy array.") - if not isinstance(bin_size, (float, str)): - raise TypeError("The 'bin_size' parameter must be a float, or str ('default').") - - if isinstance(bin_size, float) and (isinstance(nbins, (list, np.ndarray)) or nbins > 0): - raise ValueError( - f"Ambiguous value provided. Provide either nbins or bin_size. Cannot provide both. Provided values for nbins, bin_size are: ({nbins, bin_size})" + if not isinstance(ypred_background, (np.ndarray, pd.Series, gpd.GeoSeries)): + raise TypeError( + "The 'ypred_background' parameter must be a NumPy array, Pandas Series, or GeoPandas GeoSeries." ) + if not isinstance(ypred_observed, (np.ndarray, pd.Series, gpd.GeoSeries)): + raise TypeError("The 'ypred_observed' parameter must be a NumPy array, Pandas Series, or GeoPandas GeoSeries.") - # Check for NaN values and issue warnings - if np.isnan(ypred).any(): - warnings.warn("'ypred' contains NaN values, which will be ignored.", UserWarning) - ypred = ypred[~np.isnan(ypred)] - if np.isnan(yobs).any(): - warnings.warn("'yobs' contains NaN values, which will be ignored.", UserWarning) - yobs = yobs[~np.isnan(yobs)] - - ypred = np.asarray(ypred) - yobs = np.asarray(yobs) + # Remove NaNs + if np.isnan(ypred_background).any(): + warnings.warn("'ypred_background' contains NaN values, which will be ignored.", UserWarning) + ypred_background = ypred_background[~np.isnan(ypred_background)] + if np.isnan(ypred_observed).any(): + warnings.warn("'ypred_observed' contains NaN values, which will be ignored.", UserWarning) + ypred_observed = ypred_observed[~np.isnan(ypred_observed)] - if ypred.ndim != 1 or yobs.ndim != 1: - raise ValueError("Both 'ypred' and 'yobs' must be one-dimensional arrays.") + ypred_background = np.asarray(ypred_background) + ypred_observed = np.asarray(ypred_observed) - if len(ypred) == 0 or len(yobs) == 0: - raise ValueError("'ypred' or 'yobs' arrays cannot be empty.") + if ypred_background.ndim != 1 or len(ypred_background) == 0: + raise ValueError("'ypred_background' must be a non-empty one-dimensional array.") + if ypred_observed.ndim != 1 or len(ypred_observed) == 0: + raise ValueError("'ypred_observed' must be a non-empty one-dimensional array.") - mini, maxi = np.min(ypred), np.max(ypred) + mini, maxi = np.min(ypred_background), np.max(ypred_background) + intervals = get_intervals(bins, value_range=(mini, maxi)) - intervals = get_intervals(nbins, bin_size, range=[mini, maxi]) - f_scores = np.array([boyce_index(yobs, ypred, interval) for interval in intervals]) + f_scores = np.array([boyce_index(ypred_observed, ypred_background, interval) for interval in intervals]) valid = ~np.isnan(f_scores) f_valid = f_scores[valid] - intervals_mid = np.mean(intervals[valid], axis=1) + if np.sum(valid) <= 2: + warnings.warn("Not enough valid intervals to compute Spearman correlation.", UserWarning) corr = np.nan else: corr, _ = spearmanr(f_valid, intervals_mid) @@ -180,10 +173,4 @@ def continuous_boyce_index( if to_plot: plot_PE_curve(x=intervals_mid, y=f_valid) - results = { - "F.ratio": f_scores, - "Spearman.cor": corr, - "HS": intervals, - } - - return results + return f_scores, corr, intervals From 9c8d31d58b1732fec9a9b7467a327ff49b87ffea Mon Sep 17 00:00:00 2001 From: Pankaj Date: Sat, 10 May 2025 14:36:03 -0400 Subject: [PATCH 6/6] updated test-cases --- tests/test_evaluate.py | 263 +++++++++++++++++------------------------ 1 file changed, 108 insertions(+), 155 deletions(-) diff --git a/tests/test_evaluate.py b/tests/test_evaluate.py index 69e481c..dd94b37 100644 --- a/tests/test_evaluate.py +++ b/tests/test_evaluate.py @@ -1,186 +1,139 @@ import geopandas as gpd -import matplotlib.pyplot as plt import numpy as np import pandas as pd import pytest from shapely.geometry import Point -from elapid.evaluate import boyce_index, continuous_boyce_index +from elapid.evaluate import boyce_index, continuous_boyce_index, get_intervals # Test Case 1: Normal case with random data def test_normal_case(): np.random.seed(0) - ypred = np.random.rand(1000) - yobs = np.random.choice(ypred, size=100, replace=False) - results = continuous_boyce_index(yobs, ypred, nbins=10, to_plot=False) - assert "Spearman.cor" in results - assert "F.ratio" in results - spearman_cor = results["Spearman.cor"] - f_ratio = results["F.ratio"] - assert not np.isnan(spearman_cor) - assert -1 <= spearman_cor <= 1 - assert len(f_ratio) == 10 - assert not np.any(np.isnan(f_ratio)) - assert np.all(f_ratio >= 0) - - -# Test Case 2: Edge case with empty 'ypred' array -def test_empty_ypred(): - ypred = np.array([]) - yobs = np.array([0.5, 0.6, 0.7]) + ypred_background = np.random.rand(1000) + ypred_observed = np.random.choice(ypred_background, size=100, replace=False) + f_scores, corr, intervals = continuous_boyce_index(ypred_observed, ypred_background, bins=10, to_plot=False) + # Check outputs + assert isinstance(f_scores, np.ndarray) + assert isinstance(corr, float) + assert isinstance(intervals, np.ndarray) + assert len(f_scores) == 10 + assert not np.any(np.isnan(f_scores)) + assert -1 <= corr <= 1 + + +# Test Case 2: Empty background array +def test_empty_background(): + ypred_background = np.array([]) + ypred_observed = np.array([0.5, 0.6, 0.7]) with pytest.raises(ValueError) as exc_info: - continuous_boyce_index(yobs, ypred, nbins=10, to_plot=False) - assert "'ypred' or 'yobs' arrays cannot be empty." in str(exc_info.value) + continuous_boyce_index(ypred_observed, ypred_background, bins=10) + assert "'ypred_background' must be a non-empty one-dimensional array." in str(exc_info.value) -# Test Case 3: Edge case with empty 'yobs' array -def test_empty_yobs(): - ypred = np.random.rand(1000) - yobs = np.array([]) +# Test Case 3: Empty observed array +def test_empty_observed(): + ypred_background = np.random.rand(1000) + ypred_observed = np.array([]) with pytest.raises(ValueError) as exc_info: - continuous_boyce_index(yobs, ypred, nbins=10, to_plot=False) - assert "'ypred' or 'yobs' arrays cannot be empty." in str(exc_info.value) + continuous_boyce_index(ypred_observed, ypred_background, bins=10) + assert "'ypred_observed' must be a non-empty one-dimensional array." in str(exc_info.value) -# Test Case 4: 'yobs' containing NaNs -def test_yobs_with_nans(recwarn): - ypred = np.random.rand(1000) - yobs = np.random.choice(ypred, size=100, replace=False) - yobs[::10] = np.nan # Introduce NaNs into 'yobs' - results = continuous_boyce_index(yobs, ypred, nbins=10, to_plot=False) - # Check for warnings +# Test Case 4: Observed containing NaNs +def test_observed_with_nans(recwarn): + ypred_background = np.random.rand(1000) + ypred_observed = np.random.choice(ypred_background, size=100, replace=False) + ypred_observed[::10] = np.nan + f_scores, corr, intervals = continuous_boyce_index(ypred_observed, ypred_background, bins=10) + # warning should be issued w = recwarn.pop(UserWarning) - assert "'yobs' contains NaN values, which will be ignored." in str(w.message) - # Ensure function outputs are as expected - assert "Spearman.cor" in results - spearman_cor = results["Spearman.cor"] - if not np.isnan(spearman_cor): - assert -1 <= spearman_cor <= 1 - f_ratio = results["F.ratio"] - assert len(f_ratio) == 10 - - -# Test Case 5: Invalid 'nbins' value (negative number) -def test_invalid_nbins(): - ypred = np.random.rand(1000) - yobs = np.random.choice(ypred, size=100, replace=False) + assert "'ypred_observed' contains NaN values, which will be ignored." in str(w.message) + # outputs still valid + assert len(f_scores) == 10 + assert np.isnan(corr) or -1 <= corr <= 1 + + +# Test Case 5: Invalid bins value +def test_invalid_bins(): + ypred_background = np.random.rand(1000) + ypred_observed = np.random.choice(ypred_background, size=100, replace=False) with pytest.raises(ValueError): - continuous_boyce_index(yobs, ypred, nbins=-5, to_plot=False) - - -# Test Case 6: Custom 'bin_size' value -def test_custom_bin_size(): - ypred = np.random.rand(1000) - yobs = np.random.choice(ypred, size=100, replace=False) - results = continuous_boyce_index(yobs, ypred, bin_size=0.1, to_plot=False) - assert "Spearman.cor" in results - spearman_cor = results["Spearman.cor"] - assert not np.isnan(spearman_cor) - assert -1 <= spearman_cor <= 1 - f_ratio = results["F.ratio"] - assert len(f_ratio) > 0 - - -# Test Case 7: 'ypred' containing NaNs -def test_ypred_with_nans(recwarn): - ypred = np.random.rand(1000) - ypred[::50] = np.nan # Introduce NaNs into 'ypred' - yobs = np.random.choice(ypred[~np.isnan(ypred)], size=100, replace=False) - results = continuous_boyce_index(yobs, ypred, nbins=10, to_plot=False) - # Check for warnings + continuous_boyce_index(ypred_observed, ypred_background, bins=1) + + +# Test Case 6: Custom float bin width +def test_custom_bin_width(): + ypred_background = np.random.rand(1000) + ypred_observed = np.random.choice(ypred_background, size=100, replace=False) + f_scores, corr, intervals = continuous_boyce_index(ypred_observed, ypred_background, bins=0.1) + # number of bins should be ceil(range/0.1) + expected_nbins = int(np.ceil((ypred_background.max() - ypred_background.min()) / 0.1)) + assert len(f_scores) == expected_nbins + assert -1 <= corr <= 1 or np.isnan(corr) + + +# Test Case 7: Background containing NaNs +def test_background_with_nans(recwarn): + ypred_background = np.random.rand(1000) + ypred_background[::50] = np.nan + ypred_observed = np.random.choice(ypred_background[~np.isnan(ypred_background)], size=100, replace=False) + f_scores, corr, intervals = continuous_boyce_index(ypred_observed, ypred_background, bins=10) w = recwarn.pop(UserWarning) - assert "'ypred' contains NaN values, which will be ignored." in str(w.message) - # Ensure function outputs are as expected - assert "Spearman.cor" in results - spearman_cor = results["Spearman.cor"] - assert not np.isnan(spearman_cor) - assert -1 <= spearman_cor <= 1 - f_ratio = results["F.ratio"] - assert len(f_ratio) == 10 - - -# Test Case 8: 'yobs' values outside the range of 'ypred' -def test_yobs_outside_ypred_range(): - ypred = np.random.rand(1000) - yobs = np.array([1.5, 2.0, 2.5]) # Values outside the range [0, 1] - results = continuous_boyce_index(yobs, ypred, nbins=10, to_plot=False) - assert "Spearman.cor" in results - spearman_cor = results["Spearman.cor"] - # Spearman correlation may be NaN due to insufficient valid data - assert np.isnan(spearman_cor) or -1 <= spearman_cor <= 1 - f_ratio = results["F.ratio"] - assert len(f_ratio) == 10 - - -# Test Case 9: Large dataset + assert "'ypred_background' contains NaN values, which will be ignored." in str(w.message) + assert len(f_scores) == 10 + + +# Test Case 8: Observed outside background range +def test_observed_outside_range(): + ypred_background = np.random.rand(1000) + ypred_observed = np.array([ypred_background.max() + 0.5, ypred_background.max() + 1.0]) + f_scores, corr, intervals = continuous_boyce_index(ypred_observed, ypred_background, bins=10) + assert len(f_scores) == 10 + # all f_scores should be zero or nan + assert all((np.isnan(f) or f == 0) for f in f_scores) + assert np.isnan(corr) or -1 <= corr <= 1 + + +# Test Case 9: Large dataset performance def test_large_dataset(): - ypred = np.random.rand(1000000) - yobs = np.random.choice(ypred, size=10000, replace=False) - results = continuous_boyce_index(yobs, ypred, nbins=20, to_plot=False) - assert "Spearman.cor" in results - spearman_cor = results["Spearman.cor"] - assert not np.isnan(spearman_cor) - assert -1 <= spearman_cor <= 1 - f_ratio = results["F.ratio"] - assert len(f_ratio) == 20 - - -# Test Case 10: Using Pandas Series + ypred_background = np.random.rand(1000000) + ypred_observed = np.random.choice(ypred_background, size=10000, replace=False) + f_scores, corr, intervals = continuous_boyce_index(ypred_observed, ypred_background, bins=20) + assert len(f_scores) == 20 + assert -1 <= corr <= 1 + + +# Test Case 10: Pandas Series inputs def test_with_pandas_series(): np.random.seed(0) - ypred = pd.Series(np.random.rand(1000)) - yobs = ypred.sample(n=100, replace=False) - results = continuous_boyce_index(yobs, ypred, nbins=10, to_plot=False) - assert "Spearman.cor" in results - spearman_cor = results["Spearman.cor"] - assert not np.isnan(spearman_cor) - assert -1 <= spearman_cor <= 1 + ypred_background = pd.Series(np.random.rand(1000)) + ypred_observed = ypred_background.sample(n=100) + f_scores, corr, intervals = continuous_boyce_index(ypred_observed, ypred_background, bins=10) + assert len(f_scores) == 10 + assert -1 <= corr <= 1 -# Test Case 11: Using GeoPandas GeoSeries +# Test Case 11: GeoPandas GeoSeries inputs def test_with_geopandas_geoseries(): np.random.seed(0) - num_points = 1000 - x = np.random.uniform(-180, 180, num_points) - y = np.random.uniform(-90, 90, num_points) + num_points = 500 + coords = np.random.rand(num_points, 2) suitability = np.random.rand(num_points) - geometry = [Point(xy) for xy in zip(x, y)] + geometry = [Point(xy) for xy in coords] gdf = gpd.GeoDataFrame({"suitability": suitability}, geometry=geometry) + ypred_background = gdf["suitability"] + ypred_observed = ypred_background.sample(n=50) + f_scores, corr, intervals = continuous_boyce_index(ypred_observed, ypred_background, bins=10) + assert len(f_scores) == 10 + assert -1 <= corr <= 1 + - ypred = gdf["suitability"] # This is a Pandas Series - yobs = ypred.sample(n=100, replace=False) - results = continuous_boyce_index(yobs, ypred, nbins=10, to_plot=False) - assert "Spearman.cor" in results - spearman_cor = results["Spearman.cor"] - assert not np.isnan(spearman_cor) - assert -1 <= spearman_cor <= 1 - - -# Test Case 12: Both 'ypred' and 'yobs' containing NaNs -def test_both_ypred_yobs_with_nans(recwarn): - ypred = np.random.rand(1000) - ypred[::50] = np.nan # Introduce NaNs into 'ypred' - yobs = np.random.choice(ypred, size=100, replace=False) - yobs[::10] = np.nan # Introduce NaNs into 'yobs' - results = continuous_boyce_index(yobs, ypred, nbins=10, to_plot=False) - # Check for multiple warnings - warnings_list = [str(w.message) for w in recwarn.list] - assert "'ypred' contains NaN values, which will be ignored." in warnings_list - assert "'yobs' contains NaN values, which will be ignored." in warnings_list - # Ensure function outputs are as expected - assert "Spearman.cor" in results - spearman_cor = results["Spearman.cor"] - if not np.isnan(spearman_cor): - assert -1 <= spearman_cor <= 1 - f_ratio = results["F.ratio"] - assert len(f_ratio) == 10 - - -# Test Case 13: Empty arrays after removing NaNs -def test_empty_arrays_after_nan_removal(): - ypred = np.array([np.nan, np.nan]) - yobs = np.array([np.nan, np.nan]) +# Test Case 12: Both with NaNs leading to empty after removal +def test_empty_after_nan_removal(): + ypred_background = np.array([np.nan, np.nan]) + ypred_observed = np.array([np.nan, np.nan]) with pytest.raises(ValueError) as exc_info: - continuous_boyce_index(yobs, ypred, nbins=10, to_plot=False) - assert "'ypred' or 'yobs' arrays cannot be empty." in str(exc_info.value) + continuous_boyce_index(ypred_observed, ypred_background, bins=10) + # after dropping NaNs, arrays empty + assert "must be a non-empty one-dimensional array" in str(exc_info.value)