# CSV and Parquet

Upload CSV and Parquet files with [`add_tabular_data()`](https://dev.nominal.io/python/ref/generated/nominal.core.Dataset.md#nominal.core.Dataset.add_tabular_data "nominal.core.Dataset.add_tabular_data").
Each file needs one timestamp column. Every other column becomes a channel.

For creating datasets and waiting for ingestion, see [Datasets](https://dev.nominal.io/python/guides/datasets/overview.md).

## Connect the client

This guide assumes you have [installed](https://dev.nominal.io/python/guides/install.md) the client and setup your [API keys](https://dev.nominal.io/python/guides/api-keys-setup.md).

```python
from nominal.core import NominalClient

client = NominalClient.from_profile("default")
```

## Upload a file

The examples upload `flight.csv`. To run them as written, save this sample under that name:

```text
time,altitude,temperature,tail_number
2026-09-10T14:38:19Z,100.0,20.0,N456QC
2026-09-10T14:38:20Z,101.5,20.2,N456QC
2026-09-10T14:38:21Z,103.0,20.4,N456QC
```

```python
dataset = client.create_dataset("Frosty flight")
file = dataset.add_tabular_data("flight.csv", timestamp_column="time", timestamp_type="iso_8601")
file.poll_until_ingestion_completed()
```

`add_tabular_data` accepts `.csv`, `.csv.gz`, `.parquet` and `.parquet.gz`, plus `.parquet.tar`, `.parquet.tar.gz` and `.parquet.zip` archives.

CSV files must use commas between values and periods for decimals, such as `3.14`.
Convert files that use semicolons and decimal commas, such as `3,14`, before you upload them.

To upload a pandas DataFrame without writing a file, use [`upload_dataframe_to_dataset()`](https://dev.nominal.io/python/ref/generated/nominal.thirdparty.pandas.upload_dataframe_to_dataset.md#nominal.thirdparty.pandas.upload_dataframe_to_dataset "nominal.thirdparty.pandas.upload_dataframe_to_dataset").

## Timestamp formats

`timestamp_type` describes the values in the timestamp column:

| Values | `timestamp_type` |
| --- | --- |
| `2024-06-08T05:58:42.000Z` | `"iso_8601"` |
| `1717826322` | `"epoch_seconds"` |
| `1717826322000000` | `"epoch_microseconds"` |
| Other units since the Unix epoch | `"epoch_nanoseconds"`, `"epoch_milliseconds"`, `"epoch_minutes"`, and so on |
| Time since the start of the recording | [`ts.Relative`](https://dev.nominal.io/python/ref/generated/nominal.ts.Relative.md#nominal.ts.Relative "nominal.ts.Relative") |
| Any other absolute format | [`ts.Custom`](https://dev.nominal.io/python/ref/generated/nominal.ts.Custom.md#nominal.ts.Custom "nominal.ts.Custom") |

Relative timestamps need the unit and the absolute time the recording started:

```python
from datetime import datetime, timezone

from nominal import ts

recording_start = datetime(2026, 9, 10, 14, 38, 19, tzinfo=timezone.utc)
file = dataset.add_tabular_data(
    "bench.csv",
    timestamp_column="elapsed_s",
    timestamp_type=ts.Relative("seconds", start=recording_start),
)
file.poll_until_ingestion_completed()
```

A value of `1.0` in `elapsed_s` then means `2026-09-10T14:38:20Z`.
See [Timestamps](https://dev.nominal.io/python/ref/ts.md) for all formats.

## Tag the data

Tags label the points in a file so you can filter them later, such as by test or by sensor.
`tags` applies the same tags to every row, and `tag_columns` reads a tag’s value from a column:

```python
file = dataset.add_tabular_data(
    "flight.csv",
    timestamp_column="time",
    timestamp_type="iso_8601",
    tags={"test_id": "flight-042"},
    tag_columns={"aircraft": "tail_number"},  # tag key: column name
)
file.poll_until_ingestion_completed()
```

See [tags](https://dev.nominal.io/python/guides/streaming/overview.md#add-tags-to-filter-data-in-a-run-or-asset) for filtering by them.
