copro 0.1.2__tar.gz → 2.0.2__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.
Files changed (45) hide show
  1. {copro-0.1.2 → copro-2.0.2}/PKG-INFO +34 -13
  2. {copro-0.1.2 → copro-2.0.2}/README.rst +11 -11
  3. copro-2.0.2/copro/__init__.py +4 -0
  4. copro-2.0.2/copro/conflict.py +308 -0
  5. copro-2.0.2/copro/evaluation.py +151 -0
  6. copro-2.0.2/copro/io.py +107 -0
  7. copro-2.0.2/copro/machine_learning.py +344 -0
  8. copro-2.0.2/copro/models.py +372 -0
  9. copro-2.0.2/copro/nb.py +64 -0
  10. {copro-0.1.2 → copro-2.0.2}/copro/plots.py +36 -55
  11. copro-2.0.2/copro/scripts/copro_runner.py +115 -0
  12. copro-2.0.2/copro/selection.py +146 -0
  13. copro-2.0.2/copro/settings.py +201 -0
  14. copro-2.0.2/copro/utils.py +147 -0
  15. copro-2.0.2/copro/variables.py +209 -0
  16. copro-2.0.2/copro/xydata.py +457 -0
  17. {copro-0.1.2 → copro-2.0.2}/copro.egg-info/PKG-INFO +34 -13
  18. {copro-0.1.2 → copro-2.0.2}/copro.egg-info/SOURCES.txt +5 -5
  19. copro-2.0.2/copro.egg-info/requires.txt +10 -0
  20. {copro-0.1.2 → copro-2.0.2}/setup.cfg +1 -1
  21. copro-2.0.2/setup.py +65 -0
  22. {copro-0.1.2 → copro-2.0.2}/tests/test_conflict.py +7 -16
  23. copro-2.0.2/tests/test_data.py +15 -0
  24. copro-0.1.2/copro/__init__.py +0 -16
  25. copro-0.1.2/copro/conflict.py +0 -318
  26. copro-0.1.2/copro/data.py +0 -364
  27. copro-0.1.2/copro/evaluation.py +0 -280
  28. copro-0.1.2/copro/machine_learning.py +0 -191
  29. copro-0.1.2/copro/models.py +0 -200
  30. copro-0.1.2/copro/pipeline.py +0 -228
  31. copro-0.1.2/copro/scripts/copro_runner.py +0 -137
  32. copro-0.1.2/copro/selection.py +0 -191
  33. copro-0.1.2/copro/utils.py +0 -419
  34. copro-0.1.2/copro/variables.py +0 -226
  35. copro-0.1.2/copro.egg-info/requires.txt +0 -10
  36. copro-0.1.2/setup.py +0 -58
  37. copro-0.1.2/tests/test_data.py +0 -31
  38. copro-0.1.2/tests/test_machine_learning.py +0 -33
  39. copro-0.1.2/tests/test_utils.py +0 -26
  40. {copro-0.1.2 → copro-2.0.2}/LICENSE +0 -0
  41. {copro-0.1.2 → copro-2.0.2}/copro/scripts/__init__.py +0 -0
  42. {copro-0.1.2 → copro-2.0.2}/copro.egg-info/dependency_links.txt +0 -0
  43. {copro-0.1.2 → copro-2.0.2}/copro.egg-info/entry_points.txt +0 -0
  44. {copro-0.1.2 → copro-2.0.2}/copro.egg-info/not-zip-safe +0 -0
  45. {copro-0.1.2 → copro-2.0.2}/copro.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.1
1
+ Metadata-Version: 2.4
2
2
  Name: copro
3
- Version: 0.1.2
3
+ Version: 2.0.2
4
4
  Summary: Python-model build on scikit-learn functions, designed to facilitate the set-up, execution, and evaluation of machine-learning models for the study of the climate-conflict nexus.
5
5
  Home-page: https://copro.readthedocs.io/
6
6
  Author: Jannis M. Hoch
@@ -16,6 +16,27 @@ Classifier: Programming Language :: Python :: 3.9
16
16
  Classifier: Programming Language :: Python :: 3.10
17
17
  Requires-Python: >=3.9
18
18
  License-File: LICENSE
19
+ Requires-Dist: click==8.1.7
20
+ Requires-Dist: xarray==2023.12.0
21
+ Requires-Dist: pandas==2.1.4
22
+ Requires-Dist: rasterio==1.2.10
23
+ Requires-Dist: rioxarray==0.15.0
24
+ Requires-Dist: rasterstats==0.19.0
25
+ Requires-Dist: geopandas==1.1.2
26
+ Requires-Dist: numpy==1.26.3
27
+ Requires-Dist: scikit-learn==1.5.0
28
+ Requires-Dist: netcdf4==1.6.0
29
+ Dynamic: author
30
+ Dynamic: author-email
31
+ Dynamic: classifier
32
+ Dynamic: description
33
+ Dynamic: home-page
34
+ Dynamic: keywords
35
+ Dynamic: license
36
+ Dynamic: license-file
37
+ Dynamic: requires-dist
38
+ Dynamic: requires-python
39
+ Dynamic: summary
19
40
 
20
41
  ===============
21
42
  CoPro
@@ -23,9 +44,6 @@ CoPro
23
44
 
24
45
  Welcome to CoPro, a machine-learning tool for conflict risk projections based on climate, environmental, and societal drivers.
25
46
 
26
- .. image:: https://travis-ci.com/JannisHoch/copro.svg?branch=dev
27
- :target: https://travis-ci.com/JannisHoch/copro
28
-
29
47
  .. image:: https://img.shields.io/badge/License-MIT-blue.svg
30
48
  :target: https://github.com/JannisHoch/copro/blob/dev/LICENSE
31
49
 
@@ -33,10 +51,10 @@ Welcome to CoPro, a machine-learning tool for conflict risk projections based on
33
51
  :target: https://copro.readthedocs.io/en/latest/?badge=latest
34
52
 
35
53
  .. image:: https://img.shields.io/github/v/release/JannisHoch/copro
36
- :target: https://github.com/JannisHoch/copro/releases/tag/v0.0.8
54
+ :target: https://github.com/JannisHoch/copro/releases/tag/v2.0.1
37
55
 
38
56
  .. image:: https://zenodo.org/badge/254407279.svg
39
- :target: https://zenodo.org/badge/latestdoi/254407279
57
+ :target: https://doi.org/10.5281/zenodo.4061705
40
58
 
41
59
  .. image:: https://badges.frapsoft.com/os/v2/open-source.svg?v=103
42
60
  :target: https://github.com/ellerbrock/open-source-badges/
@@ -44,8 +62,8 @@ Welcome to CoPro, a machine-learning tool for conflict risk projections based on
44
62
  .. image:: https://joss.theoj.org/papers/1f03334e56413ff71f65092ecc609aa4/status.svg
45
63
  :target: https://joss.theoj.org/papers/1f03334e56413ff71f65092ecc609aa4
46
64
 
47
- .. image:: https://mybinder.org/badge_logo.svg
48
- :target: https://mybinder.org/v2/gh/JannisHoch/copro/dev?filepath=%2Fexample%2Fnb_binder.ipynb
65
+ .. .. image:: https://mybinder.org/badge_logo.svg
66
+ .. :target: https://mybinder.org/v2/gh/JannisHoch/copro/dev?filepath=%2Fexample%2Fnb_binder.ipynb
49
67
 
50
68
  Model purpose
51
69
  --------------
@@ -143,6 +161,11 @@ With this (or other) data, the provided configuration-files (cfg-files) can be u
143
161
  All output is stored in the output directory specified in the cfg-files.
144
162
  In the output directory, two folders are created: one name `_REF` for output from the reference run, and `_PROJ` for output for projections.
145
163
 
164
+ .. important::
165
+
166
+ The example data works with copro version < 2.0.0.
167
+ For the latest version, the data needs to be realigned with the new config-file requirements.
168
+
146
169
  Jupyter notebooks
147
170
  ^^^^^^^^^^^^^^^^^^
148
171
 
@@ -209,8 +232,6 @@ CoPro is released under the MIT license.
209
232
  Authors
210
233
  ----------------
211
234
 
212
- * Jannis M. Hoch (Utrecht University)
213
- * Sophie de Bruin (Utrecht University, PBL)
235
+ * Jannis M. Hoch (Utrecht University, now Fathom)
236
+ * Sophie de Bruin (Utrecht University & PBL)
214
237
  * Niko Wanders (Utrecht University)
215
-
216
- Corresponding author: Jannis M. Hoch (j.m.hoch@uu.nl)
@@ -4,9 +4,6 @@ CoPro
4
4
 
5
5
  Welcome to CoPro, a machine-learning tool for conflict risk projections based on climate, environmental, and societal drivers.
6
6
 
7
- .. image:: https://travis-ci.com/JannisHoch/copro.svg?branch=dev
8
- :target: https://travis-ci.com/JannisHoch/copro
9
-
10
7
  .. image:: https://img.shields.io/badge/License-MIT-blue.svg
11
8
  :target: https://github.com/JannisHoch/copro/blob/dev/LICENSE
12
9
 
@@ -14,10 +11,10 @@ Welcome to CoPro, a machine-learning tool for conflict risk projections based on
14
11
  :target: https://copro.readthedocs.io/en/latest/?badge=latest
15
12
 
16
13
  .. image:: https://img.shields.io/github/v/release/JannisHoch/copro
17
- :target: https://github.com/JannisHoch/copro/releases/tag/v0.0.8
14
+ :target: https://github.com/JannisHoch/copro/releases/tag/v2.0.1
18
15
 
19
16
  .. image:: https://zenodo.org/badge/254407279.svg
20
- :target: https://zenodo.org/badge/latestdoi/254407279
17
+ :target: https://doi.org/10.5281/zenodo.4061705
21
18
 
22
19
  .. image:: https://badges.frapsoft.com/os/v2/open-source.svg?v=103
23
20
  :target: https://github.com/ellerbrock/open-source-badges/
@@ -25,8 +22,8 @@ Welcome to CoPro, a machine-learning tool for conflict risk projections based on
25
22
  .. image:: https://joss.theoj.org/papers/1f03334e56413ff71f65092ecc609aa4/status.svg
26
23
  :target: https://joss.theoj.org/papers/1f03334e56413ff71f65092ecc609aa4
27
24
 
28
- .. image:: https://mybinder.org/badge_logo.svg
29
- :target: https://mybinder.org/v2/gh/JannisHoch/copro/dev?filepath=%2Fexample%2Fnb_binder.ipynb
25
+ .. .. image:: https://mybinder.org/badge_logo.svg
26
+ .. :target: https://mybinder.org/v2/gh/JannisHoch/copro/dev?filepath=%2Fexample%2Fnb_binder.ipynb
30
27
 
31
28
  Model purpose
32
29
  --------------
@@ -124,6 +121,11 @@ With this (or other) data, the provided configuration-files (cfg-files) can be u
124
121
  All output is stored in the output directory specified in the cfg-files.
125
122
  In the output directory, two folders are created: one name `_REF` for output from the reference run, and `_PROJ` for output for projections.
126
123
 
124
+ .. important::
125
+
126
+ The example data works with copro version < 2.0.0.
127
+ For the latest version, the data needs to be realigned with the new config-file requirements.
128
+
127
129
  Jupyter notebooks
128
130
  ^^^^^^^^^^^^^^^^^^
129
131
 
@@ -190,8 +192,6 @@ CoPro is released under the MIT license.
190
192
  Authors
191
193
  ----------------
192
194
 
193
- * Jannis M. Hoch (Utrecht University)
194
- * Sophie de Bruin (Utrecht University, PBL)
195
+ * Jannis M. Hoch (Utrecht University, now Fathom)
196
+ * Sophie de Bruin (Utrecht University & PBL)
195
197
  * Niko Wanders (Utrecht University)
196
-
197
- Corresponding author: Jannis M. Hoch (j.m.hoch@uu.nl)
@@ -0,0 +1,4 @@
1
+ """Top-level package."""
2
+
3
+ __author__ = """Jannis M. Hoch, Sophie de Bruin, Niko Wanders"""
4
+ __version__ = "2.0.2"
@@ -0,0 +1,308 @@
1
+ import os
2
+ import warnings
3
+ from configparser import RawConfigParser
4
+ from pathlib import Path
5
+ from typing import Literal, Union
6
+
7
+ import click
8
+ import geopandas as gpd
9
+ import numpy as np
10
+ import pandas as pd
11
+
12
+ from copro import nb, utils
13
+
14
+
15
+ def conflict_in_year_bool(
16
+ config: RawConfigParser,
17
+ conflict_gdf: gpd.GeoDataFrame,
18
+ extent_gdf: gpd.GeoDataFrame,
19
+ sim_year: int,
20
+ out_dir: click.Path,
21
+ poly_identifier="watprovID",
22
+ conflict_identifier="event_id_cnty",
23
+ ) -> list:
24
+ """Creates a list for each timestep with boolean information whether a conflict took place in a polygon or not.
25
+
26
+ Args:
27
+ config (ConfigParser-object): object containing the parsed configuration-settings of the model.
28
+ conflict_gdf (geodataframe): geo-dataframe containing georeferenced information of conflict.
29
+ extent_gdf (geodataframe): geo-dataframe containing one or more polygons \
30
+ with geometry information for which values are extracted.
31
+ sim_year (int): year for which data is extracted.
32
+ out_dir (str): path to output folder. If 'None', no output is stored.
33
+ identifier (str, optional): unique polygon identifier column name. Defaults to 'watprovID'.
34
+
35
+ Returns:
36
+ list: list containing 0/1 per polygon depending on conflict occurence.
37
+ """
38
+
39
+ click.echo(f"Checking for conflict events which occured in {sim_year}.")
40
+ # select the entries which occured in this year
41
+ temp_sel_year = conflict_gdf.loc[conflict_gdf.year == sim_year]
42
+ if temp_sel_year.empty:
43
+ warnings.warn(
44
+ f"No conflicts were found in sampled conflict data set for year {sim_year}."
45
+ )
46
+
47
+ # merge the dataframes with polygons and conflict information, creating a sub-set of polygons/regions
48
+ data_merged = gpd.sjoin(temp_sel_year, extent_gdf)
49
+
50
+ # determine the aggregated amount of fatalities in one region (e.g. water province)
51
+ fatalities_per_poly = (
52
+ data_merged[conflict_identifier]
53
+ .groupby(data_merged[poly_identifier])
54
+ .count()
55
+ .to_frame()
56
+ .rename(columns={conflict_identifier: "total_fatalities"})
57
+ )
58
+
59
+ out_dir = os.path.join(out_dir, "files")
60
+ Path.mkdir(Path(out_dir), exist_ok=True)
61
+
62
+ if sim_year == config["general"]["y_end"]:
63
+ _store_boolean_conflict_data_to_csv(
64
+ fatalities_per_poly, extent_gdf, sim_year, out_dir
65
+ )
66
+
67
+ # loop through all regions and check if exists in sub-set
68
+ # if so, this means that there was conflict and thus assign value 1
69
+ list_out = []
70
+ for i, _ in extent_gdf.iterrows():
71
+ i_poly = extent_gdf.iloc[i][poly_identifier]
72
+ if i_poly in fatalities_per_poly.index.values:
73
+ list_out.append(1)
74
+ else:
75
+ list_out.append(0)
76
+
77
+ return list_out
78
+
79
+
80
+ def conflict_in_previous_year_bool(
81
+ conflict_gdf: gpd.GeoDataFrame,
82
+ extent_gdf: gpd.GeoDataFrame,
83
+ sim_year: int,
84
+ check_neighbors: bool = False,
85
+ neighboring_matrix: Union[None, pd.DataFrame] = None,
86
+ poly_identifier="watprovID", # TODO: no kwarg, should come from config
87
+ conflict_identifier="event_id_cnty", # TODO: no kwarg, should come from config
88
+ ) -> list:
89
+ """Creates a list for each timestep with boolean information whether
90
+ a conflict took place in the previous year in a polygon or not.
91
+
92
+ Args:
93
+ conflict_gdf (gpd.GeoDataFrame): georeferenced information of conflict.
94
+ extent_gdf (gpd.GeoDataFrame): geo-dataframe containing one or more polygons \
95
+ with geometry information for which values are extracted.
96
+ sim_year (int): year for which data is extracted.
97
+ check_neighbors (bool, optional): whether to check for conflict in neighbouring polygons. Defaults to False.
98
+ neighboring_matrix (Union[None, pd.DataFrame], optional): dataframe with neighbouring polygons per polygon. \
99
+ Defaults to None.
100
+ identifier (str, optional): unique polygon identifier column name. Defaults to 'watprovID'.
101
+
102
+ Returns:
103
+ list: list containing 0/1 per polygon depending on conflict occurence.
104
+ """
105
+
106
+ if check_neighbors:
107
+ click.echo("Checking for conflicts in neighboring polygons at t-1")
108
+ else:
109
+ click.echo("Checking for conflict event in polygon at t-1")
110
+
111
+ # TODO: screening whether there is any conflict data in sim_year should be done earlier
112
+ # get conflicts at t-1
113
+ temp_sel_year = conflict_gdf[conflict_gdf.year == sim_year - 1]
114
+ if temp_sel_year.empty:
115
+ warnings.warn(
116
+ f"No conflicts were found in sampled conflict data set for year {sim_year - 1}."
117
+ )
118
+ # merge the dataframes with polygons and conflict information, creating a sub-set of polygons/regions
119
+ data_merged = gpd.sjoin(temp_sel_year, extent_gdf)
120
+ conflicts_per_poly = (
121
+ data_merged[conflict_identifier]
122
+ .groupby(data_merged[poly_identifier])
123
+ .count()
124
+ .to_frame()
125
+ .rename(columns={conflict_identifier: "conflict_count"})
126
+ )
127
+ # NOTE: WORKS UNTIL HERE
128
+
129
+ # loop through all polygons
130
+ list_out = []
131
+ for i in range(len(extent_gdf)):
132
+ i_poly = extent_gdf[poly_identifier].iloc[i]
133
+ # check if polygon is in list with conflict polygons
134
+ if i_poly in conflicts_per_poly.index.values:
135
+ # if so, check if neighboring polygons contain conflict and assign boolean value
136
+ if check_neighbors:
137
+ val = calc_conflicts_nb(i_poly, neighboring_matrix, conflicts_per_poly)
138
+ # append resulting value
139
+ list_out.append(val)
140
+ # if not, assign 1 directly
141
+ else:
142
+ list_out.append(1)
143
+ else:
144
+ # if polygon not in list with conflict polygons, assign 0
145
+ list_out.append(0)
146
+
147
+ return list_out
148
+
149
+
150
+ def read_projected_conflict(
151
+ extent_gdf: gpd.GeoDataFrame,
152
+ bool_conflict: pd.DataFrame,
153
+ check_neighbors=False,
154
+ neighboring_matrix=None,
155
+ identifier="watprovID",
156
+ ) -> list:
157
+ """Creates a list for each timestep with boolean information
158
+ whether a conflict took place in a polygon or not.
159
+ Input conflict data (`bool_conflict`) must contain an index with IDs
160
+ corresponding with the `watprovID` values of extent_gdf.
161
+ Optionally, the algorithm can be extended to the neighboring polygons.
162
+
163
+ Args:
164
+ extent_gdf (gpd.GeoDataFrame): geo-dataframe containing one or more polygons \
165
+ with geometry information for which values are extracted.
166
+ bool_conflict (pd.DataFrame): dataframe with boolean values (1) for each polygon with conflict.
167
+ check_neighbors (bool, optional): whether or not to check for conflict in neighboring polygons. \
168
+ Defaults to `False`.
169
+ neighboring_matrix (pd.DataFrame, optional): look-up dataframe listing all neighboring polygons. \
170
+ Defaults to `None`.
171
+ identifier (str, optional): unique polygon identifier column name. Defaults to 'watprovID'.
172
+
173
+ Returns:
174
+ list: 1 and 0 values for each polygon with conflict respectively without conflict. \
175
+ If `check_neighbors=True`, then 1 if neighboring polygon contains conflict and 0 is not.
176
+ """
177
+
178
+ # loop through all polygons and check if exists in sub-set
179
+ list_out = []
180
+ for i in range(len(extent_gdf)):
181
+ i_poly = extent_gdf[identifier].iloc[i]
182
+ if i_poly in bool_conflict.index.values:
183
+ if check_neighbors:
184
+ # determine log-scaled number of conflict events in neighboring polygons
185
+ val = calc_conflicts_nb(i_poly, neighboring_matrix, bool_conflict)
186
+ # append resulting value
187
+ list_out.append(val)
188
+ else:
189
+ list_out.append(1)
190
+ else:
191
+ # if polygon not in list with conflict polygons, assign 0
192
+ list_out.append(0)
193
+
194
+ return list_out
195
+
196
+
197
+ def calc_conflicts_nb(
198
+ i_poly: int, neighboring_matrix: pd.DataFrame, conflicts_per_poly: pd.DataFrame
199
+ ) -> Literal[0, 1]:
200
+ """Determines whether in the neighbouring polygons of a polygon i_poly conflict took place.
201
+ If so, a value 1 is returned, otherwise 0.
202
+
203
+ Args:
204
+ i_poly (int): ID number of polygon under consideration.
205
+ neighboring_matrix (pd.DataFrame): look-up dataframe listing all neighboring polygons.
206
+ conflicts_per_poly (pd.DataFrame): dataframe with conflict data per polygon.
207
+
208
+ Returns:
209
+ Literal: 1 if conflict took place in neighboring polygon, 0 if not.
210
+ """
211
+
212
+ # find neighbors of this polygon
213
+ nbs = nb.find_neighbors(i_poly, neighboring_matrix)
214
+
215
+ # initiate list
216
+ nb_count = []
217
+ # loop through neighbors
218
+ for k in nbs:
219
+ # check if there was conflict at t-1
220
+ if k in conflicts_per_poly.index.values:
221
+ nb_count.append(1)
222
+ # if more than one neighboring polygon has conflict, return 0
223
+ if np.sum(nb_count) > 0:
224
+ val = 1
225
+ # otherwise, return 0
226
+ else:
227
+ val = 0
228
+
229
+ return val
230
+
231
+
232
+ def check_for_correct_prediction(
233
+ X_test_ID: np.ndarray,
234
+ X_test_geom: np.ndarray,
235
+ y_test: np.ndarray,
236
+ y_pred: np.ndarray,
237
+ y_prob_0: np.ndarray,
238
+ y_prob_1: np.ndarray,
239
+ ) -> pd.DataFrame:
240
+ """Stacks together the arrays with unique identifier, geometry, test data, and predicted data into a dataframe.
241
+ Contains therefore only the data points used in the test-sample, not in the training-sample.
242
+ Additionally computes whether a correct prediction was made.
243
+
244
+ Args:
245
+ X_test_ID (np.ndarray): _description_
246
+ X_test_geom (np.ndarray): _description_
247
+ y_test (np.ndarray): _description_
248
+ y_pred (np.ndarray): _description_
249
+ y_prob_0 (np.ndarray): _description_
250
+ y_prob_1 (np.ndarray): _description_
251
+
252
+ Returns:
253
+ pd.DataFrame: _description_
254
+ """
255
+
256
+ # stack separate columns horizontally
257
+ arr = np.column_stack((X_test_ID, X_test_geom, y_test, y_pred, y_prob_0, y_prob_1))
258
+ # convert array to dataframe
259
+ df = pd.DataFrame(
260
+ arr, columns=["ID", "geometry", "y_test", "y_pred", "y_prob_0", "y_prob_1"]
261
+ )
262
+ # compute whether a prediction is correct
263
+ # if so, assign 1; otherwise, assign 0
264
+ df["correct_pred"] = np.where(df["y_test"] == df["y_pred"], 1, 0)
265
+
266
+ return df
267
+
268
+
269
+ def _store_boolean_conflict_data_to_csv(
270
+ fatalities_per_poly: pd.DataFrame,
271
+ extent_gdf: gpd.GeoDataFrame,
272
+ sim_year: int,
273
+ out_dir: click.Path,
274
+ identifier="watprovID",
275
+ ):
276
+ """Stores boolean conflict data to csv-file at the end of reference period.
277
+ Used as initial conditions for projections from there.
278
+
279
+ Args:
280
+ fatalities_per_poly (pd.DataFrame): Fatalities per polygon in `sim_year`.
281
+ extent_gdf (gpd.GeoDataFrame): All polygons considered in analysis, also those w/o conflict.
282
+ sim_year (int): Simulation year for which data is stored.
283
+ out_dir (click.Path): Path to output folder.
284
+ identifier (str, optional): Unique polygon identifier column name. Defaults to 'watprovID'.
285
+ """
286
+
287
+ # get a 1 for each polygon where there was conflict
288
+ bool_per_poly = fatalities_per_poly / fatalities_per_poly
289
+ # change column name and dtype
290
+ bool_per_poly = bool_per_poly.rename(
291
+ columns={"total_fatalities": "bool_conflict"}
292
+ ).astype(int)
293
+ # change index name to fit global_df
294
+ bool_per_poly.index = bool_per_poly.index.rename("ID")
295
+ # get list of all polygon IDs with their geometry information
296
+ global_df = utils.get_ID_geometry_lookup(extent_gdf)
297
+ # merge the boolean info with geometry
298
+ # for all polygons without conflict, set a 0
299
+ click.echo(
300
+ f"Storing boolean conflict map of year {sim_year} \
301
+ to file {os.path.join(out_dir, f'conflicts_in_{sim_year}.csv')}"
302
+ )
303
+
304
+ data_stored = pd.merge(bool_per_poly, global_df, on="ID", how="right").dropna()
305
+ data_stored.index = data_stored.index.rename(identifier)
306
+ data_stored = data_stored.drop("geometry", axis=1)
307
+ data_stored = data_stored.astype(int)
308
+ data_stored.to_csv(os.path.join(out_dir, f"conflicts_in_{sim_year}.csv"))
@@ -0,0 +1,151 @@
1
+ from typing import Union
2
+
3
+ import geopandas as gpd
4
+ import pandas as pd
5
+ from sklearn import metrics
6
+
7
+
8
+ def init_out_dict(scores: Union[list[str], None] = None) -> dict:
9
+ """Initiates the main model evaluatoin dictionary for a range of model metric scores.
10
+ The scores should match the scores used in the dictioary created in 'evaluation.evaluate_prediction()'.
11
+
12
+ Args:
13
+ scores (list, None): list containing metric scores. If 'None', a default list is used. Defaults to 'None'.
14
+
15
+ Returns:
16
+ dict: empty dictionary with metrics as keys.
17
+ """
18
+
19
+ if scores is None:
20
+ scores = [
21
+ "Accuracy",
22
+ "Precision",
23
+ "Recall",
24
+ "F1 score",
25
+ "Cohen-Kappa score",
26
+ "Brier loss score",
27
+ "ROC AUC score",
28
+ "AP score",
29
+ ]
30
+
31
+ # initialize empty dictionary with one emtpy list per score
32
+ out_dict = {}
33
+ for score in scores:
34
+ out_dict[score] = []
35
+
36
+ return out_dict
37
+
38
+
39
+ def fill_out_dict(
40
+ out_dict: dict,
41
+ eval_dict: dict,
42
+ ):
43
+ """Appends the computed metric score per run to the main output dictionary.
44
+ All metrics are initialized in init_out_dict().
45
+
46
+ Args:
47
+ out_dict (dict): main output dictionary.
48
+ eval_dict (dict): dictionary containing scores per simulation.
49
+
50
+ Returns:
51
+ dict: dictionary with collected scores for each simulation
52
+ """
53
+
54
+ for key in out_dict:
55
+ out_dict[key].append(eval_dict[key])
56
+
57
+ return out_dict
58
+
59
+
60
+ def polygon_model_accuracy(
61
+ df: pd.DataFrame, global_df: pd.DataFrame, make_proj=False
62
+ ) -> gpd.GeoDataFrame:
63
+ """Determines a range of model accuracy values for each polygon.
64
+ Reduces dataframe with results from each simulation to values per unique polygon identifier.
65
+ Determines the total number of predictions made per polygon
66
+ as well as fraction of correct predictions made for overall and conflict-only data.
67
+
68
+ Args:
69
+ df (pd.DataFrame): output dataframe containing results of all simulations.
70
+ global_df (pd.DataFrame): global look-up dataframe to associate unique identifier with geometry.
71
+ make_proj (bool, optional): whether or not this function is used to make a projection. \
72
+ If True, a couple of calculations are skipped as no observed data is available for projections. \
73
+ Defaults to 'False'.
74
+
75
+ Returns:
76
+ gpd.GeoDataFrame: model accuracy data per polygon.
77
+ """
78
+
79
+ # - create a dataframe containing the number of occurence per ID
80
+ ID_count = (
81
+ df.ID.value_counts().to_frame().rename(columns={"count": "nr_predictions"})
82
+ )
83
+
84
+ df_count = pd.DataFrame()
85
+
86
+ # - per polygon ID, compute sum of overall correct predictions and rename column name
87
+ if not make_proj:
88
+ df_count["nr_correct_predictions"] = df.correct_pred.groupby(df.ID).sum()
89
+
90
+ # - per polygon ID, compute sum of all conflict data points and add to dataframe
91
+ if not make_proj:
92
+ df_count["nr_observed_conflicts"] = df.y_test.groupby(df.ID).sum().astype(float)
93
+
94
+ # - per polygon ID, compute sum of all conflict data points and add to dataframe
95
+ df_count["nr_predicted_conflicts"] = df.y_pred.groupby(df.ID).sum().astype(float)
96
+ # - per polygon ID, compute average probability that conflict occurs
97
+ df_count["min_prob_1"] = pd.to_numeric(df.y_prob_1).groupby(df.ID).min()
98
+ df_count["probability_of_conflict"] = (
99
+ pd.to_numeric(df.y_prob_1).groupby(df.ID).mean()
100
+ )
101
+ df_count["max_prob_1"] = pd.to_numeric(df.y_prob_1).groupby(df.ID).max()
102
+
103
+ # - merge the two dataframes with ID as key
104
+ df_temp = pd.merge(ID_count, df_count, on="ID")
105
+
106
+ # - compute average correct prediction rate by dividing sum of correct predictions with number of all predicionts
107
+ if not make_proj:
108
+ df_temp["fraction_correct_predictions"] = (
109
+ df_temp.nr_correct_predictions / df_temp.nr_predictions
110
+ )
111
+
112
+ # - compute average correct prediction rate by dividing sum of correct predictions with number of all predicionts
113
+ df_temp["chance_of_conflict"] = (
114
+ df_temp.nr_predicted_conflicts / df_temp.nr_predictions
115
+ )
116
+
117
+ # - merge with global dataframe containing IDs and geometry, and keep only those polygons occuring in test sample
118
+ df_hit = pd.merge(df_temp, global_df, on="ID", how="left")
119
+ # #- convert to geodataframe
120
+ gdf_hit = gpd.GeoDataFrame(df_hit, geometry=df_hit.geometry)
121
+
122
+ return gdf_hit
123
+
124
+
125
+ def evaluate_prediction(y_test: list, y_pred: list, y_prob: list) -> dict:
126
+ """Computes a range of model evaluation metrics and appends the resulting scores to a dictionary.
127
+ This is done for each model execution separately.
128
+ Output will be stored to stderr if possible.
129
+
130
+ Args:
131
+ y_test (list): list containing test-sample conflict data.
132
+ y_pred (list): list containing predictions.
133
+ y_prob (array): array resulting probabilties of predictions.
134
+
135
+ Returns:
136
+ dict: dictionary with scores for each simulation
137
+ """
138
+
139
+ # compute value per evaluation metric and assign to list
140
+ eval_dict = {
141
+ "Accuracy": metrics.accuracy_score(y_test, y_pred),
142
+ "Precision": metrics.precision_score(y_test, y_pred),
143
+ "Recall": metrics.recall_score(y_test, y_pred),
144
+ "F1 score": metrics.f1_score(y_test, y_pred),
145
+ "Cohen-Kappa score": metrics.cohen_kappa_score(y_test, y_pred),
146
+ "Brier loss score": metrics.brier_score_loss(y_test, y_prob[:, 1]),
147
+ "ROC AUC score": metrics.roc_auc_score(y_test, y_prob[:, 1]),
148
+ "AP score": metrics.average_precision_score(y_test, y_prob[:, 1]),
149
+ }
150
+
151
+ return eval_dict