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 +21 -0
- paleoveg-0.1.0/MANIFEST.in +1 -0
- paleoveg-0.1.0/PKG-INFO +16 -0
- paleoveg-0.1.0/PaleoVeg.egg-info/PKG-INFO +16 -0
- paleoveg-0.1.0/PaleoVeg.egg-info/SOURCES.txt +14 -0
- paleoveg-0.1.0/PaleoVeg.egg-info/dependency_links.txt +1 -0
- paleoveg-0.1.0/PaleoVeg.egg-info/requires.txt +7 -0
- paleoveg-0.1.0/PaleoVeg.egg-info/top_level.txt +1 -0
- paleoveg-0.1.0/README.md +195 -0
- paleoveg-0.1.0/paleoveg/__init__.py +1 -0
- paleoveg-0.1.0/paleoveg/model_loader.py +44 -0
- paleoveg-0.1.0/paleoveg/predictor.py +209 -0
- paleoveg-0.1.0/paleoveg/setup.py +28 -0
- paleoveg-0.1.0/setup.cfg +4 -0
- paleoveg-0.1.0/setup.py +26 -0
- paleoveg-0.1.0/tests/test_predictor.py +111 -0
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
|
paleoveg-0.1.0/PKG-INFO
ADDED
|
@@ -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 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
paleoveg
|
paleoveg-0.1.0/README.md
ADDED
|
@@ -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
|
+
|
paleoveg-0.1.0/setup.cfg
ADDED
paleoveg-0.1.0/setup.py
ADDED
|
@@ -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
|
+
|