Converting data¶
Overview¶
Here is the big picture of the different components that allow for data conversion (the data download components are grayed out):
flowchart TB
AbcSourceDataDirManager
source_data@{ shape: cyl, label: "Source data" }
converted_data@{ shape: cyl, label: "Converted data" }
download@{ shape: lean-r, label: "download.py" }
provider_infra@{ shape: cloud, label: "Provider infrastructure" }
download -.->|call .start| AbcDownloader
HttpResource -.->|fetch| provider_infra
AbcDownloader -.->|create| HttpResource
AbcDownloader -.-> AbcSourceDataDirManager
HttpResource -.->|write| source_data
convert@{ shape: lean-r, label: "convert.py" }
convert -->|call .start| AbcConverter
AbcConverter -->|create| MyDatasetConverter
MyDatasetConverter --> AbcSourceDataDirManager
AbcSourceDataDirManager -->|read| source_data
MyDatasetConverter -->|write| converted_data
classDef muted fill:#D3D3D31F,stroke:lightgray,stroke-dasharray: 5 5;
class download,AbcDownloader,HttpResource,provider_infra muted;
Conversion script¶
This script can be called manually from the command line, and will be called by the DBnomics infrastructure in production.
Note the separation of concerns:
the
ConvertCLIclass handles the CLI arguments and options, then creates and runs the converter,the
AbcConverterclass takes care of the conversion logic.
AbcConverter¶
The AbcConverter class, inherited from BaseConverter, is responsible for converting source data downloaded from the infrastructure of this specific provider into the DBnomics data model.
Dataset converters¶
A dataset converter is responsible for transforming raw source data into the DBnomics data model.
Each dataset from the provider must have its own converter class that inherits from BaseDatasetConverter.
In the ABC fetcher example, we have one dataset (GAS_PRICE), so we create one dataset converter class:
Lifecycle methods¶
The BaseDatasetConverter class defines an abstract lifecycle that your converter must implement:
start(*, dataset_writer): Called once at the beginning of the conversion process for this dataset. Use this method to initialize any resources needed for the conversion, such as loading and parsing source data files._create_dataset_metadata(): Called to define the structure of the dataset. Returns aDatasetMetadataobject describing dimensions, attributes, and metadata about the dataset._iter_dataset_series(*, dataset_metadata): Called to iterate over all series in the dataset. Yields Series objects containing observations.
Modeling dimensions¶
Dimensions are the categorical axes of your data.
In the ABC fetcher, the gas price data has one dimension: KIND (the type of gas).
Dimensions are defined upfront in the _create_dataset_metadata() method:
kind_dimension = Dimension.create(
"KIND",
label="Gas kind",
values=[
DimensionValue.create(
slugify(label),
label=label,
)
for label in self._df["kind"].unique()
],
)
Each dimension has:
A code (e.g.,
"KIND") used as an identifierA label (e.g.,
"Gas kind") for display purposesA list of dimension values representing each possible value of that dimension
Dimension values are also slugified to create unique, URL-safe codes from their labels.
Creating series¶
Series represent individual time series within the dataset. For each series, you must specify:
Dimension values: The specific combination of dimension values for this series (e.g.,
{"KIND": "natural-gas"})Observations: A list of period-value pairs, with optional attributes for each observation
In the ABC fetcher, series are created by grouping the CSV data by the kind column:
def _iter_dataset_series(self, *, dataset_metadata: DatasetMetadata) -> Iterator[Series]:
df = self._df
for kind, group_df in df.groupby("kind"):
observations = [
Observation.create(
period=row["year"],
value=row["price"],
)
for _, row in group_df.sort_values("year").iterrows()
]
yield Series.create(
add_missing_dataset_dimensions=True,
dataset_dimensions=dataset_metadata.dimensions,
dimensions={"FREQ": "A", "KIND": kind},
observations=observations,
)
The add_missing_dataset_dimensions=True parameter ensures that any dimensions assigned to the series that was not defined upfront in the dataset metadata are added to it.
This allows converting datasets with dynamic dimensions, when the set of dimension values is not known in advance.
Loading and parsing source data¶
Use helper methods like _load_csv_file() to load and parse source data files downloaded by the downloader.
This keeps your conversion logic clean and testable:
def _load_csv_file(self) -> pd.DataFrame:
return pd.read_csv(
self._source_data_dir_manager.gas_price_file,
na_values={"price": ["N/A"]},
)
In this example, we use pandas to load a CSV file and handle missing values.
Note
The path of the CSV file is provided by the AbcSourceDataDirManager that was introduced in the Source data directory manager section.
Its purpose is to abstract away the details of where the source data files are located, so that the downloader and the converter can focus on their respective tasks.
Run the convert script¶
Note
TOOLBOX_NO_LOG_COLOR=1 disables colors in the logs, TOOLBOX_NO_LOG_TIMESTAMP=1 hides timestamps in the logs, and TOOLBOX_PLAIN_TRACEBACK=1 shows plain exception tracebacks, for the sake of readability in the documentation.
The convert script processes source data and generates the DBnomics data model output:
TOOLBOX_NO_LOG_COLOR=1 TOOLBOX_NO_LOG_TIMESTAMP=1 TOOLBOX_PLAIN_TRACEBACK=1 uv run convert.py source-data converted-data
This command:
Reads from the
source-datadirectory (populated by the download script)Writes to the
converted-datadirectoryCreates datasets structured according to the DBnomics data model
The converted data directory structure looks like:
converted-data/
├── provider.json # Provider metadata
└── GAS_PRICE/ # One directory per dataset
├── dataset.json # Dataset metadata (dimensions, attributes)
└── series.jsonl # Series data in JSON Lines format
Each line of series.jsonl contains a complete series object with all its dimensions and observations.
Storage adapters¶
The second argument of the convert script (converted-data) is resolved by a storage adapter — an abstraction layer that determines how and where the converted data is written.
URI resolution¶
When you pass a plain directory path like converted-data, it is automatically resolved to a filesystem URI:
filesystem:converted-data?default_series_format=JSON_LINES&single_provider=true
Note
The single_provider=true query parameter indicates that the directory contains data from a single provider, as opposed to a multi-provider storage layout where a subdirectory per provider would be expected. Since the convert script targets one provider at a time, single_provider=true is automatically set when a plain directory path is provided.
The two forms are therefore equivalent:
# Short form (auto-resolved)
uv run convert.py source-data converted-data
# Explicit URI form
uv run convert.py source-data "filesystem:converted-data?single_provider=true"
The filesystem scheme is currently the only storage adapter available. It writes data to the local filesystem.
The argument parser is setup by the ConvertCLI class.
Output format¶
By default, series data is written in JSON Lines format (series.jsonl). You can switch to TSV (tab-separated values) by passing the default_series_format query parameter in the URI:
uv run convert.py source-data "filesystem:converted-data?single_provider=true&default_series_format=TSV"
This produces a TSV file per time series instead of a single series.jsonl file.