Functional programming · Advanced (data.table)
[i, j, by] when data get large.
Q1 · as.data.table() mindset
as.data.table() converts data frames without dropping columns; printing shows head and tail of large tables.
print (DTreef) # data.table print method
transect_id site species count depth_m
<char> <char> <char> <int> <num>
1: T01 North Parrotfish 4 6.2
2: T01 North Surgeonfish 12 6.2
3: T01 North Grouper 1 6.2
4: T01 North Snapper 7 6.2
5: T01 North Damselfish 21 6.2
6: T02 South Parrotfish 6 5.2
7: T02 South Surgeonfish 7 5.2
8: T02 South Grouper 2 5.2
9: T02 South Snapper 9 5.2
10: T02 South Damselfish 20 5.2
11: T03 East Parrotfish 6 6.3
12: T03 East Surgeonfish 13 6.3
13: T03 East Grouper 4 6.3
14: T03 East Snapper 5 6.3
15: T03 East Damselfish 26 6.3
16: T04 North Parrotfish 6 8.5
17: T04 North Surgeonfish 8 8.5
18: T04 North Grouper 1 8.5
19: T04 North Snapper 2 8.5
20: T04 North Damselfish 17 8.5
21: T05 South Parrotfish 15 10.4
22: T05 South Surgeonfish 10 10.4
23: T05 South Grouper 0 10.4
24: T05 South Snapper 4 10.4
25: T05 South Damselfish 22 10.4
26: T06 East Parrotfish 11 9.9
27: T06 East Surgeonfish 10 9.9
28: T06 East Grouper 8 9.9
29: T06 East Snapper 8 9.9
30: T06 East Damselfish 26 9.9
31: T07 North Parrotfish 5 7.7
32: T07 North Surgeonfish 13 7.7
33: T07 North Grouper 2 7.7
34: T07 North Snapper 4 7.7
35: T07 North Damselfish 25 7.7
36: T08 South Parrotfish 9 9.6
37: T08 South Surgeonfish 8 9.6
38: T08 South Grouper 5 9.6
39: T08 South Snapper 5 9.6
40: T08 South Damselfish 22 9.6
41: T09 East Parrotfish 5 4.1
42: T09 East Surgeonfish 14 4.1
43: T09 East Grouper 4 4.1
44: T09 East Snapper 1 4.1
45: T09 East Damselfish 26 4.1
46: T10 North Parrotfish 6 7.9
47: T10 North Surgeonfish 8 7.9
48: T10 North Grouper 4 7.9
49: T10 North Snapper 5 7.9
50: T10 North Damselfish 25 7.9
51: T11 South Parrotfish 2 11.9
52: T11 South Surgeonfish 15 11.9
53: T11 South Grouper 6 11.9
54: T11 South Snapper 5 11.9
55: T11 South Damselfish 24 11.9
56: T12 East Parrotfish 13 3.4
57: T12 East Surgeonfish 9 3.4
58: T12 East Grouper 8 3.4
59: T12 East Snapper 4 3.4
60: T12 East Damselfish 18 3.4
transect_id site species count depth_m
class (DTreef) # data.table and data.frame
[1] "data.table" "data.frame"
dim (DTreef) # rows and columns
Q2 · [i, j, by] core
The template DT[i, j, by] uses i to filter rows, j to compute, by to group.
DTreef[site == "North" , .(mean_count = mean (count)), by = species]
species mean_count
<char> <num>
1: Parrotfish 5.25
2: Surgeonfish 10.25
3: Grouper 2.00
4: Snapper 4.50
5: Damselfish 22.00
Also:
DTfish[port == "Cairns" , .(mean_t = mean (tonnes)), by = species]
species mean_t
<char> <num>
1: Snapper 77.00000
2: Shark 69.56667
3: Grouper 53.60000
4: Tuna 13.90000
Also:
DTac[reef_zone == "Lagoon" , .(total = sum (detections)), by = receiver]
receiver total
<char> <int>
1: R9 3
2: R1 3
3: R11 6
4: R2 3
5: R12 7
Also:
DTbio[, .(max_depth = max (depth_m)), by = burst_id][1 : 5 ]
burst_id max_depth
<int> <num>
1: 1 5.419779
2: 2 5.273251
3: 3 5.460373
4: 4 6.223412
5: 5 6.181104
Q3 · .N and .I
.N counts rows in the current group; .I holds indices for within-group positions.
DTac[, .N, by = animal_id] # rows per tag
animal_id N
<char> <int>
1: A101 8
2: A102 8
3: A103 8
DTac[, .(max_det = max (detections)), by = animal_id] # peak detection per tag
animal_id max_det
<char> <int>
1: A101 7
2: A102 5
3: A103 7
Q4 · .SD group columns
.SD is the Subset of Data for each group — all non-group columns inside j.
DTbio[, lapply (.SD, mean), by = burst_id, .SDcols = c ("depth_m" , "pitch_deg" )]
burst_id depth_m pitch_deg
<int> <num> <num>
1: 1 4.868771 -10.472410
2: 2 4.429912 -4.306365
3: 3 4.762483 -1.356978
4: 4 5.566435 -1.278042
5: 5 5.259435 -4.242083
6: 6 4.977184 5.621892
7: 7 5.124734 -3.856284
8: 8 5.350061 -1.268552
9: 9 5.604608 5.217801
10: 10 3.764709 -3.136650
Also:
DTreef[, lapply (.SD, mean), by = site, .SDcols = c ("count" , "depth_m" )]
site count depth_m
<char> <num> <num>
1: North 8.80 7.575
2: South 9.80 9.275
3: East 10.95 5.925
Also:
DTfish[, .(min_t = min (tonnes), max_t = max (tonnes)), by = port]
port min_t max_t
<char> <num> <num>
1: Cairns 13.9 97.3
2: Mooloolaba 23.0 144.5
3: Broome 23.5 89.9
Q5 · := assign by reference
:= adds or updates columns in place without copying the whole table — fast but side-effectful.
DTfish[, kg : = tonnes * 1000 ] # new column in place
head (DTfish[, .(species, tonnes, kg)]) # verify conversion
species tonnes kg
<char> <num> <num>
1: Snapper 75.7 75700
2: Grouper 79.6 79600
3: Tuna 23.5 23500
4: Shark 67.7 67700
5: Snapper 58.0 58000
6: Grouper 80.6 80600
Q6 · setorder() fast sort
setorder() sorts by reference; sorting biologging by burst and second prepares time-series ops.
setorder (DTbio, burst_id, seconds) # in-place sort
head (DTbio, 5 ) # inspect first rows
burst_id seconds depth_m pitch_deg
<int> <int> <num> <num>
1: 1 0 4.798260 -27.611852
2: 1 1 5.065770 -20.483881
3: 1 2 4.755086 -17.718719
4: 1 3 4.730074 -6.960103
5: 1 4 4.665214 -24.308871
Q7 · Keys and binary search
A key indexes sorted column(s) — subsetting DT[id] uses binary search without merge syntax.
DTac_k <- copy (DTac) # avoid mutating shared object
setkey (DTac_k, animal_id) # sort and index
DTac_k["A101" ] # fast row subset
Key: <animal_id>
animal_id receiver detections reef_zone
<char> <char> <int> <char>
1: A101 R11 5 Slope
2: A101 R2 2 Crest
3: A101 R6 7 Slope
4: A101 R1 4 Crest
5: A101 R5 5 Slope
6: A101 R7 4 Slope
7: A101 R7 4 Crest
8: A101 R9 1 Crest
Q8 · merge() joins
merge() joins tables; with keys set, joins can be faster but always check nrow after.
ports <- data.table (port = c ("Cairns" , "Broome" , "Mooloolaba" ),
region = c ("GBR" , "NW" , "SEQ" ))
merge (DTfish, ports, by = "port" , all.x = TRUE ) # keep all landings
Key: <port>
port year species tonnes kg region
<char> <int> <char> <num> <num> <char>
1: Broome 2018 Tuna 23.5 23500 NW
2: Broome 2019 Snapper 58.0 58000 NW
3: Broome 2020 Tuna 43.7 43700 NW
4: Broome 2020 Shark 89.9 89900 NW
5: Broome 2021 Grouper 74.5 74500 NW
6: Broome 2022 Snapper 49.8 49800 NW
7: Broome 2022 Shark 86.4 86400 NW
8: Broome 2023 Grouper 47.5 47500 NW
9: Broome 2023 Shark 45.1 45100 NW
10: Cairns 2018 Snapper 75.7 75700 GBR
11: Cairns 2018 Shark 67.7 67700 GBR
12: Cairns 2019 Grouper 80.6 80600 GBR
13: Cairns 2019 Shark 85.1 85100 GBR
14: Cairns 2021 Snapper 97.3 97300 GBR
15: Cairns 2021 Tuna 13.9 13900 GBR
16: Cairns 2021 Shark 55.9 55900 GBR
17: Cairns 2022 Grouper 26.6 26600 GBR
18: Cairns 2023 Snapper 58.0 58000 GBR
19: Mooloolaba 2018 Grouper 79.6 79600 SEQ
20: Mooloolaba 2019 Tuna 53.4 53400 SEQ
21: Mooloolaba 2020 Snapper 144.5 144500 SEQ
22: Mooloolaba 2020 Grouper 23.0 23000 SEQ
23: Mooloolaba 2022 Tuna 39.7 39700 SEQ
24: Mooloolaba 2023 Tuna 29.2 29200 SEQ
port year species tonnes kg region
Also:
meta <- data.table (animal_id = c ("A101" , "A102" , "A103" ), cohort = c (2023 , 2023 , 2024 ))
merge (DTac, meta, by = "animal_id" , all.x = TRUE )[1 : 6 ]
Key: <animal_id>
animal_id receiver detections reef_zone cohort
<char> <char> <int> <char> <num>
1: A101 R11 5 Slope 2023
2: A101 R2 2 Crest 2023
3: A101 R6 7 Slope 2023
4: A101 R1 4 Crest 2023
5: A101 R5 5 Slope 2023
6: A101 R7 4 Slope 2023
Also:
site_meta <- data.table (site = c ("North" , "South" , "East" ), mgmt = c ("MPA" , "Open" , "MPA" ))
merge (DTreef, site_meta, by = "site" )[1 : 6 ]
Key: <site>
site transect_id species count depth_m mgmt
<char> <char> <char> <int> <num> <char>
1: East T03 Parrotfish 6 6.3 MPA
2: East T03 Surgeonfish 13 6.3 MPA
3: East T03 Grouper 4 6.3 MPA
4: East T03 Snapper 5 6.3 MPA
5: East T03 Damselfish 26 6.3 MPA
6: East T06 Parrotfish 11 9.9 MPA
Q9 · rbindlist() stack
rbindlist() binds a list of data.tables quickly — stack survey legs or batches.
north <- DTreef[site == "North" ] # subset table
south <- DTreef[site == "South" ] # another subset
rbindlist (list (north, south), use.names = TRUE ) # combined table
transect_id site species count depth_m
<char> <char> <char> <int> <num>
1: T01 North Parrotfish 4 6.2
2: T01 North Surgeonfish 12 6.2
3: T01 North Grouper 1 6.2
4: T01 North Snapper 7 6.2
5: T01 North Damselfish 21 6.2
6: T04 North Parrotfish 6 8.5
7: T04 North Surgeonfish 8 8.5
8: T04 North Grouper 1 8.5
9: T04 North Snapper 2 8.5
10: T04 North Damselfish 17 8.5
11: T07 North Parrotfish 5 7.7
12: T07 North Surgeonfish 13 7.7
13: T07 North Grouper 2 7.7
14: T07 North Snapper 4 7.7
15: T07 North Damselfish 25 7.7
16: T10 North Parrotfish 6 7.9
17: T10 North Surgeonfish 8 7.9
18: T10 North Grouper 4 7.9
19: T10 North Snapper 5 7.9
20: T10 North Damselfish 25 7.9
21: T02 South Parrotfish 6 5.2
22: T02 South Surgeonfish 7 5.2
23: T02 South Grouper 2 5.2
24: T02 South Snapper 9 5.2
25: T02 South Damselfish 20 5.2
26: T05 South Parrotfish 15 10.4
27: T05 South Surgeonfish 10 10.4
28: T05 South Grouper 0 10.4
29: T05 South Snapper 4 10.4
30: T05 South Damselfish 22 10.4
31: T08 South Parrotfish 9 9.6
32: T08 South Surgeonfish 8 9.6
33: T08 South Grouper 5 9.6
34: T08 South Snapper 5 9.6
35: T08 South Damselfish 22 9.6
36: T11 South Parrotfish 2 11.9
37: T11 South Surgeonfish 15 11.9
38: T11 South Grouper 6 11.9
39: T11 South Snapper 5 11.9
40: T11 South Damselfish 24 11.9
transect_id site species count depth_m
Q10 · dcast() wide
dcast() pivots long to wide — detection totals by animal and receiver.
dcast (DTac, animal_id ~ receiver, value.var = "detections" , fun.aggregate = sum, fill = 0 )
Key: <animal_id>
animal_id R1 R11 R12 R2 R4 R5 R6 R7 R8 R9
<char> <int> <int> <int> <int> <int> <int> <int> <int> <int> <int>
1: A101 4 5 0 2 0 5 7 8 0 1
2: A102 3 3 0 8 0 0 0 4 0 3
3: A103 0 3 14 5 7 1 0 5 3 0
Q11 · melt() long
melt() unpivots wide tables — inverse of dcast for export pipelines.
wide <- dcast (DTfish[year == 2020 ], year ~ species, value.var = "tonnes" , fill = 0 )
melt (wide, id.vars = "year" , variable.name = "species" , value.name = "tonnes" )
year species tonnes
<int> <fctr> <num>
1: 2020 Grouper 23.0
2: 2020 Shark 89.9
3: 2020 Snapper 144.5
4: 2020 Tuna 43.7
Q12 · shift() like lag
shift() moves values within groups — lag and lead for time series inside bursts.
DTbio[, depth_change : = depth_m - shift (depth_m), by = burst_id]
DTbio[! is.na (depth_change), .(burst_id, seconds, depth_change)][1 : 8 ]
burst_id seconds depth_change
<int> <int> <num>
1: 1 1 0.26751027
2: 1 2 -0.31068411
3: 1 3 -0.02501225
4: 1 4 -0.06485909
5: 1 5 0.20001095
6: 1 6 0.17244104
7: 1 7 0.38211299
8: 1 8 -0.41157372
Also:
DTf <- copy (DTfish) # do not mutate capstone copy
DTf[, year : = as.integer (year)] # ensure sort order
setorder (DTf, species, year)
DTf[, tonnes_lag : = shift (tonnes), by = species]
DTf[! is.na (tonnes_lag), .(species, year, tonnes, tonnes_lag)][1 : 6 ]
species year tonnes tonnes_lag
<char> <int> <num> <num>
1: Grouper 2019 80.6 79.6
2: Grouper 2020 23.0 80.6
3: Grouper 2021 74.5 23.0
4: Grouper 2022 26.6 74.5
5: Grouper 2023 47.5 26.6
6: Shark 2019 85.1 67.7
Also:
DTac[, det_lag : = shift (detections), by = animal_id]
DTac[! is.na (det_lag), .(animal_id, receiver, detections, det_lag)][1 : 6 ]
animal_id receiver detections det_lag
<char> <char> <int> <int>
1: A101 R2 2 5
2: A101 R6 7 2
3: A101 R1 4 7
4: A101 R5 5 4
5: A101 R7 4 5
6: A101 R7 4 4
Q13 · fcase() like case_when
fcase() evaluates vectorised conditions top-down — fast alternative to dplyr case_when inside j.
DTfish[, size_bin : = fcase (
tonnes < 30 , "small" ,
tonnes < 70 , "medium" ,
default = "large"
)]
DTfish[, .N, by = size_bin] # bin counts
size_bin N
<char> <int>
1: large 9
2: small 5
3: medium 10
Also:
DTreef[, depth_bin : = fcase (depth_m < 5 , "shallow" , depth_m < 12 , "mid" , default = "deep" )]
DTreef[, .N, by = depth_bin]
depth_bin N
<char> <int>
1: mid 50
2: shallow 10
Also:
DTac[, activity : = fcase (detections == 0 , "none" , detections < 4 , "low" , default = "high" )]
DTac[, .N, by = activity]
activity N
<char> <int>
1: high 12
2: low 12
Q14 · Grouped chain ;
Curly braces with semicolons run multiple j expressions on the same filtered table — compact EDA.
east <- DTreef[site == "East" ]
{
east[, .(mean_depth = mean (depth_m), total_fish = sum (count))];
east[, .(species, count)][order (- count)]
}
species count
<char> <int>
1: Damselfish 26
2: Damselfish 26
3: Damselfish 26
4: Damselfish 18
5: Surgeonfish 14
6: Surgeonfish 13
7: Parrotfish 13
8: Parrotfish 11
9: Surgeonfish 10
10: Surgeonfish 9
11: Grouper 8
12: Snapper 8
13: Grouper 8
14: Parrotfish 6
15: Snapper 5
16: Parrotfish 5
17: Grouper 4
18: Grouper 4
19: Snapper 4
20: Snapper 1
species count
Q15 · fread() big CSV
fread() reads large CSVs faster than read.csv from the same project-relative path.
reef_fread <- fread (data_path ("reef_transects.csv" )) # fast read; same path as
nrow (reef_fread) # should match DTreef
Q16 · Telemetry aggregation
Group acoustic detections by reef_zone and receiver — standard passive telemetry summaries.
tel <- DTac[, .(total = sum (detections), n = .N), by = .(reef_zone, receiver)]
tel[order (- total)][1 : 10 ] # top ten cells
reef_zone receiver total n
<char> <char> <int> <int>
1: Slope R7 11 3
2: Crest R2 7 2
3: Slope R6 7 1
4: Crest R12 7 1
5: Lagoon R12 7 1
6: Slope R4 7 1
7: Crest R7 6 2
8: Lagoon R11 6 3
9: Slope R11 5 1
10: Slope R5 5 1
Q17 · Fishery by year
Annual summaries with by = year mirror agency reports — sum tonnes and count records separately.
DTfish[, .(tonnes = sum (tonnes), records = .N), by = year][order (year)]
year tonnes records
<int> <num> <int>
1: 2018 246.5 4
2: 2019 277.1 4
3: 2020 301.1 4
4: 2021 241.6 4
5: 2022 202.5 4
6: 2023 179.8 4
Q18 · Biologging rolling mean
Rolling means smooth depth noise along bursts — three-point centred window using shift sums.
DTbio[, roll_depth : = (shift (depth_m, 1 , type = "lag" ) + depth_m + shift (depth_m, 1 , type = "lead" )) / 3 , by = burst_id]
DTbio[! is.na (roll_depth), .(burst_id, seconds, depth_m, roll_depth)][1 : 8 ]
burst_id seconds depth_m roll_depth
<int> <int> <num> <num>
1: 1 1 5.065770 4.873038
2: 1 2 4.755086 4.850310
3: 1 3 4.730074 4.716791
4: 1 4 4.665214 4.753504
5: 1 5 4.865225 4.856035
6: 1 6 5.037666 5.107557
7: 1 7 5.419779 5.155217
8: 1 8 5.008206 5.164646
Q19 · .BY in j
.BY exposes current by-group values inside j — handy for labels without repeating columns.
DTac[, .(
zone = .BY[[1 ]],
total = sum (detections)
), by = reef_zone]
reef_zone zone total
<char> <char> <int>
1: Slope Slope 43
2: Crest Crest 26
3: Lagoon Lagoon 22
Q20 · data.table capstone
Capstone combines aggregation, merge, and ggplot on reef totals — meeting-ready output you verify before AI optimises.
meta <- data.table (site = c ("North" , "South" , "East" ), mgmt = c ("MPA" , "Open" , "MPA" ))
reef_sum <- DTreef[, .(total = sum (count), species_n = uniqueN (species)), by = site]
out <- merge (reef_sum, meta, by = "site" )
ggplot (as.data.frame (out), aes (x = site, y = total, fill = mgmt)) +
geom_col () +
labs (title = "Reef fish totals by site and management" , y = "Sum of counts" )