b3alien 0.0.1__py3-none-any.whl

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.
b3alien/__init__.py ADDED
File without changes
@@ -0,0 +1,23 @@
1
+ """
2
+
3
+ Biodiversity data cube functions
4
+ ========================================
5
+
6
+ """
7
+
8
+ from .b3cube import OccurrenceCube
9
+ from .b3cube import plot_richness
10
+ from .b3cube import cumulative_species
11
+ from .b3cube import calculate_rate
12
+ from .b3cube import get_survey_effort
13
+
14
+ __all__ = [
15
+ "plot_richness",
16
+ "cumulative_species",
17
+ "plot_cumsum",
18
+ "filter_multiple_cells",
19
+ "filter_multiple_occ",
20
+ "calculate_rate",
21
+ "get_survey_effort"
22
+ # purposely exclude OccurrenceCube
23
+ ]
@@ -0,0 +1,511 @@
1
+ import geopandas as gpd
2
+ import pandas as pd
3
+ import xarray as xr
4
+ import sparse
5
+ import dask.array as da
6
+ import numpy as np
7
+ import matplotlib.pyplot as plt
8
+ import matplotlib
9
+ matplotlib.use("Agg")
10
+ import shapely
11
+ from shapely import geos
12
+ import gcsfs
13
+
14
+ """
15
+
16
+ Create a class object of the occurrence cube
17
+
18
+ """
19
+ class OccurrenceCube():
20
+
21
+ """
22
+ Load a GeoParquet file (local or from GCS) into a sparse xarray cube.
23
+
24
+ Parameters
25
+ ----------
26
+ filepath : str
27
+ Path to the GeoParquet file (e.g. 'gs://bucket/file.parquet').
28
+ dims : list or tuple, optional
29
+ Dimension names. Default is ['time', 'cell', 'species'].
30
+ coords : dict, optional
31
+ Optional coordinates to assign to the cube.
32
+ index_col : str or list, optional
33
+ Column(s) to use for reshaping if needed.
34
+
35
+
36
+ Returns
37
+ -------
38
+ b3cube.OccurrenceCube
39
+ A sparse data cube loaded from the GeoParquet file.
40
+ self.df contains a geopandas.DataFrame
41
+ self.data a sparse xarray.Xarray
42
+ """
43
+
44
+ def __init__(self, filepath: str, dims=None, coords=None, index_col=None):
45
+
46
+ self.filepath = filepath
47
+ self.dims = dims or ("time", "cell", "species")
48
+ self.coords = coords
49
+ self.index_col = index_col
50
+
51
+ # Load GeoParquet
52
+ self.df = self._load_geoparquet(filepath)
53
+
54
+ # Create cube
55
+ self.data = self._create_xcube(self.df)
56
+
57
+ def _load_geoparquet(self, path):
58
+ """
59
+ Load a GeoParquet file from local disk or GCS using GeoPandas.
60
+ """
61
+ if path.startswith("gs://"):
62
+ fs = gcsfs.GCSFileSystem()
63
+ with fs.open(path) as f:
64
+ gdf = gpd.read_parquet(f)
65
+ else:
66
+ gdf = gpd.read_parquet(path)
67
+
68
+ if 'geometry' not in gdf.columns:
69
+ raise ValueError("The input file must contain a 'geometry' column.")
70
+
71
+ # Ensure geometry is parsed and valid
72
+ gdf["geometry"] = gdf["geometry"].apply(wkt.loads) if gdf["geometry"].dtype == object else gdf["geometry"]
73
+ gdf = gdf.set_geometry("geometry")
74
+
75
+ return gdf
76
+
77
+ def _create_xcube(self, df):
78
+ """
79
+ Convert a GeoDataFrame into a sparse xarray cube with geometry metadata.
80
+ """
81
+ # Convert to categorical
82
+ df["yearmonth"] = pd.Categorical(df["yearmonth"])
83
+ df["cellCode"] = pd.Categorical(df["cellCode"])
84
+ df["specieskey"] = pd.Categorical(df["specieskey"])
85
+
86
+ # Align geometries with cell categories
87
+ cell_categories = df["cellCode"].cat.categories
88
+ geometry_per_cell = df.drop_duplicates("cellCode").set_index("cellCode").loc[cell_categories]["geometry"]
89
+
90
+ # Encode to integers
91
+ time_codes = df["yearmonth"].cat.codes.values
92
+ cell_codes = df["cellCode"].cat.codes.values
93
+ species_codes = df["specieskey"].cat.codes.values
94
+
95
+ # Build sparse cube
96
+ sparse_cube = sparse.COO(
97
+ coords=[time_codes, cell_codes, species_codes],
98
+ data=df["occurrences"].astype("float32").values,
99
+ shape=(
100
+ df["yearmonth"].cat.categories.size,
101
+ df["cellCode"].cat.categories.size,
102
+ df["specieskey"].cat.categories.size
103
+ )
104
+ )
105
+
106
+ # Create xarray DataArray
107
+ cube = xr.DataArray(
108
+ sparse_cube,
109
+ dims=self.dims,
110
+ coords={
111
+ self.dims[0]: df["yearmonth"].cat.categories,
112
+ self.dims[1]: df["cellCode"].cat.categories,
113
+ self.dims[2]: df["specieskey"].cat.categories,
114
+ "geometry": (self.dims[1], geometry_per_cell.values)
115
+ },
116
+ name="occurrences"
117
+ )
118
+
119
+ return cube
120
+
121
+ def _species_richness(self, normalized=False):
122
+ # 1. Binary presence
123
+ presence = (self.data > 0)
124
+
125
+ # 2. Collapse time dimension using logical OR → was the species *ever* seen in this cell?
126
+ presence_any_time = presence.any(dim="time") # shape: (cell, species)
127
+
128
+ # 3. Sum species per cell (species richness)
129
+ species_richness = presence_any_time.sum(dim="species") # shape: (cell,)
130
+
131
+ total_occurrences = self.data.sum(dim=["time", "species"])
132
+
133
+ if normalized == False:
134
+ # 4. Get the non-zero values and indices
135
+ coords = species_richness.data.coords # (1D arrays of indices)
136
+ values = species_richness.data.data # the richness values
137
+
138
+ # 5. Convert integer cell indices to real labels (from .coords['cell'])
139
+ cell_labels = species_richness.coords["cell"].values
140
+
141
+ richness_df = pd.DataFrame({
142
+ "cell": cell_labels[coords[0]],
143
+ "richness": values
144
+ })
145
+
146
+ self.richness = richness_df
147
+
148
+ else:
149
+ epsilon = 1e-6
150
+ normalized_richness = species_richness / (total_occurrences + epsilon)
151
+
152
+ coords = normalized_richness.data.coords
153
+ values = normalized_richness.data.data
154
+ cell_labels = normalized_richness.coords["cell"].values
155
+
156
+ # Build a DataFrame
157
+ norm_df = pd.DataFrame({
158
+ "cell": cell_labels[coords[0]],
159
+ "normalized_richness": values
160
+ })
161
+
162
+ self.richness = norm_df
163
+
164
+ def _filter_species(self, speciesKey):
165
+
166
+ self.df = self.df[self.df['specieskey'].eq(speciesKey)]
167
+ self.data = self.data.sel(species=speciesKey)
168
+
169
+ def plot_richness(richness_df, gdf_from_gcs, geom='cellCode'):
170
+ """
171
+ Create a plot of the species richness dataframe.
172
+
173
+ Parameters
174
+ ----------
175
+ richness_df : pandas.DataFrame
176
+ Datagrame containing the species richness per grid cell.
177
+ gdf_from_gcs : geopandas.Dataframe
178
+ GeoDataFrame containing the species occurrence cuve.
179
+ geom : str, optional
180
+ Name of the geometry column in the GeoDataFrame. Default is 'cellCode'
181
+
182
+ Returns
183
+ -------
184
+ matplotlib.plot
185
+ A plot of the species richness.
186
+ """
187
+
188
+ gdf_plot = pd.merge(richness_df, gdf_from_gcs, left_on='cell', right_on=geom)
189
+
190
+ gdf_plot = gpd.GeoDataFrame(gdf_plot, geometry="geometry", crs=gdf_from_gcs.crs)
191
+
192
+ fig, ax = plt.subplots(figsize=(10, 10))
193
+ gdf_plot.plot(
194
+ column="richness",
195
+ cmap="viridis",
196
+ legend=True,
197
+ linewidth=0.1,
198
+ edgecolor="grey",
199
+ ax=ax
200
+ )
201
+ ax.set_title("Species Richness per QDGC Cell")
202
+ ax.axis("off")
203
+ plt.show()
204
+
205
+
206
+ def cumulative_species(cube, species_to_keep):
207
+
208
+ """
209
+ Calculate the cumulative number of species in a OccurrenceCube.
210
+
211
+ Parameters
212
+ ----------
213
+ cube : b3alien.b3cube.OccurrenceCube
214
+ Species OccurrenceCube from GBIF.
215
+ species_to_keep : numpy.array
216
+ Array of GBIF speciesKeys that need to be taken into account to calculate the cumulative species number of a cube.
217
+ geom : str, optional
218
+
219
+ Returns
220
+ -------
221
+ df1 : pandas.DataFrame
222
+ Sparse dataframe that still contains the cumulative sum per grid cell.
223
+ df2 : pandas.DataFrame
224
+ Cumulative dataframe cell independent.
225
+ """
226
+
227
+ # Wrap sparse array in Dask array with one or more chunks
228
+ dask_sparse_array = da.from_array(cube.data.data, chunks=(100, 100, 1000)) # tune chunking for your use case
229
+
230
+ # Replace data in cube
231
+ cube_dask_sparse = cube.data.copy(data=dask_sparse_array)
232
+
233
+ species_mask = cube_dask_sparse["species"].isin(species_to_keep)
234
+ filtered_cube = cube_dask_sparse.where(species_mask, drop=True)
235
+
236
+ # Grab the underlying sparse.COO object from Dask
237
+ sparse_block = filtered_cube.data.compute() # Warning: loads full filtered cube into RAM!
238
+
239
+ # Extract sparse coordinates
240
+ coords = sparse_block.coords # shape: (ndim, nnz)
241
+ data = sparse_block.data # non-zero values
242
+
243
+ # Map indices to labels
244
+ time_labels = filtered_cube.coords["time"].values
245
+ species_labels = filtered_cube.coords["species"].values
246
+ cell_labels = filtered_cube.coords["cell"].values
247
+
248
+ # Use the sparse indices to create a DataFrame
249
+ df_sparse = pd.DataFrame({
250
+ "time": time_labels[coords[0]],
251
+ "cell": cell_labels[coords[1]],
252
+ "species": species_labels[coords[2]],
253
+ "occurrences": sparse_block.data
254
+ })
255
+
256
+ # Drop duplicates and compute cumulative species count
257
+ df_sparse = df_sparse.drop_duplicates()
258
+ df_sparse["seen"] = 1
259
+ df_time = (
260
+ df_sparse.groupby("time")["species"]
261
+ .nunique()
262
+ .cumsum()
263
+ .reset_index(name="cumulative_species")
264
+ )
265
+
266
+ df_time["time"] = pd.to_datetime(df_time["time"], format="%Y-%M", errors="coerce")
267
+
268
+ # fix to have the real cumsum
269
+ # Step 1: Remove duplicates (species × time)
270
+ df_sparse_unique = df_sparse[["time", "species"]].drop_duplicates()
271
+
272
+ # Step 2: Sort by time
273
+ df_sparse_unique = df_sparse_unique.sort_values("time")
274
+
275
+ # Step 3: Track cumulative species using a set
276
+ seen_species = set()
277
+ cumulative = []
278
+
279
+ for time, group in df_sparse_unique.groupby("time"):
280
+ new_species = set(group["species"])
281
+ seen_species.update(new_species)
282
+ cumulative.append((time, len(seen_species)))
283
+
284
+ # Step 4: Create cumulative DataFrame
285
+ df_cumulative = pd.DataFrame(cumulative, columns=["time", "cumulative_species"])
286
+ df_cumulative["time"] = pd.to_datetime(df_cumulative["time"], format="%Y-%M", errors="coerce")
287
+
288
+ return df_sparse, df_cumulative
289
+
290
+ def plot_cumsum(df_cumulative):
291
+ """
292
+ Create a plot of the cumulative number of species.
293
+
294
+ Parameters
295
+ ----------
296
+ df_cumulative : pandas.DataFrame
297
+ Datagrame containing the cumulative number over tume.
298
+
299
+ Returns
300
+ -------
301
+ matplotlib.plot
302
+ A plot of the cumulative number of species.
303
+ """
304
+
305
+ plt.figure(figsize=(10, 5))
306
+ plt.plot(df_cumulative["time"], df_cumulative["cumulative_species"], marker="o")
307
+ plt.title("Cumulative Unique Species Over Time")
308
+ plt.xlabel("Time")
309
+ plt.ylabel("Unique Species Observed")
310
+ plt.xticks(rotation=45)
311
+ plt.grid(True)
312
+ plt.tight_layout()
313
+ plt.show()
314
+
315
+
316
+ def filter_multiple_cells(df_sparse):
317
+ """
318
+ Only count a species established when it is present in more than one cell.
319
+
320
+ Parameters
321
+ ----------
322
+ df_sparse : pandas.DataFrame
323
+ Datagrame containing the species richness per grid cell.
324
+
325
+ Returns
326
+ -------
327
+ pandas.DataFrame
328
+ Cumulative species when in multiple cells.
329
+ """
330
+ # Ensure cell is in your DataFrame
331
+ assert "cell" in df_sparse.columns
332
+
333
+ # 1. Count unique cells per (time, species)
334
+ species_cell_counts = (
335
+ df_sparse.groupby(["time", "species"])["cell"]
336
+ .nunique()
337
+ .reset_index(name="cell_count")
338
+ )
339
+
340
+ # 2. Keep only species seen in at least 2 cells
341
+ species_multi_cell = species_cell_counts[species_cell_counts["cell_count"] >= 2]
342
+
343
+ # 3. Track cumulative set of species across time
344
+ species_multi_cell = species_multi_cell.sort_values("time")
345
+
346
+ seen_species = set()
347
+ cumulative = []
348
+
349
+ for time, group in species_multi_cell.groupby("time"):
350
+ new_species = set(group["species"])
351
+ seen_species.update(new_species)
352
+ cumulative.append((time, len(seen_species)))
353
+
354
+ df_cumulative_cells = pd.DataFrame(cumulative, columns=["time", "cumulative_species_2cells"])
355
+
356
+ return df_cumulative_cells
357
+
358
+ def filter_multiple_occ(df_sparse):
359
+ """
360
+ Only count a species established when there are multiple occurrences in a cell.
361
+
362
+ Parameters
363
+ ----------
364
+ df_sparse : pandas.DataFrame
365
+ Datagrame containing the species richness per grid cell.
366
+
367
+ Returns
368
+ -------
369
+ pandas.DataFrame
370
+ Cumulative species when multiple occurrences in a cell.
371
+ """
372
+ # Ensure 'occurrences' and 'cell' are present
373
+ assert "occurrences" in df_sparse.columns and "cell" in df_sparse.columns
374
+
375
+ # 1. Total occurrences per (time, species, cell)
376
+ species_cell_occ = (
377
+ df_sparse.groupby(["time", "species", "cell"])["occurrences"]
378
+ .sum()
379
+ .reset_index()
380
+ )
381
+
382
+ # 2. Filter for species that had ≥ 2 occurrences in any cell
383
+ species_with_2occ = (
384
+ species_cell_occ[species_cell_occ["occurrences"] >= 2]
385
+ .drop_duplicates(subset=["time", "species"])
386
+ )
387
+
388
+ # 3. Cumulative species count logic
389
+ species_with_2occ = species_with_2occ.sort_values("time")
390
+
391
+ seen_species = set()
392
+ cumulative = []
393
+
394
+ for time, group in species_with_2occ.groupby("time"):
395
+ new_species = set(group["species"])
396
+ seen_species.update(new_species)
397
+ cumulative.append((time, len(seen_species)))
398
+
399
+ df_cumulative_occ = pd.DataFrame(cumulative, columns=["time", "cumulative_species_2occ"])
400
+
401
+ return df_cumulative_occ
402
+
403
+ def calculate_rate(df_cumulative):
404
+ """
405
+ Calculate the rate of establishment from the cumulative distribution.
406
+
407
+ Parameters
408
+ ----------
409
+ df_cumulative : pandas.DataFrame
410
+ Datagrame containing the cumulative distribution.
411
+
412
+ Returns
413
+ -------
414
+ s1 : pandas.Series
415
+ Series of the time axis.
416
+ s2 : pandas.Series
417
+ Series of the rate of establishment.
418
+ """
419
+ # --- Processing GBIF data (Monthly) to get an approximate annual rate ---
420
+ df_cumulative["time"] = pd.to_datetime(df_cumulative["time"])
421
+ df_cumulative_rate = df_cumulative.sort_values(by="time").copy()
422
+
423
+ # Group data by year and calculate the total species difference for each year
424
+ annual_data = df_cumulative_rate.groupby(df_cumulative_rate['time'].dt.year).agg(
425
+ cumulative_species=('cumulative_species', 'last'), # Get the last cumulative species value for the year
426
+ first_time=('time', 'first') # Get the first timestamp for the year
427
+ )
428
+
429
+ # Calculate annual rate using the grouped data
430
+ annual_rate_gbif = []
431
+ annual_time_gbif = []
432
+
433
+ for i in range(1, len(annual_data)):
434
+ current_year_data = annual_data.iloc[i]
435
+ previous_year_data = annual_data.iloc[i - 1]
436
+
437
+ species_diff = current_year_data['cumulative_species'] - previous_year_data['cumulative_species']
438
+ time_diff_years = (current_year_data['first_time'] - previous_year_data['first_time']).days / 365.25
439
+
440
+ annual_rate = species_diff / time_diff_years
441
+ annual_rate_gbif.append(annual_rate)
442
+ annual_time_gbif.append(current_year_data.name) # Year is the index after groupby
443
+
444
+ annual_time_gbif = [int(year) for year in annual_time_gbif]
445
+
446
+ # Convert the lists to Pandas Series for easier plotting
447
+ annual_rate_gbif_series = pd.Series(annual_rate_gbif)
448
+ annual_time_gbif_series = pd.Series(annual_time_gbif)
449
+
450
+ return annual_time_gbif, annual_rate_gbif
451
+
452
+
453
+ def get_survey_effort(cube, dateFormat='%Y-%m', calc_type='total'):
454
+ """
455
+ Estimate the survey effort in an OccurrenceCube.
456
+
457
+ Parameters
458
+ ----------
459
+ cube : b3alien.b3cube.OccurrenceCube
460
+ Species OccurrenceCube from GBIF.
461
+ dateFormat : str, optional
462
+ Dateformat stored in the OccurrenceCube. Default is '%Y-%m'
463
+ calc_type : str, optional
464
+ Type of survey effort to be calculated.
465
+ 'distinct' : total number of distinct observers
466
+ 'total' : total number of occurrences
467
+ Default is total.
468
+
469
+ Returns
470
+ -------
471
+ df : pandas.DataFrame
472
+ Dataframe containing time and the chosen measurement for survey effort.
473
+ """
474
+
475
+
476
+ if calc_type == 'distinct':
477
+ # Group by 'yearmonth' and sum
478
+ distinct_observers_over_time = cube.df.groupby('yearmonth', observed=True)['distinctobservers'].sum()
479
+
480
+ # Convert the index to datetime (if it's not already)
481
+ distinct_observers_over_time.index = pd.to_datetime(
482
+ distinct_observers_over_time.index, format=dateFormat, errors='coerce'
483
+ )
484
+
485
+ # Resample to yearly frequency using new 'YE' standard
486
+ distinct_observers_yearly = distinct_observers_over_time.resample('YE').sum()
487
+
488
+ # Filter for years from 1900 onward
489
+ distinct_observers_filtered = distinct_observers_yearly[distinct_observers_yearly.index.year >= 1900]
490
+
491
+ # Convert to DataFrame
492
+ df = distinct_observers_filtered.reset_index()
493
+ df.columns = ['date', 'distinct_observers'] # Rename columns for clarity
494
+
495
+ return df
496
+
497
+ else:
498
+
499
+ total_occurrences_over_time = cube.data.sum(dim=['cell', 'species'])
500
+
501
+ # Convert the time coordinates to datetime objects if they aren't already
502
+ total_occurrences_over_time['time'] = pd.to_datetime(total_occurrences_over_time['time'].values, format=dateFormat, errors='coerce')
503
+ df = pd.DataFrame({
504
+ 'time': total_occurrences_over_time['time'].values,
505
+ 'total_occurrences': total_occurrences_over_time.data.data
506
+ })
507
+
508
+ # (Optional) Drop rows with invalid or missing time
509
+ df = df.dropna(subset=['time'])
510
+
511
+ return df
@@ -0,0 +1,14 @@
1
+ """
2
+
3
+ GRIIS checklist functions
4
+ =========================
5
+
6
+ """
7
+
8
+ from .griis import CheckList
9
+ from .griis import get_speciesKey
10
+ from .griis import do_taxon_matching
11
+ from .griis import read_checklist
12
+ from .griis import split_event_date
13
+
14
+ __all__ = ["get_speciesKey", "do_taxon_matching", "read_checklist", "split_event_date"]
b3alien/griis/griis.py ADDED
@@ -0,0 +1,198 @@
1
+ import pandas as pd
2
+ from tqdm import tqdm
3
+ tqdm.pandas() # enables .progress_apply
4
+ from pygbif import species
5
+ import numpy as np
6
+
7
+ class CheckList():
8
+
9
+ """
10
+ Load a GRIIS checklist from GBIF.
11
+
12
+ Parameters
13
+ ----------
14
+ filepath : str
15
+ Path to the distribution.txt file of the checklist.
16
+
17
+ Returns
18
+ -------
19
+ griis.Checklist
20
+ A checklist object containing the list of species.
21
+ """
22
+
23
+ def __init__(self, filePath: str):
24
+ self.filePath = filePath
25
+
26
+ # Create cube
27
+ self.species = self._load_GRIIS(filePath)
28
+
29
+ def _load_GRIIS(self, filePath):
30
+
31
+ df_merged = pd.read_csv(filePath, sep="\t")
32
+ species_to_keep = df_merged["speciesKey"].unique()
33
+ species_to_keep = np.where(species_to_keep == 'Uncertain', -1, species_to_keep)
34
+ species_to_keep = species_to_keep.astype(int)
35
+
36
+ return species_to_keep
37
+
38
+
39
+ def get_speciesKey(sciname):
40
+ """
41
+ Match text strings with the GBIF taxonomic backbone.
42
+
43
+ Parameters
44
+ ----------
45
+ sciname : str
46
+ Text string of a scientific name
47
+
48
+ Returns
49
+ -------
50
+ speciesKey: an integer speciesKey number
51
+ """
52
+ result = species.name_backbone(sciname, strict=True)
53
+ try:
54
+ speciesKey = result["speciesKey"]
55
+ except KeyError:
56
+ speciesKey = "Uncertain"
57
+ return speciesKey
58
+
59
+ def split_event_date(eventDate):
60
+ """
61
+ Interprete the event date as introduction date and date of last seen,
62
+ when this information is available in the checklist.
63
+
64
+ Parameters
65
+ ----------
66
+ eventDate : str
67
+ Text string of eventDate
68
+
69
+ Returns
70
+ -------
71
+ pd.Series
72
+ A series containing introduction date ('intro') and date last seen ('outro')
73
+ """
74
+ if isinstance(eventDate, str):
75
+ parts = eventDate.strip().split('/')
76
+ if len(parts) == 2:
77
+ intro = parts[0]
78
+ outro = parts[1]
79
+ else:
80
+ intro = outro = np.nan
81
+ return pd.Series([intro, outro])
82
+ else:
83
+ return pd.Series([np.nan, np.nan])
84
+
85
+
86
+ def do_taxon_matching(dirPath):
87
+ """
88
+ Match keys between taxon.txt and distribution.txt
89
+
90
+ Parameters
91
+ ----------
92
+ dirPath : str
93
+ Path to the directory of the checklist
94
+
95
+ Returns
96
+ -------
97
+ Saves a new checklist file 'merged_distr.txt' in the checklist directory
98
+ """
99
+
100
+ taxon = dirPath + "taxon.txt"
101
+ distribution = dirPath + "distribution.txt"
102
+
103
+ df_t = pd.read_csv(taxon, sep="\t")
104
+ df_dist = pd.read_csv(distribution, sep="\t")
105
+
106
+ # Now apply this on the whole dataframe
107
+
108
+ df_t["speciesKey"] = df_t["scientificName"].progress_apply(get_speciesKey)
109
+
110
+ df_merged = df_dist.merge(df_t[['id', 'speciesKey']], on='id', how='left')
111
+ df_merged.to_csv(dirPath + 'merged_distr.txt', sep='\t', index=False)
112
+
113
+ # The rest assumes already a merged dataset
114
+ def read_checklist(filePath, cl_type='detailed', locality='Belgium'):
115
+
116
+ distribution = filePath + "distribution.txt"
117
+
118
+ df_cl = pd.read_csv(distribution, sep='\t', low_memory=False)
119
+
120
+ df_cl["speciesKey"] = df_cl["id"].str.rsplit("/", n=1).str[-1].astype("int64")
121
+
122
+
123
+ if cl_type == 'detailed':
124
+
125
+ species_to_keep = df_cl["speciesKey"].astype("int64").unique()
126
+
127
+ # 1. Filter rows where locality == 'Belgium' and eventDate is not missing
128
+ df = df_cl[df_cl["locality"] == locality].copy()
129
+ df = df[df["eventDate"].notna()]
130
+
131
+
132
+
133
+ df[["introDate", "outroDate"]] = df["eventDate"].apply(split_event_date)
134
+
135
+ df["introDate"] = pd.to_datetime(df["introDate"], format="%Y", errors="coerce")
136
+ df["outroDate"] = pd.to_datetime(df["outroDate"], format="%Y", errors="coerce")
137
+
138
+
139
+ # 3. Clean rows with missing introDate
140
+ df_intro = df.dropna(subset=["introDate"]).copy()
141
+
142
+ # 4. Group by introDate and count species
143
+ in_species = (
144
+ df_intro.groupby("introDate", sort=True)["id"]
145
+ .count()
146
+ .reset_index(name="nspec")
147
+ )
148
+
149
+ # 5. Cumulative sum
150
+ in_species["cumn"] = in_species["nspec"].cumsum()
151
+
152
+ # 6. Clean outro side and count outgoing species
153
+ df_outro = df.dropna(subset=["outroDate"]).copy()
154
+
155
+ out_species = (
156
+ df_outro.groupby("outroDate", sort=True)["id"]
157
+ .count()
158
+ .reset_index(name="nspeco")
159
+ )
160
+
161
+ # 7. Merge intro and outro on date
162
+ n_species = pd.merge(in_species, out_species, how="outer", left_on="introDate", right_on="outroDate")
163
+
164
+ # 8. Replace NaNs with 0
165
+ n_species["nspec"] = n_species["nspec"].fillna(0).astype(int)
166
+ n_species["nspeco"] = n_species["nspeco"].fillna(0).astype(int)
167
+
168
+ # 9. Net species present at each time step
169
+ n_species["total"] = n_species["nspec"] - n_species["nspeco"]
170
+
171
+ # 10. Final frame with total species over time
172
+ tot_species = n_species[["introDate", "total"]].copy()
173
+
174
+ # 11. Optional: sort and compute cumulative total over time
175
+ tot_species = tot_species.sort_values("introDate")
176
+ tot_species["cumulative_total"] = tot_species["total"].cumsum()
177
+
178
+ return tot_species
179
+
180
+ else:
181
+ taxon = filePath + "taxon.txt"
182
+ distribution = filePath + "distribution.txt"
183
+
184
+ df_t = pd.read_csv(taxon, sep="\t")
185
+ df_dist = pd.read_csv(distribution, sep="\t")
186
+
187
+
188
+ # Now apply this on the whole dataframe
189
+
190
+ df_t["speciesKey"] = df_t["scientificName"].apply(get_speciesKey)
191
+
192
+ df_merged = df_dist.merge(df_t[['id', 'speciesKey']], on='id', how='left')
193
+
194
+ species_to_keep = df_merged["speciesKey"].unique()
195
+ species_to_keep = np.where(species_to_keep == 'Uncertain', -1, species_to_keep)
196
+ species_to_keep = species_to_keep.astype(int)
197
+
198
+ return species_to_keep
@@ -0,0 +1,11 @@
1
+ """
2
+
3
+ Simulation functions
4
+ ====================
5
+
6
+
7
+ """
8
+
9
+ from .simulation import simulate_solow_costello
10
+
11
+ __all__ = ["simulate_solow_costello"]
@@ -0,0 +1,126 @@
1
+ import numpy as np
2
+ import pandas as pd
3
+ import matplotlib.pyplot as plt
4
+ from scipy.optimize import minimize
5
+ from scipy.optimize import fmin
6
+
7
+ def count_m(t, params):
8
+ """Calculates the mean, mu, from Solow and Costello (2004)."""
9
+ m0 = params[0]
10
+ m1 = params[1]
11
+ m = np.exp(m0 + m1 * t)
12
+ return m
13
+
14
+ def count_pi(s, t, params):
15
+ """Calculates the variable pi from Solow and Costello (2004)."""
16
+ pi0 = params[2]
17
+ pi1 = params[3]
18
+ pi2 = params[4]
19
+ exponent = np.clip(pi0 + pi1 * t + pi2 * np.exp(t - s), -700, 700)
20
+ num = np.exp(exponent)
21
+ pi = np.divide(num, (1 + num), out=np.zeros_like(num), where=(1 + num) != 0)
22
+ pi = np.where(np.isinf(num), 1, pi)
23
+ return pi
24
+
25
+ def count_p(t, params):
26
+ """Calculates the value p from Solow and Costello (2004).
27
+ It uses matrix coding for efficiency.
28
+ """
29
+ S = np.tile(np.arange(1, t + 1), (t, 1))
30
+ thing = 1 - count_pi(S, S.T, params)
31
+ thing[t - 1, :] = 1
32
+ up = np.triu(np.ones_like(thing), 1)
33
+ thing2 = np.tril(thing) + up
34
+ product = np.prod(thing2, axis=0)
35
+ pst = product * count_pi(np.arange(1, t + 1), t, params)
36
+ return pst
37
+
38
+ def count_lambda(params, N):
39
+ """
40
+ This function calculates lambda from Solow and Costello, 2004.
41
+ params is a vector of parameters
42
+ """
43
+ lambda_result = np.zeros(N)
44
+ for t in range(1, N + 1):
45
+ S = np.arange(1, t + 1)
46
+ Am = count_m(S, params)
47
+ Ap = count_p(t, params)
48
+ lambda_result[t - 1] = np.dot(Am, Ap)
49
+ return lambda_result
50
+
51
+ def count_log_like(params, restrict, num_discov):
52
+ """
53
+ This function file calculates the log likelihood function for Solow and
54
+ Costello (2004). It takes into account any possible restrictions (See
55
+ below)
56
+
57
+ params is a vector of parameters
58
+ restrict is a vector (same size as params) that places restrictions on the
59
+ parameters. If restrict[i]=99, then there is no restriction for the ith
60
+ parameter. If restrict[i]=0 (for example) then the restriction is exactly
61
+ that.
62
+ """
63
+
64
+ f = np.where(restrict != 99)[0]
65
+ g = np.where(restrict == 99)[0]
66
+ new_params = params.copy()
67
+ new_params[g] = params[g]
68
+ new_params[f] = restrict[f]
69
+
70
+ summand2 = np.zeros_like(num_discov, dtype=float)
71
+ lambda_values = np.zeros_like(num_discov, dtype=float)
72
+
73
+ for t in range(1, len(num_discov) + 1):
74
+ S = np.arange(1, t + 1)
75
+ Am = count_m(S, new_params)
76
+ Ap = count_p(t, new_params)
77
+ lambda_t = np.dot(Am, Ap)
78
+ lambda_values[t - 1] = lambda_t
79
+ summand2[t - 1] = num_discov[t - 1] * np.log(lambda_t) - lambda_t if lambda_t > 0 else -lambda_t
80
+
81
+ LL = -np.sum(summand2)
82
+ return LL, lambda_values
83
+
84
+
85
+ def simulate_solow_costello(annual_time_gbif, annual_rate_gbif, vis=False):
86
+ """
87
+ Solow-Costello simulation of the rate of establishment.
88
+
89
+ Parameters
90
+ ----------
91
+ annual_time_gbif : pandas.Series
92
+ Time series of the rate of establishment.
93
+ annual_rate_gbif : pandas.Series
94
+ Rates corresponding to the time series.
95
+ vis : bool, optional
96
+ Create a plot of the simulation. Default is False.
97
+
98
+ Returns
99
+ -------
100
+ C1: numpy.Series
101
+ Result of the simulation.
102
+ """
103
+
104
+ # global num_discov; # No need for global, pass as argument
105
+ num_discov = pd.Series(annual_rate_gbif).T # Load and transpose
106
+ T = pd.Series(annual_time_gbif) #np.arange(1851, 1996) # Create the time period
107
+ # options = optimset('TolFun',.01,'TolX',.01); # Tolerance is handled differently in scipy
108
+
109
+ guess = np.array([-1.1106, 0.0135, -1.4534, 0.1, 0.1]) # Initial guess
110
+ constr = 99 * np.ones_like(guess) # Constraint vector
111
+
112
+ vec1 = fmin(lambda x: count_log_like(x, constr, num_discov)[0], guess, xtol=0.01, ftol=0.01)
113
+ val1 = count_log_like(vec1, constr, num_discov)[0] # Get the function value at the minimum
114
+
115
+
116
+ C1 = count_lambda(vec1, len(num_discov)) # Calculate the mean of Y
117
+
118
+ if vis:
119
+ # Create the plot
120
+ plt.plot(T, np.cumsum(num_discov), 'k-', T, np.cumsum(C1), 'k--')
121
+ plt.legend(['Discoveries', 'Unrestricted'])
122
+ plt.xlabel('Time')
123
+ plt.ylabel('Cumulative Discovery')
124
+ plt.show()
125
+
126
+ return C1
b3alien/utils.py ADDED
@@ -0,0 +1,73 @@
1
+ """
2
+ Additional utilities
3
+
4
+ """
5
+ import pandas as pd
6
+ import geopandas as gpd
7
+ import pyarrow
8
+
9
+
10
+ def detect_runtime():
11
+ """
12
+ Detects the runtime environment where the code is executed.
13
+
14
+ Returns:
15
+ str: One of "Jupyter Notebook", "IPython Terminal", or "Standard Python Script"
16
+ """
17
+ try:
18
+ from IPython import get_ipython
19
+ shell = get_ipython().__class__.__name__
20
+ if shell == 'ZMQInteractiveShell':
21
+ return "Jupyter Notebook"
22
+ elif shell == 'TerminalInteractiveShell':
23
+ return "IPython Terminal"
24
+ else:
25
+ return "Other IPython"
26
+ except (ImportError, AttributeError, NameError):
27
+ return "Standard Python Script"
28
+
29
+ def in_jupyter():
30
+ """Returns True if running inside a Jupyter Notebook or Lab."""
31
+ return detect_runtime() == "Jupyter Notebook"
32
+
33
+ def in_ipython():
34
+ """Returns True if running inside any IPython shell (not standard Python)."""
35
+ return detect_runtime() != "Standard Python Script"
36
+
37
+ def in_script():
38
+ """Returns True if running in a standard Python script (non-interactive)."""
39
+ return detect_runtime() == "Standard Python Script"
40
+
41
+ def to_geoparquet(csvFile, geoFile, leftID=eqdcellcode, rightID=cellCode, exportPath='./data/export.parquet'):
42
+ """
43
+ Convert a GBIF cube download into a GeoParquet file, using the geometry of a GPKG
44
+
45
+ Parameters
46
+ ----------
47
+ csvFile : str
48
+ Path to the GBIF cube csv file.
49
+ geoFile : str
50
+ Path to the GeoPackage file.
51
+ leftID : str, optional
52
+ Column name within the GBIF cube to match the geometry. Default is 'edqcellcode'.
53
+ rightID : str, optional
54
+ Column name within the GeoPackage geometry. Default is 'cellCode'
55
+ exportPath : str, optional
56
+ Path to which the GeoParquet file needs to be exported.
57
+
58
+ Returns
59
+ -------
60
+ A GeoParquet file at the location of exportPath
61
+ """
62
+
63
+ data = pd.read_csv(csvFile, sep='\t')
64
+ geoRef = gpd.read_file(geoFile, engine='pyogrio', use_arrow=True, crs="EPSG:4326")
65
+
66
+ test_merge = pd.merge(data, qdgc_ref, left_on=leftID, right_on=rightID)
67
+
68
+ gdf = gpd.GeoDataFrame(test_merge, geometry='geometry')
69
+ if gdf.crs is None:
70
+ gdf.set_crs(crs, inplace=True)
71
+
72
+ gdf.to_parquet(exportPath, engine="pyarrow", index=False)
73
+
@@ -0,0 +1,13 @@
1
+ """
2
+
3
+ Visualisation of Data Cubes and the resulting simulations
4
+ =========================================================
5
+
6
+ """
7
+
8
+
9
+ from .b3gee import initialize
10
+ from .b3gee import gdf_to_ee_featurecollection
11
+ from .visualisation import visualize_ee_layers
12
+
13
+ __all__ = ["initialize", "gdf_to_ee_featurecollection", "visualize_ee_layers"]
@@ -0,0 +1,35 @@
1
+ import ee
2
+ import json
3
+
4
+ def initialize(project):
5
+ """
6
+ Inititialise a Google project
7
+
8
+ Parameters
9
+ ----------
10
+ project : str
11
+ Name of your Google project
12
+ """
13
+ try:
14
+ ee.Initialize()
15
+ except Exception as e:
16
+ ee.Authenticate()
17
+ ee.Initialize(project=project)
18
+
19
+ def gdf_to_ee_featurecollection(gdf):
20
+ """
21
+ Transform a GeoDataFrame in to an Earth Engine Feature collection
22
+
23
+ Parameters
24
+ ----------
25
+ gdf : geopandas.DataFrame
26
+ Species OccurrenceCube from GBIF.
27
+
28
+ Returns
29
+ -------
30
+ ee.FeatureCollection
31
+ """
32
+ geojson = json.loads(gdf.to_json())
33
+ return ee.FeatureCollection(geojson)
34
+
35
+
@@ -0,0 +1,68 @@
1
+ import ee
2
+ import geemap
3
+ import folium
4
+ import tempfile
5
+ import webbrowser
6
+
7
+ def add_ee_layer(self, ee_object, vis_params, name):
8
+ try:
9
+ if isinstance(ee_object, ee.image.Image):
10
+ map_id_dict = ee_object.getMapId(vis_params)
11
+ folium.raster_layers.TileLayer(
12
+ tiles=map_id_dict['tile_fetcher'].url_format,
13
+ attr='Google Earth Engine',
14
+ name=name,
15
+ overlay=True,
16
+ control=True
17
+ ).add_to(self)
18
+ elif isinstance(ee_object, ee.featurecollection.FeatureCollection):
19
+ # Style the FeatureCollection (example with color)
20
+ styled_fc = ee_object.style(**(vis_params or {'color': 'FF0000'}))
21
+ map_id_dict = styled_fc.getMapId({})
22
+ folium.raster_layers.TileLayer(
23
+ tiles=map_id_dict['tile_fetcher'].url_format,
24
+ attr='Google Earth Engine',
25
+ name=name,
26
+ overlay=True,
27
+ control=True
28
+ ).add_to(self)
29
+ else:
30
+ raise TypeError(f"Unsupported ee object type: {type(ee_object)}")
31
+ except Exception as e:
32
+ print("Could not add layer:", e)
33
+
34
+
35
+
36
+ # Patch Folium to support EE layers
37
+ def patch_folium():
38
+ folium.Map.add_ee_layer = add_ee_layer
39
+
40
+
41
+ def visualize_ee_layers(layers, center=[0, 0], zoom=2, save_path=None, show=True):
42
+ """
43
+ Visualize multiple Earth Engine layers on a folium map.
44
+
45
+ Parameters:
46
+ layers: List of (ee_object, vis_params, layer_name)
47
+ center: Center [lat, lon] for map
48
+ zoom: Zoom level
49
+ save_path: Optional filepath to save the HTML map
50
+ show: If True, opens the map in the browser
51
+ """
52
+ m = folium.Map(location=center, zoom_start=zoom)
53
+
54
+ for ee_object, vis_params, name in layers:
55
+ m.add_ee_layer(ee_object, vis_params, name)
56
+
57
+ folium.LayerControl().add_to(m)
58
+
59
+ if save_path:
60
+ m.save(save_path)
61
+ print(f"Map saved to {save_path}")
62
+ if show:
63
+ webbrowser.open(f"file://{os.path.abspath(save_path)}")
64
+ else:
65
+ with tempfile.NamedTemporaryFile(delete=False, suffix=".html") as f:
66
+ m.save(f.name)
67
+ if show:
68
+ webbrowser.open(f.name)
@@ -0,0 +1,225 @@
1
+ Metadata-Version: 2.4
2
+ Name: b3alien
3
+ Version: 0.0.1
4
+ Summary: Calculating the CBD target 6.1 indicator from occurrence cubes
5
+ Author-email: Maarten Trekels <maarten.trekels@plantentuinmeise.be>
6
+ License: MIT
7
+ Project-URL: homepage, https://github.com/mtrekels/b3alien
8
+ Project-URL: repository, https://github.com/mtrekels/b3alien
9
+ Project-URL: documentation, https://b3alien.readthedocs.io/
10
+ Requires-Python: >=3.9
11
+ Description-Content-Type: text/markdown
12
+ License-File: LICENSE
13
+ Requires-Dist: aiohappyeyeballs>=2.6.1
14
+ Requires-Dist: aiohttp>=3.11.18
15
+ Requires-Dist: aiosignal>=1.3.2
16
+ Requires-Dist: alabaster>=1.0.0
17
+ Requires-Dist: appdirs>=1.4.4
18
+ Requires-Dist: asttokens>=3.0.0
19
+ Requires-Dist: async-timeout>=5.0.1
20
+ Requires-Dist: attrs>=25.3.0
21
+ Requires-Dist: backports.tarfile>=1.2.0
22
+ Requires-Dist: bqplot>=0.12.44
23
+ Requires-Dist: branca>=0.8.1
24
+ Requires-Dist: build>=1.2.2.post1
25
+ Requires-Dist: cachetools>=5.5.2
26
+ Requires-Dist: cattrs>=24.1.3
27
+ Requires-Dist: certifi>=2025.4.26
28
+ Requires-Dist: cffi>=1.17.1
29
+ Requires-Dist: charset-normalizer>=3.4.2
30
+ Requires-Dist: click>=8.1.8
31
+ Requires-Dist: cloudpickle>=3.1.1
32
+ Requires-Dist: colour>=0.1.5
33
+ Requires-Dist: comm>=0.2.2
34
+ Requires-Dist: contourpy>=1.3.2
35
+ Requires-Dist: coverage>=7.8.0
36
+ Requires-Dist: cryptography>=44.0.3
37
+ Requires-Dist: cycler>=0.12.1
38
+ Requires-Dist: dask>=2025.4.1
39
+ Requires-Dist: decorator>=5.2.1
40
+ Requires-Dist: docutils>=0.21.2
41
+ Requires-Dist: earthengine-api>=1.5.13
42
+ Requires-Dist: eerepr>=0.1.1
43
+ Requires-Dist: exceptiongroup>=1.2.2
44
+ Requires-Dist: executing>=2.2.0
45
+ Requires-Dist: folium>=0.19.5
46
+ Requires-Dist: fonttools>=4.57.0
47
+ Requires-Dist: frozenlist>=1.6.0
48
+ Requires-Dist: fsspec>=2025.3.2
49
+ Requires-Dist: future>=1.0.0
50
+ Requires-Dist: gcsfs>=2025.3.2
51
+ Requires-Dist: geemap>=0.35.3
52
+ Requires-Dist: geocoder>=1.38.1
53
+ Requires-Dist: geojson-rewind>=1.1.0
54
+ Requires-Dist: geomet>=1.1.0
55
+ Requires-Dist: geopandas>=1.0.1
56
+ Requires-Dist: google-api-core>=2.24.2
57
+ Requires-Dist: google-api-python-client>=2.169.0
58
+ Requires-Dist: google-auth>=2.39.0
59
+ Requires-Dist: google-auth-httplib2>=0.2.0
60
+ Requires-Dist: google-auth-oauthlib>=1.2.2
61
+ Requires-Dist: google-cloud-core>=2.4.3
62
+ Requires-Dist: google-cloud-storage>=3.1.0
63
+ Requires-Dist: google-crc32c>=1.7.1
64
+ Requires-Dist: google-resumable-media>=2.7.2
65
+ Requires-Dist: googleapis-common-protos>=1.70.0
66
+ Requires-Dist: httplib2>=0.22.0
67
+ Requires-Dist: id>=1.5.0
68
+ Requires-Dist: idna>=3.10
69
+ Requires-Dist: imagesize>=1.4.1
70
+ Requires-Dist: importlib_metadata>=8.7.0
71
+ Requires-Dist: iniconfig>=2.1.0
72
+ Requires-Dist: ipyevents>=2.0.2
73
+ Requires-Dist: ipyfilechooser>=0.6.0
74
+ Requires-Dist: ipyleaflet>=0.19.2
75
+ Requires-Dist: ipython>=8.36.0
76
+ Requires-Dist: ipytree>=0.2.2
77
+ Requires-Dist: ipywidgets>=8.1.6
78
+ Requires-Dist: jaraco.classes>=3.4.0
79
+ Requires-Dist: jaraco.context>=6.0.1
80
+ Requires-Dist: jaraco.functools>=4.1.0
81
+ Requires-Dist: jedi>=0.19.2
82
+ Requires-Dist: jeepney>=0.9.0
83
+ Requires-Dist: Jinja2>=3.1.6
84
+ Requires-Dist: jupyter-leaflet>=0.19.2
85
+ Requires-Dist: jupyterlab_widgets>=3.0.14
86
+ Requires-Dist: keyring>=25.6.0
87
+ Requires-Dist: kiwisolver>=1.4.8
88
+ Requires-Dist: llvmlite>=0.44.0
89
+ Requires-Dist: locket>=1.0.0
90
+ Requires-Dist: markdown-it-py>=3.0.0
91
+ Requires-Dist: MarkupSafe>=3.0.2
92
+ Requires-Dist: matplotlib>=3.10.1
93
+ Requires-Dist: matplotlib-inline>=0.1.7
94
+ Requires-Dist: mdit-py-plugins>=0.4.2
95
+ Requires-Dist: mdurl>=0.1.2
96
+ Requires-Dist: more-itertools>=10.7.0
97
+ Requires-Dist: multidict>=6.4.3
98
+ Requires-Dist: myst-parser>=4.0.1
99
+ Requires-Dist: narwhals>=1.37.1
100
+ Requires-Dist: nh3>=0.2.21
101
+ Requires-Dist: numba>=0.61.2
102
+ Requires-Dist: numpy>=2.2.5
103
+ Requires-Dist: oauthlib>=3.2.2
104
+ Requires-Dist: packaging>=25.0
105
+ Requires-Dist: pandas>=2.2.3
106
+ Requires-Dist: parso>=0.8.4
107
+ Requires-Dist: partd>=1.4.2
108
+ Requires-Dist: pexpect>=4.9.0
109
+ Requires-Dist: pillow>=11.2.1
110
+ Requires-Dist: platformdirs>=4.3.7
111
+ Requires-Dist: plotly>=6.0.1
112
+ Requires-Dist: pluggy>=1.5.0
113
+ Requires-Dist: prompt_toolkit>=3.0.51
114
+ Requires-Dist: propcache>=0.3.1
115
+ Requires-Dist: proto-plus>=1.26.1
116
+ Requires-Dist: protobuf>=6.30.2
117
+ Requires-Dist: ptyprocess>=0.7.0
118
+ Requires-Dist: pure_eval>=0.2.3
119
+ Requires-Dist: pyarrow>=20.0.0
120
+ Requires-Dist: pyasn1>=0.6.1
121
+ Requires-Dist: pyasn1_modules>=0.4.2
122
+ Requires-Dist: pycparser>=2.22
123
+ Requires-Dist: pygbif>=0.6.5
124
+ Requires-Dist: Pygments>=2.19.1
125
+ Requires-Dist: pyogrio>=0.10.0
126
+ Requires-Dist: pyparsing>=3.2.3
127
+ Requires-Dist: pyperclip>=1.9.0
128
+ Requires-Dist: pyproj>=3.7.1
129
+ Requires-Dist: pyproject_hooks>=1.2.0
130
+ Requires-Dist: pyshp>=2.3.1
131
+ Requires-Dist: pytest>=8.3.5
132
+ Requires-Dist: pytest-cov>=6.1.1
133
+ Requires-Dist: python-box>=7.3.2
134
+ Requires-Dist: python-dateutil>=2.9.0.post0
135
+ Requires-Dist: pytz>=2025.2
136
+ Requires-Dist: PyYAML>=6.0.2
137
+ Requires-Dist: ratelim>=0.1.6
138
+ Requires-Dist: readme_renderer>=44.0
139
+ Requires-Dist: requests>=2.32.3
140
+ Requires-Dist: requests-cache>=1.2.1
141
+ Requires-Dist: requests-oauthlib>=2.0.0
142
+ Requires-Dist: requests-toolbelt>=1.0.0
143
+ Requires-Dist: rfc3986>=2.0.0
144
+ Requires-Dist: rich>=14.0.0
145
+ Requires-Dist: rsa>=4.9.1
146
+ Requires-Dist: scipy>=1.15.2
147
+ Requires-Dist: scooby>=0.10.1
148
+ Requires-Dist: SecretStorage>=3.3.3
149
+ Requires-Dist: shapely>=2.1.0
150
+ Requires-Dist: six>=1.17.0
151
+ Requires-Dist: snowballstemmer>=3.0.0.1
152
+ Requires-Dist: sparse>=0.16.0
153
+ Requires-Dist: Sphinx>=8.1.3
154
+ Requires-Dist: sphinx-rtd-theme>=3.0.2
155
+ Requires-Dist: sphinxcontrib-applehelp>=2.0.0
156
+ Requires-Dist: sphinxcontrib-devhelp>=2.0.0
157
+ Requires-Dist: sphinxcontrib-htmlhelp>=2.1.0
158
+ Requires-Dist: sphinxcontrib-jquery>=4.1
159
+ Requires-Dist: sphinxcontrib-jsmath>=1.0.1
160
+ Requires-Dist: sphinxcontrib-qthelp>=2.0.0
161
+ Requires-Dist: sphinxcontrib-serializinghtml>=2.0.0
162
+ Requires-Dist: stack-data>=0.6.3
163
+ Requires-Dist: tdqm>=0.0.1
164
+ Requires-Dist: tomli>=2.2.1
165
+ Requires-Dist: toolz>=1.0.0
166
+ Requires-Dist: tqdm>=4.67.1
167
+ Requires-Dist: traitlets>=5.14.3
168
+ Requires-Dist: traittypes>=0.2.1
169
+ Requires-Dist: twine>=6.1.0
170
+ Requires-Dist: typing_extensions>=4.13.2
171
+ Requires-Dist: tzdata>=2025.2
172
+ Requires-Dist: uritemplate>=4.1.1
173
+ Requires-Dist: url-normalize>=2.2.1
174
+ Requires-Dist: urllib3>=2.4.0
175
+ Requires-Dist: wcwidth>=0.2.13
176
+ Requires-Dist: widgetsnbextension>=4.0.14
177
+ Requires-Dist: xarray>=2025.4.0
178
+ Requires-Dist: xyzservices>=2025.4.0
179
+ Requires-Dist: yarl>=1.20.0
180
+ Requires-Dist: zipp>=3.21.0
181
+ Provides-Extra: dev
182
+ Requires-Dist: pytest; extra == "dev"
183
+ Requires-Dist: flake8; extra == "dev"
184
+ Dynamic: license-file
185
+
186
+ # b3alien: a Python package to calculate the Target 6.1 headline indicator of the CBD
187
+
188
+
189
+ ## Introduction
190
+
191
+ The historic Kunming-Montreal Global Biodiversity
192
+ Framework, which supports the achievement of the
193
+ Sustainable Development Goals and builds on the
194
+ Convention on Biological Diversity’s (CBD) previous
195
+ Strategic Plans, sets out an ambitious pathway to reach
196
+ the global vision of a world living in harmony with nature
197
+ by 2050. Among the Framework’s key elements are 23
198
+ targets for 2030. In order to track the progress on the
199
+ targets, a number of indicators were agreed upon for
200
+ each target. The B3ALIEN software provides a technical
201
+ solution to track Target 6: “Reduce the Introduction of
202
+ Invasive Alien Species by 50% and Minimize Their
203
+ Impact.” It mainly focusses on the headline indicator: rate
204
+ of invasive alien species establishment, but can provide
205
+ input to some of the complementary indicators.
206
+
207
+ Decision makers at local, regional, national and
208
+ international levels need accurate and reliable
209
+ information about status, trends, threats, and they need
210
+ data presented in an actionable and understandable
211
+ format, with measures of uncertainty. Furthermore, we
212
+ need synthesized data products that can be combined
213
+ with other environmental data, such as climate, soil
214
+ chemistry, land use, altitude... B3ALIEN is built upon the
215
+ concept of data cubes developed in the Horizon Europe
216
+ Biodiversity Building Blocks for Policy project (b-
217
+ cubed.eu). It uses the solid foundations of the GBIF
218
+ infrastructure, where tools such as the GBIF Taxonomic
219
+ Backbone and the Global Registry of Introduced and
220
+ Invasive Species are available by default. Readily available occurrence data is used to determine and estimate
221
+ accurately the rate of introduction of alien species..
222
+
223
+ ## Architecture
224
+
225
+ put schema here
@@ -0,0 +1,16 @@
1
+ b3alien/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
2
+ b3alien/utils.py,sha256=4gyPatTcOY9uD9hZdcBgEpDVL2H9qEihoabmOiBgbTY,2361
3
+ b3alien/b3cube/__init__.py,sha256=oAJXCVhF88Rkl5Z_Fn28WKThwxu63ZDrbOEe_Kpq7TQ,487
4
+ b3alien/b3cube/b3cube.py,sha256=tVmaGPxXJX3YR-joX3W8q7tFuxF8OHflFH5SKKin-dY,17351
5
+ b3alien/griis/__init__.py,sha256=jp_pTU7hPntJBBBOiCBKNdNC37zZ7ggpBARXu8zowKc,321
6
+ b3alien/griis/griis.py,sha256=9M1g3fp-PmJFe32Eg3EB45S9yyvewSXp74FNGbAcboo,5926
7
+ b3alien/simulation/__init__.py,sha256=YEU2Uot9gD8NUseC9xxtpJ5E1JBLcB4AQhtNgV9UELE,140
8
+ b3alien/simulation/simulation.py,sha256=rSl8Lr3-i27WKpJEkymgPXCp-VaAoFATqIMoCtL7RPE,4276
9
+ b3alien/visualisation/__init__.py,sha256=smoKdeDgfZFdNfQnOFwekUY94vGV5LSePlRR1p45TsM,331
10
+ b3alien/visualisation/b3gee.py,sha256=FIANrsk_d451OeKY3fUWAHy7Z3P1hf9t6PX9K6ryYVY,733
11
+ b3alien/visualisation/visualisation.py,sha256=Pvl8e62y4AlkiYKDi0__9XHXTJXvR7AUHtQhiAmygjU,2249
12
+ b3alien-0.0.1.dist-info/licenses/LICENSE,sha256=OpaLK4BvTJYb7yT8EDbVlAZQP6DkkrNUXzPvosajAeE,1077
13
+ b3alien-0.0.1.dist-info/METADATA,sha256=ZSDq_IDMp1gOvXRSX3wvWHyqdsoRy1oGvrKDshdi9fA,8005
14
+ b3alien-0.0.1.dist-info/WHEEL,sha256=DnLRTWE75wApRYVsjgc6wsVswC54sMSJhAEd4xhDpBk,91
15
+ b3alien-0.0.1.dist-info/top_level.txt,sha256=Pvcrm6IMnotsQRwRY2z9XT_ooE-bRZHaZK7DJmkcYrw,8
16
+ b3alien-0.0.1.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (80.4.0)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Botanic Garden Meise
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
+ b3alien