Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/python-package.yml
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ jobs:
python -m pip install -r requirements.txt
- name: Lint with pylint
run: |
python3 -m pylint utils prepare_layers prepare_species
python3 -m pylint utils prepare_layers prepare_species threats
- name: Tests
run: |
python3 -m pytest ./tests
41 changes: 30 additions & 11 deletions prepare_species/extract_species_data_psql.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import argparse
import importlib
import json
import logging
import math
import os
Expand Down Expand Up @@ -38,10 +39,24 @@
"scientific_name",
"family_name",
"class_name",
"threats",
"category",
"category_weight",
"geometry"
]

# From Muir et al: For each species, a global STAR threat abatement (START) score
# is defined. This varies from zero for species of Least Concern to 100
# for Near Threatened, 200 for Vulnerable, 300 for Endangered and
# 400 for Critically Endangered species (using established weighting
# ratios7,8)
CATEGORY_WEIGHTS = {
'NT': 100,
'VU': 200,
'EN': 300,
'CR': 400,
}

MAIN_STATEMENT = """
SELECT
assessments.sis_taxon_id as id_no,
Expand Down Expand Up @@ -84,10 +99,12 @@

THREATS_STATEMENT = """
SELECT
supplementary_fields->>'scope' AS scope,
supplementary_fields->>'severity' AS severity
threat_lookup.code,
assessment_threats.supplementary_fields->>'scope' AS scope,
assessment_threats.supplementary_fields->>'severity' AS severity
FROM
assessment_threats
LEFT JOIN threat_lookup ON assessment_threats.threat_id = threat_lookup.id
WHERE
assessment_id = %s
AND (supplementary_fields->>'timing' is NULL OR supplementary_fields->>'timing' <> 'Past, Unlikely to Return')
Expand Down Expand Up @@ -225,7 +242,7 @@ def process_systems(
]
DEFAULT_SEVERITY = "slow, significant declines"

# Taken from Muir et al 2021, indexed by SCOPE and then SEVERITY
# Taken from Muir et al 2021 Supplementary Table 2, indexed by SCOPE and then SEVERITY
THREAT_WEIGHTING_TABLE = [
[63, 24, 10, 1, 0, 10],
[52, 18, 9, 0, 0, 9],
Expand All @@ -236,18 +253,19 @@ def process_threats(
threat_data: List,
report: SpeciesReport,
) -> bool:
total = 0
for scope, severity in threat_data:
cleaned_threats = []
for code, scope, severity in threat_data:
if scope is None or scope.lower() == "unknown":
scope = DEFAULT_SCOPE
if severity is None or severity.lower() == "unknown":
severity = DEFAULT_SEVERITY
scope_index = SCOPES.index(scope.lower())
severity_index = SEVERITIES.index(severity.lower())
score = THREAT_WEIGHTING_TABLE[scope_index][severity_index]
total += score
report.has_threats = total != 0
return total != 0
if score > 0:
cleaned_threats.append((code, score))
report.has_threats = len(cleaned_threats) > 0
return cleaned_threats

def process_habitats(
habitats_data: List[List[str]],
Expand Down Expand Up @@ -329,7 +347,6 @@ def process_row(
presence += (4,)
report.possibly_extinct = True # pylint: disable=W0201


cursor.execute(SYSTEMS_STATEMENT, (assessment_id,))
systems_data = cursor.fetchall()
try:
Expand All @@ -340,8 +357,8 @@ def process_row(

cursor.execute(THREATS_STATEMENT, (assessment_id,))
raw_threats = cursor.fetchall()
threatened = process_threats(raw_threats, report)
if not threatened:
threats = process_threats(raw_threats, report)
if len(threats) == 0:
return report

cursor.execute(HABITATS_STATEMENT, (assessment_id,))
Expand Down Expand Up @@ -370,7 +387,9 @@ def process_row(
scientific_name,
family_name,
class_name,
json.dumps(threats),
category,
CATEGORY_WEIGHTS[category],
geometry
]],
columns=COLUMNS,
Expand Down
12 changes: 6 additions & 6 deletions tests/test_species_filter.py
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,7 @@ def test_empty_threat_list():

def test_no_serious_threats():
threats_data = [
("Minority (<50%)", "No decline"),
("3.5", "Minority (<50%)", "No decline"),
]
report = SpeciesReport(1, 2, "name")
res = process_threats(threats_data, report)
Expand All @@ -114,19 +114,19 @@ def test_no_serious_threats():

def test_serious_threats():
threats_data = [
("Whole (>90%)", "Very rapid declines"),
("3.4", "Whole (>90%)", "Very rapid declines"),
]
report = SpeciesReport(1, 2, "name")
res = process_threats(threats_data, report)
assert res
assert res == [("3.4", 63)]
assert report.has_threats

def test_mixed_threats():
threats_data = [
("Whole (>90%)", "Very rapid declines"),
("Minority (<50%)", "No decline"),
("3.4", "Whole (>90%)", "Very rapid declines"),
("3.5", "Minority (<50%)", "No decline"),
]
report = SpeciesReport(1, 2, "name")
res = process_threats(threats_data, report)
assert res
assert res == [("3.4", 63)]
assert report.has_threats
83 changes: 83 additions & 0 deletions threats/threat_processing.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
import argparse
import json
import os
import sys

import geopandas as gpd
from pyogrio.errors import DataSourceError
from yirgacheffe.layers import RasterLayer

def threat_processing_per_species(
species_data_path: str,
aoh_path: str,
output_directory_path: str,
) -> None:
try:
data = gpd.read_file(species_data_path)
except DataSourceError:
sys.exit(f"Failed to read {species_data_path}")

with RasterLayer.layer_from_file(aoh_path) as aoh:

os.makedirs(output_directory_path, exist_ok=True)

taxon_id = data.id_no[0]
category_weight = int(data.category_weight[0])
threat_data = json.loads(data.threats[0])

try:
aoh_base, _ = os.path.splitext(aoh_path)
aoh_data_path = aoh_base + ".json"
with open(aoh_data_path, "r", encoding="UTF-8") as f:
aoh_data = json.load(f)
aoh_total = aoh_data["aoh_total"]
except (FileNotFoundError, KeyError):
aoh_total = aoh.sum()

proportional_aoh_per_pixel = aoh / aoh_total
weighted_species = proportional_aoh_per_pixel * category_weight

total_threat_weight = sum(x[1] for x in threat_data)
for threat_id, weight in threat_data:
proportional_threat_weight = weight / total_threat_weight
per_threat_per_species_score = weighted_species * proportional_threat_weight

threat_dir_path = os.path.join(output_directory_path, str(threat_id))
os.makedirs(threat_dir_path, exist_ok=True)
output_path = os.path.join(threat_dir_path, f"{taxon_id}.tif")
with RasterLayer.empty_raster_layer_like(aoh, filename=output_path) as result:
per_threat_per_species_score.save(result)

def main() -> None:
parser = argparse.ArgumentParser(description="Calculate per species threat layers")
parser.add_argument(
'--speciesdata',
type=str,
help="Single species/seasonality geojson.",
required=True,
dest="species_data_path"
)
parser.add_argument(
'--aoh',
type=str,
help="AoH raster of speices.",
required=True,
dest="aoh_path"
)
parser.add_argument(
'--output',
type=str,
help='Directory where per species/threat layers are stored',
required=True,
dest='output_directory_path',
)
args = parser.parse_args()

threat_processing_per_species(
args.species_data_path,
args.aoh_path,
args.output_directory_path,
)

if __name__ == "__main__":
main()
Loading