Downsampling adalah prakomputasi jawaban kasar sebelum pertanyaannya datang. Topik ini menjelaskan konsep downsampling sebagai keputusan skema kelas satu, alasan penyimpanannya berbeda dari agregasi ad-hoc, dan trade-off antara ukuran, resolusi, serta keakuratan. Setelah membaca topik ini, pilihan downsampling tidak lagi menjadi optimasi yang ditunda, melainkan keputusan desain yang digunakan sejak tabel pertama diciptakan.
Prakomputasi jawaban kasar#
Pada beban ad-hoc, agregasi atas jendela waktu dihitung setiap kali query diterima. Bila seratus dashboard meminta “rerata suhu per lima belas menit untuk hari ini” pada saat yang bersamaan, basis data memproses seratus agregasi yang setara, masing-masing memindai 1.440 titik per sensor. Pola ini berlebihan, sebab jawaban yang diharapkan tidak berubah selama jendela waktu yang diproses belum menerima titik baru. Downsampling menyingkirkan kerja berlebihan tersebut dengan menghitung agregat sekali, lalu menyimpannya sebagai tabel materialisasi tersendiri.
Hasil downsampling disebut rollup, yaitu deret baru yang berisi ringkasan per interval lebih besar. Sebuah deret dengan 1.440 titik per hari pada resolusi satu menit dapat di-downsample menjadi 96 titik pada resolusi lima belas menit, 24 titik pada resolusi satu jam, atau satu titik pada resolusi satu hari. Setiap titik rollup menyimpan agregat yang relevan, yaitu min, max, mean, count, dan kadang sum. Pilihan agregat bergantung pada pertanyaan yang akan dijawab oleh rollup tersebut, sebab tidak semua agregat dapat dihitung ulang dari agregat lain. Rerata, misalnya, memerlukan sum dan count agar dapat dikombinasikan kembali pada resolusi yang lebih besar.
Kapan downsampling digunakan#
Downsampling digunakan pada dua situasi yang sering tumpang tindih. Pertama, ketika data mentah terlalu padat untuk ditampilkan pada layar dashboard. Sebuah grafik dengan lebar 1.080 piksel tidak mampu menampilkan 1.440 titik per hari tanpa tumpang tindih, sehingga visualisasi sebaiknya menggunakan rollup lima belas menit atau satu jam. Kedua, ketika retensi data mentah lebih pendek daripada riwayat yang ingin dipertahankan. Pada pemantauan, data mentah per detik sering hanya disimpan tujuh hari, sedangkan rollup per menit dapat disimpan satu tahun penuh dengan ukuran yang jauh lebih kecil.
Kedua situasi ini membuat downsampling menjadi keputusan desain, bukan optimasi yang ditambahkan belakangan. Skema sebaiknya menetapkan rollup bersamaan dengan tabel utama, sebab menambahkannya kemudian berarti memproses ulang seluruh data historis. Karena itu, downsampling sering disebut keputusan skema kelas satu (first-class schema decision), setara dengan pemilihan tipe data dan indeks. Mesin time-series menyediakan mekanisme otomatis untuk ini, misalnya continuous aggregates pada TimescaleDB atau downsampling task pada InfluxDB 3, sehingga rollup terbangun dan terbarui tanpa intervensi manual.
Trade-off ukuran, resolusi, dan keakuratan#
Downsampling tidak gratis. Setiap tingkat resolusi yang dipertahankan memakan ruang penyimpanan, dan setiap agregat yang dipilih menentukan keakuratan jawaban yang dapat diberikan. Rerata per lima belas menit menyembunyikan fluktuasi singkat yang mungkin penting bagi deteksi anomali, sedangkan minimum dan maksimum per lima belas menit masih menyimpan jejak puncak lokal. Pilihan agregat, dengan kata lain, adalah pilihan informasi mana yang dipertahankan dan mana yang dibuang.
Trade-off kedua adalah antara ukuran dan kelincahan query. Rollup satu jam dapat menjawab “rerata suhu jam 13:00” dengan cepat, tetapi tidak dapat menjawab “rerata suhu menit ke-13:25” tanpa kembali ke data mentah. Bila rollup satu jam digunakan sebagai sumber tunggal, pertanyaan dengan resolusi yang lebih halus menjadi tidak terjawab. Karena itu, banyak skema yang mempertahankan beberapa tingkat rollup, yaitu lima menit untuk dashboard, satu jam untuk laporan harian, dan satu hari untuk riwayat panjang. Setiap tingkat melayani satu kelas pertanyaan, dan total ukurannya tetap lebih kecil daripada data mentah.
Perbandingan ukuran secara konkret#
Skrip Python berikut membangkitkan 1.440 titik telemetri (satu hari penuh, interval satu menit), lalu menghitung tiga rollup, yaitu lima menit, lima belas menit, dan satu jam. Skrip mengukur ukuran CSV dari setiap versi dan menghitung rasio kompresi terhadap data mentah. Tujuannya menunjukkan bahwa downsampling memberikan penghematan yang tidak linier, yaitu pengurangan jumlah baris yang berbanding lurus dengan lebar bucket, tetapi penghematan ukuran yang biasanya lebih besar karena baris rollup lebih teratur dan mudah dipadatkan.
"""Demonstrasi konsep downsampling pada data time-series.
Skrip membangkitkan 1440 titik telemetri (satu hari, interval satu menit),
lalu menghitung tiga versi downsampling, yaitu 5 menit, 15 menit, dan 1 jam.
Setiap versi memuat agregat min, max, mean, dan count. Tujuannya
menunjukkan bahwa downsampling adalah prakomputasi jawaban kasar sebelum
pertanyaannya datang, dengan tradeoff ukuran penyimpanan dan resolusi.
"""
from __future__ import annotations
import io
from datetime import datetime, timezone
import numpy as np
import pandas as pd
AWAL = datetime(2026, 1, 1, 0, 0, tzinfo=timezone.utc)
JUMLAH_TITIK = 1_440
def bangun_telemetri() -> pd.DataFrame:
cap_waktu = pd.date_range(AWAL, periods=JUMLAH_TITIK, freq="1min", tz="UTC")
rng = np.random.default_rng(42)
suhu = 24.0 + np.sin(np.arange(JUMLAH_TITIK) / 60.0) + rng.normal(0, 0.2, JUMLAH_TITIK)
return pd.DataFrame({"ts": cap_waktu, "suhu_c": np.round(suhu, 2)}).set_index("ts")
def downsample(df: pd.DataFrame, frekuensi: str) -> pd.DataFrame:
return (
df.resample(frekuensi)["suhu_c"]
.agg(["min", "max", "mean", "count"])
.round(2)
)
def ukuran_kb(df: pd.DataFrame) -> float:
buffer = io.StringIO()
df.to_csv(buffer)
return len(buffer.getvalue()) / 1024.0
def main() -> None:
df = bangun_telemetri()
raw_kb = ukuran_kb(df)
print("data mentah:")
print(f" jumlah titik: {len(df)}")
print(f" ukuran CSV: {raw_kb:.2f} KB")
print()
tabel = []
for frek, label in [("5min", "5 menit"), ("15min", "15 menit"), ("1h", "1 jam")]:
agg = downsample(df, frek)
kb = ukuran_kb(agg)
tabel.append({
"resolusi": label,
"jumlah_baris": len(agg),
"ukuran_kb": round(kb, 2),
"rasio_kompresi": round(raw_kb / kb, 1),
})
ringkas = pd.DataFrame(tabel)
print("perbandingan ukuran setelah downsampling:")
print(ringkas.to_string(index=False))
print()
agg5 = downsample(df, "5min")
print("5 baris pertama downsampling 5 menit:")
print(agg5.head(5).to_string())
print()
agg1h = downsample(df, "1h")
print("5 baris pertama downsampling 1 jam:")
print(agg1h.head(5).to_string())
if __name__ == "__main__":
main()data mentah:
jumlah titik: 1440
ukuran CSV: 44.85 KB
perbandingan ukuran setelah downsampling:
resolusi jumlah_baris ukuran_kb rasio_kompresi
5 menit 288 12.86 3.5
15 menit 96 4.39 10.2
1 jam 24 1.11 40.2
5 baris pertama downsampling 5 menit:
min max mean count
ts
2026-01-01 00:00:00+00:00 23.68 24.24 23.99 5
2026-01-01 00:05:00+00:00 23.82 24.13 24.02 5
2026-01-01 00:10:00+00:00 24.21 24.44 24.33 5
2026-01-01 00:15:00+00:00 24.08 24.47 24.26 5
2026-01-01 00:20:00+00:00 24.21 24.60 24.35 5
5 baris pertama downsampling 1 jam:
min max mean count
ts
2026-01-01 00:00:00+00:00 23.68 25.11 24.47 60
2026-01-01 01:00:00+00:00 24.50 25.25 24.92 60
2026-01-01 02:00:00+00:00 23.98 25.32 24.57 60
2026-01-01 03:00:00+00:00 22.86 24.40 23.67 60
2026-01-01 04:00:00+00:00 22.50 23.74 23.06 60Keluaran di atas menunjukkan dua hal. Pertama, pengurangan jumlah baris berbanding lurus dengan lebar bucket, yaitu dari 1.440 menjadi 288 pada resolusi lima menit, 96 pada lima belas menit, dan 24 pada satu jam. Kedua, rasio kompresi ukuran tumbuh lebih cepat daripada rasio jumlah baris, yaitu 3,5 kali, 10,2 kali, dan 40,2 kali. Pertumbuhan yang tidak linier ini muncul karena baris rollup lebih teratur, memuat kolom agregat yang padat, dan tidak membawa cap waktu per menit. Pada skala produksi, penghematan ini menjadi semakin besar seiring bertambahnya rentang waktu yang disimpan.
Demo visualisasi raw vs downsampled#
Perbedaan resolusi paling mudah dilihat secara visual. Demo berikut membandingkan 60 titik raw (interval satu menit) dengan tiga pilihan downsampling, yaitu lima menit, satu jam, dan kembali ke raw. Pembaca dapat berpindah antar resolusi dengan tombol di bagian atas, lalu mengamati bagaimana garis biru berubah dari bucket ke bucket sementara garis merah tetap menampilkan data mentah sebagai pembanding.
Kode sumber demo
<!doctype html>
<html lang="id">
<head>
<meta charset="utf-8" />
<title>Perbandingan raw vs downsampled</title>
<style>
body { font-family: system-ui, -apple-system, sans-serif; margin: 16px; color: #1f2937; }
h2 { font-size: 16px; margin: 0 0 8px 0; font-weight: 600; }
.kontrol { display: flex; gap: 8px; margin-bottom: 8px; font-size: 13px; }
.kontrol button { padding: 4px 10px; font-size: 13px; cursor: pointer; border: 1px solid #d1d5db; background: #fff; border-radius: 4px; }
.kontrol button.aktif { background: #1e3a8a; color: #fff; border-color: #1e3a8a; }
.legend { display: flex; gap: 16px; margin-bottom: 8px; font-size: 12px; }
.legend span { display: inline-flex; align-items: center; gap: 6px; }
.swatch { width: 12px; height: 12px; border-radius: 2px; }
svg { width: 100%; height: 240px; background: #fafafa; border: 1px solid #e5e7eb; }
.grid { stroke: #e5e7eb; stroke-width: 1; }
.axis { font-size: 10px; fill: #6b7280; }
.line-raw { fill: none; stroke: #ef4444; stroke-width: 1; opacity: 0.5; }
.line-agg { fill: none; stroke: #1d4ed8; stroke-width: 2; }
.dot-agg { fill: #1d4ed8; }
.stat { margin-top: 8px; font-size: 12px; color: #374151; }
</style>
</head>
<body>
<h2>Telemetri 60 menit: raw vs downsampled</h2>
<div class="kontrol">
<button data-resolusi="1min">1 menit (raw)</button>
<button data-resolusi="5min">5 menit</button>
<button data-resolusi="1h">1 jam</button>
</div>
<div class="legend">
<span><span class="swatch" style="background:#ef4444"></span> Raw (1 menit)</span>
<span><span class="swatch" style="background:#1d4ed8"></span> Downsampled</span>
</div>
<svg id="chart" viewBox="0 0 480 240" preserveAspectRatio="none">
<line class="grid" x1="0" y1="60" x2="480" y2="60" />
<line class="grid" x1="0" y1="120" x2="480" y2="120" />
<line class="grid" x1="0" y1="180" x2="480" y2="180" />
<polyline id="line-raw" class="line-raw" />
<polyline id="line-agg" class="line-agg" />
<g id="dots-agg"></g>
<text class="axis" x="6" y="14">25.5</text>
<text class="axis" x="6" y="124">23.5</text>
<text class="axis" x="6" y="234">21.5</text>
<text class="axis" x="200" y="234">00:00</text>
<text class="axis" x="430" y="234">01:00</text>
</svg>
<div class="stat" id="statistik">resolusi: 1 menit, 60 titik</div>
<script>
// 60 titik raw dengan interval 1 menit (suhu sintetis 23-25).
const raw = [23.68, 24.13, 24.05, 24.02, 24.24, 23.82, 24.13, 24.04, 23.93, 24.21,
24.44, 24.36, 24.27, 24.18, 24.39, 24.47, 24.08, 24.21, 24.18, 24.27,
24.60, 24.51, 24.40, 24.33, 24.55, 24.62, 24.49, 24.50, 24.56, 24.60,
24.71, 24.65, 24.58, 24.50, 24.62, 24.68, 24.71, 24.65, 24.55, 24.51,
24.36, 24.27, 24.18, 24.09, 24.21, 24.13, 24.04, 23.95, 23.86, 23.78,
23.69, 23.61, 23.55, 23.50, 23.45, 23.41, 23.38, 23.36, 23.35, 23.36];
// Agregat 5 menit: 12 bucket, masing-masing mean dari 5 titik raw.
const agg5 = [];
for (let i = 0; i < raw.length; i += 5) {
const slice = raw.slice(i, i + 5);
const mean = slice.reduce((a, b) => a + b, 0) / slice.length;
agg5.push(mean);
}
// Agregat 1 jam: 1 bucket, mean dari seluruh 60 titik.
const agg1 = [raw.reduce((a, b) => a + b, 0) / raw.length];
const W = 480, H = 240, padX = 12, padY = 12;
const yMin = 21.5, yMax = 25.5;
function skalakanX(idx, total) {
const xStep = (W - 2 * padX) / Math.max(total - 1, 1);
return padX + idx * xStep;
}
function skalakanY(v) {
return H - padY - ((v - yMin) / (yMax - yMin)) * (H - 2 * padY);
}
function polyline(data, total) {
return data.map((v, i) => skalakanX(i, total).toFixed(1) + "," + skalakanY(v).toFixed(1)).join(" ");
}
const lineRaw = document.getElementById("line-raw");
const lineAgg = document.getElementById("line-agg");
const dotsAgg = document.getElementById("dots-agg");
const statistik = document.getElementById("statistik");
function perbarui(resolusi) {
lineRaw.setAttribute("points", polyline(raw, raw.length));
if (resolusi === "1min") {
lineAgg.setAttribute("points", "");
dotsAgg.innerHTML = "";
statistik.textContent = "resolusi: 1 menit (raw), " + raw.length + " titik";
} else if (resolusi === "5min") {
lineAgg.setAttribute("points", polyline(agg5, raw.length));
dotsAgg.innerHTML = agg5.map((v, i) => {
const x = skalakanX(i, raw.length);
const y = skalakanY(v);
return `<circle class="dot-agg" cx="${x.toFixed(1)}" cy="${y.toFixed(1)}" r="3"/>`;
}).join("");
statistik.textContent = "resolusi: 5 menit, " + agg5.length + " bucket (dari " + raw.length + " titik raw)";
} else {
const satuTitik = [agg1[0]];
lineAgg.setAttribute("points", polyline(satuTitik, raw.length));
dotsAgg.innerHTML = `<circle class="dot-agg" cx="${skalakanX(0, raw.length).toFixed(1)}" cy="${skalakanY(agg1[0]).toFixed(1)}" r="4"/>`;
statistik.textContent = "resolusi: 1 jam, 1 bucket (dari " + raw.length + " titik raw)";
}
}
document.querySelectorAll(".kontrol button").forEach(btn => {
btn.addEventListener("click", () => {
document.querySelectorAll(".kontrol button").forEach(b => b.classList.remove("aktif"));
btn.classList.add("aktif");
perbarui(btn.dataset.resolusi);
});
});
// inisialisasi: aktifkan tombol pertama
const tombolAwal = document.querySelector('.kontrol button[data-resolusi="1min"]');
tombolAwal.classList.add("aktif");
perbarui("1min");
</script>
</body>
</html>Pada resolusi satu menit, 60 titik membentuk garis yang relatif halus. Pada resolusi lima menit, garis biru menampilkan 12 titik yang masing-masing adalah rerata dari lima titik mentah, sehingga puncak dan lembah yang sempit memudar. Pada resolusi satu jam, hanya satu titik yang tersisa, yaitu rerata seluruh jam, dan informasi tentang puncak lokal hilang sepenuhnya. Inilah alasan visual mengapa pilihan resolusi adalah keputusan desain, bukan sekadar optimasi.
Kesalahan umum#
- Menggunakan rerata sebagai satu-satunya agregat pada rollup. Rerata menyembunyikan puncak dan lembah, sehingga deteksi anomali pada rollup menjadi mustahil.
mindanmaxsebaiknya disimpan bersama rerata. - Menyimpan rollup pada resolusi tunggal. Satu resolusi tidak dapat melayani semua kelas pertanyaan, sehingga beberapa tingkat rollup biasanya diperlukan.
- Menghitung rollup secara ad-hoc pada setiap query. Pendekatan ini menyebabkan setiap dashboard memproses ulang agregasi yang sama, sehingga keuntungan prakomputasi hilang.
Ringkasan#
- Downsampling adalah prakomputasi jawaban kasar sebelum pertanyaannya datang, sehingga beban ad-hoc berulang dapat dihilangkan.
- Rollup sebaiknya disimpan bersamaan dengan tabel utama, sebab menambahkannya kemudian berarti memproses ulang data historis.
- Trade-off utama adalah antara ukuran, resolusi, dan keakuratan, dengan pilihan agregat yang menentukan informasi yang dipertahankan.
- Beberapa tingkat rollup biasanya diperlukan, sebab satu resolusi tidak dapat melayani semua kelas pertanyaan.
Referensi#
- Continuous aggregates — Timescale — Penjelasan resmi mekanisme continuous aggregates TimescaleDB sebagai rollup termaterialisasi yang menyegarkan diri.
- Process data — InfluxDB 3 — Panduan resmi InfluxDB 3 Core untuk memproses dan menurunkan resolusi data melalui processing engine.
- Resample — pandas
— Dokumentasi resmi operasi
resamplepada pandas sebagai padanan klien untuk downsampling.