GIFT-Eval-CTX / src /explorer.py
Xu Liu
upload code
b5f29f5
Raw History Blame Contribute Delete
3.85 kB
"""Read individual examples and align forecasts by their full sample key."""
from __future__ import annotations
import json
import math
from pathlib import Path
from typing import Sequence
import pyarrow.parquet as pq
from src.config import SAMPLE_KEYS, Method, load_methods
def keyed_rows(table) -> dict[tuple, int]:
columns = [table[name].to_pylist() for name in SAMPLE_KEYS]
keys = list(zip(*columns))
if any(any(value is None for value in key) for key in keys):
raise ValueError("Null sample keys")
result = {key: index for index, key in enumerate(keys)}
if len(result) != len(keys):
raise ValueError("Duplicate sample keys")
return result
def clean_series(values: list) -> list:
# Keep missing observations at their original time positions.
return [float(value) if value is not None and math.isfinite(value) else None for value in values]
class SampleRepository:
def __init__(self, directory: Path, methods: Sequence[Method] | None = None):
self.directory = directory
self.methods = load_methods(directory) if methods is None else tuple(methods)
self._evaluations = {}
self.samples = pq.read_table(directory / "data" / "structured_effects.parquet")
self.index = keyed_rows(self.samples)
if not self.index:
raise ValueError("No examples available")
micro = self.samples["micro"].to_pylist()
self.entries = [
{"id": row, **dict(zip(SAMPLE_KEYS, key)), "effects": sorted({
"/".join(str(event[field]) for field in ("family", "subtype") if event.get(field))
for event in json.loads(micro[row])
})}
for key, row in self.index.items()
]
def catalog(self) -> dict:
return {"samples": self.entries, "methods": [method.to_dict() for method in self.methods]}
def _evaluation(self, method: Method):
if method.id not in self._evaluations:
table = pq.read_table(self.directory / method.relative_path,
columns=SAMPLE_KEYS + ["forecast", "mase"])
index = keyed_rows(table)
if index.keys() != self.index.keys():
raise ValueError(f"{method.filename}: sample keys do not match the examples")
self._evaluations[method.id] = table, index
return self._evaluations[method.id]
def sample(self, sample_id: int) -> dict:
if sample_id < 0 or sample_id >= len(self.samples):
raise KeyError(sample_id)
result = {name: self.samples[name][sample_id].as_py() for name in self.samples.column_names}
key = tuple(result[name] for name in SAMPLE_KEYS)
result["id"] = sample_id
result["macro"] = json.loads(result["macro"])
result["micro"] = json.loads(result["micro"])
for name in ("history", "future", "tsfm_average_forecast"):
result[name] = clean_series(result[name])
if len(result["history"]) != result["n_history"] or len(result["future"]) != result["n_horizon"]:
raise ValueError("Series lengths do not match example metadata")
forecasts = []
for method in self.methods:
table, index = self._evaluation(method)
row = index[key]
values = clean_series(table["forecast"][row].as_py())
mase = table["mase"][row].as_py()
if len(values) != result["n_horizon"]:
raise ValueError(f"{method.filename}: forecast length does not match the horizon")
if mase is None or not math.isfinite(mase) or mase < 0:
raise ValueError(f"{method.filename}: invalid MASE")
forecasts.append({**method.to_dict(), "forecast": values, "mase": float(mase)})
result["forecasts"] = forecasts
return result