# Exporting data

uproot stores every piece of data your experiment collects in an [append-only log](https://uproot.science/building/data.md). Nothing is ever overwritten or lost. When you download your data, you do not have to pick and choose: you always get a single ZIP archive (the *data briefcase*) that contains your session’s complete data in all key formats, ready for Excel, R, or Python.

This page shows you how to download that ZIP and how to analyze its contents. If you want to automate exports, work with the REST API, or analyze the database directly in Python, see [Advanced data access](https://uproot.science/running/export-advanced.md).

## Downloading your data

On a session’s page in the admin interface, click **Download data**. You will see a short form:

- **Latest × Field(s):** This adds an extra format with one row per player per grouping you specify (for example, one row per player per `round`). You can uncheck this if your study does not make use of multiple apps or rounds. Leave it checked if you are not sure; this format encompasses the “latest” format.
- **File type:** **CSV** (opens in Excel and every stats package) or **JSONL** (preserves data types; see [CSV or JSONL?](#csv-or-jsonl) below).
- **Apply reasonable filters:** This is checked by default. It cleans up uproot-internal fields so your data focuses on what your experiment actually collected.

Click **Download** and you will receive one ZIP file named after the session and the current date, for example `mysession_2026-07-04_1412.zip`.

## What’s inside the ZIP

Unpacking the ZIP gives you a single folder named after your session:

```
mysession/
├── README.txt            ← explains the contents, right inside the ZIP
├── DATA_DICTIONARY.json  ← defines the !-prefixed columns
├── page_times.csv        ← when each player entered/left each page
├── SHA256SUMS            ← checksums for verifying all other files
├── latest/
│   ├── player.csv
│   ├── group.csv
│   └── session.csv
├── sparse/
│   ├── player.csv
│   ├── group.csv
│   └── session.csv
└── ultralong/
    ├── player.csv
    ├── group.csv
    └── session.csv
```

If you check **Latest × Field(s)**, there is one more folder, named after your grouping variable(s). With the default grouping variables (`app`, `block`, and `round`), it is `latest_by_app_block_round/`.

Each subfolder contains the same data in a different *format* (explained below). Within each format, the data is split into one file per kind of storage:

| File | Contains |
|------|----------|
| `player.csv` | Data stored on players; usually what you want |
| `group.csv` | Data stored on groups (only if your session has groups) |
| `session.csv` | Session-level data |
| `model.csv` | [Custom data models](https://uproot.science/advanced/models.md) (only if your apps use them) |

Each file only has columns for fields that actually occur in that kind of storage, so player files are not cluttered with session-level columns and vice versa.

!!! tip "In a hurry?"
    **`latest/player.csv`** or **`latest_by_…/player.csv`** is the file most people want: one row per participant, one column per field, showing each field’s final value (at the end of each round when grouping by `round`).

## The three formats

Every briefcase contains the same underlying data at three levels of detail:

### “Latest”: one row per player

One row per storage (e.g., per player), showing the most recent value of every field. This is the most compact format and what you will typically use for analysis if your study does not involve multiple apps or multiple rounds.

#### “Latest × Field(s)”: one row per player per level of your grouping variable(s)

If you enable **Latest × Field(s)**, the `latest_by_…/` folder contains one row per player *per combination of your grouping variable(s)*—for example, each participant’s state at the end of each round. This is ideal for panel-style analyses.

By default, uproot groups by `app`, `block`, and `round`. This gives you one row per participant for every round of every app and [block](https://uproot.science/building/operators.md#blocks). Apps without rounds get one row each, showing the participant’s state at the end of that app.

!!! note
    Grouped snapshots include every field known at that point in time (except `page_order` with [reasonable filters](#reasonable-filters)). The grouping variables determine *when* snapshots are taken, not which columns they carry. Fields set before the grouping variable appear in the output as expected.

### “Sparse”: one row per change, one column per field

Each row represents a single change. Every field has its own column, and only the field that changed at that moment is filled in. This produces a wide but mostly empty table. Useful when you want to reconstruct *how* values evolved but still prefer one column per field.

### “Ultralong”: the raw event log

One row per field change, in full detail. Every time a field’s value was set, there is a row recording what changed, when, and where in the code. This is the most detailed format and preserves the complete history. Use it for temporal analyses, audits, or debugging.

## Understanding the columns

Columns that start with `!` come from uproot itself; they sort to the front and can never clash with your own field names. In `ultralong` files you will find all of them:

| Column | Description |
|--------|-------------|
| `!storage` | Which storage this row belongs to (e.g., `player/mysession/9wpsj`) |
| `!field` | Field name |
| `!time` | Unix timestamp of the change |
| `!seq` | Sequence number of the change (for exact ordering) |
| `!context` | Code location that made the change |
| `!unavailable`{ .text-nowrap } | Whether this row marks a deletion (a tombstone) |
| `!data` | The value |

`latest` files just have `!storage`, `!time`, and `!seq` (the time and sequence of the most recent change reflected in the row), followed by one column per field.

The full machine-readable definitions live in `DATA_DICTIONARY.json` inside the briefcase.

The last part of `!storage` is the participant’s uproot name. For instance, `player/mysession/9wpsj` is participant `9wpsj` in session `mysession`.

## Reasonable filters

The **Apply reasonable filters** option (on by default) cleans up uproot-internal fields:

- It renames `_uproot_group` to `group` (with values like `group/mysession/g1`, matching the `!storage` column of `group.csv`; this is handy for merging, see below). When a participant leaves a group—for example, when they reach the next app’s `GroupCreatingWait` page—`group` becomes empty until they join a new one.
- It renames `_uproot_session` to `session`.
- In `group.csv` and `session.csv`, it renames `_uproot_players` to `players`: a list of the members, with values like `player/mysession/9wpsj`.
- In `session.csv`, it renames `_uproot_groups` to `groups`: a list of the session’s groups, with values like `group/mysession/g1`.
- It keeps `_uproot_dropout` and `_uproot_settings`.
- It removes all other internal `_uproot_*` fields.
- It leaves `page_order` out of `latest_by_…/`, where it would repeat every page in every row. It stays in the other formats, and `page_times` tells you which page each participant saw when.

Turn filters off only if you need to inspect uproot’s internal bookkeeping.

## CSV or JSONL?

**CSV** is the safe default: It opens directly in Excel, R, Python, Stata, and everything else. Booleans are written as `TRUE`/`FALSE`, missing values as empty cells, and lists or dictionaries as JSON text within the cell.

**JSONL** ([JSON Lines](https://jsonlines.org/): one JSON object per line) is worth choosing when your data contains lists, dictionaries, or mixed types. Unlike CSV, it keeps the distinction between numbers, strings, Booleans, and nested structures. That is, no more guessing whether `"1"` is a number or a string. Both pandas and R read it with one line of code.

Whichever you choose, the briefcase has the same structure—just with `.jsonl` files instead of `.csv` files.

## Analyzing your data

You do not even have to unpack the ZIP: Both R and Python can read individual files straight out of the archive, entirely in memory. The examples below assume you downloaded `mysession_2026-07-04_1412.zip` and that the session is called `mysession`. (Adjust the names accordingly. Of course, if you prefer, you can also unpack the ZIP and read the files from disk; the reading code is the same, minus the ZIP part.)

=== "R"

    With base R, `unz()` opens a single file inside a ZIP as a connection—no extraction needed:

    ```r
    zipfile <- "mysession_2026-07-04_1412.zip"

    players <- read.csv(unz(zipfile, "mysession/latest/player.csv"),
                        check.names = FALSE)
    ```

    (`check.names = FALSE` keeps column names like `!storage` intact instead of mangling them to `X.storage`.)

    With **readr/dplyr**, `read_csv()` accepts the same connection and preserves column names by default:

    ```r
    library(readr)

    players <- read_csv(unz(zipfile, "mysession/latest/player.csv"))
    ```

    With **data.table**, let `fread()` pull the file out of the ZIP via the `unzip` command:

    ```r
    library(data.table)

    players <- fread(cmd = paste("unzip -p", zipfile, "mysession/latest/player.csv"))
    ```

    Column names starting with `!` are perfectly legal in R; just wrap them in backticks:

    ```r
    library(dplyr)

    players |>
        select(`!storage`, payoff) |>
        arrange(desc(payoff))
    ```

    **Merging players with their groups.** With filters applied, each player’s `group` column matches the `!storage` column of `group.csv`:

    ```r
    library(dplyr)

    players <- read_csv(unz(zipfile, "mysession/latest/player.csv"))
    groups <- read_csv(unz(zipfile, "mysession/latest/group.csv"))

    full <- left_join(players, groups,
                      by = c("group" = "!storage"),
                      suffix = c("", ".group"))
    ```

    Or with data.table:

    ```r
    full <- merge(players, groups,
                  by.x = "group", by.y = "!storage",
                  suffixes = c("", ".group"))
    ```

    **JSONL** briefcases work exactly the same way, with jsonlite:

    ```r
    library(jsonlite)

    players <- stream_in(unz(zipfile, "mysession/latest/player.jsonl"))
    ```

=== "Python"

    Python’s built-in `zipfile` module opens a single file inside a ZIP (no extraction needed), and **pandas** reads directly from it:

    ```python
    import zipfile
    import pandas as pd

    with zipfile.ZipFile("mysession_2026-07-04_1412.zip") as zf:
        with zf.open("mysession/latest/player.csv") as f:
            players = pd.read_csv(f)
    ```

    Column names starting with `!` require bracket notation:

    ```python
    players[["!storage", "payoff"]].sort_values("payoff", ascending=False)
    ```

    **Merging players with their groups.** With filters applied, each player’s `group` column matches the `!storage` column of `group.csv`:

    ```python
    with zipfile.ZipFile("mysession_2026-07-04_1412.zip") as zf:
        with zf.open("mysession/latest/player.csv") as f:
            players = pd.read_csv(f)
        with zf.open("mysession/latest/group.csv") as f:
            groups = pd.read_csv(f)

    full = players.merge(groups, how="left",
                         left_on="group", right_on="!storage",
                         suffixes=("", "_group"))
    ```

    **JSONL** briefcases need just one extra argument:

    ```python
    with zipfile.ZipFile("mysession_2026-07-04_1412.zip") as zf:
        with zf.open("mysession/latest/player.jsonl") as f:
            players = pd.read_json(f, lines=True)
    ```

## Verifying your download

Every briefcase includes a `SHA256SUMS` file listing the checksum of every other file in the archive. To confirm that nothing was corrupted or modified—for instance, before archiving data for publication—run this inside the unpacked folder:

```bash
cd mysession
sha256sum -c SHA256SUMS
```

(On macOS, use `shasum -a 256 -c SHA256SUMS`.)

## Page times

Every briefcase includes a `page_times.csv` (or `.jsonl`) file tracking when each player entered and left each page. This is useful for measuring response times. Its columns are:

| Column | Description |
|--------|-------------|
| `sname` | Session name |
| `uname` | Player name |
| `show_page`{ .text-nowrap } | Page index |
| `page_name` | Page class name |
| `entered` | Unix timestamp when the player entered the page |
| `left` | Unix timestamp when the player left the page |
| `context` | Round/context information |

Page times are derived from the players’ `show_page` and `page_order` histories.

## Going further

For everything beyond the download button, see [Advanced data access](https://uproot.science/running/export-advanced.md):

- Downloading briefcases automatically via the [REST API](https://uproot.science/reference/admin-api.md)
- Streaming JSONL exports
- Full database backups with `uproot dump`
- Offline analysis of the database in Python with `uproot.read`
