Accessing Monarch Biodiversity Data with New Wolfram Language Functions

October 6, 2020 — Jofre Espigule-Pons, Machine Learning

Earth has experienced five major extinctions since life first appeared almost four billion years ago. The sixth is happening right now; the current extinction rate is between one hundred and one thousand times greater than what it was before 1800.

Despite the alarming extinction rate, it’s easier than ever to document biodiversity with the help of the Wolfram Language. Using the monarch butterfly as an example, I will explore the new biodiversity data access functions in the Wolfram Function Repository and how they can help you join a community of thousands of citizen scientists from iNaturalist in preserving biodiversity.

I developed a set of biodiversity data access functions—INaturalistImport, INaturalistSearch, GBIFImport, GBIFSearch and GloBIData—to better enable researchers, citizen scientists, educators and Wolfram users to access global biodiversity data from the iNaturalist community, Global Biodiversity Information Facility (GBIF) and Global Biotic Interactions (GloBI).

Becoming a Citizen Scientist  Crowdsourced data and observations from citizen scientists can be valuable for researchers, allowing global communities to track biodiversity worldwide. Over the last year, thanks to the iNaturalist app, I observed and identified hundreds of different species during hikes and got invaluable feedback from many different experts, each specializing in some particular life form. There are currently more than 290,000 different species recorded on the platform, a handful of which I was the first iNaturalist user to observe.

A great starting place as a beginner citizen scientist is to learn more about the species you want to find. If you know the scientific name—for the monarch butterfly, it’s Danaus plexippus—WikidataSearch allows you to better visualize what you’re looking for:

WikidataSearch["Danaus Plexippus"]

ImageResize[Import@First@WikidataData[(ExternalIdentifierURL[ExternalIdentifier["WikidataID","Q212398",\[LeftAssociation]"Label"->"Danaus plexippus","Description"->"milkweed butterfly in the family Nymphalidae"\[RightAssociation]], "Wikidata", "Danaus plexippus", http://www.wikidata.org/entity/Q212398]),"Image"],600]

I can also check for some more specific properties, like wingspan, with WikidataData:

WikidataData[(ExternalIdentifierURL[ExternalIdentifier["WikidataID","Q212398",\[LeftAssociation]"Label"->"Danaus plexippus","Description"->"milkweed butterfly in the family Nymphalidae"\[RightAssociation]], "Wikidata", "Danaus plexippus", http://www.wikidata.org/entity/Q212398]),(ExternalIdentifierURL[ExternalIdentifier["WikidataID","P2050",\[LeftAssociation]"Label"->"wingspan","Description"->"distance from one wingtip to the other, of an airplane or an animal"\[RightAssociation]], "Wikidata", "wingspan", http://www.wikidata.org/entity/P2050])]

Or ask for their maximum speed using its species entity (Ctrl + =):

Note that Anosia is a synonym name for the taxon of the genus Danau:

(Entity["Anosia plexippus", Entity["Species","Species:AnosiaPlexippus"], "Entity[\"Species\", \"Species:AnosiaPlexippus\"]", "species specification"])[(EntityProperty["maximum speed", EntityProperty["Species","MaximumSpeed"], "EntityProperty[\"Species\", \"MaximumSpeed\"]"])]

Let’s convert the units into more familiar speed formats:

UnitConvert[(Quantity[18000000, "mm""/""h", millimeters per hour, (("Millimeters")/("Hours"))]),(Quantity[, "km""/""h", kilometers per hour, (("Kilometers")/("Hours"))])]

N@UnitConvert[(Quantity[18000000, "mm""/""h", millimeters per hour, (("Millimeters")/("Hours"))]),(Quantity[, "mi""/""h", miles per hour, (("Miles")/("Hours"))])]

Mapping Species Geography  Monarch butterflies are one of the most amazing insects, capable of flying thousands of kilometers. The majority of them overwinter in a specific region of Mexico and fly back toward the north of North America in four generations (around a two-month period). During the end of summer when the sunlight hours decrease and temperatures drop, a different generation of monarch flies all the way down to Mexico in a single generation.

Using INaturalistSearch, I can easily create a map with all of the North American iNaturalist observations since yesterday:

ResourceFunction["INaturalistSearch"]

monarchData=[ "Danaus plexippus" ,"ObservationGeoRange"->GeoBounds[(Entity["North America", Entity["GeographicRegion","NorthAmerica"], "Entity[\"GeographicRegion\", \"NorthAmerica\"]", "geographic region"])],"ObservationDateRange"->{Yesterday,Today},"QualityGrade"->"Research" ];

The output is a large Dataset; we can check the first observation, for example:

First@monarchData

Let's use a custom GeoMarker icon image to represent each recorded observation of the monarch butterfly:

monarchIcon=Rotate[,-30Degree];

GeoGraphics[{GeoMarker[monarchData[All,"GeoPosition"],monarchIcon,"Scale"->0.06]},ImageSize->500,GeoRange->GeoBounds[Polygon[{(Entity["United States", Entity["Country","UnitedStates"], "Entity[\"Country\", \"UnitedStates\"]", "country"]),(Entity["Mexico", Entity["Country","Mexico"], "Entity[\"Country\", \"Mexico\"]", "country"])}]]]

Next, I would like to visualize their annual migration. But I want to first check the total count of observations over a year (2019), because if there are too many (over one thousand observations), then we should avoid using INaturalistSearch multiple times (See the iNaturalist API recommended practices page for details):

[ "Danaus plexippus" ,"Count","ObservationGeoRange"->GeoBounds[(Entity["North America", Entity["GeographicRegion","NorthAmerica"], "Entity[\"GeographicRegion\", \"NorthAmerica\"]", "geographic region"])],"ObservationDateRange"->{DateObject[{2019,1,1}],DateObject[{2019,12,31}]} ]

There are over thirty thousand observations! So it is clear that I will need to use either INaturalistImport or GBIFImport.

As a first step, we need to go to the GBIF site and download a large dataset as a CSV file.

Now that we have the file, we are ready to import the data using GBIFImport:

monarchData=["NorthAmericaOccurrences.csv"]//Quiet;

GBIF gathers data from multiple institutions; by using Counts on the "InstitutionCode" property, we can see that the vast majority of observations on monarch butterflies are coming from iNaturalist users:

Counts@monarchData[All,"InstitutionCode"]

We can create a DateHistogram to see when there are more observations of monarchs:

observationDates= monarchData[All,"Date"];

DateHistogram[observationDates,"Day",ColorFunction->"SolarColors",ImageSize->Large,PlotTheme->"Detailed"]

Not surprisingly, observations rise in March when the northward migration starts, and peak in August when monarch butterflies are highly present in gardens of the northern US—where the density of iNaturalist users is higher.

As a fun experiment, we can compare daily page hits for two related Wikipedia articles (“Monarch butterfly” and “Monarch butterfly migration”) using WikipediaData and DateListLogPlot:

monarchButterfly=WikipediaData["Monarch butterfly","DailyPageHits"];
monarchMigration=WikipediaData["Monarch butterfly migration","DailyPageHits"];

DateListLogPlot[{monarchButterfly,monarchMigration},PlotRange->{{DateObject[{2019}],DateObject[{2019,12,31}]},All},PlotLegends->{"Monarch butterfly","Monarch butterfly migration"},ImageSize->600,PlotTheme->"Detailed"]

The “Monarch butterfly” article gets a higher number of daily visits during July and August when the presence of monarchs is higher in the US. We can also observe two peaks for the “Monarch butterfly migration” article corresponding to the start of northward migration in March and another in September when they fly southwest toward Mexico.

It seems that people search for info about the butterflies when they hear about the start of the northward migration, but it isn’t until summer when they actively go out and record their observations of monarchs.

If we compare this with page views of the Spanish version of the article, the graph shows a decrease during summer when monarchs are mainly in the US:

monarchButterfly=WikipediaData["Monarch butterfly","DailyPageHits",Language->{(Entity["English", Entity["Language","English"], "Entity[\"Language\", \"English\"]", "language"]) -> (Entity["Spanish", Entity["Language","Spanish"], "Entity[\"Language\", \"Spanish\"]", "language"])}];

DateListLogPlot[monarchButterfly,PlotRange->{{DateObject[{2019}],DateObject[{2019,12,31}]},All},PlotLegends->{"Monarch butterfly (Spanish page)"},ImageSize->600,PlotTheme->"Detailed"]

To create the map animation of the migration, we need to group the observations by date. This is easily achieved in the dataset using GroupBy:

observationsPerDay=monarchData[GroupBy["Date"]];

Next, we need to create some plotting utilities to make a more interesting map:

colorDateFunction[date_DateObject]:=ColorData["BrightBands"][ToExpression@DateValue[date,"ISOYearDayShort"]/365]

The previous function will allow us to represent observations on the map with different colors that correspond to their observation dates. We can create its corresponding legend using (HyperlinkTemplate[SwatchLegend, https://reference.wolfram.com/language/ref/SwatchLegend.htmlNone, https://reference.wolfram.com/language/ref/SwatchLegend.html, HyperlinkActionRecycled, HyperlinkActive, , ]):

legend=SwatchLegend[Map[ColorData["BrightBands"][(#-0.5)/12]&,Range[12]],Map[Style[#,FontSize->18]&,{"January","February","March","April","May","June","July","August","September","October","November","December"}],LegendFunction->(Framed[#,RoundingRadius->5,FrameStyle->White,Background->White]&),LegendLayout->{"Column",2},LegendMarkerSize->15,LegendMarkers->"Bubble"]

We also need to get the geographic positions associated with each day:

getGeoPositionsPerDate[date_DateObject]:=KeyTake[observationsPerDay,date][[1,All,"GeoPosition"]]

The above one-liner will get a list of GeoPosition objects corresponding to the observations of a specific date.

The following function will get a list of all days since the start of 2019 until the specified date:

getDates[date_DateObject]:=DateRange[DateObject[{2019,1,1}],date]

Finally, we can add some random tumbling to the monarch icons using Rotate and RandomReal, to give them a little life and make them look like they’re flying:

mapNorthMigrationByDate[date_]:=Rasterize@GeoGraphics[Join[Flatten@Map[{colorDateFunction[#],Opacity[0.8],PointSize[.009],Point[getGeoPositionsPerDate[#]]}&,getDates[date]],{Map[GeoMarker[#,Rotate[monarchIcon,RandomReal[{-20,20}] Degree],"Scale"->0.08]&,getGeoPositionsPerDate[date]]},
{Black,Inset[Framed[Style["Observation Date\n"<>DateString[date,"ISODate"],FontFamily->"Helvetica",FontSize->22],FrameStyle->White,RoundingRadius->4,Background->White],Scaled[{0.11,0.88}]],
Black,
Inset[legend,Scaled[{0.15,0.2}]]}],
ImageSize->{1010,480},
GeoRange->{{14,48},{-124,-67}},
GeoBackground->"StreetMapNoLabels"]

Let’s try a map for a particular day:

mapNorthMigrationByDate[DateObject[{2019,4,17}]]

We can do the same for the migration toward the south that starts in September by rotating the icons top to bottom:

mapSouthMigrationByDate[date_]:=Rasterize@GeoGraphics[Join[Flatten@Map[{colorDateFunction[#],Opacity[0.8],PointSize[.009],Point[getGeoPositionsPerDate[#]]}&,getDates[date]],{Map[GeoMarker[#,Rotate[monarchIcon,RandomReal[{-170,-210}] Degree],"Scale"->0.08]&,getGeoPositionsPerDate[date]]},
{Black,Inset[Framed[Style["Observation Date\n"<>DateString[date,"ISODate"],FontFamily->"Helvetica",FontSize->22],FrameStyle->White,RoundingRadius->4,Background->White],Scaled[{0.11,0.88}]],
Black,
Inset[legend,Scaled[{0.15,0.2}]]}],
ImageSize->{1010,480},
GeoRange->{{14,48},{-124,-67}},
GeoBackground->"StreetMapNoLabels"]

Cool! Now we only need to create one map for each day of the year and use Export to obtain an APNG of each one (note that it will take a while to create that many frames with 22,000 observations):

Export["MonarchMigration.apng",Join[Map[mapNorthMigrationByDate ,DateRange[DateObject[{2019,1,1}],DateObject[{2019,8,31}]]],Map[mapSouthMigrationByDate ,DateRange[DateObject[{2019,9,1}],DateObject[{2019,12,31}]]]],"DisplayDurations"->0.5,"AnimationRepetitions"->Infinity]

Classifying Male and Female Specimens  Let’s try some machine learning techniques on monarch butterflies. The following photo was taken by one of my colleagues, Cassidy, who has a beautiful garden in Champaign, Illinois, with many milkweed plants, the feeding source for monarch caterpillars:

I will start with something simple, ImageIdentify:

ImageIdentify[,(Entity["insects", Entity["Species","Class:Insecta"], "Entity[\"Species\", \"Class:Insecta\"]", "species specification"]),10,"Probability"]

Some iNaturalist observations have annotations attached. For example, users can annotate a specimen’s sex as female or male. This info might be useful to scientists studying the ratio of females to males. But in reality, only a small percentage of users specify whether the monarch they observed was a male or a female. If we could automate these manual annotations of a specimen’s sex, we could create a much larger dataset in order to study female vs. male populations. Let’s try to gather images of monarch females and males and train our own sex classifier with Classify:

males=Map[Import,Normal[[ "Danaus plexippus" ,"Sex"->"Male" ,"HasImage"-> True, "QualityGrade"-> "Research" ][All,"SquareImageURL"]]]//Quiet;

females=Map[Import,Normal[[ "Danaus plexippus" ,"Sex"->"Female" ,"HasImage"-> True ,"QualityGrade"-> "Research"][All,"SquareImageURL"]]]//Quiet;

Create a dataset with a randomized list of key values:

dataset=RandomSample@Flatten[Thread/@Thread[{males,females}-> {"Male","Female"}]];

dataset[[1]]

Separate the dataset into a training set and a test set:

trainingSet=dataset[[;;150]];
testSet=dataset[[151;;]];

Then we can create a classifier using Classify:

c=Classify[trainingSet,PerformanceGoal->"Quality"]

The resulting classifier seems to have a fairly high accuracy of around 75%. For example, I don’t know how to distinguish male from female monarchs without having a reference:

Information[c]

Now we can easily plot the confusion matrix of the classifier using the test set:

cm=ClassifierMeasurements[c,testSet,"ConfusionMatrixPlot"]

Nice! Let’s try a couple of specific examples:

testSet[[2]]

c[]

testSet[[7]]

c[]

It seems to be working decently well with stock images. Let’s try using a photo of a monarch taken by my friend Cassidy in her garden. She told me it was a female—will our classifier confirm her guess?

c[,"Probabilities"]

You could try to improve the current classifier by using more images or data augmentation techniques and (HyperlinkTemplate[training a custom neural network, https://www.wolfram.com/language/12/neural-network-framework/perform-net-surgery-for-transfer-learning.htmlNone, https://www.wolfram.com/language/12/neural-network-framework/perform-net-surgery-for-transfer-learning.html, HyperlinkActionRecycled, HyperlinkActive, , ]). You can start with a neural net model from the Wolfram Neural Net Repository used for image classification problems, like the Squeeze-and-Excitation Net or the Wolfram ImageIdentify Net V1.

Exploring Biotic Interactions  Another way to help you document species is to study what they eat so you know where to find them. Let’s take a closer look at the monarch butterfly’s diet. The Global Biotic Interactions platform has several gigabytes of data containing millions of interactions among species extracted from scientific papers, the Encyclopedia of Life (EOL) and research-grade observations from iNaturalist and GBIF. Here we will explore the food web of monarchs, the animals that feed on milkweed plants (the main diet of monarch caterpillars) and finally, potential monarch parasites:

ResourceFunction["GloBIData","Description"]

On the first argument of GloBIData, we need to specify the taxon. As an option, we can retrieve specific interaction types:

["Danaus Plexippus","InteractionType"->"Eats"]

Wow, that’s a huge food web! Its diet contains many species of plants, including a milkweed (genus Asclepias) that larvae feed on and that also appears in the left cluster. Let’s take that information and use it to find out what the milkweed looks like:

(Entity["milkweed", Entity["Plant","Genus:Asclepias"], "Entity[\"Plant\", \"Genus:Asclepias\"]", "plant"])["Image"]

Last year, during the 2019 Wolfram Summer School in Waltham, Massachusetts, I found a common milkweed during a forest walk near Bentley University. Importing my photo to help other citizen scientists find milkweed—and perhaps monarchs feeding on it—is now straightforward with INaturalistSearch:

ImageResize[Import@First[INaturalistSearch["Asclepias syriaca","UserLogin"-> "jofree"][All,"ImageURL"]],600]

Monarch butterflies have high concentrations of a toxic molecule extracted from milkweed plants. This prevents them from being eaten by many potential predators. If a predator eats them, the toxins cause it to get sick or even suffer cardiac arrest.

With GloBIData, I can also do the reverse and ask for moths and butterflies feeding on milkweed plants by specifying a "TargetTaxon":

[(Entity["milkweed", Entity["Plant","Genus:Asclepias"], "Entity[\"Plant\", \"Genus:Asclepias\"]", "plant"]),"InteractionType"->"EatenBy","TargetTaxon"->(Entity["moths and butterflies", Entity["Species","Order:Lepidoptera"], "Entity[\"Species\", \"Order:Lepidoptera\"]", "species specification"])]

Although the monarch larvae are the main consumers of milkweed, many other types of butterflies and moths also like it.

I can also get a taxonomic graph of organisms that are parasites of monarchs using the "HasParasite" interaction type:

["Danaus Plexippus","InteractionType"->"HasParasite"]

Monarch Butterfly Biosphere Reserve During winter, monarchs travel by the thousands to mountain forests in Mexico. One of the most famous wintering sanctuaries is the Monarch Butterfly Biosphere Reserve, a World Heritage Site in Michoacán, which is about 60 miles northwest of Mexico City. Let’s find the exact location using WikidataSearch:

WikidataSearch["monarch butterfly biosphere"]

winteringLocation=First@WikidataData[(ExternalIdentifierURL[ExternalIdentifier["WikidataID","Q852546",\[LeftAssociation]"Label"->"Monarch Butterfly Biosphere Reserve","Description"->"biosphere reserve"\[RightAssociation]], "Wikidata", "Monarch Butterfly Biosphere Reserve", http://www.wikidata.org/entity/Q852546]),(ExternalIdentifierURL[ExternalIdentifier["WikidataID","P625",\[LeftAssociation]"Label"->"coordinate location","Description"->"geocoordinates of the subject. For Earth, please note that only WGS84 coordinating system is supported at the moment"\[RightAssociation]], "Wikidata", "coordinate location", http://www.wikidata.org/entity/P625])];

We can easily see the massive distance that, for example, Cassidy’s monarch (born in Champaign) will need to fly:

GeoDistance[(Entity["Champaign", Entity["City",{"Champaign","Illinois","UnitedStates"}], "Entity[\"City\", {\"Champaign\", \"Illinois\", \"UnitedStates\"}]", "city"]),winteringLocation]

We can also plot the trip with GeoPath and Arrow:

GeoGraphics[{Red,Thick,Arrow@GeoPath[{(Entity["Champaign", Entity["City",{"Champaign","Illinois","UnitedStates"}], "Entity[\"City\", {\"Champaign\", \"Illinois\", \"UnitedStates\"}]", "city"]),winteringLocation}]},ImageSize->600,GeoBackground->"Satellite",GeoRange->Polygon[{(Entity["United States", Entity["Country","UnitedStates"], "Entity[\"Country\", \"UnitedStates\"]", "country"]),(Entity["Mexico", Entity["Country","Mexico"], "Entity[\"Country\", \"Mexico\"]", "country"])}]]

The Wolfram Language is also capable of getting satellite images. For example, we can look at the Biosphere Reserve using GeoImage:

GeoImage[winteringLocation,GeoServer->"DigitalGlobe"]

As a final exploration, we can also check the observations of threatened species near the previous location using GeoDisk and INaturalistSearch:

threatenedSpecies=[All,"ObservationGeoRange"-> GeoBounds[GeoDisk[winteringLocation,(QuantityUnit[3])]],"Threatened"-> True,
"QualityGrade"-> "Research",
"HasImage"-> True];

For a more visual understanding, we can create a PieChart of the different types of threatened life forms from the previous list using the "IconicTaxon" property and Counts:

PieChart[Counts@Normal@threatenedSpecies[All,"IconicTaxon"],]

We can see that the majority of observations are from insects, monarchs in particular. But there are some mushrooms, plants and even a bird and an amphibian counted as threatened species.

Similar to our visual pie chart, an image is worth a thousand words. So let’s create a final ImageCollage out of all the observations of threatened species from the forest sanctuary:

ImageCollage@Map[Import,Normal@threatenedSpecies[All,"SquareImageURL"]]

Computing a More Biodiverse Future  I hope you enjoyed these explorations. The number of applications that these new biodiversity data access functions open up is limited only by our imaginations, especially if we start combining them with the variety of built-in Wolfram Language functions. For example, use ScheduledTask to automatically alert you via email with SendMail to the presence of an invasive species in a certain region or new sightings of endangered species in a forest with risk of deforestation.

If you’re looking for more ideas on some of the different ways you can use the Wolfram Language in your adventures, check out my last blog post on the invasive stink bug, as well as my Wolfram Community posts on marine worms and Moon phases and the great horned owl.

Finally, I want to thank Wolfram Research programmer John Cassel for his valuable advice during my journey to bring these functions to life. I would also like to thank professor Gareth Russell of the Department of Ecology at the New Jersey Institute of Technology for his valuable feedback on the functions. The ability to directly import GBIF data is an important step toward the implementation of species distribution modeling using the Wolfram Language. These models are widely used in ecology, and they help to monitor changes in biodiversity and, consequently, help to protect our marvelous planet.

The golden age for becoming a citizen scientist is now. Humanity has counted almost two million distinct life forms—only a small fraction of what scientists believe is still waiting to be found. How many will you find?