Fine-tune Chronos-2 with covariates across a portfolio

Towards Data Science published Shuai Guo’s walkthrough of five ways to fine-tune Chronos-2, Amazon’s 120M-parameter time series model, and one comparison stuck with me. Fine-tuning on the raw target series moved error from 8.3 to 7.6 percent WAPE. Handing the model known-future covariates first, then fine-tuning, moved it from 4.0 to 2.8. The adapter is the small lever, and what you feed the model is the big one.

The fix people reach for first

The case study is a fleet of eight commercial buildings with hourly electricity demand and one-week-ahead forecasts. Swap buildings for SKUs and kilowatts for units sold and you have the store forecasting problem: the zero-shot model is decent but keeps missing the same kinds of weeks, and someone asks whether we can just train it on our data.

The obvious move is target-only fine-tuning: freeze the base weights and learn a LoRA adapter, rank 8, alpha 16, on the q, k, v and o projections plus the output patch embedding. The source ran this and it worked. Every building improved, but by under a percentage point each: 8.3 to 7.6 on Building 03. The improvement is real but small.

There’s a second reason not to start here. The AutoGluon Chronos-2 tutorial, which covers this with the library most people will actually use, recommends fine-tuning only with a lot of series and long histories: on the order of more than 100 series with a median history over three times the prediction length. One store, or a store with 30 SKUs, doesn’t clear that bar. I’d still try it, but I wouldn’t promise much.

Fine-tune Chronos-2 with covariates

The setup that pays off is giving the model what you already know about the forecast week, then fine-tuning around it. In the article those signals are outdoor temperature, occupancy, solar irradiance and a weekend flag. On store work the equivalents are a promo calendar, holiday flags, weekend flags and scheduled email sends, things that exist before the week starts. Each training task gains past_covariates for the historical values and future_covariates to declare which columns will be known at forecast time, and at inference you pass the real future values through future_df.

The gains stack: covariate-informed zero-shot was already at 4.0 percent for Building 03, and fine-tuning the covariate adapter took it to 2.8. Pooling all eight buildings into one shared adapter with covariates took the fleet from 8.4 to 2.8 percent. For deployment, the held-out run matters most. A building excluded from fine-tuning still improved from 4.2 to 3.1, which means you can train one adapter on the assets you have and hand it to new ones as they arrive. I’m not sure that transfer holds for unrelated products, since eight simulated buildings are a friendlier fleet than most catalogs.

One caveat: the covariates have to be genuinely known in advance. If the promo calendar is aspirational, or the holiday feed lags by two days, the model will learn a relationship that doesn’t exist at inference time, and it will learn it well.

Cost is usually where these ideas die, so here it isn’t. The fine-tune in the source ran on an 8 GB laptop GPU in about 42 seconds, and the artifact is a small adapter file you can version next to the code. The model card on Hugging Face lists it as encoder-only with a group attention mechanism, which is why one adapter can cover a fleet. I’ve written before about running Chronos-2 zero-shot on WooCommerce order data; the fine-tuning loop is the same export job plus one training step.

The validation window most people get wrong

One detail in the walkthrough will bite anyone who copies the code. Chronos-2 validates on the last prediction_length steps of each validation task, no matter how much history you feed it, so a longer validation dataframe still gives you exactly one validation window. Rolling validation means building one task per cutoff:

cutoffs = pd.date_range(val_start, val_end, freq="7D")  # one horizon per task
for sku, sku_df in validation_df.groupby("sku"):
    for cutoff in cutoffs[1:]:
        window = sku_df[sku_df["timestamp"] <= cutoff]
        validation_inputs.append(
            {"target": window[["qty_sold"]].to_numpy(dtype="float32").T}
        )

With a single window you’re picking a checkpoint on one week. Pick a week with a promo in it and you’ll happily keep the adapter that fits promo weeks and nothing else. I’d fix this before trusting any fine-tuning result, including my own.

If you have a store with a real promo calendar and a couple of years of order history, this is the kind of work I take on: getting the export right, building the covariate table, running the baseline, then deciding whether the adapter earns its place.

On a client site this week I’d do it in that order, because it’s the order the numbers support: covariates first, pooling second, adapter last. And I wouldn’t show anyone a single fine-tuned forecast until the validation windows were rolling.

author avatar
Ahmad Wael
I'm a WordPress and WooCommerce developer with 15+ years of experience building custom e-commerce solutions and plugins. I specialize in PHP development, following WordPress coding standards to deliver clean, maintainable code. Currently, I'm exploring AI and e-commerce by building multi-agent systems and SaaS products that integrate technologies like Google Gemini API with WordPress platforms, approaching every project with a commitment to performance, security, and exceptional user experience.

Leave a Comment