Analyzing Your Data#
Captured data comes off the phone as plain files — no server, no sync tool, no export step. The target folder is a complete, self-describing dataset: its layout is the schema you authored the procedure with, so loading it takes one call and the same schema class.
Step 1: Copy the Target off the Phone#
- Connect your device and open Finder (or use the Files app and share to iCloud/AirDrop).
- Select the device, open the Files tab, and expand CaptureGraph.
- Drag
CaptureData/<YourTarget>to your computer (here,./MySurvey).
Step 2: Load It#
cg.load walks the target with your schema and returns the loaded Struct. Reuse the same Survey class you wrote in Your First Procedure:
import capturegraph as cg
class Session(cg.Struct):
photo: cg.Image
notes: cg.String
rating: cg.Number
class Survey(cg.Struct):
sessions: cg.Map[cg.Date, Session]
_metadata: cg.Metadata
survey = cg.load("./MySurvey", Survey)
Each position is decoded by its type: JSON scalars become typed values (cg.Number, cg.String, ...), and file scalars like cg.Image hold a path plus decode helpers, so no pixels are read until you ask.
Step 3: Walk the Sessions#
survey.sessions is a cg.Map. It keeps its keys (the capture times) and broadcasts attribute access over its values:
sessions = survey.sessions
for when, session in sessions.items(): # keys are datetimes
print(when, session.notes.value)
list(sessions.keys()) # every capture time
sessions.rating # broadcast: an Array of Number
sessions.rating.value.to_numpy() # chain through .value: [5, 3, 4, ...]
list(sessions.notes.value) # ['first attempt', 'cloudy today', ...]
A scalar loads wrapped — sessions.notes is a column of cg.String, and .value unwraps each to a plain Python value. Chaining .value over a broadcast column gives an Array you can call .to_numpy() on.
A file scalar loads as its path value — a cg.Image with its decode helpers as methods:
for session in sessions.values():
print(session.photo.exif().datetime_original) # typed EXIF, off the loaded image
Step 4: Handle Missing Data#
Field captures are messy: sessions get interrupted, optional steps get skipped. A missing position loads as cg.Missing, a null object that absorbs further access so a chain never throws. Test it with cg.is_missing:
for session in sessions.values():
if cg.is_missing(session.notes):
print("no notes")
else:
print(session.notes.value)
Step 5: Into pandas#
Broadcast columns drop straight into a DataFrame:
import pandas as pd
sessions = survey.sessions
df = pd.DataFrame({
"date": list(sessions.keys()),
"rating": sessions.rating.value.to_numpy(),
"notes": list(sessions.notes.value),
})
print(df)
date rating notes
0 2026-06-10 17:42:11.371082 5 first attempt
1 2026-06-11 09:03:56.114209 3 cloudy today
2 2026-06-11 18:30:02.992716 4 golden hour!
Because the schema was fixed by the procedure before capture began, this script keeps working as new sessions arrive — write it on day one and re-run it for the life of the study.
Where to Go Next#
You've now completed the full loop: plan, capture, analyze. From here:
- Loading Captures —
cg.load, broadcasting containers, and missing-data handling in depth. - Containers — how
cg.Arrayandcg.Mapbroadcast. - Defining Procedures — the full DSL: type system, recorder, and control flow.
- Working with a team? Run a server to host shared targets, schedule captures so the dataset stays evenly distributed, and sync data incrementally to your machine.