-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathexplore_scripps.ucsd.edu_pelagic-invertebrate-collection.qmd
More file actions
1537 lines (1259 loc) · 50.4 KB
/
Copy pathexplore_scripps.ucsd.edu_pelagic-invertebrate-collection.qmd
File metadata and controls
1537 lines (1259 loc) · 50.4 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
---
title: "Explore Scripps PIC Matches with CalCOFI Ichthyoplankton"
execute:
echo: true
warning: false
message: true
editor_options:
chunk_output_type: console
format:
html:
code-fold: true
editor:
markdown:
wrap: 72
---
::: {.callout-caution collapse="true"}
## Claude Code Interactions {.unnumbered}
Used [Claude Code by Anthropic](https://claude.com/product/claude-code)
from the command line to develop this notebook.
```
# from working directory of ~/Github/CalCOFI, with Claude CODE in plan mode sent:
Evaluate the PROMPT and expand the notebook @workflows/explore_zooplankton.qmd
```
### Creation Prompt {.unnumbered}
The goal is to explore the SIO / CalCOFI zooplankton data (more at
[Zooplankton –
CalCOFI](https://calcofi.org/data/marine-ecosystem-data/zooplankton/))
to see where there are matches to the ichthyoplankton data. The data
files from the CalCOFI data manager Linsey include:
- plankton_data:
`@~/My Drive/projects/calcofi/data-public/scripps.ucsd.edu/pelagic-invertebrate-collection/SIOPIC_DB_PNTtable_allRecords_wUUID_9Feb2026.csv`
- plankton_metadata:
`@~/My Drive/projects/calcofi/data-public/scripps.ucsd.edu/pelagic-invertebrate-collection/SIOPIC_DB_PNTtable_fieldDetails_9Feb2026.xlsx'`
And notes from Linsey:
> I included our Parent Net Tow (= pnt suffix you will see in fields)
> Table with all net tow records and a second "ReadMe" file with field
> definitions and "tips" for those.
>
> Our ask is to see where PIC net tow samples are a match to SWF
> ichthyo. samples potentially utilizing: Expedition, Expedition Code,
> Station Line, Station Number, Latitude/Longitude polygon?, Net Type,
> Mesh Size?, Tow Type?, Fixative?, Preservative?
>
> I introduced a new field 'Expedition_Type_pnt' and was able to easily
> integrate term = CalCOFI for all cruises we'd want in this first pass
> that represent the most formal CalCOFI net tow records. I will have to
> at some point integrate additional terms e.g. 'NOAA Other' for cruises
> that have CalCOFI stations sampled for special purposes (e.g. SaKE,
> CPS surveys, STARR, MERRP, ORCA WHALE Expeditions, etc.) but this
> would have taken too much time to integrate universally atm.
>
> So, I believe it would be good to run this first pass for the records
> that have the Expedition_Type_pnt = CalCOFI and then the complete
> record set as second priority.
Use the database of ichthyoplankton data now available via `@calcofi4r/`
ie with:
``` r
# remotes::install_github("calcofi/calcofi4r") # install package (once, and per version)
library(calcofi4r) # load the package
con <- cc_get_db()# connect to latest frozen release
cc_list_tables() # list available tables
DBI::dbGetQuery(con, "SELECT COUNT(*) FROM ichthyo") # query with SQL
```
Reference the space-time windowing in the `@db-viz-hex/app/` workflow (but
with older version of database having different schema). Try different
approaches to matching the data, including:
- single nearest cast (by time)
- single nearest cast (by space)
.](./figures/int-app_scatterplot.png)
#### Response {.unnumbered}
```
⏺ The plan is complete. I explored:
1. The zooplankton CSV (148K records, 20 columns) and Excel metadata
2. The calcofi4r database schema (ichthyo → net → tow → site → cruise chain with integer IDs)
3. The prep_splot() space-time matching reference in db-viz-hex/app/functions.R
4. The merge_ichthyo_bottle.qmd notebook for style/pattern reference
The plan covers 6 notebook sections: Setup, Zooplankton exploration, Ichthyoplankton exploration, Station-based matching (line + station + cruise), Space-time window matching (adapted from prep_splot()),
and a Comparison summary.
────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
Ready to code?
Here is Claude's plan:
╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌
Plan: Expand workflows/explore_zooplankton.qmd
Context
The CalCOFI data manager Linsey provided SIO zooplankton (PIC) net tow data (148K records) and wants to find where these match SWFSC ichthyoplankton samples. This notebook will explore both datasets, try
multiple matching strategies, and report match rates to guide integration.
File to modify
- /Users/bbest/Github/CalCOFI/workflows/explore_zooplankton.qmd
Key reference files
- db-viz-hex/app/functions.R:716-788 — prep_splot() space-time matching pattern
- calcofi4r/R/read.R — cc_get_db(), cc_read_*() convenience functions
- workflows/merge_ichthyo_bottle.qmd — notebook style reference (librarian::shelf, datatable, dir_data pattern)
- workflows/ingest_swfsc_ichthyo.qmd:600-630 — DB schema (cruise_key, site_id, tow_id, net_id)
Database schema (confirmed)
cruise (cruise_key PK, ship_key FK, date_ym)
→ site (site_id PK, cruise_key FK, orderocc, latitude, longitude, line, station)
→ tow (tow_id PK, site_id FK, tow_type_key, tow_number, time_start)
→ net (net_id PK, tow_id FK, side, std_haul_factor, vol_sampled_m3, ...)
→ ichthyo (ichthyo_uuid PK, net_id FK, species_id FK, tally, ...)
Zooplankton CSV columns
EXPEDITION_pnt, EXPED_CODE_pnt, Expedition_Type_pnt, SHIP_pnt, SWFOrder_Occ, STATION_LINE_pnt, STATION_NUMBER_pnt, LAT_DECIMAL_pnt, LONG_DECIMAL_pnt, SAMPLE_DATE_pnt, START_TIME_pnt, END_TIME_pnt,
DEPTH_MIN_pnt, DEPTH_MAX_pnt, MAX_MWO_pnt, NET_TYPE_pnt, MESH_SIZE_pnt, TOW_TYPE_pnt, FIXATIVE_pnt, PRESERVATIVE_pnt
---
Notebook Sections
1. Setup
- librarian::shelf(calcofi/calcofi4r, DBI, dplyr, DT, glue, here, leaflet, lubridate, mapview, purrr, readr, readxl, sf, stringr, tibble, tidyr, quiet = T)
- Define file paths using dir_data pattern from merge_ichthyo_bottle.qmd (Linux vs macOS toggle)
- Read metadata Excel with readxl::read_excel(), display with DT::datatable()
2. Read & Explore Zooplankton Data
- Read CSV with readr::read_csv()
- Parse datetimes: mdy(SAMPLE_DATE_pnt) for date; str_pad(START_TIME_pnt, 4, pad = "0") for time; combine into datetime_start with ymd_hm() and tz = "America/Los_Angeles"
- Summary stats: nrow, date range, expedition type breakdown via count() + datatable()
- Filter: Expedition_Type_pnt == "CalCOFI" → d_zoo_cc (82K records, first pass per Linsey)
- Attribute tables: net types, tow types, mesh sizes, fixatives for CalCOFI subset
- Map stations: distinct lat/lon → sf::st_as_sf() → mapview() colored by line
3. Read & Explore Ichthyoplankton Data
- Connect: con <- cc_get_db(); cc_list_tables(); cc_describe_table("site")
- Build tow-level flat view via dbplyr:
d_ich_tows <- tbl(con, "tow") |>
left_join(tbl(con, "site"), by = "site_id") |>
left_join(tbl(con, "cruise"), by = "cruise_key") |>
select(tow_id, site_id, cruise_key, ship_key, date_ym,
orderocc, latitude, longitude, line, station,
tow_type_key, tow_number, time_start) |>
collect()
- Summary stats: nrow, date range, station count
- Map stations: distinct lat/lon → mapview() colored by line
4. Match by Station (Line + Station Number)
- Prepare: Convert STATION_LINE_pnt and STATION_NUMBER_pnt to numeric; inspect EXPED_CODE_pnt format (YYMM vs YYYYMM)
- Cruise code reconciliation: cruise_key is YYMMKK format. EXPED_CODE_pnt is YYMM or YYYYMM. Extract YY and MM from both for comparison, or derive a cruise_ym from date_ym
- Match 1 — line + station only: inner_join(by = c("line", "station")) → many-to-many (all cruises)
- Match 2 — line + station + cruise date: add expedition code matching
- Report: match counts, percentage of zoo records matched, unmatched records via anti_join()
- Summary by year: count(year) + datatable()
5. Match by Space-Time Window
Adapt the prep_splot() approach from db-viz-hex/app/functions.R:
- Register zoo data in DuckDB: DBI::dbWriteTable(con, "zoo_pnt", d_zoo_db, overwrite = TRUE) after selecting/renaming key columns
- Load spatial extension: DBI::dbExecute(con, "INSTALL spatial; LOAD spatial;")
- Create ichthyo tow view: SQL view joining tow → site → cruise with lat/lon and time_start
- Space-time match function using SQL:
- Time window: ich_datetime BETWEEN zoo_datetime - INTERVAL N HOUR AND zoo_datetime + INTERVAL N HOUR
- Distance: ST_Distance_Sphere(ST_Point(zoo_lon, zoo_lat), ST_Point(ich_lon, ich_lat)) <= M
- Return: both coordinates, time_diff_hrs, dist_m
- Parameter sweep: try combinations of 6h/12h/24h × 2km/5km, report match rates in a summary table
- Method 1 — nearest by time: group_by(zoo_row) |> slice_min(time_diff_hrs)
- Method 2 — nearest by distance: group_by(zoo_row) |> slice_min(dist_m)
- Map matched pairs: sf linestrings connecting zoo→ichthyo points, colored by distance
6. Comparison & Summary
- Summary table: all methods side-by-side (station match, station+cruise, space-time variants) with counts and percentages
- Gap analysis: zoo records unmatched by any method, by year
- Recommendations: which approach gives best coverage, optimal thresholds, next steps (full dataset beyond CalCOFI type)
---
Technical notes
- Timezone: Zoo times are PST. Ichthyo time_start needs inspection — may be UTC. Handle explicitly.
- Memory: Don't collect full ichthyo table (~830K rows). Work at tow level (~75K) for matching.
- DuckDB spatial: Must INSTALL spatial; LOAD spatial; before using ST_Distance_Sphere(). The cc_get_db() connection may be read-only views on GCS parquet — need to handle temp table creation carefully (may
need a separate local DuckDB, or use duckdb() in-memory with both datasets registered).
- Code style: 2-space indent, |> pipe, snake_case, lowercase comments, librarian::shelf(), datatable() for tables, mapview() for maps.
Verification
1. Render with quarto::quarto_render("workflows/explore_zooplankton.qmd")
2. Check that all sections produce output (tables, maps, match statistics)
3. Verify the space-time matching function returns reasonable match rates
4. Confirm the summary comparison table shows all methods
╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌
Claude has written up a plan and is ready to execute. Would you like to proceed?
❯ 1. Yes, clear context and auto-accept edits (shift+tab)
2. Yes, auto-accept edits
3. Yes, manually approve edits
4. Type here to tell Claude what to change
```
1
```
⏺ The notebook has been written. Here's a summary of what was implemented in workflows/explore_zooplankton.qmd:
6 sections, ~450 lines of R/Quarto code:
1. Setup — librarian::shelf() for all packages, Google Drive file paths, metadata Excel display
2. Read & Explore Zooplankton — CSV read, datetime parsing (PST), expedition type breakdown, CalCOFI filter, attribute tables (net/tow/mesh/fixative), station
map via mapview()
3. Read & Explore Ichthyoplankton — cc_get_db() connection, tow-level flat view via dbplyr (tow → site → cruise join), schema inspection, station map
4. Match by Station — Line+station numeric conversion, EXPED_CODE_pnt → YYMM reconciliation with cruise_key YYMMKK, two match methods (line+station only,
line+station+cruise YYMM), year-by-year breakdown
5. Match by Space-Time Window — Separate in-memory DuckDB (since cc_get_db() is read-only), both datasets registered, match_spacetime() SQL function using
ST_Distance_Sphere() + time interval, parameter sweep (6h/12h/24h × 2km/5km), nearest-by-time and nearest-by-distance methods, matched-pair map with sf
linestrings
6. Comparison & Summary — Side-by-side method comparison table, gap analysis by year, recommendations for next steps
Key technical decisions:
- Separate duckdb() in-memory connection for space-time matching (the cc_get_db() connection creates read-only views on GCS parquet)
- Working at tow level (~75K rows) rather than full ichthyo table (~830K rows)
- Zoo times treated as PST; ichthyo time_start inspected explicitly
```
The pandoc portion of rendering seems stuck. How to fix or explore,
inspect running processes? processing file: explore_zooplankton.qmd
```
⏺ Pandoc is running at 99.6% CPU and has been going for 5+ minutes. This is almost certainly due to the large HTML widgets (multiple mapview maps with many points
embedded inline). Let me check the intermediate file size.
⏺ Read 1 file (ctrl+o to expand)
⏺ The intermediate markdown is 21 MB — that's what's making pandoc slow. The mapview widgets with many points generate massive embedded JSON/HTML. Pandoc is still
working (99% CPU), so it will likely finish — it's just slow with a 21MB input.
You have two options:
Option 1: Wait it out — Pandoc is actively processing (not actually stuck). With a 21MB file it may take 10-15 minutes total.
Option 2: Kill and re-render with smaller maps — I can reduce the station point counts to speed things up.
To reduce map size, I'd limit mapview() calls to show fewer points (e.g., sample down to ~500 stations instead of all distinct stations). The main culprits are:
- map_zoo — all distinct CalCOFI zoo stations
- map_ichthyo — all distinct ichthyo stations
- map_matched_pairs — 500 linestrings (already capped)
Want me to:
1. Wait for pandoc to finish (it is progressing, just slowly)?
2. Kill pandoc and optimize the maps to reduce the HTML size, then re-render?
2
⏺ Rendered successfully! Let me verify the output.
⏺ Read 1 file (ctrl+o to expand)
⏺ Rendered successfully. The fix was switching from mapview() to leaflet() with markerClusterOptions() — the HTML dropped from 21 MB (causing pandoc to churn for
10+ minutes) down to 2.5 MB, and pandoc completed quickly.
What changed:
- map_zoo — leaflet with clustered circle markers instead of mapview
- map_ichthyo — same leaflet clustering approach
- map_matched_pairs — leaflet polylines with colorNumeric palette + legend, capped at 200 pairs instead of 500
✻ Crunched for 1m 13s
```
### Update with files containing UUIDs {.unnumbered}
Update @workflows/explore_zooplankton.qmd with the following file
updates from Linsey, which now include the net tow primary keys and
UUIDs.
> I realized my first pass at this I did not incorporate our net tow
> primary keys nor UUIDs. They are now integrated in the updated files:
- plankton_data:
`@~/My Drive/projects/calcofi/data-public/scripps.ucsd.edu/pelagic-invertebrate-collection/SIOPIC_DB_PNTtable_allRecords_wUUID_9Feb2026.csv`
- plankton_metadata:
`@~/My Drive/projects/calcofi/data-public/scripps.ucsd.edu/pelagic-invertebrate-collection/SIOPIC_DB_PNTtable_fieldDetails_9Feb2026.xlsx`
(Retain the "Claude Code Interactions" section untouched)
#### Response {.unnumbered}
```
⏺ Done. Three changes made (leaving the "Claude Code Interactions" section untouched):
1. dir_data — updated from calcofi.org/zooplankton to scripps.ucsd.edu/pelagic-invertebrate-collection
2. CSV filename — updated from ...allRecords_9Feb2026.csv to ...allRecords_wUUID_9Feb2026.csv
3. register_zoo chunk — added zoo_pk = ParentNetTowEventID_pk and zoo_uuid = occurenceID_UUID_pnt to the DuckDB registration so the new primary key and UUID
columns flow through to the space-time matching results
The xlsx filename stayed the same (just the directory changed). The Overview description was updated to note "22 columns incl. primary keys + UUIDs".
```
### Fix messages {.unnumbered}
Why are `message()` statements not outputting to rendered html?
#### Response {.unnumbered}
```
⏺ message() writes to stderr, which knitr captures separately as a "message" output class. In Quarto HTML with code-fold: true, these can be visually collapsed or
styled in a way that makes them easy to miss — and some configurations suppress them.
The simplest fix: switch to cat() for output you want visible in the rendered HTML. cat() writes to stdout, which always renders as standard console output.
For example, change:
message(glue("Zooplankton PNT records: {format(nrow(d_zoo), big.mark = ',')}"))
to:
cat(glue("Zooplankton PNT records: {format(nrow(d_zoo), big.mark = ',')}"), "\n")
Want me to replace all message(glue(...)) calls with cat(glue(...), "\n") throughout the notebook?
```
yes
```
⏺ All 28 message() calls replaced with cat(). The output will now render as standard console text in the HTML instead of going to stderr.
```
### Incorporate feedback from Ed {.unnumbered}
In @explore_scripps.ucsd.edu_pelagic-invertebrate-collection.qmd, incorporate the following feedback from Ed and leave "## Claude Code Interactions" section untouched:
The 91% space-time query is pretty good news. I spotted a couple of
things that would tighten this up slightly.
1. Our db begins in 1951. We do have some pre-1951 data but things were
a little bit different and we don't consider them to be part of the
standard data set. So I suggest we just eliminate those for now as a low
priority item.
2. I think preservative must be added to all queries, e.g., space +
time + preservative. This is because you are creating 1:1 matches by
paired tow, not net/codend. That is, sometimes there is a formalin
sample from the starboard net and an ethanol sample from the port net,
both coming from the same tow. So, for example, here are two records
from a paired tow. I think your sql spatio-temporal query will just
match the first one in the table. Not sure what glue, etc. are doing.
| Expedition | Exped Code | Type | Ship | Line | Station | Lat | Lon | Date | Start | End | Depth Min | Depth Max | Net Type | Mesh Size | Tow Type | Fixative | Preservative | PK | UUID |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| CALCOFI 9807 | 9807 | CalCOFI | NEW HORIZON | 76.7 | 49 | 35.083 | -120.78 | 7/24/98 | 458 | 504 | 61 | 90 | CALBOBL | 0.505 | OBLIQUE | formaldehyde | formaldehyde | 100768 | 90CB05C3-32C9-41BE-8552-5F52C251AFA8 |
| CALCOFI 9807 | 9807 | CalCOFI | NEW HORIZON | 76.7 | 49 | 35.083 | -120.78 | 7/24/98 | 458 | 504 | 61 | 90 | CALBOBL | 0.505 | OBLIQUE | ethanol | ethanol | 107972 | 8059E029-2B4D-42DD-99D3-DF55D76E57EF |
I'm not sure if I'm restating your suggested next steps in a different
way but given the good space-time match, shouldn't we start with that
and play the "one of these things is not like the others" game within
the spatio-temporal matches that do not match for cruise, line, station
(e.g., noaa cruise, pic cruise, noaa line, pic line, noaa station, pic
station)? That would get us to a \>91% match pretty quickly. Then we
could tackle the problem children.
Also for the problem children, I could do a quick sanity check in our db
to screen out anything that we definitely don't have for whatever
reason.
#### Response {.unnumbered}
```
⏺ Good — the Claude Code Interactions section is untouched. Here's a summary of all changes made:
Changes incorporated from Ed's feedback:
1. Pre-1951 filter (filter_calcofi chunk, line 569-574): Added year >= 1951 filter with comment explaining the ichthyo DB begins in 1951 and pre-1951
data is non-standard.
2. Preservative carried through all queries:
- Added preservatives summary table in zoo_attributes chunk
- Added preserv = PRESERVATIVE_pnt to d_zoo_match for station matching
- Added zoo_preserv = PRESERVATIVE_pnt to DuckDB registration in register_zoo
- Added z.zoo_preserv plus zoo_line, zoo_stn, zoo_ym, ich_line, ich_stn, ich_ym to the space-time SQL SELECT for downstream diagnostics
3. New "Diagnostics" section (between Map matched pairs and Comparison & Summary) with 4 sub-sections:
- Classify matches by field agreement — checks which space-time matches also agree on line, station, cruise YYMM
- Breakdown of discrepancy types — counts by combination of field agreement
- Preservative breakdown — distribution in matched records
- Inspect mismatched records — datatable of records where space-time matched but cruise/line/station disagree
4. Updated Overview — notes preservative and pre-1951 filter; added "Field diagnostics" as 4th matching strategy
5. Updated Recommendations — reframed per Ed's workflow: start with space-time, compare fields within matches, investigate discrepancies, tackle
problem children with Ed's DB sanity check, then expand to full dataset
```
:::
## Overview {.unnumbered}
**Goal**: Explore the SIO / CalCOFI zooplankton (PIC) net tow data to
find where records match SWFSC ichthyoplankton samples. More at
[Zooplankton –
CalCOFI](https://calcofi.org/data/marine-ecosystem-data/zooplankton/).
**Data sources**:
- **Zooplankton**: SIO PIC Parent Net Tow table (148K records, 22
columns incl. primary keys + UUIDs) from CalCOFI data manager Linsey
- **Ichthyoplankton**: SWFSC database via `calcofi4r::cc_get_db()`
**Matching strategies** (preservative carried through all queries to
distinguish paired tows; pre-1951 records excluded per Ed):
1. **Station match** — line + station number
2. **Station + cruise match** — line + station + expedition code
3. **Space-time window** — adapted from `prep_splot()` in the [CalCOFI
integrated app](https://app.calcofi.io/db-viz-hex/)
4. **Field diagnostics** — within space-time matches, compare
cruise/line/station to identify discrepancies
**Notes from Linsey**:
> I included our Parent Net Tow (= pnt suffix you will see in fields)
> Table with all net tow records and a second "ReadMe" file with field
> definitions and "tips" for those.
>
> Our ask is to see where PIC net tow samples are a match to SWF
> ichthyo. samples potentially utilizing: Expedition, Expedition Code,
> Station Line, Station Number, Latitude/Longitude polygon?, Net Type,
> Mesh Size?, Tow Type?, Fixative?, Preservative?
>
> I introduced a new field 'Expedition_Type_pnt' and was able to easily
> integrate term = CalCOFI for all cruises we'd want in this first pass
> that represent the most formal CalCOFI net tow records.
>
> So, I believe it would be good to run this first pass for the records
> that have the Expedition_Type_pnt = CalCOFI and then the complete
> record set as second priority.
## Setup
```{r}
#| label: setup
librarian::shelf(
calcofi / calcofi4r,
DBI,
dplyr,
DT,
duckdb,
glue,
here,
leaflet,
lubridate,
mapview,
purrr,
readr,
readxl,
sf,
stringr,
tibble,
tidyr,
quiet = T
)
options(readr.show_col_types = FALSE)
options(DT.options = list(scrollX = TRUE))
# data directory (Google Drive)
dir_data <- "~/My Drive/projects/calcofi/data-public/scripps.ucsd.edu/pelagic-invertebrate-collection"
# file paths (updated with primary keys + UUIDs)
zoo_csv <- file.path(
dir_data,
"SIOPIC_DB_PNTtable_allRecords_wUUID_9Feb2026.csv"
)
zoo_xlsx <- file.path(
dir_data,
"SIOPIC_DB_PNTtable_fieldDetails_9Feb2026.xlsx"
)
stopifnot(
"Zooplankton CSV not found" = file.exists(zoo_csv),
"Zooplankton Excel not found" = file.exists(zoo_xlsx)
)
```
### Metadata
```{r}
#| label: metadata
d_meta <- read_excel(zoo_xlsx)
datatable(
d_meta,
caption = "Zooplankton PNT field definitions"
)
```
## Read & Explore Zooplankton Data
### Read CSV
```{r}
#| label: read_zoo
d_zoo <- read_csv(zoo_csv)
cat(
glue(
"Zooplankton PNT records: {format(nrow(d_zoo), big.mark = ',')}"
),
"\n"
)
cat(
glue(
"Columns: {ncol(d_zoo)}"
),
"\n"
)
```
### Parse datetimes
```{r}
#| label: parse_zoo_datetime
d_zoo <- d_zoo |>
mutate(
# parse date (M/D/YYYY format)
date_sample = mdy(SAMPLE_DATE_pnt),
# pad time to 4 chars (e.g. 830 → "0830")
time_str = str_pad(
as.character(START_TIME_pnt),
4,
pad = "0"
),
# combine date + time as PST
datetime_start = ymd_hm(
paste(date_sample, time_str),
tz = "America/Los_Angeles"
),
year = year(date_sample)
) |>
select(-time_str)
cat(
glue(
"Date range: {min(d_zoo$date_sample, na.rm = TRUE)} to ",
"{max(d_zoo$date_sample, na.rm = TRUE)}"
),
"\n"
)
cat(
glue(
"Parsed datetime: {sum(!is.na(d_zoo$datetime_start))} of ",
"{nrow(d_zoo)} records"
),
"\n"
)
```
### Summary stats
```{r}
#| label: zoo_summary
# expedition type breakdown
d_zoo |>
count(Expedition_Type_pnt, name = "n_records") |>
arrange(desc(n_records)) |>
datatable(caption = "Zooplankton records by expedition type")
```
### Filter to CalCOFI
```{r}
#| label: filter_calcofi
# filter to CalCOFI expeditions, and exclude pre-1951 records since
# the ichthyo DB begins in 1951 (per Ed: pre-1951 data is non-standard)
d_zoo_cc <- d_zoo |>
filter(
Expedition_Type_pnt == "CalCOFI",
year >= 1951)
cat(
glue(
"CalCOFI subset (1951+): {format(nrow(d_zoo_cc), big.mark = ',')} records ",
"({round(100 * nrow(d_zoo_cc) / nrow(d_zoo), 1)}% of total)"
),
"\n"
)
```
### Attribute tables
```{r}
#| label: zoo_attributes
# net types
d_zoo_cc |>
count(NET_TYPE_pnt, name = "n") |>
arrange(desc(n)) |>
datatable(caption = "CalCOFI zooplankton net types")
# tow types
d_zoo_cc |>
count(TOW_TYPE_pnt, name = "n") |>
arrange(desc(n)) |>
datatable(caption = "CalCOFI zooplankton tow types")
# mesh sizes
d_zoo_cc |>
count(MESH_SIZE_pnt, name = "n") |>
arrange(desc(n)) |>
datatable(caption = "CalCOFI zooplankton mesh sizes")
# fixatives
d_zoo_cc |>
count(FIXATIVE_pnt, name = "n") |>
arrange(desc(n)) |>
datatable(caption = "CalCOFI zooplankton fixatives")
# preservatives (important: paired tows can have different preservatives,
# e.g. formalin starboard + ethanol port from same tow)
d_zoo_cc |>
count(PRESERVATIVE_pnt, name = "n") |>
arrange(desc(n)) |>
datatable(caption = "CalCOFI zooplankton preservatives")
```
### Map stations
```{r}
#| label: map_zoo
# distinct station locations
sf_zoo <- d_zoo_cc |>
filter(
!is.na(LAT_DECIMAL_pnt),
!is.na(LONG_DECIMAL_pnt)
) |>
distinct(
STATION_LINE_pnt,
STATION_NUMBER_pnt,
LAT_DECIMAL_pnt,
LONG_DECIMAL_pnt
) |>
st_as_sf(
coords = c("LONG_DECIMAL_pnt", "LAT_DECIMAL_pnt"),
crs = 4326
)
cat(
glue(
"Distinct CalCOFI zoo stations: {nrow(sf_zoo)}"
),
"\n"
)
# use leaflet with clustering for lighter HTML output
leaflet(sf_zoo) |>
addProviderTiles("CartoDB.Positron") |>
addCircleMarkers(
radius = 3,
stroke = FALSE,
fillOpacity = 0.6,
popup = ~ paste0(
"Line: ",
STATION_LINE_pnt,
"<br>Station: ",
STATION_NUMBER_pnt
),
clusterOptions = markerClusterOptions()
)
```
## Read & Explore Ichthyoplankton Data
### Connect to database
```{r}
#| label: connect_db
con <- cc_get_db()
tbls <- cc_list_tables()
cat(glue("Tables: {paste(tbls, collapse = ', ')}"), "\n")
# show site schema
cc_describe_table("site") |>
datatable(caption = "Site table schema")
# show tow schema
cc_describe_table("tow") |>
datatable(caption = "Tow table schema")
# show cruise schema
cc_describe_table("cruise") |>
datatable(caption = "Cruise table schema")
```
### Build tow-level flat view
```{r}
#| label: read_ichthyo
# build tow-level view joining tow → site → cruise
d_ich_tows <- tbl(con, "tow") |>
left_join(tbl(con, "site"), by = "site_id") |>
left_join(tbl(con, "cruise"), by = "cruise_key") |>
select(
tow_id,
site_id,
cruise_key,
ship_key,
date_ym,
orderocc,
latitude,
longitude,
line,
station,
tow_type_key,
tow_number,
time_start
) |>
collect()
cat(
glue(
"Ichthyo tow records: {format(nrow(d_ich_tows), big.mark = ',')}"
),
"\n"
)
cat(
glue(
"Date range: {min(d_ich_tows$date_ym, na.rm = TRUE)} to ",
"{max(d_ich_tows$date_ym, na.rm = TRUE)}"
),
"\n"
)
cat(
glue(
"Distinct stations (line-station): ",
"{d_ich_tows |> distinct(line, station) |> nrow()}"
),
"\n"
)
```
### Inspect ichthyo time_start
```{r}
#| label: inspect_time_start
# check what time_start looks like
d_ich_tows |>
filter(!is.na(time_start)) |>
slice_sample(n = 10) |>
select(tow_id, cruise_key, date_ym, time_start, line, station) |>
datatable(caption = "Sample ichthyo tow records with time_start")
```
### Map stations
```{r}
#| label: map_ichthyo
sf_ich <- d_ich_tows |>
filter(!is.na(latitude), !is.na(longitude)) |>
distinct(line, station, latitude, longitude) |>
st_as_sf(
coords = c("longitude", "latitude"),
crs = 4326
)
cat(
glue(
"Distinct ichthyo stations: {nrow(sf_ich)}"
),
"\n"
)
# use leaflet with clustering for lighter HTML output
leaflet(sf_ich) |>
addProviderTiles("CartoDB.Positron") |>
addCircleMarkers(
radius = 3,
stroke = FALSE,
fillOpacity = 0.6,
popup = ~ paste0(
"Line: ",
line,
"<br>Station: ",
station
),
clusterOptions = markerClusterOptions()
)
```
## Match by Station (Line + Station Number)
### Prepare matching keys
```{r}
#| label: prep_keys
# prepare zoo keys: convert line/station to numeric, carry preservative
d_zoo_match <- d_zoo_cc |>
mutate(
line = as.numeric(STATION_LINE_pnt),
station = as.numeric(STATION_NUMBER_pnt),
preserv = PRESERVATIVE_pnt
) |>
filter(!is.na(line), !is.na(station))
cat(
glue(
"Zoo records with valid line+station: ",
"{format(nrow(d_zoo_match), big.mark = ',')} of ",
"{format(nrow(d_zoo_cc), big.mark = ',')}"
),
"\n"
)
# inspect expedition code format
d_zoo_match |>
mutate(
code_len = nchar(as.character(EXPED_CODE_pnt))
) |>
count(code_len, name = "n") |>
datatable(caption = "Expedition code lengths")
```
### Cruise code reconciliation
```{r}
#| label: cruise_codes
# cruise_key is YYMMKK (6 chars: 2-yr, 2-mo, 2-ship)
# EXPED_CODE_pnt appears to be YYMM (4 chars) or YYYYMM (6 chars)
# extract year-month from zoo expedition code
d_zoo_match <- d_zoo_match |>
mutate(
exped_str = as.character(EXPED_CODE_pnt),
# handle both YYMM and YYYYMM formats
zoo_ym = case_when(
nchar(exped_str) == 4 ~ exped_str,
nchar(exped_str) >= 6 ~ substr(exped_str, 3, 6),
TRUE ~ NA_character_
)
)
# extract year-month from ichthyo cruise_key (first 4 chars = YYMM)
d_ich_match <- d_ich_tows |>
mutate(
ich_ym = substr(cruise_key, 1, 4)
)
# show sample codes
d_zoo_match |>
distinct(EXPED_CODE_pnt, exped_str, zoo_ym) |>
slice_head(n = 20) |>
datatable(caption = "Sample zoo expedition codes → YYMM")
d_ich_match |>
distinct(cruise_key, ich_ym) |>
slice_head(n = 20) |>
datatable(caption = "Sample ichthyo cruise_key → YYMM")
```
### Match 1 — line + station only
```{r}
#| label: match_line_station
# count zoo records that have a matching ichthyo line+station
n_m1_zoo <- d_zoo_match |>
semi_join(
d_ich_match |> distinct(line, station),
by = c("line", "station")
) |>
nrow()
cat(
glue(
"Match 1 (line + station only):"
),
"\n"
)
cat(
glue(
" Zoo records matched: {format(n_m1_zoo, big.mark = ',')} of ",
"{format(nrow(d_zoo_match), big.mark = ',')} ",
"({round(100 * n_m1_zoo / nrow(d_zoo_match), 1)}%)"
),
"\n"
)
```
### Match 2 — line + station + cruise YYMM
```{r}
#| label: match_line_station_cruise
n_m2_zoo <- d_zoo_match |>
semi_join(
d_ich_match |> distinct(line, station, ich_ym),
by = c("line", "station", "zoo_ym" = "ich_ym")
) |>
nrow()
cat(
glue(
"Match 2 (line + station + cruise YYMM):"
),
"\n"
)
cat(
glue(
" Zoo records matched: {format(n_m2_zoo, big.mark = ',')} of ",
"{format(nrow(d_zoo_match), big.mark = ',')} ",
"({round(100 * n_m2_zoo / nrow(d_zoo_match), 1)}%)"
),
"\n"
)
# unmatched records
d_m2_unmatched <- d_zoo_match |>
anti_join(
d_ich_match |> distinct(line, station, ich_ym),
by = c("line", "station", "zoo_ym" = "ich_ym")
)
cat(
glue(
" Unmatched: {format(nrow(d_m2_unmatched), big.mark = ',')}"
),
"\n"
)
```
### Matched by year
```{r}
#| label: match_by_year
# tag each zoo record as matched or not via semi/anti join
d_m2_matched <- d_zoo_match |>
semi_join(
d_ich_match |> distinct(line, station, ich_ym),
by = c("line", "station", "zoo_ym" = "ich_ym")
) |>
mutate(matched = TRUE)
d_m2_unmatched_tagged <- d_m2_unmatched |>
mutate(matched = FALSE)
d_m2_year <- bind_rows(d_m2_matched, d_m2_unmatched_tagged) |>
count(year, matched) |>
pivot_wider(
names_from = matched,
values_from = n,
values_fill = 0
) |>
rename(
unmatched = `FALSE`,
matched = `TRUE`
) |>
mutate(
total = matched + unmatched,
pct = round(100 * matched / total, 1)
)
d_m2_year |>
datatable(caption = "Match 2 results by year")
```
## Match by Space-Time Window
Adapted from `prep_splot()` in `db-viz-hex/app/functions.R`.
### Create local DuckDB for matching
```{r}
#| label: create_match_db