Strategi downsampling

Apa yang disimpan (min, max, avg, count) dan tradeoff retensi data mentah.

Daftar isi

Downsampling mengubah deret mentah menjadi rollup, dan pilihan agregat yang disimpan pada rollup menentukan pertanyaan apa yang dapat dijawab tanpa kembali ke data mentah. Topik ini menjelaskan empat agregat dasar yang biasa disimpan, yaitu min, max, avg, dan count, kelemahan avg sebagai agregat tunggal, peran min dan max sebagai penjaga puncak lokal, serta tradeoff retensi antara data mentah dan rollup pada beberapa resolusi. Setelah membaca topik ini, sebuah rollup dapat dirancang dengan agregat yang menjawab pertanyaan dashboard tanpa membunuh kemampuan deteksi anomali.

Pilihan agregat: apa yang disimpan#

Rollup menyimpan ringkasan per bucket, dan ringkasan itu terdiri atas agregat yang dipilih pada saat pembuatan rollup. Empat agregat dasar yang paling sering digunakan adalah min, max, avg, dan count. min dan max menangkap puncak dan lembah lokal, sehingga deteksi anomali pada rollup tetap mungkin. avg menangkap kecenderungan tengah, sehingga tren jangka panjang terlihat tanpa noise. count menangkap jumlah titik mentah per bucket, sehingga kelengkapan data dapat diperiksa tanpa kembali ke data mentah. Beberapa rollup tambahan memuat sum, terutama bila rerata pada resolusi yang lebih besar perlu dihitung ulang dari rollup yang lebih halus, sebab rerata pada resolusi besar memerlukan sum dan count agar dapat dikombinasikan kembali.

Pilihan agregat bergantung pada pertanyaan yang akan dijawab oleh rollup tersebut, sebab tidak semua agregat dapat dihitung ulang dari agregat lain. Rerata pada resolusi yang lebih besar, misalnya, tidak dapat dihitung dari rerata pada resolusi yang lebih halus tanpa count, sebab rerata tanpa bobot akan bias terhadap bucket dengan sedikit titik. Puncak pada resolusi yang lebih besar tidak dapat dihitung dari avg pada resolusi yang lebih halus, sebab avg menyembunyikan puncak lokal. Karena itu, rollup produksi biasanya menyimpan min, max, avg, dan count sekaligus, sehingga kelas pertanyaan yang dapat dijawab meliputi tren, anomali, dan kelengkapan.

avg-only menyembunyikan puncak#

Skema paling hemat adalah avg saja, sebab rollup hanya memuat satu kolom agregat. Hemat ini datang dengan biaya besar, yaitu puncak lokal yang singkat dapat terkubur di dalam bucket. Sebuah puncak suhu lima menit yang nilainya 30 derajat dapat terlihat sebagai 24 derajat pada bucket lima belas menit yang juga memuat sepuluh menit suhu 22 derajat. Akibatnya, alert berbasis ambang batas yang dipicu oleh puncak mentah tidak akan terpicu pada rollup avg-only, sehingga anomali yang berbahaya lolos tanpa terdeteksi. Skrip berikut membangkitkan 1.440 titik telemetri dengan dua puncak lokal singkat, lalu menghitung tiga skema agregat pada resolusi lima belas menit untuk memperlihatkan selisih puncak yang terlihat.

python strategi_downsampling.py
"""Demonstrasi strategi downsampling: agregat yang disimpan dan tradeoff-nya.

Skrip membangkitkan 1440 titik telemetri interval satu menit dengan dua
puncak lokal singkat di menit 200 dan 800. Skrip kemudian menghitung tiga
skema agregat pada resolusi 15 menit, yaitu hanya avg, avg+count, dan
min+max+avg+count. Tujuannya memperlihatkan bahwa skema avg-only
menyembunyikan puncak lokal, sedangkan skema lengkap dengan min dan max
tetap menangkap puncak. Skrip juga menghitung estimasi retensi satu tahun
untuk tiga resolusi, sehingga tradeoff antara ukuran dan kekayaan agregat
terlihat secara kasar.
"""
from __future__ import annotations

import io

import numpy as np
import pandas as pd

AWAL = "2026-11-01 00:00:00"
JUMLAH_TITIK = 1440  # satu hari, 1 titik/menit


def bangun_telemetri() -> pd.DataFrame:
    rng = np.random.default_rng(11)
    ts = pd.date_range(AWAL, periods=JUMLAH_TITIK, freq="1min", tz="UTC")
    dasar = 22.0 + np.sin(np.arange(JUMLAH_TITIK) / 60.0)
    puncak1 = 8.0 * np.exp(-((np.arange(JUMLAH_TITIK) - 200) ** 2) / 8.0)
    puncak2 = 6.0 * np.exp(-((np.arange(JUMLAH_TITIK) - 800) ** 2) / 8.0)
    suhu = dasar + puncak1 + puncak2 + rng.normal(0, 0.15, JUMLAH_TITIK)
    return pd.DataFrame({"ts": ts, "suhu_c": np.round(suhu, 2)}).set_index("ts")


def downsample(df: pd.DataFrame, freq: str, agregat: list[str]) -> pd.DataFrame:
    return df.resample(freq)["suhu_c"].agg(agregat).round(2)


def ukuran_kb(df: pd.DataFrame) -> float:
    buf = io.StringIO()
    df.to_csv(buf)
    return len(buf.getvalue()) / 1024.0


def main() -> None:
    df = bangun_telemetri()
    puncak_mentah = df["suhu_c"].max()

    print(f"data mentah: {len(df)} titik, puncak = {puncak_mentah}")
    print()

    skema = [
        ("hanya avg", ["mean"]),
        ("avg+count", ["mean", "count"]),
        ("min+max+avg+count", ["min", "max", "mean", "count"]),
    ]
    print("perbandingan skema agregat pada bucket 15 menit (96 baris):")
    baris = []
    for label, agg in skema:
        roll = downsample(df, "15min", agg)
        puncak_terlihat = roll["max"].max() if "max" in roll.columns else roll["mean"].max()
        baris.append({
            "skema": label,
            "kolom": len(roll.columns),
            "ukuran_kb": round(ukuran_kb(roll), 2),
            "puncak_terlihat": puncak_terlihat,
        })
    print(pd.DataFrame(baris).to_string(index=False))
    print()

    avg_only = downsample(df, "15min", ["mean"])
    penuh = downsample(df, "15min", ["min", "max", "mean", "count"])
    print("bucket 15 menit yang memuat puncak lokal (menit 200):")
    print(penuh.loc["2026-11-01 03:00:00":"2026-11-01 03:15:00"].to_string())
    print()
    print("puncak yang terlihat pada avg-only:", avg_only["mean"].max())
    print("puncak yang terlihat pada min+max+avg+count:", penuh["max"].max())
    print("selisih:", round(penuh["max"].max() - avg_only["mean"].max(), 2))
    print()

    harian = downsample(df, "1D", ["min", "max", "mean", "count"])
    print("perbandingan retensi 1 tahun (asumsi skema harian tetap):")
    raw_total = len(df) * 365
    menit15_total = len(downsample(df, "15min", ["min", "max", "mean", "count"])) * 365
    harian_total = len(harian) * 365
    print(f"  raw (1 menit):   {raw_total:>8} baris/tahun, {ukuran_kb(df) * 365 / 1024:.1f} MB/tahun (CSV)")
    print(f"  15 menit:        {menit15_total:>8} baris/tahun, {ukuran_kb(downsample(df, '15min', ['min','max','mean','count'])) * 365 / 1024:.1f} MB/tahun (CSV)")
    print(f"  harian:          {harian_total:>8} baris/tahun, {ukuran_kb(harian) * 365 / 1024:.2f} MB/tahun (CSV)")
    print()
    print("padanan SQL TimescaleDB (rollup 15 menit dengan min,max,avg,count):")
    print("  CREATE MATERIALIZED VIEW suhu_15min")
    print("  WITH (timescaledb.continuous = true) AS")
    print("  SELECT time_bucket('15 minutes', ts) AS bucket,")
    print("         sensor_id,")
    print("         min(suhu_c), max(suhu_c), avg(suhu_c), count(*)")
    print("  FROM suhu_sensor GROUP BY bucket, sensor_id WITH NO DATA;")


if __name__ == "__main__":
    main()
text Keluaran
data mentah: 1440 titik, puncak = 29.89

perbandingan skema agregat pada bucket 15 menit (96 baris):
            skema  kolom  ukuran_kb  puncak_terlihat
        hanya avg      1       2.99            24.70
        avg+count      2       3.28            24.70
min+max+avg+count      4       4.39            29.89

bucket 15 menit yang memuat puncak lokal (menit 200):
                             min    max   mean  count
ts                                                   
2026-11-01 03:00:00+00:00  21.81  22.22  21.99     15
2026-11-01 03:15:00+00:00  21.65  29.89  24.46     15

puncak yang terlihat pada avg-only: 24.7
puncak yang terlihat pada min+max+avg+count: 29.89
selisih: 5.19

perbandingan retensi 1 tahun (asumsi skema harian tetap):
  raw (1 menit):     525600 baris/tahun, 16.0 MB/tahun (CSV)
  15 menit:           35040 baris/tahun, 1.6 MB/tahun (CSV)
  harian:               365 baris/tahun, 0.02 MB/tahun (CSV)

padanan SQL TimescaleDB (rollup 15 menit dengan min,max,avg,count):
  CREATE MATERIALIZED VIEW suhu_15min
  WITH (timescaledb.continuous = true) AS
  SELECT time_bucket('15 minutes', ts) AS bucket,
         sensor_id,
         min(suhu_c), max(suhu_c), avg(suhu_c), count(*)
  FROM suhu_sensor GROUP BY bucket, sensor_id WITH NO DATA;

Keluaran di atas memperlihatkan dua hal yang penting. Pertama, puncak mentah bernilai 29.89, sedangkan puncak yang terlihat pada skema avg-only hanya 24.70, sehingga selisih 5.19 derajat terkubur di dalam bucket lima belas menit. Kedua, skema min+max+avg+count yang hanya menambah tiga kolom tetap menangkap puncak penuh, dengan ongkos ukuran sekitar 1.4 kali lipat dibandingkan avg-only. Pada skala satu tahun, rollup harian hanya menyimpan 365 baris dengan 0.02 MB CSV, sehingga retensi panjang tetap murah tanpa kehilangan kemampuan dashboard harian.

min dan max sebagai penjaga puncak#

min dan max adalah agregat yang paling tidak ternilai pada rollup yang melayani deteksi anomali. Kedua agregat ini menangkap puncak dan lembah lokal yang terjadi di dalam bucket, sehingga alert berbasis ambang batas tetap dapat dipicu dari rollup tanpa kembali ke data mentah. Pada contoh sebelumnya, bucket 03:15 memuat max sebesar 29.89, yang merupakan puncak sebenarnya, sedangkan avg pada bucket yang sama hanya 24.46, yang merupakan rerata lima belas titik. Selisih antara max dan avg pada bucket yang sama juga menjadi sinyal anomali yang kuat, sebab bucket dengan puncak lokal biasanya memiliki selisih max-avg yang besar. Bila hanya avg yang disimpan, sinyal ini hilang sepenuhnya.

min dan max juga memungkinkan penggabungan rollup pada resolusi yang lebih besar. Sebuah rollup harian dapat dihitung dari rollup lima belas menit dengan mengambil min(min) dan max(max) lintas bucket, sehingga rollup harian tidak perlu memindai data mentah. Sifat ini tidak dimiliki avg, sebab avg lintas bucket memerlukan count sebagai bobot. Karena itu, rollup dengan min dan max lebih komposisional daripada rollup dengan avg saja, yaitu dapat digunakan sebagai sumber untuk rollup yang lebih kasar tanpa kehilangan informasi penting.

count sebagai indikator kelengkapan#

count adalah agregat yang sering diabaikan, tetapi sangat berharga untuk menilai kelengkapan data. Sebuah bucket dengan count kurang dari yang diharapkan menandakan adanya celah, sebagaimana dijelaskan pada topik sebelumnya. Pada rollup lima belas menit, bucket yang ideal memuat 15 titik, sehingga count 10 menandakan lima titik hilang. Tanpa count, rollup avg-only tidak dapat membedakan avg 24.46 dari lima belas titik sehat dengan avg 24.46 dari lima titik yang kebetulan tinggi. Padanan InfluxDB 3 untuk kelengkapan serupa adalah menyimpan count sebagai field pada rollup yang dihitung oleh processing engine, sehingga pemeriksaan kelengkapan tetap mungkin tanpa kembali ke data mentah.

Tradeoff retensi: raw vs rollup#

Tradeoff utama downsampling adalah antara ukuran penyimpanan dan resolusi yang dapat dijawab. Data mentah pada resolusi satu menit selama satu tahun menyimpan 525.600 baris atau 16 MB CSV per sensor, sedangkan rollup lima belas menit menyimpan 35.040 baris atau 1.6 MB CSV, dan rollup harian menyimpan 365 baris atau 0.02 MB CSV. Pada skala seribu sensor, perbedaan ini menjadi 16 GB versus 20 MB, sehingga kebijakan retensi yang mempertahankan data mentah selama tujuh hari dan rollup harian selama setahun menjadi pilihan yang umum. Kebijakan ini mempertahankan kemampuan investigasi mendalam pada data mentah untuk insiden terbaru, sekaligus menjawab pertanyaan tren jangka panjang dari rollup harian yang murah.

Pemilihan resolusi rollup juga bergantung pada kelas pertanyaan yang akan dijawab. Rollup lima menit cocok untuk dashboard real-time, sebab delay antara titik mentah dan rollup hanya beberapa menit. Rollup lima belas menit cocok untuk laporan harian, sebab 96 bucket per hari cukup granular untuk melihat pola harian tanpa memuat terlalu banyak baris. Rollup harian cocok untuk riwayat panjang, sebab 365 baris per tahun adalah ukuran yang dapat dipindai sepenuhnya dalam memori. Banyak skema produksi mempertahankan ketiganya, dengan kebijakan retensi yang berbeda untuk masing-masing, sehingga setiap kelas pertanyaan dilayani oleh rollup yang sesuai.

Diagram perbandingan agregat#

Diagram berikut memperlihatkan satu bucket dengan lima titik mentah, dengan puncak lokal pada titik ketiga. Garis biru di sebelah kanan menunjukkan avg, yang berada jauh di bawah puncak mentah. Kotak hijau di sebelah kanan menunjukkan rentang min-max, yang menangkap puncak mentah sepenuhnya. Diagram ini mengilustrasikan mengapa rollup avg-only tidak memadai untuk deteksi anomali, sebab puncak lokal yang penting terkubur di bawah rerata.

satu bucket 5 titik, puncak pada titik ke-3titik mentah (puncak berwarna merah)avgpuncak hilangmin - maxpuncak tertangkappuncak mentah
Satu bucket dengan lima titik mentah: avg menyembunyikan puncak, sedangkan rentang min-max menangkapnya

Kesalahan umum#

  • Menggunakan avg sebagai satu-satunya agregat pada rollup yang melayani deteksi anomali. Puncak lokal terkubur di bawah rerata, sehingga alert berbasis ambang batas tidak terpicu.
  • Menghitung rerata lintas bucket tanpa count sebagai bobot. Rerata tanpa bobot bias terhadap bucket dengan sedikit titik, sehingga tren jangka panjang menjadi tidak akurat.
  • Menyimpan rollup pada satu resolusi saja. Satu resolusi tidak dapat melayani semua kelas pertanyaan, sehingga beberapa tingkat rollup biasanya diperlukan.

Ringkasan#

  • Empat agregat dasar yang biasa disimpan adalah min, max, avg, dan count. min dan max menangkap puncak lokal, avg menangkap tren, dan count menandai kelengkapan.
  • Skema avg-only menyembunyikan puncak lokal, sehingga alert berbasis ambang batas tidak terpicu. Skema min+max+avg+count hanya menambah beberapa kolom tetapi menangkap puncak penuh.
  • Tradeoff retensi utama adalah antara ukuran penyimpanan dan resolusi. Banyak skema mempertahankan data mentah tujuh hari dan rollup harian satu tahun, sehingga insiden terbaru dapat diinvestigasi mendalam sementara tren jangka panjang tetap terjangkau.

Referensi#

  • time_bucket() — Timescale — Penjelasan resmi fungsi time_bucket beserta agregat min, max, avg, dan count yang biasa digunakan pada rollup.
  • Continuous aggregates — Timescale — Panduan resmi pembuatan continuous aggregate sebagai wadah rollup dengan agregat min+max+avg+count.
  • Resample — pandas — Dokumentasi resmi operasi resample pada pandas sebagai padanan klien untuk downsampling dengan beberapa agregat.