User Guide¶
This notebook walks through functionality of the Aspia Data API and who's primary role is to visualise customer data in QA to ensure completeness, data quality and API access.
A prerequisite to this notebook is to have a file in your enviromnent with your access credentials in, to be fed to the client.
import json
from aspiaspace import AspiaSpace
from aspiaspace.helpers.alt import cell_to_shapely
import logging
from dotenv import dotenv_values
import numpy as np
import pandas as pd
import geopandas as gp
import contextily as cx
import matplotlib
import matplotlib.pyplot as plt
from shapely import from_geojson
from tqdm import notebook
def format_features(features):
ids = []
geoms = []
for row in features:
geom = from_geojson(json.dumps(row["geometry"]))
id = row["aspia_uuid"]
geoms.append(geom)
ids.append(id)
shp = gp.GeoDataFrame({"aspia_uuid":ids,"geometry":geoms},crs="EPSG:4326")
return shp
def load_secrets(path="test_client.txt"):
return dotenv_values(path)
We firstly configure the Aspia Client with credentials and set the logging level
secrets = load_secrets(path="/Users/lukefleming/Documents/AspiaSpace/aspia-api-notebooks/test_client.txt")
logger = logging.getLogger(__name__)
logger.setLevel(logging.WARNING)
handler = logging.StreamHandler()
handler.setLevel(logging.WARNING)
formatter = logging.Formatter(
"%(asctime)s - %(name)s - %(levelname)s - %(message)s"
)
handler.setFormatter(formatter)
logger.addHandler(handler)
aspia = AspiaSpace(
client_id=secrets["CLIENT_ID"],
client_secret=secrets["CLIENT_SECRET"],
token_url=secrets["IDP"],
base_url=secrets["API"]
)
We start by obtaining the areas to ensure that they are loaded into our platform. We also query the vigour endpoint to ensure data is existing for all areas. We print the outputs and also run an inner join against the two results on the aspia_uuid.
features = aspia.areas.all()
vigour = aspia.areas.vigour(start="2025-08-15",end="2025-08-17")
vigour["ndvi"] = vigour["ndvi"].astype(float)
vigour["gcvi"] = vigour["gcvi"].astype(float)
vigour["ndwi"] = vigour["ndwi"].astype(float)
print(vigour.head())
shp = format_features(features)
id aspia_uuid date ndvi gcvi \
0 17 833d885e-fe87-4a2e-baae-70a9a82dc458 2025-08-17 0.253141 1.391391
1 31 adea295e-5fca-411c-a3f2-8a6c61b06491 2025-08-17 0.865985 7.297172
2 21 99a0a73f-903d-4cf6-abc4-f34f430b308d 2025-08-17 0.826972 6.853135
3 23 edd66084-37c4-4cae-914d-2110d0f414a2 2025-08-17 0.801617 6.091085
4 24 8e4e81dd-f3f7-48cd-b21a-de08f364fe6e 2025-08-17 0.857003 7.193424
ndwi
0 -0.399560
1 -0.783446
2 -0.760971
3 -0.741232
4 -0.777662
merged_df = pd.merge(
shp, vigour, left_on="aspia_uuid", right_on="aspia_uuid", how="inner"
)
print(merged_df.head())
aspia_uuid \
0 74169a50-be2a-49fe-b6cc-09c2d0bdddd3
1 74169a50-be2a-49fe-b6cc-09c2d0bdddd3
2 99688d83-f2ca-4692-8565-9d13be6323ef
3 99688d83-f2ca-4692-8565-9d13be6323ef
4 9b87ca64-17bf-46c8-a44f-4410d3ed500e
geometry id date \
0 POLYGON ((-1.79177 51.72123, -1.7917 51.72115,... 1 2025-08-17
1 POLYGON ((-1.79177 51.72123, -1.7917 51.72115,... 1 2025-08-15
2 POLYGON ((-1.76902 51.72301, -1.76955 51.72289... 10 2025-08-17
3 POLYGON ((-1.76902 51.72301, -1.76955 51.72289... 10 2025-08-15
4 POLYGON ((-1.76871 51.72626, -1.76963 51.72609... 11 2025-08-17
ndvi gcvi ndwi
0 0.673071 3.610543 -0.633870
1 0.725679 4.332160 -0.670744
2 0.195660 1.054656 -0.341313
3 0.190530 1.027956 -0.336261
4 0.306246 1.484454 -0.422317
We plot the polygons and their average NDVI values for the date.
shp = gp.GeoDataFrame(merged_df,geometry="geometry",crs="EPSG:4326")
cmap = matplotlib.colors.LinearSegmentedColormap.from_list(
"",
[
"#820000",
"#c80000",
"#f05000",
"#f09800",
"#f0d719",
"#d7f07d",
"#98ee00",
"#65be00",
"#007d00",
"#003200",
],
N=512,
)
ax = shp.plot(column="ndvi",cmap=cmap,vmin=0,vmax=1,legend=True)
cx.add_basemap(ax=ax,crs=shp.crs.to_string())
plt.show()
Next, we try querying one of the known dates for a UUID against the ALT endpoint to see we are getting a response.
uuid, date = shp.aspia_uuid.iloc[0], shp.date.iloc[0]
alts_df = aspia.products.alt(uuid,date)
print(alts_df.head())
Hex NDVI GCVI NDWI veg_mask veg_fraction \
0 8c1958b2b7313ff 0.453671 2.180550 -0.520784 1 100
1 8c1958b2b7349ff 0.461794 2.171033 -0.520401 1 100
2 8c1958b2b7343ff 0.464445 2.235726 -0.526182 1 100
3 8c1958b2b734dff 0.467998 2.295536 -0.530088 1 100
4 8c1958b2b731bff 0.478281 2.399116 -0.545246 1 100
Date
0 2025-08-17
1 2025-08-17
2 2025-08-17
3 2025-08-17
4 2025-08-17
Seeing that we do, we can take the response and plot it, with the help of a small helper function which gets the geometry of a hexagon given its ID. We also plot the distribution of NDVI values to ensure that it looks sensible. We want to see a fairly close clustering around a mean NDVI value given that we are looking at a single polygon (unless the use case isn't agricultural, in which case that might not necessarily be the case).
alts_df["geometry"] = alts_df["Hex"].apply(cell_to_shapely)
alts_df["NDVI"] = alts_df["NDVI"].astype(float)
alts_df["GCVI"] = alts_df["GCVI"].astype(float)
alts_df["NDWI"] = alts_df["NDWI"].astype(float)
alts_df["NDVI"].hist()
plt.show()
shp_alt = gp.GeoDataFrame(alts_df,geometry="geometry",crs="EPSG:4326")
fig,ax=plt.subplots()
shp_alt.plot(ax=ax,column="NDVI",cmap=cmap,vmin=0,vmax=1)
shp.iloc[[0]].plot(ax=ax,facecolor="None",edgecolor="k",linewidth=0.8)
ax.tick_params(left=False,labelleft=False,bottom=False,labelbottom=False)
fig.show()
Next, we plot a few AOIs just to get a better view of what different AOIs look like at the ALT.
fig,ax=plt.subplots(2,2,figsize=(10,10))
ax = ax.flatten()
idx = np.random.choice(len(shp),size=4,replace=False)
for i,u in notebook.tqdm(enumerate(shp["aspia_uuid"][idx])):
d = aspia.products.alt(u,date)
d["geometry"] = d["Hex"].apply(cell_to_shapely)
shp_alts = gp.GeoDataFrame(d,geometry="geometry",crs="EPSG:4326")
shp.iloc[shp["aspia_uuid"] == u].plot(ax=ax[i],facecolor="None",edgecolor="k",linewidth=0.4)
shp_alts.plot(ax=ax[i],cmap=cmap,column="NDVI",legend=True,vmin=0,vmax=1)
ax[i].tick_params(left=False,labelleft=False,bottom=False,labelbottom=False,right=False,top=False)
#ax[i].set_title(u)
fig.show()
0it [00:00, ?it/s]
Using the same date, we now query every UUID on that date and use it to make a full image of all ALTs on that date.
alts_df = pd.DataFrame()
for u in notebook.tqdm(shp["aspia_uuid"]):
d = aspia.products.alt(u,date)
alts_df = pd.concat([alts_df,d],ignore_index=True)
alts_df["geometry"] = alts_df["Hex"].apply(cell_to_shapely)
shp_alts = gp.GeoDataFrame(alts_df,geometry="geometry",crs="EPSG:4326")
fig,ax=plt.subplots()
shp.plot(ax=ax,facecolor="None",edgecolor="k",linewidth=0.4)
shp_alts.plot(ax=ax,cmap=cmap,column="NDVI",legend=True)
ax.tick_params(left=False,labelleft=False,bottom=False,labelbottom=False,right=False,top=False)
fig.show()
0%| | 0/62 [00:00<?, ?it/s]
vigour = aspia.areas.vigour(start="2025-01-01",end="2026-06-10")
print(vigour)
id aspia_uuid date \
0 2 27eea9bf-1630-4e8e-9343-aaa928ae2531 2026-06-09
1 7 def1799c-b2ac-4f31-a7ec-76f21d50f85f 2026-06-09
2 5 dd3d035b-6d84-4b60-abab-738930607630 2026-06-09
3 13 37738b0e-656b-4d92-89b9-8ee610389375 2026-06-09
4 12 7989c7d7-d6d8-48ea-8d5f-c01daf8182c2 2026-06-09
... .. ... ...
12891 12 7989c7d7-d6d8-48ea-8d5f-c01daf8182c2 2025-01-02
12892 25 79fb5a43-62d5-4366-ae98-553b863ca2aa 2025-01-02
12893 29 e9e111f5-d71d-4667-b703-0a7b7089d20e 2025-01-02
12894 9 1817d3b0-7e32-41cf-9e35-a6c2e86c742b 2025-01-02
12895 4 50ddb84e-092f-4331-99b4-439406d2723c 2025-01-02
ndvi gcvi ndwi
0 0.295209258008696 1.38703252598558 -0.391292407166739
1 0.714978623381777 3.42410393538226 -0.62812487595898
2 0.763777528929798 4.88177020238154 -0.707726792421563
3 0.734290618040739 4.22173893507618 -0.675483870806689
4 0.760957985183357 4.78698961739488 -0.702043417796332
... ... ... ...
12891 0.680123926308288 3.07508298750459 -0.59473197917832
12892 0.6624079928362 2.98020747495392 -0.573162734578152
12893 0.68769015482674 3.82674043725676 -0.653445719643778
12894 0.543920524896375 2.29163188406877 -0.531623888614933
12895 0.507531952502281 1.9527851072788 -0.490445595212508
[12896 rows x 6 columns]
To check the individual hexagons over time (for a single polygon), we query the UUID over all time in the ALT endpoint and construct overlapped time series.
dates = sorted(list(set(vigour["date"].loc[vigour["aspia_uuid"] == uuid])))
df = pd.DataFrame()
for date in notebook.tqdm(dates):
alts_df = aspia.products.alt(uuid,date)
df = pd.concat([df,alts_df],ignore_index=True)
print(df.head())
0%| | 0/416 [00:00<?, ?it/s]
Hex NDVI GCVI NDWI veg_mask veg_fraction \
0 8c1958b2b7725ff 0.365868 1.425652 -0.416051 1 100
1 8c1958b2b7727ff 0.385637 1.478554 -0.424737 1 100
2 8c1958b2b7705ff 0.393933 1.437926 -0.417922 1 100
3 8c1958b2b7703ff 0.407071 1.483588 -0.425555 1 100
4 8c1958b2b772dff 0.409116 1.504106 -0.429001 1 100
Date
0 2025-01-02
1 2025-01-02
2 2025-01-02
3 2025-01-02
4 2025-01-02
hex = list(set(df["Hex"]))
df["Date"] = pd.to_datetime(df["Date"])
fig,ax=plt.subplots(figsize=(12,4))
for h in np.random.choice(hex,size=20,replace=False):
subset = df.loc[df["Hex"] == h]
subset.sort_values(by="Date",inplace=True)
ax.plot(subset["Date"],subset["NDVI"])
ax.tick_params(rotation=45)
fig.show()