Maps are one of the most common and useful data visualisation tools in an ecologist’s tool belt. Making a quick and simple map of species observations is especially useful when first investigating where a species has occurred. Viewing locations of points can also help to understand the extent of your data (and spot possible errors or outliers).
In this post, we will use either Python or R to make a map of observations of Peron’s tree frog (Litoria peronii) in New South Wales since 2018 recorded by FrogID.
Download data
Peron’s Tree frog is one of the most recorded frog species in the Atlas of Living Australia. Growing up to 7cm in length, it is well-known for its eyes which often look like they have a black cross on them!
First, let’s import galah-python
.
import galah
Search for taxa
When trying to download data about any species or clade, we can search using search_taxa()
. It’s recommended to use search_taxa()
to check whether a taxonomic search returns what you were expecting (even if you know the scientific name)! We can check taxonomic information about Peron’s tree frog with search_taxa()
, which returns extra details about the species when a match is found.
"Litoria peronii") galah.search_taxa(
scientificName scientificNameAuthorship ... vernacularName issues
0 Litoria peronii (Tschudi, 1838) ... Peron's Tree Frog noIssue
[1 rows x 12 columns]
Search for fields
Next, we can search for fields and field IDs for filtering our query. In this case, we are interested in filtering to a specific year, state/territory and data resource.
We can start by searching for any fields containing the word “year” using search_all()
.
galah.search_all(="year"
fields )
id description type link
0 year The year in which an occurrence was observed. ... field https://github.com/AtlasOfLivingAustralia/ala-...
1 endDayOfYear http://rs.tdwg.org/dwc/terms/endDayOfYear field NaN
2 datePrecision The precision of the date information for the ... field NaN
3 occurrenceYear Year ranges for a search. Calculated based on ... field NaN
4 startDayOfYear http://rs.tdwg.org/dwc/terms/startDayOfYear field NaN
5 namePublishedInYear http://rs.tdwg.org/dwc/terms/namePublishedInYear field NaN
We can do the same kind of search to find fields with information of australian states/territories and data resource names.
galah.search_all(="states"
fields )
id description type link
0 cl22 Australian States and Territories Australian S... layers
1 cl927 States including coastal waters States (includ... layers
2 cl2013 ASGS Australian States and Territories Austral... layers
3 cl10902 Forests of Australia (2013) v2.0 Forests of Au... layers
4 cl10903 Tenure of Australia's forests (2013) v2.0 Tenu... layers
5 cl10925 PSMA States (2016) Australian State boundaries layers
6 cl10930 NRM Regions 2017 The Natural Resource Manageme... layers
galah.search_all(="dataresourcename"
fields )
id description type link
0 dataResourceUid A list (concatenated and separated) of prepara... field NaN
1 dataResourceName The data resource that supplies the record. Th... field NaN
2 raw_dataResourceUid NaN field NaN
3 raw_dataResourceName NaN field NaN
If you are ever uncertain which field ID to choose, you can use show_values()
to see what possible values are within a field. For example, let’s see what values are in field ID cl22
.
="cl22") galah.show_values(field
field category
0 cl22 New South Wales
1 cl22 Victoria
2 cl22 Queensland
3 cl22 South Australia
4 cl22 Western Australia
5 cl22 Northern Territory
6 cl22 Australian Capital Territory
7 cl22 Tasmania
8 cl22 Unknown1
9 cl22 Coral Sea Islands
10 cl22 Ashmore and Cartier Islands
We can also search for a value using search_values()
, which might be handy to check that “FrogID” is a value in dataResourceName
.
="dataResourceName", value="frogid") galah.search_values(field
field category
0 dataResourceName FrogID
Download observations
Now we are ready to build our query to download observations of Peron’s tree frog in New South Wales since 2018 recorded by FrogID.
For those unfamiliar with Australian geography, New South Wales is this one:
First, let’s find the number of records that match our query. This is good practice before downloading occurrence records because you can check exactly how many records you are intending to download (and avoid an accidental massive download)!
We’ll use atlas_counts()
to download record counts, specifying the taxon using the taxa
argument, and narrowing the year range, state/territory and data resource using the filters
argument.
# *Download record counts*
galah.atlas_counts( ="litoria peronii", # *of Peron's tree frog*
taxa=["year>=2018", # *since 2018*
filters"cl22=New South Wales", # *in New South Wales*
"dataResourceName=FrogID"] # *by FrogID*
)
totalRecords
0 27647
Now we can use atlas_occurrences()
to download occurrence records, which returns each observation’s location coordinates and event date.
You will need to first provide a registered email with the ALA using galah_config()
before retrieving records.
= "your-email-here")
galah.galah_config(email = galah.atlas_occurrences(
frogs ="litoria peronii",
taxa=["year>=2018",
filters"cl22=New South Wales",
"dataResourceName=FrogID"]
) frogs
decimalLatitude decimalLongitude eventDate scientificName taxonConceptID recordID dataResourceName occurrenceStatus
0 -37.246800 149.375000 2020-12-27T00:00:00Z Litoria peronii https://biodiversity.org.au/afd/taxa/c584f24b-... a5cd2fcd-5225-4d19-977c-b16ca5e8f1dd FrogID PRESENT
1 -37.089036 149.699526 2020-12-14T00:00:00Z Litoria peronii https://biodiversity.org.au/afd/taxa/c584f24b-... eebde5ef-cac4-4897-af00-cb2e39a0684f FrogID PRESENT
2 -37.077693 149.874402 2018-01-06T00:00:00Z Litoria peronii https://biodiversity.org.au/afd/taxa/c584f24b-... 35340478-97c1-48a4-a463-991fe3a8daa0 FrogID PRESENT
3 -37.077241 149.874787 2018-01-06T00:00:00Z Litoria peronii https://biodiversity.org.au/afd/taxa/c584f24b-... a7abc9f3-362f-469e-9076-5b55a2447b69 FrogID PRESENT
4 -37.070746 149.896011 2020-12-13T00:00:00Z Litoria peronii https://biodiversity.org.au/afd/taxa/c584f24b-... 1cc9dda8-f2d4-4f55-acf6-11c93b26da9e FrogID PRESENT
... ... ... ... ... ... ... ... ...
27642 -28.207514 153.442592 2018-11-15T00:00:00Z Litoria peronii https://biodiversity.org.au/afd/taxa/c584f24b-... b094fed1-5bff-4df8-b556-cabd693c533a FrogID PRESENT
27643 -28.207472 153.442497 2018-11-15T00:00:00Z Litoria peronii https://biodiversity.org.au/afd/taxa/c584f24b-... 5cc24fbd-8c6b-4a76-9b28-fec76ee08f37 FrogID PRESENT
27644 -28.207442 153.442328 2020-02-07T00:00:00Z Litoria peronii https://biodiversity.org.au/afd/taxa/c584f24b-... 61aa50a1-4c79-4fc3-b3ab-93538faa37b1 FrogID PRESENT
27645 -28.207108 153.443021 2021-02-19T00:00:00Z Litoria peronii https://biodiversity.org.au/afd/taxa/c584f24b-... 0324b8d1-77b0-4bf3-9ec2-6ad9efff18f2 FrogID PRESENT
27646 -28.186157 153.445556 2018-11-16T00:00:00Z Litoria peronii https://biodiversity.org.au/afd/taxa/c584f24b-... bcf83a54-a900-4265-960a-9436356a7107 FrogID PRESENT
[27647 rows x 8 columns]
Make a map
It’s time to make our map!
In order to draw our map of New South Wales, we’ll download a shapefile of the latest state and territory boundaries from the Australian Bureau of statistics. Download the “States and Territories - 2021 - Shapefile” zip file, and save the zip file in the same folder you are coding in.
Let’s load our States and Territories shapefile with read_file()
and save it as states
. Then, we will filter the shapefile to New South Wales and quickly plot it (specifying that the edges are black, the inside is white, and the figure size is 12 x 6 inches).
from matplotlib import pyplot as plt
import geopandas as gpd
# Load Australian state and territory boundaries
= gpd.read_file("STE_2021_AUST_GDA94.shp")
states
# Filter to New South Wales and plot
"STE_NAME21"] == "New South Wales"].plot(edgecolor = "#5A5A5A", linewidth = 0.5, facecolor = "white", figsize = (12,6)) states[states[
Our shapefile has plotted nicely, but there are many different ways to display our shape of NSW, which exists on a spherical globe (the Earth), onto a flat surface (our map). Our shapefile already has a projection, determined by its Coordinate Reference System (CRS) of GDA94.
# see the details of our shape's CRS
nsw.crs
<Geographic 2D CRS: EPSG:4283>
Name: GDA94
Axis Info [ellipsoidal]:
- Lat[north]: Geodetic latitude (degree)
- Lon[east]: Geodetic longitude (degree)
Area of Use:
- name: Australia including Lord Howe Island, Macquarie Island, Ashmore and Cartier Islands, Christmas Island, Cocos (Keeling) Islands, Norfolk Island. All onshore and offshore.
- bounds: (93.41, -60.55, 173.34, -8.47)
Datum: Geocentric Datum of Australia 1994
- Ellipsoid: GRS 1980
- Prime Meridian: Greenwich
To make it clearer how the CRS changes the projection of a map, here are 3 maps of NSW projected with 3 different CRS:
- GDA94 (the current projection of our NSW shapefile)
- EPSG:8058 (a state/territory-specific projection for NSW)
- EPSG:2955 (intended for Canadian territories)
Code
= "#5A5A5A", facecolor = "white")
nsw.plot(edgecolor = nsw.to_crs(8058)
nsw_gda2020 = "#5A5A5A", facecolor = "white")
nsw_gda2020.plot(edgecolor = nsw.to_crs(2955)
nsw_nad83 = "#5A5A5A", facecolor = "white") nsw_nad83.plot(edgecolor
The CRS projection of ALA data is EPSG:4326 (also known as “WGS84”). Reprojecting the CRS of our shapefile allows us to make sure the points of our data align correctly with our shapefile.
= nsw.to_crs(4326) nsw
Now, we will add species observations to our map. First, we will plot our reprojected shapefile. Then, we will overlay a scatter plot using decimalLongitude
as your x axis and decimalLatitude
as your y axis. We’ll set the colour (c
) and adjust the alpha
to make our points partially transparent.
= "#5A5A5A", linewidth = 0.5, facecolor = "white", figsize = (12,6))
nsw.plot(edgecolor 'decimalLongitude'],frogs['decimalLatitude'], c = "#6fab3f", alpha = 0.5) plt.scatter(frogs[
For some final touches (to make the map prettier), we can add a title and remove the border.
We’ll add a custom Google font, Roboto, by downloading it from Google Fonts, saving the folder in your current directory, unzipping the folder, and loading it with the matplotlib
library.
import matplotlib as mpl
from matplotlib import font_manager
= font_manager.findSystemFonts(fontpaths="Roboto/")
font_files for ff in font_files:
font_manager.fontManager.addfont(ff)
For further information or troubleshooting tips for installing Roboto on your computer, this post is an excellent guide, as well as the matplotlib.font_manager documentation.
= "#5A5A5A", linewidth = 0.5, facecolor = "white", figsize = (12,6))
nsw.plot(edgecolor 'decimalLongitude'],frogs['decimalLatitude'], c = "#6fab3f", alpha = 0.5, label = "Litoria peronii")
plt.scatter(frogs["Peron's tree frog",fontsize=36,font='Roboto')
plt.suptitle("FrogID observations in New South Wales since 2018",fontsize=28,font='Roboto')
plt.title('off') plt.axis(
(140.0937672995, 160.0147167105, -37.972572696499995, -27.6894786335)
To save your plot in your current folder, you can use:
"perons_tree_frog_nsw.png") plt.savefig(
Final thoughts
We hope this post has helped make the basic steps of making a map simple and easy to understand. For more advanced mapping in Python, check out our ALA Labs article on how to map invasive species.
Expand for session info
-----
galah 0.8.1
geopandas 0.14.1
matplotlib 3.8.2
natsort 8.4.0
pandas 2.1.3
session_info 1.0.0
-----
Python 3.12.0 (tags/v3.12.0:0fb18b0, Oct 2 2023, 13:03:39) [MSC v.1935 64 bit (AMD64)]
Windows-10-10.0.19045-SP0
-----
Session information updated at 2024-02-13 11:02
Download data
Peron’s Tree frog is one of the most recorded frog species in the Atlas of Living Australia. Growing up to 7cm in length, it is well-known for its eyes which often look like they have a black cross on them!
First, let’s load some packages.
library(galah)
library(ggplot2)
library(dplyr)
library(sf)
library(here)
library(showtext)
Search for taxa
When trying to download data about any species or clade, we can search using search_taxa()
. It’s recommended to use search_taxa()
to check whether a taxonomic search returns what you were expecting (even if you know the scientific name)! We can check taxonomic information about Peron’s tree frog with search_taxa()
, which returns extra details about the species when a match is found.
search_taxa("Litoria peronii")
# A tibble: 1 × 15
search_term scientific_name scientific_name_autho…¹ taxon_concept_id rank
<chr> <chr> <chr> <chr> <chr>
1 Litoria peronii Litoria peronii (Tschudi, 1838) https://biodive… spec…
# ℹ abbreviated name: ¹scientific_name_authorship
# ℹ 10 more variables: match_type <chr>, kingdom <chr>, phylum <chr>,
# class <chr>, order <chr>, family <chr>, genus <chr>, species <chr>,
# vernacular_name <chr>, issues <chr>
Search for fields
Next, we can search for fields and field IDs for filtering our query. In this case, we are interested in filtering to a specific year, state/territory and data resource.
We can start by searching for any fields containing the word “year” using search_all()
.
search_all(fields, "year")
# A tibble: 8 × 3
id description type
<chr> <chr> <chr>
1 year Year fields
2 raw_year Year (unprocessed) fields
3 endDayOfYear End Day Of Year fields
4 startDayOfYear Start Day Of Year fields
5 occurrenceYear Date (by decade) fields
6 raw_endDayOfYear <NA> fields
7 raw_startDayOfYear <NA> fields
8 namePublishedInYear Name Published In Year fields
We can do the same kind of search to find fields with information of australian states/territories and data resource names.
search_all(fields, "states")
# A tibble: 5 × 3
id description type
<chr> <chr> <chr>
1 cl2013 ASGS Australian States and Territories fields
2 cl22 Australian States and Territories fields
3 cl927 States including coastal waters fields
4 cl10925 PSMA States (2016) fields
5 cl110925 PSMA States - Abbreviated (2016) fields
search_all(fields, "resource")
# A tibble: 8 × 3
id description type
<chr> <chr> <chr>
1 resourceID Resource ID fields
2 dataResourceUid Dataset fields
3 dataResourceName Dataset fields
4 relatedResourceID Related Resource ID fields
5 raw_dataResourceUid Dataset (unprocessed) fields
6 raw_dataResourceName Dataset (unprocessed) fields
7 relationshipOfResource Relationship Of Resource fields
8 resourceRelationshipID Resource Relationship ID fields
If you are ever uncertain which field ID to choose, you can use show_values()
to see what possible values are within a field. For example, let’s see what values are in field ID cl22
.
search_all(fields, "cl22") |>
show_values()
• Showing values for 'cl22'.
# A tibble: 11 × 1
cl22
<chr>
1 New South Wales
2 Victoria
3 Queensland
4 South Australia
5 Western Australia
6 Northern Territory
7 Australian Capital Territory
8 Tasmania
9 Unknown1
10 Coral Sea Islands
11 Ashmore and Cartier Islands
We can also search for a value using search_values()
, which might be handy to check that “FrogID” is a value in dataResourceName
.
search_all(fields, "dataResourceName") |>
search_values("FrogID")
! Search returned 2 matched fields.
• Showing values for 'dataResourceName'.
# A tibble: 1 × 1
dataResourceName
<chr>
1 FrogID
Download observations
Now we are ready to build our query to download observations of Peron’s tree frog in New South Wales since 2018 recorded by FrogID.
For those unfamiliar with Australian geography, New South Wales is this one:
First, let’s find the number of records that match our query. This is good practice before downloading occurrence records because you can check exactly how many records you are intending to download (and avoid an accidental massive download)!
We’ll use atlas_counts()
to download record counts, specifying the taxon using galah_identify()
, and narrowing the year range, state/territory and data resource using galah_filter()
.
galah_call() |> # Begin a query
galah_identify("Litoria peronii") |> # Peron's tree frog
galah_filter(year >= 2018, # since 2018
== "New South Wales", # in New South Wales
cl22 == "FrogID") |> # by FrogID
dataResourceName atlas_counts() # Download record counts
# A tibble: 1 × 1
count
<int>
1 27647
Now we can use atlas_occurrences()
to download occurrence records, which returns each observation’s location coordinates and event date.
You will need to first provide a registered email with the ALA using galah_config()
before retrieving records.
galah_config(email = "your-email-here")
<- galah_call() |>
frogs galah_identify("Litoria peronii") |>
galah_filter(year >= 2018,
== "New South Wales",
cl22 == "FrogID") |>
dataResourceName atlas_occurrences()
frogs
# A tibble: 27,647 × 8
recordID scientificName taxonConceptID decimalLatitude decimalLongitude
<chr> <chr> <chr> <dbl> <dbl>
1 0008c41d-fd35… Litoria peron… https://biodi… -33.2 152.
2 000c1c20-bec3… Litoria peron… https://biodi… -33.0 150.
3 000e46ad-9ace… Litoria peron… https://biodi… -31.3 149.
4 001229c0-4c48… Litoria peron… https://biodi… -33.7 150.
5 0014b8ff-ddd5… Litoria peron… https://biodi… -33.4 151.
6 00183dd1-c9cd… Litoria peron… https://biodi… -36.6 150.
7 002088e5-3a73… Litoria peron… https://biodi… -32.1 150.
8 002108e1-9933… Litoria peron… https://biodi… -33.7 151.
9 00251c30-9dfd… Litoria peron… https://biodi… -34.3 151.
10 00259fce-87db… Litoria peron… https://biodi… -30.5 153.
# ℹ 27,637 more rows
# ℹ 3 more variables: eventDate <dttm>, occurrenceStatus <chr>,
# dataResourceName <chr>
Make a map
It’s time to make our map!
In order to draw our map of New South Wales, we’ll download a shapefile of the latest state and territory boundaries from the Australian Bureau of Statistics. Download the “States and Territories - 2021 - Shapefile” zip file, and save the zip file in the same folder you are coding in.
Let’s load our States and Territories shapefile with read_file()
and save it as states
. Then, we will filter the shapefile to New South Wales and quickly plot it to check it plots correctly (specifying that the border is grey, and the inside is white).
# Load Australian state and territory boundaries
<- st_read(here("STE_2021_AUST_GDA94.shp"),
states quiet = TRUE)
# Filter to New South Wales
<- states |>
nsw filter(STE_NAME21 == "New South Wales")
# Plot
ggplot() +
geom_sf(data = nsw,
colour = "grey60",
fill = "white")
Our shapefile has plotted nicely, but there are many different ways to display our shape of NSW, which exists on a spherical globe (the Earth), onto a flat surface (our map). Our shapefile already has a projection, determined by its Coordinate Reference System (CRS) of GDA94.
# see the details of our shape's geometry
|> st_geometry() nsw
Geometry set for 1 feature
Geometry type: MULTIPOLYGON
Dimension: XY
Bounding box: xmin: 140.9993 ymin: -37.50516 xmax: 159.1092 ymax: -28.15689
Geodetic CRS: GDA94
MULTIPOLYGON (((159.0623 -31.50887, 159.0622 -3...
To make it clearer how the CRS changes the projection of a map, here are 3 maps of NSW projected with 3 different CRS:
- GDA94 (the current projection of our NSW shapefile)
- EPSG:8058 (a state/territory-specific projection for NSW)
- EPSG:2955 (intended for Canadian territories)
Code
<- nsw |>
p1 ggplot() +
geom_sf(colour = "grey60",
fill = "white")
<- nsw |>
p2 st_transform(crs = st_crs(8058)) |>
ggplot() +
geom_sf(colour = "grey60",
fill = "white")
<- nsw |>
p3 st_transform(crs = st_crs(2955)) |>
ggplot() +
geom_sf(colour = "grey60",
fill = "white")
Data from the ALA use CRS EPSG:4326 (also known as “WGS84”). Reprojecting our shapefile to the same CRS allows us to make sure the points of our data align correctly with our shapefile.
<- nsw |>
nsw st_transform(crs = st_crs(4326))
Now, we will add species observations to our map. First, we will plot our reprojected shapefile. Then, we will overlay a scatter plot using decimalLongitude
as your x axis and decimalLatitude
as your y axis. We’ll set the colour
and adjust the alpha
to make our points partially transparent.
ggplot() +
geom_sf(data = nsw,
colour = "grey60",
fill = "white") +
geom_point(data = frogs,
aes(x = decimalLongitude,
y = decimalLatitude),
colour = "#6fab3f",
alpha = 0.5,
size = 1.1)
For some final touches (to make the map prettier), we can add a centred title and add a minimal theme.
We’ll add a custom Google font, Roboto, using the showtext package, loading it into R with font_add_google()
.
# Add font
font_add_google("Roboto")
showtext_auto(enable = TRUE)
ggplot() +
geom_sf(data = nsw,
colour = "grey60",
fill = "white") +
geom_point(data = frogs,
mapping = aes(x = decimalLongitude,
y = decimalLatitude),
colour = "#6fab3f",
alpha = 0.5,
size = 1.1) +
labs(title = "Peron's tree frog",
subtitle = "FrogID observations in New South Wales since 2018") +
theme_void() +
theme(
plot.title = element_text(hjust = 0.5, # horizontally centre
family = "Roboto", # add font
size = 31), # change font size
plot.subtitle = element_text(hjust = 0.5, # horizontally centre
family = "Roboto", # add font
size = 25) # change font size
)
To save your plot in your current folder, you can use:
# set dpi for text
showtext_opts(dpi = 320)
# save
ggsave(here("map_perons-tree-frog.png"),
height = 9, width = 10,
unit = "in",
dpi = 320)
Final thoughts
We hope this post has helped make the basic steps of making a map simple and easy to understand. For more advanced mapping in R, check out our ALA Labs articles on exploring dingo observations and how to make a choropleth map with multiple colour scales.
Expand for session info
─ Session info ───────────────────────────────────────────────────────────────
setting value
version R version 4.3.2 (2023-10-31 ucrt)
os Windows 10 x64 (build 19045)
system x86_64, mingw32
ui RTerm
language (EN)
collate English_Australia.utf8
ctype English_Australia.utf8
tz Australia/Sydney
date 2024-02-13
pandoc 2.12 @ C:/Users/KEL329/ANACON~1/Scripts/ (via rmarkdown)
─ Packages ───────────────────────────────────────────────────────────────────
package * version date (UTC) lib source
dplyr * 1.1.4 2023-11-17 [1] CRAN (R 4.3.2)
galah * 2.0.1 2024-02-06 [1] CRAN (R 4.3.2)
ggplot2 * 3.4.4 2023-10-12 [1] CRAN (R 4.3.1)
here * 1.0.1 2020-12-13 [1] CRAN (R 4.3.2)
htmltools * 0.5.7 2023-11-03 [1] CRAN (R 4.3.2)
sessioninfo * 1.2.2 2021-12-06 [1] CRAN (R 4.3.2)
sf * 1.0-14 2023-07-11 [1] CRAN (R 4.3.2)
showtext * 0.9-6 2023-05-03 [1] CRAN (R 4.3.2)
showtextdb * 3.0 2020-06-04 [1] CRAN (R 4.3.2)
sysfonts * 0.8.8 2022-03-13 [1] CRAN (R 4.3.2)
[1] C:/Users/KEL329/R-packages
[2] C:/Users/KEL329/AppData/Local/Programs/R/R-4.3.2/library
─ Python configuration ───────────────────────────────────────────────────────
python: C:/Users/KEL329/OneDrive - CSIRO/Documents/ALA/Github/ala-labs/.venv/Scripts/python.exe
libpython: C:/Users/KEL329/AppData/Local/Programs/Python/Python312/python312.dll
pythonhome: C:/Users/KEL329/OneDrive - CSIRO/Documents/ALA/Github/ala-labs/.venv
version: 3.12.0 (tags/v3.12.0:0fb18b0, Oct 2 2023, 13:03:39) [MSC v.1935 64 bit (AMD64)]
Architecture: 64bit
numpy: C:/Users/KEL329/OneDrive - CSIRO/Documents/ALA/Github/ala-labs/.venv/Lib/site-packages/numpy
numpy_version: 1.26.2
NOTE: Python version was forced by VIRTUAL_ENV
──────────────────────────────────────────────────────────────────────────────