Gilles Colling

@gilles-colling.bsky.social

PhD researcher in biology, studying alien plant species and global change. Background in numerical physics and astrophysics.

Third use-after-free in a month. Only triggers above 4 million points — never shows up in testing. Rewrite the backend in Rust. The borrow checker catches out-of-bounds errors at COMPILE time that would've been segfaults weeks later. 1/3

Puzzle: how many copies does x$a[1] <- 99L trigger on a data.frame? TWO. One value changed, two full copies made. A data.frame is a list of column vectors. The list and each column are separate objects with separate reference counts. Modify one column and R copies the column AND the container. 1/2

5 km apart: 73% species overlap. 50 km: 31%. 200 km: 8%. Fitting the decay curve takes five minutes. But the residuals are where it gets interesting — pairs at the same distance that are consistently more or less similar than predicted. 1/3

Relative abundance went up 30%. Which part moved — focal species increased, or everything else decreased? A ratio collapses two biological processes into one number. Can't tell which drove it. numdenom fits numerator and denominator as a joint model with shared random effects. 1/2

Square grid cells at the equator are 2x the area of cells at 60°N. Bigger cells capture more observations — and if a spatial process spans more than a few degrees of latitude, that area distortion might mess up ALL the downstream statistics. 1/2

left_join() with relationship = "many-to-many" suppresses the warning. But a warning you can silence isn't a contract. join_strict() makes cardinality explicit: 1:1, 1:many, many:many. State what you expect. Get a hard error when it's violated. #rstats

Relative abundance went up 30%. Sounds like good news — until you realize the focal species stayed flat and everything else crashed. A ratio can't tell you which side moved. numdenom models numerator and denominator jointly so you actually know. #rstats

Package depends on X. X depends on Y. Y breaks on the new R release. Your package breaks and you didn't change a single line. Every Imports entry is a bet that the maintainer will keep up with R-core's release cycle. 1/2

Without `force(x)`: "before" prints, then nothing — `x` is never evaluated because nothing used it. Arguments in R aren't evaluated when the function is CALLED. They're evaluated when they're first ACCESSED. This is lazy evaluation. 1/3

The species list says 120 species COULD live here based on habitat and climate. You observed 74. That gap — 46 species absent from suitable habitat — is dark diversity. High observed richness with low dark diversity: the community is saturated. 1/2

`g()` sees `x` because R uses LEXICAL scoping — functions look up variables in the environment where they were DEFINED, not where they were called. `g` was defined inside `f`, so `g`'s parent environment is `f`'s execution environment. This is why closures work. 1/3

Variables as nodes. Draw edges where correlation exceeds the threshold. Take the complement graph — flip every edge. Now every maximal clique in the complement is a valid subset where NO pair exceeds the threshold. Bron-Kerbosch (1973) finds ALL maximal cliques. 1/2

Zero rows. One row. NA in the key column. Duplicate keys. Column name with a space. Factor with unused levels. These aren't exotic inputs. They show up in real pipelines every week. If your function hasn't been tested against each one, it works on demos, not data. 1/2

This isn't a string. It's a LANGUAGE OBJECT with class `formula`. R parsed it into an abstract syntax tree — `~` is the root, left branch is `y`, right branch is `+` with children `x1` and `x2`. Every modelling function in R walks this tree. 1/2

Profile first. The inner loop runs 10 million times. Vectorized R: 4.2 seconds. Rewrite JUST that loop in C++: 0.08 seconds. Rcpp isn't a style choice. It's a scalpel. Find the bottleneck with `profvis`, confirm it's a tight loop that can't vectorize, rewrite THAT function. 1/2

Overall trend: species A is declining. Split by habitat: increasing in forests, increasing in grasslands, increasing in wetlands. Every habitat shows a gain. The aggregate shows a loss. How? Sampling effort shifted toward habitats where the species was ALREADY rare. 1/3

42 million rows. You need 3 columns and records from 2024. Loading the whole file to filter afterward wastes RAM on data you're about to throw away. Filter runs inside the parser. Non-matching rows are skipped as raw bytes — never tokenized, never allocated. 1/2

y doesn't copy anything. R just points two names at the same memory. The moment you write `y[1] <- 99L`, R copies the ENTIRE vector — 40 MB — to keep x unchanged. This is copy-on-modify. It's why "R is slow" complaints often come from code that triggers copies inside loops. Modify in place? 1/2

Add sites from south to north. Richness climbs fast — looks like a latitudinal gradient. Randomize the order. Same sites, same species, slower climb. The curve shape reflects the ORDER you added sites, not the diversity of the landscape. 1/2

Pipe a grouped tibble into a function that doesn't expect groups. No error. Silent wrong results. keyed tags the key columns as metadata — downstream functions can CHECK whether the data is grouped by what they expect before running. #rstats