Every analysis begins with data entry, and it is the step we talk about least. A method is only as good as the table it starts from, and most mistakes that matter are made before any sorting happens — a species miscoded, a column shifted, a presence lost. This chapter is about getting a two-way table into coenosr cleanly, in whatever form your data already takes, and confirming it is right before you go on.
coenosr accepts three forms of input, and they all arrive at the same place: a tidy table of which species occur in which sites, ready for everything that follows. We will read each form, then look at the one habit worth building — displaying what you just read — and finish with a small but real problem, choosing good abbreviations for species names.
Show the code
## --- Standard packages ---## data wrangling and tidy reshapinglibrary(tidyverse)## formatted tableslibrary(gt)## --- Package from github/kimbridges ---## install once with: install_github("kimbridges/coenosr")library(coenosr)
5.1 Three forms, one destination
Data about what occurs where tends to arrive in one of three shapes. You may have a list per site — each site followed by the species found there. You may have a wide table — species down the rows, sites across the columns, a value in each cell. Or, if you are working with old vegetation studies, you may have a COENOS file in the original .REL format. coenosr has a reader for each: read_lists(), read_2way(), and read_rel(). Each returns the same kind of object, so once your data is in, the path forward is identical no matter where it came from.
We will use a small example throughout: fifteen species across ten sites, small enough to see whole.
5.2 The ragged form: as you collect it
The most natural way to record this data is one line per site: name the site, then list what is there. This is how a field survey actually happens — you stand in a plot and call out the species, perhaps into a recorder, and a line of the file is one plot’s worth of names. The lines are not the same length, because each site has its own species, and that is fine.
read_lists() reads it. The first item on each line is the site; the rest are its species.
Show the code
## read the "site, then its species" formrel <-read_lists("data/simple_10x15_simplified.txt")## look at the first few occurrences (its own coenos_rel summary, kept as-is)print(head(rel))
<coenos_rel> 10 releves x 15 species, 6 occurrences
releve species cover
1 1 G 1
2 1 M 1
3 1 R 1
4 1 A 1
5 1 L 1
6 1 C 1
The result is a long table with one row per species-in-a-site. This dataset has 10 sites and 15 species. Absence is simply unrecorded — a site lists only what was present — which keeps the file short and matches how the data was gathered.
If your records carry cover values — how much of each plot a species filled — write them right after the species name, and read_lists() will pick them up automatically:
1, G 1, M +, R 4, A 5, L +, C 2, P 1
A trailing Braun-Blanquet symbol (+, R) or a digit is read as the cover; anything else is part of the species name. The same function reads both styles, so you do not choose a reader based on whether you happened to record cover.
5.3 The wide form: the spreadsheet
If your data is already a rectangle — species in rows, sites in columns, a 1 or a cover value where a species occurs and a 0 or blank where it does not — that is the form read_2way() expects. It is the shape most people reach for in a spreadsheet, and it is the literal two-way table.
Show the code
## read the wide species-by-sites matrixrel_wide <-read_2way("data/simple_10x15_input.txt")## the two readers produce the same datasame_data <-setequal(paste(rel$releve, rel$species),paste(rel_wide$releve, rel_wide$species))
The wide file and the list file describe the same survey, and coenosr confirms it: reading them gives the same set of occurrences (TRUE). Use whichever form your data is already in.
5.4 COENOS files
For the original 1991 data, read_rel() reads the COENOS .REL format directly. The package bundles a few historical examples; you are unlikely to have your own, but if you are reviving an old study, this is the door.
A .REL file has a simple shape. Each relevé opens with its name on a line of its own, then one line per species: a short code — four letters of the genus and four of the species — followed by a cover value, either a number or a Braun-Blanquet symbol like +. A lone $ closes the relevé, and the next one begins. The first relevé of the Dieren file looks like this:
BILL01
CARE LYNG 5
ELEO PALU 2
JUNC ARCT 2
SCIR ACUT 2
LILA OCCI +
CENT MINI +
TRIG MARI +
TILL AQUA +
ISOE MARI +
ELAT TRIA +
$
The examples in this book show only the opening of each file; the bundled datasets hold many more relevés. You can read any of them in full with read_rel() and look at the result, or open the .REL in a text editor to see the whole thing.
Show the code
## a bundled COENOS file, read straight from the packagedieren <-read_rel(system.file("extdata", "DIEREN.REL", package ="coenosr"))
That file holds 116 species across 18 relevés — a real coastal vegetation survey, in the format the program used.
5.5 Confirm what you read
Here is the one habit worth keeping for every dataset you ever read: display it immediately. A reader that runs without error has not promised you the data is right, only that it could parse the file. The way to check a two-way table is to look at it as a two-way table.
Our internal form is long — one row per occurrence — which is ideal for computing but not for reading. So we pivot it back to the wide, human-shaped table. This is a good moment for the rectangular-minded reader: the long form and the wide form hold the same data, and tidyr::pivot_wider() turns one into the other.
Show the code
## pivot the long table back to a wide species-by-sites viewtwo_way <- rel |>pivot_wider(names_from = releve, # sites become columnsvalues_from = cover, # the cell valuevalues_fill ="") # absent cells left blank## show it as a tabletwo_way |>gt()
species
1
2
3
4
5
6
7
8
9
10
G
1
1
1
1
M
1
1
1
1
R
1
1
1
1
1
A
1
1
1
1
L
1
1
1
1
1
1
1
1
1
1
C
1
1
1
1
1
1
1
1
P
1
1
1
1
1
1
1
1
1
S
1
1
1
1
K
1
1
1
1
V
1
1
1
1
B
1
1
1
1
H
1
1
1
1
F
1
1
1
U
1
Y
1
There it is: species down the side, sites across the top, a mark where a species occurs and a blank where it does not. Most of the table is empty, exactly as a two-way table should be. Read across a row to see where a species was found; read down a column to see what grew in a site. If something is wrong — a species in the wrong place, a column that should not be empty — you will see it here, before it costs you anything. This is also, not incidentally, the unsorted table: the scatter that the next chapter will sort into pattern.
5.6 Names and abbreviations
Real data needs names, and names raise a practical problem worth a moment. The analysis works with short codes — G, L, C in our toy data — because long names will not fit across a table. But a code like G means nothing on its own. Somewhere you need a key from each code to its full name, and for living things the full name is a Latin binomial, ideally with its naming authority.
To make this concrete, picture a different kind of two-way table: a survey of farmers’ markets, where the “species” are produce and a site is a market that sells them. Here is a names table for a few of the items.
Show the code
## read the names key: common name, genus, species, variety, authorproduce <-read.csv("data/produce_names.csv", stringsAsFactors =FALSE)## build the scientific name as markdown: *italic binomial* then roman author.## the asterisks mark italics; fmt_markdown() renders them per cell, which lets## the binomial be italic while the author stays upright -- in one cell.produce$scientific <-paste0("*", produce$genus, " ", produce$species, "* ", produce$author)produce |>select(common, scientific) |>gt() |>fmt_markdown(columns = scientific) |># the trick: render the per-cell markdowncols_label(common ="Produce", scientific ="Scientific name")
Produce
Scientific name
apple
Malus domestica (Suckow) Borkh.
broccoli
Brassica oleracea L.
cabbage
Brassica oleracea L.
cauliflower
Brassica oleracea L.
lemon
Citrus limon (L.) Osbeck
potato
Solanum tuberosum L.
watermelon
Citrullus lanatus (Thunb.) Matsum. & Nakai
The display detail is worth keeping. A binomial is set in italics and its author is not, and the two have to share one table cell. The way to do it is to write the cell as Markdown — asterisks around the binomial, the author left plain — and let gt::fmt_markdown() render it. The italics fall on the binomial alone.
Now the codes. The traditional COENOS convention builds a code from the first four letters of the genus and the first four of the species. For most of our produce that gives a clean, unique code:
Show the code
## four letters of genus + four of speciesab4 <-function(a, b) {paste0(toupper(substr(a, 1, 4)), toupper(substr(b, 1, 4)))}## apply the conventionproduce$code4 <-ab4(produce$genus, produce$species)produce |>select(common, genus, species, code4) |>gt() |>cols_label(common ="Produce", genus ="Genus", species ="Species",code4 ="Code (genus + species)")
Produce
Genus
Species
Code (genus + species)
apple
Malus
domestica
MALUDOME
broccoli
Brassica
oleracea
BRASOLER
cabbage
Brassica
oleracea
BRASOLER
cauliflower
Brassica
oleracea
BRASOLER
lemon
Citrus
limon
CITRLIMO
potato
Solanum
tuberosum
SOLATUBE
watermelon
Citrullus
lanatus
CITRLANA
Read down the codes and a problem appears. Broccoli, cabbage, and cauliflower are all Brassica oleracea — three different market items, but one biological species — so the convention hands all three the same code, BRASOLER. The mechanical rule has collided.
This is the ordinary fate of any naming scheme: sooner or later the world hands it something it cannot tell apart, and you make a judgment. Here the judgment is easy. The three differ by variety — the Italica, Capitata, and Botrytis cultivar groups — so when the species cannot separate them, reach for what can:
Show the code
## when the species collides, code from genus + variety insteadproduce$code <-ifelse(produce$variety =="",ab4(produce$genus, produce$species), # the usual caseab4(produce$genus, produce$variety)) # the collision fixproduce |>select(common, genus, species, variety, code) |>gt() |>sub_missing(missing_text ="") |>cols_label(common ="Produce", genus ="Genus", species ="Species",variety ="Variety", code ="Code")
Produce
Genus
Species
Variety
Code
apple
Malus
domestica
MALUDOME
broccoli
Brassica
oleracea
italica
BRASITAL
cabbage
Brassica
oleracea
capitata
BRASCAPI
cauliflower
Brassica
oleracea
botrytis
BRASBOTR
lemon
Citrus
limon
CITRLIMO
potato
Solanum
tuberosum
SOLATUBE
watermelon
Citrullus
lanatus
CITRLANA
BRASITAL, BRASCAPI, BRASBOTR — distinct again. The point is not the particular fix but the habit: a coding convention is a convention, not a law, and a few minutes spent giving every item a clear, unique code is repaid every time you read the table afterward.
5.7 Onward
Your data is in, in whatever form it arrived, and you have looked at it and confirmed it is what you meant. That is the unglamorous work that everything else rests on. With a clean table in hand, the next chapter turns to what coenosr was built to do: set aside the extremes, find the species that share a distribution, and sort the scatter into a readable two-way table.