Extract wildlife observation data with SensingClues
SensingClues allows you to record, monitor and analyze wildlife observations to support nature conservation initiatives. This notebook shows the following:
Core: the main SensingClues-functionality of
Extracting observation data
Extracting track data
Advanced: additional functionality including
A hierarchy of available concepts (e.g. animal species or type of (illegal) activity), which enhances reporting and analysis of the observation data.
Extraction and visualization of layer data from SensingClues.
You can adapt this notebook to extract your own observation data. For more detail on what you can configure as a user, see the API-documentation of the sensingcluespy-package here.
Before you start
To run this notebook, you should: - Install the sensingcluespy-package in a virtual python environment (pip install -e . from the main directory of the repository). - Install the requirements in requirements.txt (if not already installed automatically in the previous step). This includes the plotting libraries matplotlib and folium used in this notebook.
[Optional] Create your own user account
For the purpose of this tutorial, we use a read-only user called “demo”. If you want to continue using SensingClues for your own work (of course you want to! :-) ), then please do the following: - Create a personal account at SensingClues using the Cluey Data Collector app, which can be downloaded from the Google Playstore (not supported for iOS currently). Also see here. - Create a file ‘.env’ in the root of the wildcat-api-python-repository, containing your SensingClues credentials. These will be read in this notebook to log in. The file should look like this:
# SensingClues credentials
USERNAME=your_username
PASSWORD=your_password
Configuration
[1]:
# N.B. While sensingcluespy does not require you to install visualization packages, this tutorial does.
# To run this tutorial in full, please install matplotlib and folium (as contained in requirements.txt).
import folium
import geopandas as gpd
import matplotlib.pyplot as plt
import os
from dotenv import load_dotenv
from sensingcluespy import sclogging
from sensingcluespy.api_calls import SensingClues
from sensingcluespy.src import helper_functions as helpers
from sensingcluespy.src import visualization as viz
[2]:
plt.style.use("ggplot")
[3]:
logger = sclogging.get_sc_logger()
sclogging.set_sc_log_level("INFO")
[4]:
load_dotenv()
[4]:
True
[5]:
%load_ext autoreload
%autoreload 2
[6]:
# N.B. We recommend to place your credentials in an environment file and read them like so:
# username = os.getenv("USERNAME")
# password = os.getenv("PASSWORD")
# However, for the purpose of this demo, we use a read-only demo user:
username = "demo"
password = "demo"
Connect to SensingClues
[7]:
sensing_clues = SensingClues(username, password)
2024-11-15 10:52:34 [api_calls.py:57] INFO - Successfully logged in to SensingClues.
Check available data
By default, you have access to several groups of data, such as a demo dataset and a large dataset offered by Global Forest Watch.
[8]:
info = sensing_clues.get_groups()
info
[8]:
| name | description | n_records | |
|---|---|---|---|
| 0 | focus-project-3494596 | Demo Cluey Group | 6 |
| 1 | focus-project-1234 | Demo Upload | 797 |
| 2 | focus-project-GFW | GFW | 54767 |
[9]:
# Specify the group(s) to extract data from
# For this tutorial, focus-project-1234 contains demo observations,
# while focus-project-3494596 contains demo tracks.
groups = [
"focus-project-3494596",
"focus-project-1234",
]
Core functionality
Time to collect and plot some observation and track data!
Get observations
You can filter the extracted observations in multiple ways, such as timestamps, coordinates (bounding box) and concepts. Some key features are shown here:
Date and time: set
date_fromand/ordate_until(in format %Y-%m-%d, assumes UTC).Coordinates: set
coord, e.g. {“north”: 32, “east”: 20, “south”: 31, “west”: 17}.Concepts: set
conceptsto include, e.g. ‘animal’. See detailed example later in this notebook.
For full detail on the options, see the documentation of the API here.
Usage notes
Reading all data in a group can take minutes or longer, depending on the size of the dataset. If you want to do a quick test, you can limit the number of pages to read by setting
page_nbr_sample.Each observation has a unique
entityIdand may have multiple concepts (labels) associated with it, in which case the number of records in the observations-dataframe is larger than the number of observations mentioned by the logger.
[10]:
# A quick check of the number of available records
obs_sample = sensing_clues.get_observations(groups=groups, page_nbr_sample=1)
2024-11-15 10:52:35 [api_calls.py:478] INFO - Scope ['focus-project-3494596', 'focus-project-1234'] contains 803 records for data type 'observations', when not applying any filters.
2024-11-15 10:52:35 [api_calls.py:492] INFO - When applying your filters, 801 records remain.
2024-11-15 10:52:36 [api_calls.py:531] INFO - Started reading available records for group focus-project-3494596.
2024-11-15 10:52:36 [api_calls.py:549] INFO - Finished reading available records for group focus-project-3494596.
2024-11-15 10:52:36 [api_calls.py:531] INFO - Started reading available records for group focus-project-1234.
2024-11-15 10:52:36 [api_calls.py:549] INFO - Finished reading available records for group focus-project-1234.
[11]:
observations = sensing_clues.get_observations(
groups=groups,
date_from="2024-07-01",
coord={"north": -17, "east": 30, "south": -19, "west": 20}
)
2024-11-15 10:52:36 [api_calls.py:478] INFO - Scope ['focus-project-3494596', 'focus-project-1234'] contains 803 records for data type 'observations', when not applying any filters.
2024-11-15 10:52:36 [api_calls.py:492] INFO - When applying your filters, 97 records remain.
2024-11-15 10:52:37 [api_calls.py:531] INFO - Started reading available records for group focus-project-3494596.
2024-11-15 10:52:37 [api_calls.py:549] INFO - Finished reading available records for group focus-project-3494596.
2024-11-15 10:52:37 [api_calls.py:531] INFO - Started reading available records for group focus-project-1234.
2024-11-15 10:52:38 [api_calls.py:549] INFO - Finished reading available records for group focus-project-1234.
Visualize these observations
The standard plotting-function plot_observation shows a separate layer for all observation types (typically [‘community_work’, ‘animal’, ‘community’, ‘poi’, ‘hwc’], where ‘poi’ = ‘point of interest’ and ‘hwc’ = ‘human-wildlife-conflict’).
[12]:
viz.plot_observations(
observations,
show_heatmap="hwc_animal",
padding=(25, 25)
)
[12]:
[13]:
# You can explore the observations per observationType like so:
observation_type = "animal"
# observation_type = "hwc"
observations.loc[observations["observationType"] == observation_type, "conceptLabel"].value_counts()
[13]:
Animal sighting 13
Elephant 2
Dropping 2
Photographed 2
_Fresh 2
Ibis 2
Kingfisher 1
Heron 1
Stork 1
Guineafowl 1
Name: conceptLabel, dtype: int64
Get tracks
You can filter the extracted observations in multiple ways, such as data, coordinates (bounding box) and concepts, similar to get_observations.
[14]:
tracks = sensing_clues.get_tracks(
groups=groups,
# date_from="2024-07-01",
# coord={"north": -17, "east": 30, "south": -19, "west": 20}
)
2024-11-15 10:52:39 [api_calls.py:478] INFO - Scope ['focus-project-3494596', 'focus-project-1234'] contains 803 records for data type 'tracks', when not applying any filters.
2024-11-15 10:52:39 [api_calls.py:492] INFO - When applying your filters, 2 records remain.
2024-11-15 10:52:39 [api_calls.py:531] INFO - Started reading available records for group focus-project-3494596.
2024-11-15 10:52:39 [api_calls.py:549] INFO - Finished reading available records for group focus-project-3494596.
2024-11-15 10:52:39 [api_calls.py:508] WARNING - No data available for 'tracks', returning empty dataframe for group focus-project-1234.
[15]:
tracks.head()
[15]:
| entityId | entityType | projectId | projectName | featureType | length | startWhen | endWhen | agentName | patrolDuration_hours | |
|---|---|---|---|---|---|---|---|---|---|---|
| 0 | G3494596-6fe952b4-9ab3-463e-82fd-5a2be1c8e85e | track | 3494596 | Demo Cluey Group | track/onVehiclePatrol | 7.251837 | 2024-08-29 17:02:24+02:00 | 2024-08-29 18:39:42+02:00 | jankees | 1.621667 |
| 1 | G3494596-0c3d987b-038f-400e-8b83-8b1694a026f5 | track | 3494596 | Demo Cluey Group | track/onDuty | 42.096071 | 2024-08-24 11:34:56+02:00 | 2024-08-24 22:31:42+02:00 | jankees | 10.946111 |
Visualize tracks
If available, you can add geojson-data (including geometries) to the tracks and subsequently visualize the tracks.
[16]:
tracks_geo = sensing_clues.add_geojson_to_tracks(tracks)
[17]:
track_map = viz.plot_tracks(tracks_geo["geometry"])
track_map
[17]:
Advanced functionality
Get all available concepts and their hierarchy
SensingClues offers hierarchies containing the available concepts (e.g. animals). As shown later in this notebook, you can use this information to subsequently query: - the details for a specific concept - check the occurrence of each concept in the group(s) of observations you have access to.
[18]:
hierarchy = sensing_clues.get_hierarchy(scope="SCCSS")
hierarchy.info()
<class 'pandas.core.frame.DataFrame'>
Int64Index: 1236 entries, 0 to 1579
Data columns (total 6 columns):
# Column Non-Null Count Dtype
--- ------ -------------- -----
0 id 1236 non-null object
1 parent 1236 non-null object
2 label 1236 non-null object
3 altLabels 771 non-null object
4 children 160 non-null object
5 isTopConcept 1236 non-null bool
dtypes: bool(1), object(5)
memory usage: 59.1+ KB
Get details for specific concepts in the hierarchy
You can get information on children or the parents of a concept in the hierarchy by filtering on its label or id. Use the available helper functions to do so. For example, you could do the following for the concept of a “Kite” (oid = “https://sensingclues.poolparty.biz/SCCSSOntology/222”):
oid = "https://sensingclues.poolparty.biz/SCCSSOntology/222"
helpers.get_children_for_id(hierarchy, oid)
helpers.get_parent_for_id(hierarchy, oid)
helpers.get_label_for_id(hierarchy, oid)
or, if filtering on the label itself:
label = 'Kite'
helpers.get_children_for_label(hierarchy, label)
helpers.get_parent_for_label(hierarchy, label)
helpers.get_id_for_label(hierarchy, label)
N.B. Alternatively, you could directly filter the hierarchy-dataframe yourself of course.
Tell me, what animal belongs to this concept id?
[19]:
oid = "https://sensingclues.poolparty.biz/SCCSSOntology/222"
helpers.get_label_for_id(hierarchy, oid)
[19]:
'Kite'
Does this Kite have any child concepts?
[20]:
label = "Kite"
children_label = helpers.get_children_for_label(hierarchy, label)
children_label
[20]:
['https://sensingclues.poolparty.biz/SCCSSOntology/224',
'https://sensingclues.poolparty.biz/SCCSSOntology/223']
What are the details for these children?
[21]:
hierarchy.loc[hierarchy["id"].isin(children_label)]
[21]:
| id | parent | label | altLabels | children | isTopConcept | |
|---|---|---|---|---|---|---|
| 461 | https://sensingclues.poolparty.biz/SCCSSOntolo... | https://sensingclues.poolparty.biz/SCCSSOntolo... | Kite black | [Black_kite, Kite_black, Milvus_migrans, Black... | NaN | False |
| 1127 | https://sensingclues.poolparty.biz/SCCSSOntolo... | https://sensingclues.poolparty.biz/SCCSSOntolo... | Kite red | [red_kite, Kite_red, Milvus milvus, Milvus_mil... | NaN | False |
Filter observations on concept
Here we show an example of filtering the data on concepts. The example filters on the concepts of Impala and Giraffe.
Instructions: - Set concepts to include, e.g. ‘animal’, specified as a Pool Party URL, e.g. “https://sensingclues.poolparty.biz/SCCSSOntology/186”. - Note that you can infer the URL’s available for a certain common name by using the helper function helpers.get_label_for_id(hierarchy, oid), as shown above. - Further, if you want to exclude subconcepts, i.e. keep observations with the label ‘animal’ but exclude observations with the label ‘elephant’, set include_subconcepts=False.
[22]:
concept_animal = [
"https://sensingclues.poolparty.biz/SCCSSOntology/308", # Impala
"https://sensingclues.poolparty.biz/SCCSSOntology/319", # Giraffe
# or infer the id using a label, for instance:
# helpers.get_id_for_label(hierarchy, "Animal sighting"),
]
concept_observations = sensing_clues.get_observations(
groups=groups,
concepts=concept_animal,
# date_from="2024-07-01",
# coord={"north": -17, "east": 30, "south": -19, "west": 20}
)
2024-11-15 10:52:40 [api_calls.py:478] INFO - Scope ['focus-project-3494596', 'focus-project-1234'] contains 803 records for data type 'observations', when not applying any filters.
2024-11-15 10:52:41 [api_calls.py:492] INFO - When applying your filters, 8 records remain.
2024-11-15 10:52:41 [api_calls.py:508] WARNING - No data available for 'observations', returning empty dataframe for group focus-project-3494596.
2024-11-15 10:52:41 [api_calls.py:531] INFO - Started reading available records for group focus-project-1234.
2024-11-15 10:52:41 [api_calls.py:549] INFO - Finished reading available records for group focus-project-1234.
[23]:
concept_observations.head()
[23]:
| entityId | entityType | entityName | projectId | projectName | observationType | when | where | agentName | conceptLabel | conceptId | lon | lat | geometry | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 0 | O1234-6977171871396613895-2059 | human activity | Observation | 1234 | Demo Upload | human activity | 2024-06-24T00:00:00+02:00 | {'type': 'Point', 'coordinates': [24.75065, -1... | Carcass | https://sensingclues.poolparty.biz/SCCSSOntolo... | 24.75065 | -17.50172 | POINT (24.75065 -17.50172) | |
| 1 | O1234-6977171871396613895-2059 | human activity | Observation | 1234 | Demo Upload | human activity | 2024-06-24T00:00:00+02:00 | {'type': 'Point', 'coordinates': [24.75065, -1... | Giraffe | https://sensingclues.poolparty.biz/SCCSSOntolo... | 24.75065 | -17.50172 | POINT (24.75065 -17.50172) | |
| 2 | O1234-6977171871396613895-2059 | human activity | Observation | 1234 | Demo Upload | human activity | 2024-06-24T00:00:00+02:00 | {'type': 'Point', 'coordinates': [24.75065, -1... | Machete | https://sensingclues.poolparty.biz/SCCSSOntolo... | 24.75065 | -17.50172 | POINT (24.75065 -17.50172) | |
| 3 | O1234-6977171871396613895-2059 | human activity | Observation | 1234 | Demo Upload | human activity | 2024-06-24T00:00:00+02:00 | {'type': 'Point', 'coordinates': [24.75065, -1... | Recorded | https://sensingclues.poolparty.biz/SCCSSOntolo... | 24.75065 | -17.50172 | POINT (24.75065 -17.50172) | |
| 4 | O1234-6977171871396613895-2059 | human activity | Observation | 1234 | Demo Upload | human activity | 2024-06-24T00:00:00+02:00 | {'type': 'Point', 'coordinates': [24.75065, -1... | Observations | https://sensingclues.poolparty.biz/SCCSSOntolo... | 24.75065 | -17.50172 | POINT (24.75065 -17.50172) |
Get layers
[26]:
# check all available layers
layers = sensing_clues.get_all_layers()
layers
[26]:
| pid | lid | layerName | description | geometryType | |
|---|---|---|---|---|---|
| 0 | 1234 | 0 | Demo_places | All Point geometries for layer Demo_places | Point |
| 1 | 1234 | 1 | Demo_roads | All LineString geometries for layer Demo_roads | LineString |
| 2 | 1234 | 2 | Demo_countries | All MultiPolygon geometries for layer Demo_cou... | MultiPolygon |
| 3 | 1234 | 3 | Demo zones | All Polygon geometries for layer Demo zones | Polygon |
Visualize an individual layer
Get features for an individual and visualize it.
[30]:
layer = sensing_clues.get_layer_features(layer_name="Demo_countries")
viz.plot_layer(layer)
[30]:
Miscellaneous
[28]:
# You should have logged in automatically by calling the class.
# If not, you can call the login-method separately.
# status = sensing_clues.login(username, password)
[29]:
# It is not necessary to log out, but you can do so by calling:
# sensing_clues.logout()