PaleoVeg 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of PaleoVeg might be problematic. Click here for more details.

paleoveg-0.1.0/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2024 Philipp Schlüter
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1 @@
1
+ exclude paleoveg/model.pkl.bz2
@@ -0,0 +1,16 @@
1
+ Metadata-Version: 2.1
2
+ Name: PaleoVeg
3
+ Version: 0.1.0
4
+ Summary: Predict paleovegetation from bioclimatic variables.
5
+ Home-page: https://github.com/roink/PaleoVeg
6
+ Author: Philipp Schlüter
7
+ Author-email: p.schlueter@uni-koeln.de
8
+ Requires-Python: >=3.7
9
+ License-File: LICENSE
10
+ Requires-Dist: scikit-learn>=1.5.2
11
+ Requires-Dist: numpy>=2.1.3
12
+ Requires-Dist: pandas>=2.2.3
13
+ Requires-Dist: joblib
14
+ Requires-Dist: matplotlib
15
+ Requires-Dist: cartopy
16
+ Requires-Dist: requests
@@ -0,0 +1,16 @@
1
+ Metadata-Version: 2.1
2
+ Name: PaleoVeg
3
+ Version: 0.1.0
4
+ Summary: Predict paleovegetation from bioclimatic variables.
5
+ Home-page: https://github.com/roink/PaleoVeg
6
+ Author: Philipp Schlüter
7
+ Author-email: p.schlueter@uni-koeln.de
8
+ Requires-Python: >=3.7
9
+ License-File: LICENSE
10
+ Requires-Dist: scikit-learn>=1.5.2
11
+ Requires-Dist: numpy>=2.1.3
12
+ Requires-Dist: pandas>=2.2.3
13
+ Requires-Dist: joblib
14
+ Requires-Dist: matplotlib
15
+ Requires-Dist: cartopy
16
+ Requires-Dist: requests
@@ -0,0 +1,14 @@
1
+ LICENSE
2
+ MANIFEST.in
3
+ README.md
4
+ setup.py
5
+ PaleoVeg.egg-info/PKG-INFO
6
+ PaleoVeg.egg-info/SOURCES.txt
7
+ PaleoVeg.egg-info/dependency_links.txt
8
+ PaleoVeg.egg-info/requires.txt
9
+ PaleoVeg.egg-info/top_level.txt
10
+ paleoveg/__init__.py
11
+ paleoveg/model_loader.py
12
+ paleoveg/predictor.py
13
+ paleoveg/setup.py
14
+ tests/test_predictor.py
@@ -0,0 +1,7 @@
1
+ scikit-learn>=1.5.2
2
+ numpy>=2.1.3
3
+ pandas>=2.2.3
4
+ joblib
5
+ matplotlib
6
+ cartopy
7
+ requests
@@ -0,0 +1 @@
1
+ paleoveg
@@ -0,0 +1,195 @@
1
+ ---
2
+
3
+ # **PaleoVeg**
4
+
5
+ PaleoVeg is a Python package for predicting vegetation types based on bioclimatic variables. It is designed for paleovegetation reconstruction, allowing users to model vegetation based on past climate data or other environmental predictors.
6
+
7
+ ---
8
+
9
+ ## **Features**
10
+ - Predict vegetation types for single or multiple locations using bioclimatic variables.
11
+ - Apply optional C3/C4 competition corrections for higher CO2 levels.
12
+ - Flexible output options:
13
+ - Probabilities for each vegetation type.
14
+ - Dominant vegetation type.
15
+ - Supports input as:
16
+ - Pandas DataFrames for tabular data.
17
+ - Numpy arrays for map-like (grid-based) data.
18
+
19
+ ---
20
+
21
+ ## **Predicted Vegetation/Biome Types**
22
+
23
+ PaleoVeg predicts the following vegetation/biome types:
24
+
25
+ 1. Evergreen Needleleaf Forest
26
+ 2. Evergreen Broadleaf Forest
27
+ 3. Deciduous Needleleaf Forest
28
+ 4. Deciduous Broadleaf Forest
29
+ 5. Mixed Forest
30
+ 6. Woodland
31
+ 7. Wooded Grassland
32
+ 8. Closed Shrubland
33
+ 9. Open Shrubland
34
+ 10. Grassland
35
+ 11. Bare Ground
36
+
37
+ ---
38
+
39
+ ## **Installation**
40
+
41
+ 1. Clone the repository:
42
+ ```bash
43
+ git clone https://github.com/roink/PaleoVeg.git
44
+ cd PaleoVeg
45
+ ```
46
+
47
+ 2. Install the package:
48
+ ```bash
49
+ pip install .
50
+ ```
51
+
52
+ ---
53
+
54
+ ## **Usage**
55
+
56
+ ### **1. Import the Package**
57
+ ```python
58
+ from paleoveg import predict, predict_map
59
+ ```
60
+
61
+ ### **2. Predict Using Tabular Data**
62
+
63
+ #### **Input Data**
64
+ Provide a Pandas DataFrame with the following required columns:
65
+ ```plaintext
66
+ bio1, bio4, bio5, bio6, bio7, bio8, bio9, bio10, bio11, bio12, bio13, bio14, bio15, bio16, bio17, bio18, bio19
67
+ ```
68
+
69
+ #### **Example**
70
+ ```python
71
+ import pandas as pd
72
+ from paleoveg import predict
73
+
74
+ # Example input DataFrame
75
+ data = {
76
+ "bio1": [10.2, 12.5],
77
+ "bio4": [15.6, 14.1],
78
+ "bio5": [20.1, 18.4],
79
+ "bio6": [5.3, 7.2],
80
+ "bio7": [14.8, 16.5],
81
+ "bio8": [12.4, 11.8],
82
+ "bio9": [9.7, 8.9],
83
+ "bio10": [8.5, 9.2],
84
+ "bio11": [7.9, 6.5],
85
+ "bio12": [300.5, 310.2],
86
+ "bio13": [100.2, 102.4],
87
+ "bio14": [150.4, 148.3],
88
+ "bio15": [180.6, 175.9],
89
+ "bio16": [170.3, 165.2],
90
+ "bio17": [160.9, 155.8],
91
+ "bio18": [140.7, 145.4],
92
+ "bio19": [130.8, 128.7],
93
+ }
94
+
95
+ df = pd.DataFrame(data)
96
+
97
+ # Predict probabilities
98
+ probabilities = predict(df, dominant="exclude", c3_c4_correction=False)
99
+
100
+ # Predict dominant vegetation type
101
+ dominant_class = predict(df, dominant="only", c3_c4_correction=True)
102
+
103
+ # Include probabilities and dominant type
104
+ result = predict(df, dominant="include", c3_c4_correction=True)
105
+ ```
106
+
107
+ ---
108
+
109
+ ### **3. Predict Using Map-Like Data**
110
+
111
+ #### **Input Data**
112
+ Provide a Numpy array with shape `(m, n, 17)` where:
113
+ - `m` and `n` represent the grid dimensions.
114
+ - `17` represents the bioclimatic variables in the required order.
115
+
116
+ #### **Example**
117
+ ```python
118
+ import numpy as np
119
+ from paleoveg import predict_map
120
+
121
+ # Example 3D data: m = 5, n = 5, 17 predictors
122
+ m, n = 5, 5
123
+ data = np.random.rand(m, n, 17)
124
+
125
+ # Predict probabilities (m x n x 11)
126
+ probabilities_map = predict_map(data, dominant="exclude", c3_c4_correction=False)
127
+
128
+ # Predict dominant type (m x n x 1)
129
+ dominant_map = predict_map(data, dominant="only", c3_c4_correction=True)
130
+ ```
131
+
132
+ ---
133
+
134
+ ## **Functions**
135
+
136
+ ### **1. `predict(input_df, dominant="exclude", c3_c4_correction=False)`**
137
+ Predict vegetation types for tabular data.
138
+
139
+ #### **Parameters**:
140
+ - `input_df` (pd.DataFrame): Input DataFrame with 17 bioclimatic variables.
141
+ - `dominant` (str): Specifies the output format.
142
+ - `"exclude"`: Returns probabilities for each vegetation type.
143
+ - `"include"`: Returns probabilities and the dominant type.
144
+ - `"only"`: Returns only the dominant type.
145
+ - `c3_c4_correction` (bool): If `True`, applies C3/C4 competition correction.
146
+
147
+ #### **Returns**:
148
+ - Pandas DataFrame: Probabilities and/or dominant type based on `dominant`.
149
+
150
+ ---
151
+
152
+ ### **2. `predict_map(data, dominant="exclude", c3_c4_correction=False)`**
153
+ Predict vegetation types for 3D map-like data.
154
+
155
+ #### **Parameters**:
156
+ - `data` (np.ndarray): Input Numpy array with shape `(m, n, 17)`.
157
+ - `dominant` (str): Specifies the output format.
158
+ - `"exclude"`: Returns probabilities `(m x n x 11)`.
159
+ - `"only"`: Returns the dominant type `(m x n x 1)`.
160
+ - `c3_c4_correction` (bool): If `True`, applies C3/C4 competition correction.
161
+
162
+ #### **Returns**:
163
+ - Numpy array: Probabilities or dominant type based on `dominant`.
164
+
165
+ ---
166
+
167
+ ## **C3/C4 Competition Factors**
168
+ When `c3_c4_correction=True`, the following factors are applied to adjust probabilities for higher CO2 levels:
169
+
170
+ | Vegetation Type | Competition Factor |
171
+ |--------------------------------|--------------------|
172
+ | Evergreen Needleleaf Forest | 0.28 |
173
+ | Evergreen Broadleaf Forest | 0.73 |
174
+ | Deciduous Needleleaf Forest | 0.20 |
175
+ | Deciduous Broadleaf Forest | 0.76 |
176
+ | Mixed Forest | 1.00 |
177
+ | Closed Shrubland | 1.66 |
178
+ | Open Shrubland | 1.66 |
179
+ | Woodland | 1.62 |
180
+ | Wooded Grassland | 1.62 |
181
+ | Grassland | 1.69 |
182
+ | Bare Ground | 1.15 |
183
+
184
+ ---
185
+
186
+ ## **License**
187
+ This project is licensed under the MIT License.
188
+
189
+ ---
190
+
191
+ ## **Contributing**
192
+ Contributions are welcome! Please submit a pull request or open an issue if you have suggestions or find bugs.
193
+
194
+ ---
195
+
@@ -0,0 +1 @@
1
+ from .predictor import predict, predict_map
@@ -0,0 +1,44 @@
1
+ import os
2
+ import requests
3
+ import bz2
4
+ import pickle
5
+ from sklearn.ensemble import RandomForestClassifier
6
+
7
+ def download_model():
8
+ """Download the model file from GitHub Releases if it does not exist locally."""
9
+ model_path = os.path.join(os.path.dirname(__file__), "model.pkl.bz2")
10
+ if not os.path.exists(model_path):
11
+ url = "https://github.com/roink/PaleoVeg/releases/download/v0.1.0/model.pkl.bz2"
12
+ print(f"Downloading model from {url}...")
13
+ response = requests.get(url, stream=True)
14
+ if response.status_code == 200:
15
+ with open(model_path, "wb") as f:
16
+ f.write(response.content)
17
+ print("Download complete.")
18
+ else:
19
+ raise Exception(f"Failed to download model. HTTP Status: {response.status_code}")
20
+ return model_path
21
+
22
+ def load_model():
23
+ """
24
+ Load the compressed model and validate its type.
25
+
26
+ Returns:
27
+ model: The loaded Random Forest Classifier.
28
+
29
+ Raises:
30
+ ValueError: If the loaded model is not a RandomForestClassifier.
31
+ """
32
+ model_path = download_model() # Ensure the model is downloaded
33
+
34
+ # Load the model
35
+ with bz2.BZ2File(model_path, "rb") as f:
36
+ model = pickle.load(f)
37
+
38
+ # Validate the loaded model
39
+ if not isinstance(model, RandomForestClassifier):
40
+ raise ValueError(f"The loaded model is not a RandomForestClassifier. Loaded type: {type(model)}")
41
+
42
+ return model
43
+
44
+
@@ -0,0 +1,209 @@
1
+ import numpy as np
2
+ import pandas as pd
3
+ from .model_loader import load_model
4
+ import warnings
5
+
6
+ # Load the model once when the module is imported
7
+ _model = load_model()
8
+
9
+ # Define the required column names
10
+ REQUIRED_COLUMNS = [
11
+ "bio1", "bio4", "bio5", "bio6", "bio7", "bio8", "bio9", "bio10",
12
+ "bio11", "bio12", "bio13", "bio14", "bio15", "bio16", "bio17", "bio18", "bio19"
13
+ ]
14
+
15
+ # Define class names in the order used by the model
16
+ CLASS_NAMES = [
17
+ 'Evergreen Needleleaf Forest', 'Evergreen Broadleaf Forest',
18
+ 'Deciduous Needleleaf Forest', 'Deciduous Broadleaf Forest',
19
+ 'Mixed Forest', 'Woodland', 'Wooded Grassland', 'Closed Shrubland',
20
+ 'Open Shrubland', 'Grassland', 'Bare Ground'
21
+ ]
22
+
23
+ # Define the C3/C4 competition factors
24
+ COMPETITION_FACTORS = {
25
+ 'Evergreen Needleleaf Forest': 0.28,
26
+ 'Evergreen Broadleaf Forest': 0.73,
27
+ 'Deciduous Needleleaf Forest': 0.20,
28
+ 'Deciduous Broadleaf Forest': 0.76,
29
+ 'Mixed Forest': 1.00,
30
+ 'Closed Shrubland': 1.66,
31
+ 'Open Shrubland': 1.66,
32
+ 'Woodland': 1.62,
33
+ 'Wooded Grassland': 1.62,
34
+ 'Grassland': 1.69,
35
+ 'Bare Ground': 1.15,
36
+ }
37
+
38
+ # Min and max values for bioclimatic variables in the training set
39
+ BIOCLIM_RANGES = {
40
+ "bio1": (-54.72659, 30.94079),
41
+ "bio4": (0.0, 2361.791),
42
+ "bio5": (-29.69077, 48.16399),
43
+ "bio6": (-72.50053, 25.17328),
44
+ "bio7": (1.0, 72.02339),
45
+ "bio8": (-66.30019, 37.73918),
46
+ "bio9": (-54.83078, 37.51987),
47
+ "bio10": (-37.78683, 38.28006),
48
+ "bio11": (-66.33826, 29.06288),
49
+ "bio12": (0.0, 7953.816),
50
+ "bio13": (0.0, 2561.44),
51
+ "bio14": (0.0, 487.3488),
52
+ "bio15": (0.0, 229.0017),
53
+ "bio16": (0.0, 5597.44),
54
+ "bio17": (0.0, 1526.664),
55
+ "bio18": (0.0, 5190.888),
56
+ "bio19": (0.0, 4756.88),
57
+ }
58
+
59
+ def validate_ranges(input_df):
60
+ """
61
+ Validate that input values are within reasonable ranges.
62
+
63
+ Parameters:
64
+ input_df (pd.DataFrame): Input DataFrame.
65
+
66
+ Returns:
67
+ None: Issues warnings for out-of-range values.
68
+ """
69
+ for col, (min_val, max_val) in BIOCLIM_RANGES.items():
70
+ if col in input_df.columns:
71
+ out_of_range = (input_df[col] < min_val) | (input_df[col] > max_val)
72
+ if out_of_range.any():
73
+ warnings.warn(f"Column '{col}' contains values outside the range ({min_val}, {max_val}).")
74
+
75
+ def validate_map_ranges(data):
76
+ """
77
+ Validate that map input values are within reasonable ranges.
78
+
79
+ Parameters:
80
+ data (np.ndarray): 3D array (m x n x 17) of predictor values.
81
+
82
+ Returns:
83
+ None: Issues warnings for out-of-range values.
84
+ """
85
+ for i, (min_val, max_val) in enumerate(BIOCLIM_RANGES.values()):
86
+ out_of_range = (data[..., i] < min_val) | (data[..., i] > max_val)
87
+ if out_of_range.any():
88
+ warnings.warn(f"Layer {i} contains values outside the range ({min_val}, {max_val}).")
89
+
90
+
91
+ def validate_input(input_df):
92
+ """
93
+ Validate the input DataFrame for prediction.
94
+ Ensures it is a pandas DataFrame and contains the required columns.
95
+
96
+ Parameters:
97
+ input_df (pd.DataFrame): Input data for prediction.
98
+
99
+ Returns:
100
+ None
101
+
102
+ Raises:
103
+ ValueError: If input is not a DataFrame or required columns are missing.
104
+ """
105
+ if not isinstance(input_df, pd.DataFrame):
106
+ raise ValueError("Input must be a pandas DataFrame.")
107
+
108
+ missing_columns = [col for col in REQUIRED_COLUMNS if col not in input_df.columns]
109
+ if missing_columns:
110
+ raise ValueError(f"Missing required columns: {missing_columns}")
111
+
112
+ def predict(input_df, dominant="exclude", c3_c4_correction=False):
113
+ """
114
+ Predict vegetation based on bioclimatic features and return probabilities or dominant class.
115
+
116
+ Parameters:
117
+ input_df (pd.DataFrame): Bioclimatic variables for prediction.
118
+ dominant (str): One of "exclude", "include", or "only".
119
+ - "exclude": Return probabilities for each class.
120
+ - "include": Include probabilities and a column with the dominant class.
121
+ - "only": Return only the dominant class.
122
+ c3_c4_correction (bool): If True, apply C3/C4 competition correction.
123
+
124
+ Returns:
125
+ pd.DataFrame: DataFrame containing probabilities and/or dominant vegetation class.
126
+ """
127
+ # Validate input (assuming validate_input has already been defined)
128
+ validate_input(input_df)
129
+
130
+ # Use only the required columns for prediction
131
+ input_df = input_df[REQUIRED_COLUMNS]
132
+
133
+ # Check for out-of-range values
134
+ validate_ranges(input_df)
135
+
136
+ # Predict probabilities
137
+ probabilities = _model.predict_proba(input_df)
138
+
139
+ # Create a dataframe for the probabilities
140
+ prob_df = pd.DataFrame(probabilities, columns=CLASS_NAMES, index=input_df.index)
141
+
142
+ # Apply C3/C4 competition correction if enabled
143
+ if c3_c4_correction:
144
+ correction_factors = np.array([COMPETITION_FACTORS[class_name] for class_name in CLASS_NAMES])
145
+ prob_df = prob_df.multiply(correction_factors, axis=1)
146
+ prob_df = prob_df.div(prob_df.sum(axis=1), axis=0) # Normalize to sum to 1
147
+
148
+ if dominant == "exclude":
149
+ return prob_df
150
+ elif dominant == "include":
151
+ # Add a column with the dominant class
152
+ prob_df["Dominant Class"] = prob_df.idxmax(axis=1)
153
+ return prob_df
154
+ elif dominant == "only":
155
+ # Return only the dominant class
156
+ dominant_classes = prob_df.idxmax(axis=1)
157
+ return dominant_classes.to_frame(name="Dominant Class")
158
+ else:
159
+ raise ValueError("Invalid value for 'dominant'. Must be 'exclude', 'include', or 'only'.")
160
+
161
+
162
+ def predict_map(data, dominant="exclude", c3_c4_correction=False):
163
+ """
164
+ Predict vegetation for a 3D map input (m x n x 17) and return probabilities or dominant types.
165
+
166
+ Parameters:
167
+ data (numpy.ndarray): 3D array (m x n x 17) of predictor values.
168
+ dominant (str): One of "exclude" or "only".
169
+ - "exclude": Return probabilities for each class (m x n x 11).
170
+ - "only": Return the dominant type as integers (m x n x 1).
171
+ c3_c4_correction (bool): If True, apply C3/C4 competition correction.
172
+
173
+ Returns:
174
+ numpy.ndarray: m x n x 11 (probabilities) or m x n x 1 (dominant type).
175
+ """
176
+ if not isinstance(data, np.ndarray):
177
+ raise ValueError("Input data must be a numpy array.")
178
+
179
+ if data.ndim != 3 or data.shape[2] != 17:
180
+ raise ValueError("Input data must have shape (m x n x 17).")
181
+
182
+ validate_map_ranges(data)
183
+
184
+ # Reshape the 3D array into a 2D array (flatten grid cells)
185
+ m, n, _ = data.shape
186
+ flat_data = data.reshape(-1, 17)
187
+
188
+ # Predict probabilities
189
+ probabilities = _model.predict_proba(flat_data)
190
+
191
+ # Apply C3/C4 correction if enabled
192
+ if c3_c4_correction:
193
+ correction_factors = np.array([COMPETITION_FACTORS[class_name] for class_name in CLASS_NAMES])
194
+ probabilities *= correction_factors
195
+ probabilities /= probabilities.sum(axis=1, keepdims=True) # Normalize to sum to 1
196
+
197
+ # Reshape probabilities back to m x n x 11
198
+ probabilities_reshaped = probabilities.reshape(m, n, -1)
199
+
200
+ if dominant == "exclude":
201
+ # Return probabilities (m x n x 11)
202
+ return probabilities_reshaped
203
+ elif dominant == "only":
204
+ # Find dominant class (integer indices)
205
+ dominant_classes = probabilities.argmax(axis=1).reshape(m, n, 1)
206
+ return dominant_classes
207
+ else:
208
+ raise ValueError("Invalid value for 'dominant'. Must be 'exclude' or 'only'.")
209
+
@@ -0,0 +1,28 @@
1
+ from setuptools import setup, find_packages
2
+
3
+ setup(
4
+ name="PaleoVeg",
5
+ version="0.1.0",
6
+ description="Predict paleovegetation from bioclimatic variables.",
7
+ author="Philipp Schlüter",
8
+ author_email="p.schlueter@uni-koeln.de",
9
+ url="https://github.com/roink/PaleoVeg", # Replace with actual URL
10
+ packages=find_packages(),
11
+ include_package_data=True,
12
+ install_requires=[
13
+ "scikit-learn>=1.0",
14
+ "numpy>=1.21",
15
+ pandas,
16
+ joblib,
17
+ matplotlib,
18
+ cartopy,
19
+ pickle,
20
+ bz2,
21
+ requests
22
+ ],
23
+ package_data={
24
+ "paleoveg": ["model.pkl.bz2"], # Include the model file
25
+ },
26
+ python_requires=">=3.6",
27
+ )
28
+
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,26 @@
1
+ from setuptools import setup, find_packages
2
+
3
+ setup(
4
+ name="PaleoVeg",
5
+ version="0.1.0",
6
+ description="Predict paleovegetation from bioclimatic variables.",
7
+ author="Philipp Schlüter",
8
+ author_email="p.schlueter@uni-koeln.de",
9
+ url="https://github.com/roink/PaleoVeg",
10
+ packages=find_packages(),
11
+ include_package_data=True,
12
+ install_requires=[
13
+ "scikit-learn>=1.5.2",
14
+ "numpy>=2.1.3",
15
+ "pandas>=2.2.3",
16
+ "joblib",
17
+ "matplotlib",
18
+ "cartopy",
19
+ "requests"
20
+ ],
21
+ package_data={
22
+ "paleoveg": ["model.pkl.bz2"], # Include the model file
23
+ },
24
+ python_requires=">=3.7",
25
+ )
26
+
@@ -0,0 +1,111 @@
1
+ import numpy as np
2
+ import pandas as pd
3
+ import pytest
4
+ from paleoveg import predict, predict_map
5
+
6
+ BIOCLIM_RANGES = {
7
+ "bio1": (-54.72659, 30.94079),
8
+ "bio4": (0.0, 2361.791),
9
+ "bio5": (-29.69077, 48.16399),
10
+ "bio6": (-72.50053, 25.17328),
11
+ "bio7": (1.0, 72.02339),
12
+ "bio8": (-66.30019, 37.73918),
13
+ "bio9": (-54.83078, 37.51987),
14
+ "bio10": (-37.78683, 38.28006),
15
+ "bio11": (-66.33826, 29.06288),
16
+ "bio12": (0.0, 7953.816),
17
+ "bio13": (0.0, 2561.44),
18
+ "bio14": (0.0, 487.3488),
19
+ "bio15": (0.0, 229.0017),
20
+ "bio16": (0.0, 5597.44),
21
+ "bio17": (0.0, 1526.664),
22
+ "bio18": (0.0, 5190.888),
23
+ "bio19": (0.0, 4756.88),
24
+ }
25
+
26
+
27
+ # Mock input data for tabular predictions
28
+ @pytest.fixture
29
+ def tabular_data():
30
+ data = {
31
+ "bio1": [-10.0, 15.0], # Within range (-54.72659, 30.94079)
32
+ "bio4": [1000.0, 500.0], # Within range (0.0, 2361.791)
33
+ "bio5": [20.0, 30.0], # Within range (-29.69077, 48.16399)
34
+ "bio6": [-20.0, 5.0], # Within range (-72.50053, 25.17328)
35
+ "bio7": [10.0, 50.0], # Within range (1.0, 72.02339)
36
+ "bio8": [-30.0, 20.0], # Within range (-66.30019, 37.73918)
37
+ "bio9": [-40.0, 10.0], # Within range (-54.83078, 37.51987)
38
+ "bio10": [-20.0, 10.0], # Within range (-37.78683, 38.28006)
39
+ "bio11": [-30.0, 15.0], # Within range (-66.33826, 29.06288)
40
+ "bio12": [300.0, 700.0], # Within range (0.0, 7953.816)
41
+ "bio13": [100.0, 200.0], # Within range (0.0, 2561.44)
42
+ "bio14": [50.0, 100.0], # Within range (0.0, 487.3488)
43
+ "bio15": [10.0, 50.0], # Within range (0.0, 229.0017)
44
+ "bio16": [300.0, 500.0], # Within range (0.0, 5597.44)
45
+ "bio17": [100.0, 200.0], # Within range (0.0, 1526.664)
46
+ "bio18": [200.0, 400.0], # Within range (0.0, 5190.888)
47
+ "bio19": [100.0, 300.0], # Within range (0.0, 4756.88)
48
+ }
49
+ return pd.DataFrame(data)
50
+
51
+ # Mock input data for map-like predictions
52
+ @pytest.fixture
53
+ def map_data():
54
+ m, n = 5, 5
55
+ data = np.random.uniform(
56
+ low=[BIOCLIM_RANGES[var][0] for var in BIOCLIM_RANGES.keys()],
57
+ high=[BIOCLIM_RANGES[var][1] for var in BIOCLIM_RANGES.keys()],
58
+ size=(m, n, 17)
59
+ )
60
+ return data
61
+
62
+ def test_predict_probabilities(tabular_data):
63
+ # Test predict function with probabilities output
64
+ result = predict(tabular_data, dominant="exclude", c3_c4_correction=False)
65
+ assert isinstance(result, pd.DataFrame)
66
+ assert result.shape == (2, 11) # 2 rows, 11 vegetation types
67
+ assert np.isclose(result.sum(axis=1).values, 1).all() # Probabilities sum to 1
68
+
69
+ def test_predict_dominant_only(tabular_data):
70
+ # Test predict function with dominant output
71
+ result = predict(tabular_data, dominant="only", c3_c4_correction=False)
72
+ assert isinstance(result, pd.DataFrame)
73
+ assert result.shape == (2, 1) # 2 rows, 1 dominant type
74
+ assert result.iloc[:, 0].isin([
75
+ 'Evergreen Needleleaf Forest', 'Evergreen Broadleaf Forest',
76
+ 'Deciduous Needleleaf Forest', 'Deciduous Broadleaf Forest',
77
+ 'Mixed Forest', 'Woodland', 'Wooded Grassland',
78
+ 'Closed Shrubland', 'Open Shrubland', 'Grassland', 'Bare Ground'
79
+ ]).all()
80
+
81
+ def test_predict_map_probabilities(map_data):
82
+ # Test predict_map function with probabilities output
83
+ result = predict_map(map_data, dominant="exclude", c3_c4_correction=False)
84
+ assert isinstance(result, np.ndarray)
85
+ assert result.shape == (5, 5, 11) # m x n x 11
86
+ assert np.isclose(result.sum(axis=2), 1).all() # Probabilities sum to 1 for each grid cell
87
+
88
+ def test_predict_map_dominant_only(map_data):
89
+ # Test predict_map function with dominant output
90
+ result = predict_map(map_data, dominant="only", c3_c4_correction=False)
91
+ assert isinstance(result, np.ndarray)
92
+ assert result.shape == (5, 5, 1) # m x n x 1
93
+ assert np.issubdtype(result.dtype, np.integer)
94
+
95
+ def test_c3_c4_correction(tabular_data):
96
+ # Test C3/C4 correction
97
+ result_no_correction = predict(tabular_data, dominant="exclude", c3_c4_correction=False)
98
+ result_with_correction = predict(tabular_data, dominant="exclude", c3_c4_correction=True)
99
+ assert not result_no_correction.equals(result_with_correction) # Results should differ
100
+ assert np.isclose(result_with_correction.sum(axis=1).values, 1).all() # Probabilities still sum to 1
101
+
102
+ def test_predict_out_of_range(tabular_data):
103
+ tabular_data["bio1"][0] = -100.0 # Out-of-range value
104
+ with pytest.warns(UserWarning, match="Column 'bio1' contains values outside the range"):
105
+ predict(tabular_data)
106
+
107
+ def test_predict_map_out_of_range(map_data):
108
+ map_data[0, 0, 0] = -100.0 # Out-of-range value for bio1
109
+ with pytest.warns(UserWarning, match="Layer 0 contains values outside the range"):
110
+ predict_map(map_data)
111
+