Skip to content

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#

  1. Connect your device and open Finder (or use the Files app and share to iCloud/AirDrop).
  2. Select the device, open the Files tab, and expand CaptureGraph.
  3. 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: