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
Gilles Colling
@gilles-colling.bsky.social
PhD researcher in biology, studying alien plant species and global change. Background in numerical physics and astrophysics.
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
New alien plant species per decade in Austria: 1950s → 14 2010s → 47 No plateau. The rate itself is accelerating. #biodiversity
VIF says drop temperature. But there are 23 valid subsets of uncorrelated variables — and temperature survives in 19 of them. VIF picked one subset and hid the other 22. corrselect enumerates ALL of them. You choose. #rstats
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
80% of eBird checklists in Austria are within 500m of a parking lot. That species gradient you're mapping? It's a gradient of where people stop their cars. #ecology
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
100 species observed. Is that 100 out of 100 or 100 out of 300? Extrapolation curves flatten when sampling is near-complete — each additional sample adds almost nothing. 1/3
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
Run richness at 1 km, 10 km, 100 km. Hotspots invert. The pattern you find depends on the grain you chose. If your conclusions flip at a different resolution, the resolution IS the finding. #ecology
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
Point falls on the boundary between two polygons. Which one gets it? Depends on the algorithm, the floating-point precision, and sometimes the order of polygons in the file. 1/2
R's own functions: `is.numeric`, `as.data.frame`, `read.csv`, `seq.int`. Base R chose dot.case decades ago. Writing `is_numeric` next to `is.numeric` in the same pipe creates visual noise. Match the ecosystem. #rstats
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
80% of eBird checklists in Austria are within 500m of a parking lot. The species gradient tracks access, not ecology. Any spatial model trained on this data learns where birders walk, not where birds live. #ecology
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
Mahalanobis distance on a full community matrix. Species that co-occur in weird combinations light up as multivariate outliers — BORG flags the records, you decide what they mean. One function, no training step. #rstats
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
Six equidistant neighbors. Every neighbor shares an edge, not a corner. No orientation bias — north-south and east-west distances are equal. Squares can't do any of this. Hexagons are the spatial unit your analysis deserves. #rstats
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