Discord

Checklist IOAI Indonesia 2026 National Training Camp 1 – Simulation 2 · 2.1 task

Hybrid Riverflow

Predict the mean river discharge over the next 12 days from a historical daily series of 50–60 observations.

  • Time series
  • Time-series regression
  • Indonesian original · English translation

The task

Each sample is one synthetic historical series of daily river discharge from a monitoring site, with an actual length T between 50 and 60 days. The target is y = (1/12) Σ_{t=T+1}^{T+12} x_t, the mean discharge over the following 12 days; the daily values themselves are not predicted.

Series may show trends, seasonality, mean reversion or local changes near their end, so a single global pattern is not assumed. Observations are stored in columns x_1x_60, and NaN after obs_len marks a shorter series rather than missing data. The starter notebook (by Mushthofa) provides a local Exponential Smoothing / Holt baseline per series.

Abridged and translated by SOTA from the official Indonesian materials. The official statement has the exact rules, and it wins wherever this summary differs.

In English

This task was published in Indonesian. SOTA translated its 3 files into English on 17 September 2026. Only the words changed in the notebooks: markdown, code comments, messages and printed output. The code, file names and paths are the original's, so a translated notebook runs with the original data.

Read the task notebook in English 820 words and 10 code cells

Simulation 2, Pelatnas 1 IOAI 2026 — Participant Starter Notebook

Predicting the Future Mean River Discharge from a Historical Time Series (by: Mushthofa)

This notebook is a starter pack for participants. It covers:

  1. the official problem description,
  2. the specification of the data files and the technical parameters,
  3. how to download the data from Google Drive to Google Colab,
  4. a simple baseline based on local forecasting per time series,
  5. how to create the submission file,
  6. an explanation of the evaluation metric, MAE.

1. Problem Description

A water-resources monitoring agency collects daily river discharge data from many observation sites.
Each sample in this task represents one historical time series of river discharge from one particular site/time window.

For each sample, historical observations covering 50 to 60 days are given. If the observation length for a sample is TT, then the target to be predicted is:

y=112t=T+1T+12xty = \frac{1}{12}\sum_{t=T+1}^{T+12} x_t

where:

  • x1,x2,,xTx_1, x_2, \dots, x_T = the historical observations,
  • yy = the mean river discharge over the next 12 days.

Participants are not asked to predict each of the 12 future daily values one by one, but only their mean value.

Challenge

Not all samples come from the same temporal pattern. Some time series may have:

  • a trend,
  • a seasonal pattern,
  • mean-reverting dynamics,
  • or a distinctive local change at the end of the series.

Therefore, an approach that treats all samples as coming from a single pattern is not necessarily optimal.

2. Official Technical Parameters

This task uses the following parameters:

  • Length of the historical observations per sample: between 50 and 60 days
  • Future horizon: H=12H = 12 days
  • Target:

    y=112t=T+1T+12xty = \frac{1}{12}\sum_{t=T+1}^{T+12} x_t

    where TT is the actual observation length for that sample
  • Official evaluation metric: Mean Absolute Error (MAE)
Notes
  • The data in this competition can be regarded as synthetic data representing daily river discharge patterns.
  • Not all samples have the same observation length.
  • To make the data easier to distribute, the observations are stored in the column format x_1 to x_60, and the unused part is filled with NaN.

3. Data Files

This notebook assumes that the organisers share one ZIP file, for example data_bundle.zip, which contains:

  • train.csv
  • test.csv
  • sample_submission.csv
Format of train.csv

Columns:

  • id
  • obs_len
  • x_1, x_2, ..., x_60
  • target

Description:

  • obs_len = the actual observation length for that sample
  • x_1 to x_60 store the historical observations
  • if obs_len < 60, the columns after the last observation are filled with NaN
  • target is the mean discharge over the next 12 days
Format of test.csv

Columns:

  • id
  • obs_len
  • x_1, x_2, ..., x_60
Format of sample_submission.csv

Columns:

  • id
  • target
Important note

In this data, NaN in the observation columns does not mean randomly missing data; it means that the sample genuinely has a shorter historical observation.

# If running in Google Colab, run this cell once.
!pip -q install gdown scikit-learn pandas numpy matplotlib statsmodels

4. Download the Data from Google Drive to Colab

The easiest way is to share one ZIP file containing all the dataset files, and then download it with gdown.

Replace DATA_BUNDLE_FILE_ID below with the Google Drive file ID shared by the organisers.


import subprocess
from pathlib import Path

# The file is at https://drive.google.com/file/d/180yS7AFlg8YXv0edEoDOg_EZxxes05EK/view?usp=sharing
DATA_BUNDLE_FILE_ID = "180yS7AFlg8YXv0edEoDOg_EZxxes05EK"

DATA_DIR = Path("data")
ZIP_PATH = Path("data_bundle.zip")

if DATA_BUNDLE_FILE_ID.startswith("PASTE_"):
    raise ValueError(
        "Please replace DATA_BUNDLE_FILE_ID with your Google Drive file ID first."
    )

DATA_DIR.mkdir(exist_ok=True)

subprocess.run(
    ["gdown", "--id", DATA_BUNDLE_FILE_ID, "-O", str(ZIP_PATH)],
    check=True
)

subprocess.run(
    ["unzip", "-o", "-q", str(ZIP_PATH), "-d", str(DATA_DIR)],
    check=True
)

print("Contents of the data folder:")
for p in sorted(DATA_DIR.iterdir()):
    print("-", p.name)

5. Loading the Data


import pandas as pd
import numpy as np

train = pd.read_csv(DATA_DIR / "train.csv")
test = pd.read_csv(DATA_DIR / "test.csv")
sample_submission = pd.read_csv(DATA_DIR / "sample_submission.csv")

print("Train shape:", train.shape)
print("Test shape :", test.shape)
print("Sample submission shape:", sample_submission.shape)

display(train.head())

6. Initial Checks

def sort_feature_cols(cols):
    ts_cols = [c for c in cols if c.startswith("x_")]
    return sorted(ts_cols, key=lambda c: int(c.split("_")[1]))

FEATURE_COLS = sort_feature_cols(train.columns)

assert "id" in train.columns, "Column 'id' not found in train.csv"
assert "obs_len" in train.columns, "Column 'obs_len' not found in train.csv"
assert "target" in train.columns, "Column 'target' not found in train.csv"

assert "id" in test.columns, "Column 'id' not found in test.csv"
assert "obs_len" in test.columns, "Column 'obs_len' not found in test.csv"

assert list(sample_submission.columns) == ["id", "target"], \
    "The sample_submission columns must be ['id', 'target']"

print("Number of observation columns:", len(FEATURE_COLS))
print("Example observation columns:", FEATURE_COLS[:5], "...", FEATURE_COLS[-5:])

print()
print("Range of obs_len in train:", int(train["obs_len"].min()), "to", int(train["obs_len"].max()))
print("Range of obs_len in test :", int(test["obs_len"].min()), "to", int(test["obs_len"].max()))

print()
print("Total NaN in train (padding only):", int(train[FEATURE_COLS].isna().sum().sum()))
print("Total NaN in test  (padding only):", int(test[FEATURE_COLS].isna().sum().sum()))

7. Visualising a Few Samples

This cell is only meant to help you see the general shape of a few time series.

Because the observation length can differ between samples, we plot each sample only up to its own obs_len.

import matplotlib.pyplot as plt

plt.figure(figsize=(12, 5))

for i in range(min(5, len(train))):
    obs_len = int(train.loc[i, "obs_len"])
    values = train.loc[i, FEATURE_COLS].values[:obs_len]
    plt.plot(range(1, obs_len + 1), values, alpha=0.8, label=f"sample_{i} (len={obs_len})")

plt.xlabel("Day")
plt.ylabel("Discharge")
plt.title("A few example training time series")
plt.legend()
plt.show()

8. Simple Local Validation

Because the labels for the official test data are not available, we still create a validation split from the training data to get a rough picture of a method's performance.

Note that the baseline we will use below is local forecasting per time series, so conceptually it does not learn from the other training samples. The validation split here is mainly useful so that participants have a consistent local evaluation framework when they later try more complex methods.

from sklearn.model_selection import train_test_split

train_split, valid_split = train_test_split(
    train, test_size=0.2, random_state=42
)

print("Train split:", train_split.shape)
print("Valid split:", valid_split.shape)

9. Simple Baseline: Local ETS/Holt per Time Series

As an initial baseline, we use a local forecasting approach:

  1. for each time series, take only the valid historical observations,
  2. fit an Exponential Smoothing / Holt-Winters model to that series,
  3. forecast 12 steps ahead,
  4. take their mean as the target prediction.
from statsmodels.tsa.holtwinters import ExponentialSmoothing
from sklearn.metrics import mean_absolute_error
import warnings

warnings.filterwarnings("ignore")

def get_valid_series_from_row(row, feature_cols):
    obs_len = int(row["obs_len"])
    values = row[feature_cols].values[:obs_len].astype(float)
    return values

def safe_recent_mean(series, horizon=12):
    k = min(horizon, len(series))
    return float(np.mean(series[-k:]))

def local_ets_holt_forecast_mean(series, horizon=12, seasonal_periods=12):
    """
    Simple baseline:
    - try ETS with additive trend + additive seasonality
    - if that fails, fall back to Holt with additive trend
    - if that still fails, fall back to the recent mean
    """
    s = np.asarray(series, dtype=float)

    try:
        model = ExponentialSmoothing(
            s,
            trend="add",
            damped_trend=True,
            seasonal="add",
            seasonal_periods=seasonal_periods,
            initialization_method="estimated",
        )
        fitted = model.fit(optimized=True, use_brute=True)
        fcst = fitted.forecast(horizon)
        return float(np.mean(fcst))
    except Exception:
        pass

    try:
        model = ExponentialSmoothing(
            s,
            trend="add",
            damped_trend=True,
            seasonal=None,
            initialization_method="estimated",
        )
        fitted = model.fit(optimized=True, use_brute=True)
        fcst = fitted.forecast(horizon)
        return float(np.mean(fcst))
    except Exception:
        return safe_recent_mean(s, horizon=horizon)

valid_pred = []
for _, row in valid_split.iterrows():
    series = get_valid_series_from_row(row, FEATURE_COLS)
    pred = local_ets_holt_forecast_mean(series, horizon=12, seasonal_periods=12)
    valid_pred.append(pred)

valid_pred = np.array(valid_pred, dtype=float)
valid_mae = mean_absolute_error(valid_split["target"].values, valid_pred)

print(f"Validation MAE (local ETS/Holt baseline): {valid_mae:.6f}")

10. Creating a Submission with the Local ETS/Holt Baseline

test_pred = []

for _, row in test.iterrows():
    series = get_valid_series_from_row(row, FEATURE_COLS)
    pred = local_ets_holt_forecast_mean(series, horizon=12, seasonal_periods=12)
    test_pred.append(pred)

test_pred = np.array(test_pred, dtype=float)

submission_baseline = sample_submission.copy()
submission_baseline["target"] = test_pred

display(submission_baseline.head())
print(submission_baseline.shape)

11. Save the Submission File


SUBMISSION_PATH = "submission_baseline.csv"
submission_baseline.to_csv(SUBMISSION_PATH, index=False)

print(f"Submission file saved to: {SUBMISSION_PATH}")

12. Checking the Submission Format

The following cell checks whether the submission file has the correct format.


submission_check = pd.read_csv(SUBMISSION_PATH)

assert list(submission_check.columns) == ["id", "target"],     "The submission columns must be exactly: ['id', 'target']"

assert len(submission_check) == len(test),     "The number of submission rows must equal the number of rows in test.csv"

assert submission_check["id"].equals(test["id"]),     "The id column in the submission must be in the same order as the id column in test.csv"

assert np.isfinite(submission_check["target"]).all(),     "The target column in the submission must not contain NaN or inf"

print("Submission format is valid.")

13. Official Evaluation Method: Mean Absolute Error (MAE)

The official competition score uses the Mean Absolute Error (MAE):

MAE=1ni=1ny^iyi\operatorname{MAE} = \frac{1}{n}\sum_{i=1}^{n} |\hat y_i - y_i|

where:

  • nn = the number of samples in the test data,
  • y^i\hat y_i = the participant's prediction for the ii-th sample,
  • yiy_i = the true target.

Translated by SOTA. The Indonesian original is the official version and wins wherever the two differ. The starter notebook by Mushthofa holds the full statement and a local ETS/Holt baseline; the Kaggle pages add the evaluation, submission format and an alternative data link. If you organise this olympiad and would like the translation removed, email [email protected] and we will take it down.

At a glance

You get
train.csv (id, obs_len, x_1x_60, target), test.csv (without target) and sample_submission.csv, distributed as a ZIP on Google Drive.
You submit
submission.csv with columns id and target.
Scoring
Mean Absolute Error (lower is better).
Rules
  • Individual participation (maximum team size 1).
Format
Pelatnas 1 IOAI 2026 (first national training-and-selection camp), Simulation 2 on Kaggle; competition window 17 April 2026 15:35 UTC – 18 April 2026 07:30 UTC; individual; up to 50 submissions per day.

Details

Year
2026, Kaggle
Round
National Training Camp 1 – Simulation 2 · 2.1 task
Language
Indonesian; English translation by SOTA
License
Subject to Competition Rules (Kaggle), as stated by the source