R package coasts, the shared hub of Peskas: storage/Mongo/KoBo/Airtable helpers used by every country pipeline, PDS ingestion, the WIO regional portal data (grids, GAUL maps, regional metrics) and the tracks-app data. It also runs its own pipeline for the regional products. Ecosystem context (other repos, data flow, cross-repo contracts): loaded by the peskas Claude Code plugin (repo peskas-context).
Commands
devtools::load_all()
devtools::test() # tests/testthat
devtools::check()
devtools::document() # after any roxygen change; man/ is committed- Pipeline steps: read
.github/workflows/data-pipeline.yaml(oneRscript -e 'coasts::fn()'per job, exceptpredict-pds-tracksandexport-pds-spatial, which run two each). - Other workflows:
sync-devices-users.yaml(Airtable devices to tracks-app users),collection-stats.yaml(weekly Airtablecollection_stats),tracks-backup.yaml,app-usage-report.yaml,release.yaml(GitHub release from the top block ofNEWS.mdon push tomain). - CI image:
Dockerfile.prod(rocker/geospatial base).
Architecture
-
Hub role. Country pipelines call coasts as
coasts::fn(..., package = "peskas.<country>.data.pipeline").packagegoes toread_config(package =), which loads that package’sinst/conf.ymlorinst/config.yml, so the same function runs against the country’s buckets and databases. Any exported function with apackageargument is a cross-repo API. -
Where things live. PDS ingestion (
ingest_pds_trips,ingest_pds_tracks,describe_pds_tracks,select_country_trips,backup_tracks) is inR/ingestion-pds.R.generate_fleet_analysis(the GPS tracker method, boats from the censusframe_units; where ARTFISH runs, its catch per trip is ARTFISH’s units weighted by their census boats,fao_catch_per_trip, without the on-foot units) is inR/model-fishery.R; withpds.fleet_location: landingit places trackers by where their trips land (landing_trip_data) instead of the Airtable link. It also raises the same totals with the FAO ARTFISH method (raise_catch_fao,R/model-fao.R) for a country whose config hasfao.surveys(the validated survey file with the weekly fishing-days answer), from the API trips and the snapshot’s censusframe_units(one row per district × boat type × main gear, a dimension left empty where a country’s census does not count it: gear in Mozambique). Boat types and gears are grouped byvessel_groups(set per country; Mozambique’s stay apart) andgear_groups, their own tables because the pipelines joinvesselsandgearwhole into their surveys. It stores them asfaoin the same file;export_portalpublishes them as*_faometrics. The catch by species (estimate_species_fao: each unit’s raised catch split by the species mix of the trips its catch per trip came from, a trip’s species rows scaled to its total) is stored beside it asfao_species;export_portalpublishes it per district, month and species as theestimated_catch_tn_faometric oftaxa_summaries(fao_portal_taxa), whichpeskas.dashboardshows from release 2.4.0.R/ingestion.Rholdsget_kobo_dataandingest_assets.export_portal,export_geosandexport_fishers_statsare inR/export.R;sync_device_usersis inR/airtable.R. -
API district-months.
export_portalalso writesdistrict-months__*.parquetto<country>/aggregated/in the peskas-api bucket (api_district_months):districts_summarieskeyed by GAUL code plus both methods’ estimates and the ARTFISH relative errors (fao_district_error). Its 23 columns are a contract with peskas-api’sfield_metadata.py(tests/testthat/test-api-district-months.Rpins them). -
Portal.
export_portalruns once per country, called from each country pipeline with that country’s config (conf$country), and pushes thesummarize_data()tables to the country’sportal-*database (collections and the FishBase-traits dependency: the Peskas context “Portal collections” and “FishBase traits”).export_geosin the coasts pipeline reads the per-country<country>_monthly_summaries_mapfiles (hard-coded list: kenya, zanzibar, mozambique, timor) and pushes the combinedwio_gaul*,metrics_gaul*andpds_gridscollections.export_pds_spatialalso pushes the coasts portal’s effort cells and fishing grounds, tagged bycountry, aspds_effortandpds_fishing_grounds; the dashboard’s effort map (peskas.dashboard) reads them there, filtered to its country. -
api.tripsininst/conf.yml(peskas-api-dev/peskas-api-prod) lists each country’sraw/validatedpaths in the peskas-api bucket.merge_survey_trips()(R/match-trips.R) reads Kenya’s, Mozambique’s and Zanzibar’s validated trips from there (not Timor’s), plus theirsurveys.*merged files from country buckets. -
pds.customers/select_byininst/conf.ymlpick the trips coasts ingests for its own WIO-wide products, covering every country at once. Country pipelines carry their ownpdsblock.?select_country_tripsexplains the device/community/exclude rules. -
Tracks app.
storage.mongodb.tracks_appcollections are shared withtracks-explorer.sync_device_usersupdates the device users inusersin place, by IMEI (mdb_users_upsert); the app writes its own users there too (administrators, self-registered fishers, changed passwords), so never empty that collection.export_fishers_statsreads the app’susersandcatch-eventsand writesfishers-statsandfishers-performance. App readers:api/auth/login.jsandapi/users.js(users),api/catch-events.js,api/fisher-stats/andapi/fisher-performance/. -
Collection stats.
export_collection_stats(weekly,collection-stats.yaml,mainonly) writes one row per country and district to the Airtable frame tablecollection_stats, upserted onkey. It reads every country’s API trips, the validation database’ssurveys_flags-<form>for enumerators, and from each country bucketpds-tripsand<country>-aggregated. The default profile names the tablecollection_stats_dev, which does not exist, so a dev run fails rather than overwriting production numbers. -
Currency.
export_geosconverts RPUE and price per kg to USD with hard-coded rates inR/export.R, one per currency: TZS 0.00038, KES 0.0077, MZN 0.016. -
mdb_collection_push(geo = TRUE)drops the collection, reinserts, and builds ageometry2dsphere index.
Rules
- Ship a breaking change to anything a pipeline calls (signature, config keys read, output columns) as a coasts release first (bump DESCRIPTION, add the
NEWS.mdblock), then bump each pipeline. Grep../peskas.*.data.pipelinefor callers before changing a signature. - When a function starts reading a new config key, add it to every calling country’s
inst/config.ymltoo (the Peskas context: coasts reads country config). - Add a name to
pds.customersonly when all its devices belong to one country; one entry widens the filter for the whole regional product. - Adding a country to the regional portal means editing the hard-coded list in
export_geosand, if it reports in local currency, the conversion rates.
Gotchas
-
read_config()stops if the calling package ships neitherinst/conf.ymlnorinst/config.yml. -
select_country_tripserrors on zero kept trips on purpose: an empty parquet would becomelatestand empty the portal. Do not soften it to a warning. - Before switching any config to
select_by: community, run both rules over one trip table and compare counts: communities come from current device records, so a site with no device left drops its trips. - Renaming a tracks-app collection or field breaks
tracks-explorer; renaming portal collections breakspeskas.dashboard(see the Peskas context). - Stray
*.rds/*.parquetfiles in the repo root are local downloads (gitignored); do not rely on them. -
export_frame_data()is run by hand when a new census arrives (every few years), not in a workflow. It readsframe_units; foot fishers count as fishers, not boats, and Timor-Leste’s rows (boats only, no fishers) are left out. Mozambique’s six “Unallocated” rows have no boats, so no estimate uses them, but they hold fishers the portal counts: keep them.
