Windowing mengelompokkan baris time-series berdasarkan jendela waktu dengan interval sembarang, sedangkan pengisian celah menentukan apakah hasil query jujur terhadap ketidakhadiran data. Topik ini menjelaskan pola time_bucket pada TimescaleDB dan padanannya pada pandas resample, cara asfreq membongkar celah menjadi nilai kosong eksplisit, strategi pengisian LOCF dan interpolasi linear, serta batas kejujuran yang dipikul kode klien setelah celah diisi. Setelah membaca topik ini, sebuah deret dengan titik hilang dapat diolah tanpa menyembunyikan fakta bahwa sebagian titik tidak pernah ditulis ke basis data.
Pola time_bucket dan alignment bucket#
Fungsi time_bucket adalah padanan TimescaleDB untuk date_trunc, tetapi menerima interval sembarang, bukan hanya satuan kalender seperti hour atau day. Interval boleh berbentuk '5 minutes', '13 minutes', atau '1 hour', sehingga pemilihan bucket tidak dibatasi oleh satuan kalender. Fungsi ini mengembalikan cap waktu awal bucket yang memuat nilai masukan, sehingga GROUP BY pada hasilnya menghasilkan ringkasan per bucket tanpa pemindaian tambahan. Alignment bucket diatur oleh argumen origin, yang bila tidak ditentukan diisi oleh awal era Unix pada 1 Januari 1970. Akibatnya, bucket lima menit selalu dimulai pada menit ke-0, 5, 10, dst, terlepas dari kapan titik pertama ditulis. Sifat ini membuat bucket konsisten lintas query, sebab rentang yang sama selalu menghasilkan batas bucket yang sama.
Padanan paling dekat pada pandas adalah operasi resample, yang juga menerima interval sembarang dan mengelompokkan baris ke bucket berdasarkan indeks waktu. Operasi resample('5min').mean() pada sebuah DataFrame dengan indeks DatetimeIndex menghasilkan agregat per bucket lima menit, dengan batas bucket yang mengikuti awal hari secara default. Perbedaan utama antara time_bucket dan resample terletak pada perilaku terhadap bucket kosong, yang menjadi pokok bahasan pada bagian berikutnya.
Celah pada deret dan asfreq#
Pada deret nyata, celah sering muncul karena sensor gagal, jaringan terputus, atau penulisan tertunda. Operasi resample tanpa pengisian hanya menghasilkan bucket yang memuat setidaknya satu baris, sehingga bucket kosong tidak terlihat dalam hasil. Akibatnya, laporan yang dihitung dari hasil tersebut menyiratkan kelengkapan yang tidak ada, sebab bucket tanpa titik tidak muncul sama sekali. Padanan TimescaleDB untuk masalah ini adalah time_bucket_gapfill, yang mengisi bucket kosong dengan nilai NULL agregat. Sedangkan pada pandas, fungsi asfreq menghidupkan kembali baris yang hilang pada frekuensi yang ditetapkan, sehingga celah menjadi NaN eksplisit.
Skrip berikut membangkitkan 12 titik telemetri dengan interval satu menit, lalu menghapus dua titik untuk membuat celah pada menit ke-3 dan ke-7. Operasi asfreq kemudian mengubah celah menjadi NaN, sehingga keberadaan celah dapat diperiksa tanpa ambiguitas. Skrip juga menghitung bucket lima menit dari data mentah yang bercelah, sehingga bucket dengan count kurang dari lima terlihat sebagai tanda keberadaan celah.
"""Demonstrasi windowing dan bucketing pada data time-series.
Skrip membangkitkan 12 titik telemetri interval satu menit, lalu menghapus
dua titik untuk membuat celah. Operasi asfreq menghidupkan kembali baris
yang hilang pada frekuensi yang ditetapkan, sehingga celah terlihat sebagai
NaN eksplisit. Skrip kemudian menerapkan dua strategi pengisian, yaitu
LOCF (last observation carried forward) dan interpolasi linear, untuk
memperlihatkan bagaimana masing-masing strategi mengganti NaN dengan
nilai hasil hitung. Padanan SQL TimescaleDB ditampilkan sebagai komentar.
"""
from __future__ import annotations
import numpy as np
import pandas as pd
AWAL = "2026-11-01 00:00:00"
BUCKET = "5min"
def bangun_telemetri_bercelah() -> pd.DataFrame:
rng = np.random.default_rng(7)
ts = pd.date_range(AWAL, periods=12, freq="1min", tz="UTC")
suhu = 22.0 + np.sin(np.arange(12) / 3.0) + rng.normal(0, 0.2, 12)
df = pd.DataFrame({"ts": ts, "suhu_c": np.round(suhu, 2)}).set_index("ts")
df = df.drop(index=[ts[3], ts[7]])
return df
def main() -> None:
df = bangun_telemetri_bercelah()
print("telemetri mentah (ada celah pada menit ke-3 dan ke-7):")
print(df.to_string())
print()
full = df.resample("1min").asfreq()
print("resample 1min + asfreq (celah menjadi NaN eksplisit):")
print(full.to_string())
print()
locf = full.ffill().round(2)
print("LOCF (last observation carried forward):")
print(locf.to_string())
print()
interpolated = full.interpolate().round(2)
print("interpolasi linear:")
print(interpolated.to_string())
print()
bucket_raw = df.resample(BUCKET)["suhu_c"].agg(["min", "max", "mean", "count"]).round(2)
print("bucket 5 menit dari data mentah (count != 5 menandakan celah):")
print(bucket_raw.to_string())
print()
print("padanan SQL TimescaleDB (time_bucket_gapfill + locf/interpolate):")
print(" SELECT")
print(" time_bucket_gapfill('5 minutes', ts,")
print(" '2026-11-01 00:00:00+00', '2026-11-01 00:15:00+00') AS bucket,")
print(" locf(avg(suhu_c)) AS suhu_locf,")
print(" interpolate(avg(suhu_c)) AS suhu_linear")
print(" FROM suhu_sensor")
print(" WHERE sensor_id = 'S01'")
print(" GROUP BY bucket;")
print()
print("Catatan: nilai NaN pada tabel di atas adalah penanda ketidakhadiran")
print("data, bukan nol. LOCF dan interpolasi menggantikan NaN dengan nilai")
print("yang dihitung, sehingga kejujuran sumber data berpindah dari basis")
print("data ke kode klien.")
if __name__ == "__main__":
main()telemetri mentah (ada celah pada menit ke-3 dan ke-7):
suhu_c
ts
2026-11-01 00:00:00+00:00 22.00
2026-11-01 00:01:00+00:00 22.39
2026-11-01 00:02:00+00:00 22.56
2026-11-01 00:04:00+00:00 22.88
2026-11-01 00:05:00+00:00 22.80
2026-11-01 00:06:00+00:00 22.92
2026-11-01 00:08:00+00:00 22.36
2026-11-01 00:09:00+00:00 22.02
2026-11-01 00:10:00+00:00 21.91
2026-11-01 00:11:00+00:00 21.57
resample 1min + asfreq (celah menjadi NaN eksplisit):
suhu_c
ts
2026-11-01 00:00:00+00:00 22.00
2026-11-01 00:01:00+00:00 22.39
2026-11-01 00:02:00+00:00 22.56
2026-11-01 00:03:00+00:00 NaN
2026-11-01 00:04:00+00:00 22.88
2026-11-01 00:05:00+00:00 22.80
2026-11-01 00:06:00+00:00 22.92
2026-11-01 00:07:00+00:00 NaN
2026-11-01 00:08:00+00:00 22.36
2026-11-01 00:09:00+00:00 22.02
2026-11-01 00:10:00+00:00 21.91
2026-11-01 00:11:00+00:00 21.57
LOCF (last observation carried forward):
suhu_c
ts
2026-11-01 00:00:00+00:00 22.00
2026-11-01 00:01:00+00:00 22.39
2026-11-01 00:02:00+00:00 22.56
2026-11-01 00:03:00+00:00 22.56
2026-11-01 00:04:00+00:00 22.88
2026-11-01 00:05:00+00:00 22.80
2026-11-01 00:06:00+00:00 22.92
2026-11-01 00:07:00+00:00 22.92
2026-11-01 00:08:00+00:00 22.36
2026-11-01 00:09:00+00:00 22.02
2026-11-01 00:10:00+00:00 21.91
2026-11-01 00:11:00+00:00 21.57
interpolasi linear:
suhu_c
ts
2026-11-01 00:00:00+00:00 22.00
2026-11-01 00:01:00+00:00 22.39
2026-11-01 00:02:00+00:00 22.56
2026-11-01 00:03:00+00:00 22.72
2026-11-01 00:04:00+00:00 22.88
2026-11-01 00:05:00+00:00 22.80
2026-11-01 00:06:00+00:00 22.92
2026-11-01 00:07:00+00:00 22.64
2026-11-01 00:08:00+00:00 22.36
2026-11-01 00:09:00+00:00 22.02
2026-11-01 00:10:00+00:00 21.91
2026-11-01 00:11:00+00:00 21.57
bucket 5 menit dari data mentah (count != 5 menandakan celah):
min max mean count
ts
2026-11-01 00:00:00+00:00 22.00 22.88 22.46 4
2026-11-01 00:05:00+00:00 22.02 22.92 22.52 4
2026-11-01 00:10:00+00:00 21.57 21.91 21.74 2
padanan SQL TimescaleDB (time_bucket_gapfill + locf/interpolate):
SELECT
time_bucket_gapfill('5 minutes', ts,
'2026-11-01 00:00:00+00', '2026-11-01 00:15:00+00') AS bucket,
locf(avg(suhu_c)) AS suhu_locf,
interpolate(avg(suhu_c)) AS suhu_linear
FROM suhu_sensor
WHERE sensor_id = 'S01'
GROUP BY bucket;
Catatan: nilai NaN pada tabel di atas adalah penanda ketidakhadiran
data, bukan nol. LOCF dan interpolasi menggantikan NaN dengan nilai
yang dihitung, sehingga kejujuran sumber data berpindah dari basis
data ke kode klien.Keluaran di atas memperlihatkan tiga hal. Pertama, asfreq menghidupkan kembali dua titik yang hilang sebagai NaN, sehingga kelengkapan deret dapat diperiksa dengan sekadar memeriksa keberadaan NaN. Kedua, LOCF mengisi celah dengan nilai terdekat sebelumnya, sehingga dua titik yang sebelumnya hilang kini memuat nilai 22.56 dan 22.92, bukan nilai terukur. Ketiga, interpolasi linear mengisi celah dengan rata-rata tetangga, sehingga nilai 22.72 dan 22.64 muncul sebagai hasil hitung. Pada agregasi bucket lima menit, bucket dengan count kurang dari lima langsung terlihat sebagai penanda celah, sebab bucket 00:00 dan 00:05 hanya memuat empat titik, sedangkan bucket 00:10 hanya memuat dua titik.
Strategi pengisian: LOCF dan interpolasi#
LOCF mengganti NaN dengan nilai non-NaN terdekat sebelumnya, sehingga strategi ini cocok untuk deret yang berubah perlahan, misalnya status konfigurasi atau nilai biner. Pada deret yang berubah cepat, LOCF menyesatkan, sebab nilai yang dibawa maju seolah-olah sistem diam pada rentang celah, padahal sistem mungkin berubah. Interpolasi linear mengganti NaN dengan nilai rata-rata tetangga, sehingga transisi tetap mulus. Strategi ini cocok untuk pengukuran kontinu seperti suhu atau tegangan, tetapi menciptakan titik sintetis yang tidak pernah benar-benar diukur. Padanan TimescaleDB untuk kedua strategi ini adalah hiperfungsi locf() dan interpolate(), yang digunakan di atas time_bucket_gapfill. Kedua hiperfungsi menerima agregat per bucket, lalu mengisi bucket kosong sesuai strategi yang diminta.
SELECT
time_bucket_gapfill('5 minutes'::interval, ts,
TIMESTAMPTZ '2026-11-01 00:00:00+00', TIMESTAMPTZ '2026-11-01 00:15:00+00') AS bucket,
locf(avg(suhu_c)) AS suhu_locf,
interpolate(avg(suhu_c)) AS suhu_linear
FROM suhu_sensor
WHERE sensor_id = 'S01'
AND ts >= '2026-11-01 00:00:00+00'
AND ts < '2026-11-01 00:15:00+00'
GROUP BY bucket
ORDER BY bucket;bucket|suhu_locf|suhu_linear
2026-11-01 00:00:00+00|24|24
2026-11-01 00:05:00+00|24.1|24.1
2026-11-01 00:10:00+00|24.2|24.2Pilihan antara LOCF dan interpolasi bergantung pada sifat data dan keputusan kejujuran yang dipikul kode klien. LOCF jujur tentang nilai, sebab nilai yang dibawa adalah nilai nyata yang pernah diukur, tetapi berbohong tentang waktu, sebab sistem sebenarnya tidak diam pada rentang celah. Interpolasi linear berbohong tentang kedua hal, sebab nilai hasil hitung tidak pernah diukur dan sistem tidak berubah secara linear pada rentang celah. Bila kedua strategi digunakan, kolom tambahan seperti is_filled sebaiknya disimpan, sehingga analisis lanjutan dapat membedakan titik terukur dari titik hasil hitung.
Kejujuran interpolasi#
Setiap strategi pengisian memindahkan beban kejujuran dari basis data ke kode klien. NULL pada bucket kosong adalah pernyataan jujur bahwa tidak ada data pada rentang itu, sedangkan LOCF dan interpolasi adalah pernyataan terselubung bahwa sistem diam atau berubah secara linear. Padanan untuk kode klien adalah menyimpan flag tambahan, misalnya kolom is_filled bernilai true pada titik hasil pengisian dan false pada titik terukur. Tanpa flag itu, pembaca grafik tidak dapat membedakan titik terukur dari titik hasil hitung, dan analisis lanjutan seperti deteksi anomali menjadi rapuh.
Diagram bucket dan celah#
Diagram berikut memperlihatkan 12 titik mentah pada interval satu menit, dengan dua celah pada menit ke-3 dan ke-7. Garis biru putus-putus menandai batas bucket lima menit, sehingga pembaca dapat melihat bagaimana titik mentah dikelompokkan menjadi tiga bucket. Bucket 00:00 dan 00:05 masing-masing memuat empat titik karena satu titik hilang, sedangkan bucket 00:10 hanya memuat dua titik karena data berhenti pada menit ke-11.
Demo interaktif pengisian celah#
Demo berikut menggambarkan bagaimana tiga strategi pengisian celah mengubah garis deret. Tombol di atas grafik mengganti strategi antara “Tanpa pengisian”, “LOCF”, dan “Interpolasi linear”. Pada mode tanpa pengisian, garis terputus pada celah, sehingga jeda terlihat sebagai lubang. Pada mode LOCF, garis menjadi mendatar di celah, sebab nilai terakhir dibawa maju. Pada mode interpolasi, garis menghubungkan kedua sisi celah secara lurus, sehingga titik sintetis terlihat seperti data nyata.
Kode sumber demo
<!DOCTYPE html>
<html lang="id">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Perbandingan strategi pengisian celah</title>
<style>
body { font-family: ui-sans-serif, system-ui, sans-serif; margin: 0; padding: 12px; color: #1f2937; }
.baris-tombol { display: flex; gap: 8px; margin-bottom: 8px; flex-wrap: wrap; }
button { padding: 6px 12px; border: 1px solid #2563eb; background: #fff; color: #1e3a8a; border-radius: 4px; cursor: pointer; font-size: 13px; }
button.aktif { background: #2563eb; color: #fff; }
.legenda { font-size: 12px; color: #4b5563; margin-top: 6px; }
.legenda span { display: inline-block; margin-right: 12px; }
.swatch { display: inline-block; width: 10px; height: 10px; vertical-align: middle; margin-right: 4px; }
svg { display: block; width: 100%; height: 280px; }
</style>
</head>
<body>
<div class="baris-tombol">
<button id="btn-tanpa" class="aktif" type="button">Tanpa pengisian</button>
<button id="btn-locf" type="button">LOCF</button>
<button id="btn-interpolasi" type="button">Interpolasi linear</button>
</div>
<svg id="grafik" viewBox="0 0 640 280" preserveAspectRatio="none">
<line x1="40" y1="240" x2="620" y2="240" stroke="#374151" stroke-width="1"/>
<line x1="40" y1="20" x2="40" y2="240" stroke="#374151" stroke-width="1"/>
<text x="20" y="25" text-anchor="end" font-size="10">25</text>
<text x="20" y="135" text-anchor="end" font-size="10">23</text>
<text x="20" y="245" text-anchor="end" font-size="10">21</text>
<g id="grid-h" stroke="#e5e7eb" stroke-width="0.5">
<line x1="40" y1="135" x2="620" y2="135"/>
<line x1="40" y1="25" x2="620" y2="25"/>
</g>
<polyline id="garis" fill="none" stroke="#2563eb" stroke-width="2"/>
<g id="titik"></g>
</svg>
<div class="legenda">
<span><span class="swatch" style="background:#2563eb"></span>garis deret</span>
<span><span class="swatch" style="background:#1f2937;border-radius:50%"></span>titik terukur</span>
<span><span class="swatch" style="background:#fff;border:1.5px solid #dc2626;border-radius:50%"></span>celah (titik hilang)</span>
</div>
<script>
// data: 12 titik mentah interval 1 menit, 2 celah (menit ke-3 dan ke-7)
const data = [
{t: 0, y: 22.00, ada: true},
{t: 1, y: 22.39, ada: true},
{t: 2, y: 22.56, ada: true},
{t: 3, y: null, ada: false},
{t: 4, y: 22.88, ada: true},
{t: 5, y: 22.80, ada: true},
{t: 6, y: 22.92, ada: true},
{t: 7, y: null, ada: false},
{t: 8, y: 22.36, ada: true},
{t: 9, y: 22.02, ada: true},
{t: 10, y: 21.91, ada: true},
{t: 11, y: 21.57, ada: true},
];
const lebar = 640, tinggi = 280;
const marginKiri = 40, marginBawah = 40, marginAtas = 20;
const plotW = lebar - marginKiri - 20;
const plotH = tinggi - marginBawah - marginAtas;
const xMin = 0, xMax = 11, yMin = 21, yMax = 25;
function xScale(t) { return marginKiri + (t - xMin) / (xMax - xMin) * plotW; }
function yScale(y) { return marginAtas + (yMax - y) / (yMax - yMin) * plotH; }
function isi(mode) {
const hasil = data.map(d => ({...d}));
if (mode === "locf") {
let terakhir = null;
for (const d of hasil) {
if (d.ada) terakhir = d.y;
else d.y = terakhir;
}
} else if (mode === "interpolasi") {
for (let i = 0; i < hasil.length; i++) {
if (!hasil[i].ada) {
let kiri = i - 1;
while (kiri >= 0 && !hasil[kiri].ada) kiri--;
let kanan = i + 1;
while (kanan < hasil.length && !hasil[kanan].ada) kanan++;
if (kiri >= 0 && kanan < hasil.length) {
const yKiri = hasil[kiri].y;
const yKanan = hasil[kanan].y;
const tKiri = hasil[kiri].t;
const tKanan = hasil[kanan].t;
hasil[i].y = yKiri + (yKanan - yKiri) * (hasil[i].t - tKiri) / (tKanan - tKiri);
}
}
}
}
// bangun polyline hanya dari titik yang ada datanya
const titikAda = hasil.filter(d => d.y !== null);
const poin = titikAda.map(d => `${xScale(d.t)},${yScale(d.y)}`).join(" ");
document.getElementById("garis").setAttribute("points", poin);
const gTitik = document.getElementById("titik");
gTitik.innerHTML = "";
hasil.forEach(d => {
const cx = xScale(d.t);
const c = document.createElementNS("http://www.w3.org/2000/svg", "circle");
c.setAttribute("cx", cx);
c.setAttribute("cy", yScale(d.y));
c.setAttribute("r", "4");
if (d.ada) {
c.setAttribute("fill", "#1f2937");
} else {
c.setAttribute("fill", "#fff");
c.setAttribute("stroke", "#dc2626");
c.setAttribute("stroke-width", "1.5");
}
gTitik.appendChild(c);
});
}
function aktifkan(id) {
document.querySelectorAll("button").forEach(b => b.classList.remove("aktif"));
document.getElementById(id).classList.add("aktif");
}
document.getElementById("btn-tanpa").addEventListener("click", () => { aktifkan("btn-tanpa"); isi("tanpa"); });
document.getElementById("btn-locf").addEventListener("click", () => { aktifkan("btn-locf"); isi("locf"); });
document.getElementById("btn-interpolasi").addEventListener("click", () => { aktifkan("btn-interpolasi"); isi("interpolasi"); });
isi("tanpa");
</script>
</body>
</html>Demo di atas mengilustrasikan mengapa interpolasi linear paling berbahaya dari sudut kejujuran. Garis hasil interpolasi terlihat mulus, sehingga pemeriksa grafik cenderung tidak menyadari bahwa dua titik di dalamnya tidak pernah diukur. Mode tanpa pengisian paling jujur, tetapi kurang ramah bagi pembaca yang mengharapkan garis utuh. Mode LOCF menempati posisi tengah, yaitu jujur tentang nilai tetapi berbohong tentang waktu, sebab sistem sebenarnya tidak diam pada rentang celah.
Kesalahan umum#
- Menggunakan
resample('5min').mean()tanpaasfrequntuk mengecek celah. Bucket kosong tidak muncul dalam hasil, sehingga laporan menyiratkan kelengkapan yang tidak ada. - Menganggap
interpolate()menghasilkan nilai terukur. Titik hasil interpolasi adalah hasil hitung, bukan pengukuran, sehingga analisis lanjutan perlu mengabaikan atau menandainya. - Mengabaikan argumen
originpadatime_bucket. Tanpaoriginyang ditentukan, bucket berawal pada era Unix, sehingga bucket lima menit dimulai pada menit ke-0, 5, 10, dst. Bila rentang data dimulai pada menit ke-3, bucket pertama hanya memuat dua titik, dan ketidakseimbangan ini sering disalahartikan sebagai anomali data.
Ringkasan#
time_bucketdanresamplemengelompokkan baris ke bucket waktu, tetapi hanya variangapfillatauasfreqyang menghidupkan kembali bucket kosong sebagaiNaN.- LOCF mengisi celah dengan nilai terakhir, sedangkan interpolasi linear mengisi dengan rata-rata tetangga. Keduanya mengganti
NULLatauNaNdengan nilai hasil hitung. - Kejujuran interpolasi bergantung pada kode klien. Tanpa flag tambahan seperti
is_filled, pembaca grafik tidak dapat membedakan titik terukur dari titik hasil hitung.
Referensi#
- time_bucket() — Timescale
— Penjelasan resmi fungsi
time_bucket,time_bucket_gapfill, dan hiperfungsilocf/interpolateuntuk pengisian celah. - Resample — pandas
— Dokumentasi resmi operasi
resample,asfreq,ffill, daninterpolatepada pandas sebagai padanan klien untuk windowing dan pengisian celah. - Query data with SQL — InfluxDB 3
— Panduan resmi query SQL pada InfluxDB 3 Core, termasuk pola
date_truncyang berperilaku serupa dengantime_bucketuntuk pengelompokan waktu.