ibis
Table Joins¶
Learning Goals¶
use
join()
to combine two tables on a key column
import ibis
from ibis import _
import ibis.selectors as s
con = ibis.duckdb.connect()
Last time we started getting comfortable with lazy evaluation (head()
and execute()
) in ibis, and began to learn how to select()
(subset columns) and filter()
(subset rows), as well as looking at distinct values. Today we will continue to draw on these skills as we go deeper into the fisheries data in search of the evidence of the North Atlantic Cod collapse. In the process, we shall pick up some new methods as well.
As before, let’s start with reading in data. Rather than focus on the metrics table, this time we will connect to several tables at the same time. Note how we can reuse the base_url
to avoid extra typing, but take care that we reading the right CSV file in each case! As before, we explicitly set the nullstr
value as well to ensure missing value codes are correctly interpreted.
base_url = "https://huggingface.co/datasets/cboettig/ram_fisheries/resolve/main/v4.65/"
stock = con.read_csv(base_url + "stock.csv", nullstr="NA")
timeseries = con.read_csv(base_url + "timeseries.csv", nullstr="NA")
assessment = con.read_csv(base_url + "assessment.csv", nullstr="NA")
Fish ‘stocks’¶
Like most real world data science problems, understanding these tables requires both a bit of background in fisheries science and a lot of splunking into the data. For our purposes, one of the key things you should know is that fisheries are divided into “stocks”, which you can think of as a particular species of fish in a particular area of the ocean. Let’s use the stock table to explore this idea a bit more. Let’s begin with a peek at the stock table:
stock
DatabaseTable: ibis_read_csv_v4fblezl6jahlpkem4wb2wqefq stockid string tsn int64 scientificname string commonname string areaid string stocklong string region string primary_country string primary_FAOarea int64 ISO3_code string GRSF_uuid string GRSF_areaid string inmyersdb int64 myersstockid string state string
Ah! commonname
looks like a good place as any to go looking for Atlantic cod. Of course if we knew (or looked up) the scientific name of the species, that might be even better – after all, common names are not always as precise. Let’s see what we can find:
(stock
.filter(_.commonname == "Atlantic cod")
.select(_.stockid, _.scientificname, _.commonname,
_.areaid, _.region, _.primary_country, _.ISO3_code)
.head()
.execute()
)
stockid | scientificname | commonname | areaid | region | primary_country | ISO3_code | |
---|---|---|---|---|---|---|---|
0 | COD1ABCDE | Gadus morhua | Atlantic cod | multinational-ICES-1ABCDE | Canada East Coast | Greenland | GRL |
1 | COD1F-XIV | Gadus morhua | Atlantic cod | multinational-ICES-1F-XIV | Europe non EU | Greenland | GRL |
2 | COD1IN | Gadus morhua | Atlantic cod | multinational-ICES-1IN | Canada East Coast | Greenland | GRL |
3 | COD2J3KL | Gadus morhua | Atlantic cod | Canada-DFO-2J3KL | Canada East Coast | Canada | CAN |
4 | COD3M | Gadus morhua | Atlantic cod | multinational-NAFO-3M | Canada East Coast | Portugal | PRT |
Lots of stocks of Atlantic cod! Each row begins with a unique stockid
. A column that uniquely identifies each row in a given table is often referred to as the “primary key” for that table (and is often but not necessarily listed first).
The rows that follow give us some sense of what defines a “stock” as a species in an area: we see a few different identifiers for the species: commonname
, scientificname
.
We also see information abot the area the stock occurs in – such as areaid, region, and primary country. (For display purposes we selected only a subset of columns).
While we have found the Cod, we haven’t yet found any data about the cod catch over time! For that we will need to look in the timeseries data. Let’s see how it is organized:
timeseries.head().execute()
assessid | stockid | stocklong | tsid | tsyear | tsvalue | |
---|---|---|---|---|---|---|
0 | ABARES-BGRDRSE-1960-2011-CHING | BGRDRSE | Blue grenadier Southeast Australia | CdivMEANC-ratio | 1960 | NaN |
1 | ABARES-BGRDRSE-1960-2011-CHING | BGRDRSE | Blue grenadier Southeast Australia | CdivMEANC-ratio | 1961 | NaN |
2 | ABARES-BGRDRSE-1960-2011-CHING | BGRDRSE | Blue grenadier Southeast Australia | CdivMEANC-ratio | 1962 | NaN |
3 | ABARES-BGRDRSE-1960-2011-CHING | BGRDRSE | Blue grenadier Southeast Australia | CdivMEANC-ratio | 1963 | NaN |
4 | ABARES-BGRDRSE-1960-2011-CHING | BGRDRSE | Blue grenadier Southeast Australia | CdivMEANC-ratio | 1964 | NaN |
We again have a column called stockid
. While we no longer have columns such as commonname
or scientificname
to tell us what species each row in the timeseries is measuring, we now know that we can look up that information in the stock table using the stockid
. Such a column is often called a “foreign key”, because it matches the primary key of a separate table. (it appears the timeseries data has no ‘primary key’ of it’s own – no column that has a unique value for each row.). Rather than have to switch back and forth between two tables, we can join the two tables on stockid
:
(stock
.filter(_.commonname == "Atlantic cod")
.join(timeseries, "stockid")
.head()
.select(_.stockid, _.scientificname, _.tsid, _.tsyear,
_.tsvalue, _.stocklong, _.stocklong_right) # subset of columns to keep display narrow
.execute()
)
stockid | scientificname | tsid | tsyear | tsvalue | stocklong | stocklong_right | |
---|---|---|---|---|---|---|---|
0 | COD4VsW | Gadus morhua | CdivMEANC-ratio | 1958 | 0.997804 | Atlantic cod Eastern Scotian Shelf | Atlantic cod Eastern Scotian Shelf |
1 | COD4VsW | Gadus morhua | CdivMEANC-ratio | 1959 | 1.706091 | Atlantic cod Eastern Scotian Shelf | Atlantic cod Eastern Scotian Shelf |
2 | COD4VsW | Gadus morhua | CdivMEANC-ratio | 1960 | 1.308003 | Atlantic cod Eastern Scotian Shelf | Atlantic cod Eastern Scotian Shelf |
3 | COD4VsW | Gadus morhua | CdivMEANC-ratio | 1961 | 1.713846 | Atlantic cod Eastern Scotian Shelf | Atlantic cod Eastern Scotian Shelf |
4 | COD4VsW | Gadus morhua | CdivMEANC-ratio | 1962 | 1.685411 | Atlantic cod Eastern Scotian Shelf | Atlantic cod Eastern Scotian Shelf |
Effectively all this has done is take our timeseries table and for each stockid, add extra columns explaining what the stock table tells us about the stockid - species names, areas, and so on. The join has made our data is much wider than before – we have all the columns from both tables. (Note that both tables happened to have one column with the same name, stocklong
. A truly tidy database would not have done this – we can easily see that this information belongs in the stock table. Because our database cannot assume these are the same when we join, it has renamed the one on the “right” (from timeseries) as “stocklong_right” to distinguish them). Because each stockid
was repeated in the timeseries table, now all this other information is repeated too. This is not as inefficient as it may sound, thanks to internal optimizations in the database.
While it is clear even from this head()
preview that we have the columns from both tables, what about the rows? Our stock table was already filtered to a subset of rows containing only Cod stocks. This join (technically called an “inner join”) has kept only those stockid
s, so we now have timeseries only about Cod! In fact, we could have instead joined the full tables for all stock ids, and then applied the filter for commonname
.
Exercise¶
Try further exploring this resulting table using select()
and distinct()
to get a better sense of what rows are here. You will notice additional “*id” columns, like asssesid
or areaid
matching other tables in the data. Explore filtering and joinging with these tables as well.
assessment
DatabaseTable: ibis_read_csv_52rlwfdjjbf4rffp5d34j4cuxa assessid string assessorid string stockid string stocklong string recorder string daterecorded timestamp(6) dateloaded timestamp(6) assessyear string assesssource string contacts string notes string pdffile string assess int64 refpoints int64 assessmethod string assesscomments string xlsfilename string mostrecent int64
assessment.group_by(_.stockid).agg(n=_.count()).order_by(_.n.desc()).execute()
stockid | n | |
---|---|---|
0 | CODIIIaW-IV-VIId | 12 |
1 | POLLIEG | 11 |
2 | HAKENRTN | 11 |
3 | HERRNIRS | 11 |
4 | HERRNS-IIIa-VIId | 11 |
... | ... | ... |
1507 | SSALMLWASH | 1 |
1508 | STROUT22-32 | 1 |
1509 | BLRAYVIIe | 1 |
1510 | SGRAYVI-VII | 1 |
1511 | PLAICIIIa | 1 |
1512 rows × 2 columns
assessment.group_by(_.stockid).order_by(_.assessyear).agg(assessid=_.assessid.last()).filter(_.stockid == "CODIIIaW-IV-VIId").execute()
stockid | assessid | |
---|---|---|
0 | CODIIIaW-IV-VIId | WGNSSK-CODIIIaW-IV-VIId-1963-2021-ICESIMP2021-2 |
timeseries
DatabaseTable: ibis_read_csv_qoxovyrtjfbifanq72wnhpfbau assessid string stockid string stocklong string tsid string tsyear int64 tsvalue float64
stock
DatabaseTable: ibis_read_csv_v4fblezl6jahlpkem4wb2wqefq stockid string tsn int64 scientificname string commonname string areaid string stocklong string region string primary_country string primary_FAOarea int64 ISO3_code string GRSF_uuid string GRSF_areaid string inmyersdb int64 myersstockid string state string