TL;DR
Cara ubah Jupyter Notebook jadi skrip terjadwal ada empat langkah: ekspor notebook jadi file .py, rapikan isinya jadi fungsi dengan satu entry point, pindahin konfigurasi dan rahasia ke file .env, lalu daftarkan ke cron di Linux atau Task Scheduler di Windows. Tambahin logging supaya kamu tau skripnya gagal tanpa harus buka terminal.
Cara ubah Jupyter Notebook jadi skrip terjadwal itu empat langkah: ekspor jadi file .py, rapikan isinya jadi fungsi, pindahin konfigurasi ke file terpisah, lalu daftarkan ke penjadwal sistem.
Notebook itu tempat mikir. Kamu jalanin sel, lihat hasilnya, ganti angka, jalanin lagi.
Masalahnya muncul waktu analisis itu jadi kerjaan rutin. Tiap Senin pagi kamu buka laptop, buka notebook, klik Run All, tungguin, lalu kirim hasilnya ke grup WhatsApp.
Aku ngelakuin itu selama 7 bulan sebelum sadar total waktunya 21 jam setahun cuma buat nungguin dan copy-paste. Skrip yang gantiin kerjaan itu jalannya 11 detik dan aku gak perlu buka laptop.
Karena notebook nyimpen urutan eksekusi, bukan urutan kode. Sel nomor 12 bisa aja dijalanin sebelum sel nomor 3, dan hasilnya tetap kelihatan bener di layar. Begitu file itu dijalanin dari atas ke bawah oleh mesin, error muncul di tempat yang gak kamu duga.
Ada empat sifat notebook yang jadi masalah pas dijadwalin:
Notebook tetap berguna. Pakai buat eksplorasi awal dan buat presentasi hasil. Buat yang jalan tiap hari, pindahin ke file .py.
Mulai dari ekspor otomatis, lalu rapikan manual. Perintah nbconvert bawaan Jupyter ngubah notebook jadi file .py dalam sekali jalan, tapi hasilnya masih berupa kode berurutan tanpa struktur. Bagian rapikannya yang bikin skrip kamu tahan dijadwalin.
jupyter nbconvert --to script analisis-penjualan.ipynb
Hasilnya analisis-penjualan.py yang isinya semua sel digabung, lengkap dengan komentar nomor sel. Jangan langsung dijadwalin. Ada lima hal yang harus kamu benerin dulu.
df.head(), print(df.shape), dan plot yang cuma buat lihat-lihat.if __name__ == "__main__": di paling bawah.Pola yang aku pakai selalu sama: konfigurasi di atas, fungsi-fungsi kecil di tengah, satu fungsi main() yang ngatur urutan, entry point di bawah. Struktur ini bikin kamu bisa tes satu bagian tanpa jalanin semuanya.
"""Rekap penjualan harian Toko Berkah."""
import logging
import os
from datetime import date, timedelta
from pathlib import Path
import pandas as pd
FOLDER_DATA = Path(os.environ.get("FOLDER_DATA", "./data"))
FOLDER_OUTPUT = Path(os.environ.get("FOLDER_OUTPUT", "./output"))
AMBANG_MINIMAL_BARIS = 50
def ambil_data(tanggal: date) -> pd.DataFrame:
berkas = FOLDER_DATA / f"penjualan-{tanggal:%Y-%m-%d}.csv"
if not berkas.exists():
raise FileNotFoundError(f"File {berkas} gak ketemu")
return pd.read_csv(berkas, parse_dates=["tanggal"])
def bersihin(df: pd.DataFrame) -> pd.DataFrame:
df = df.dropna(subset=["cabang", "nilai"])
df = df[df["nilai"] > 0]
return df
def ringkas(df: pd.DataFrame) -> pd.DataFrame:
hasil = (
df.groupby("cabang", as_index=False)
.agg(transaksi=("nilai", "size"), omzet=("nilai", "sum"))
.sort_values("omzet", ascending=False)
)
return hasil
def simpan(df: pd.DataFrame, tanggal: date) -> Path:
FOLDER_OUTPUT.mkdir(parents=True, exist_ok=True)
tujuan = FOLDER_OUTPUT / f"rekap-{tanggal:%Y-%m-%d}.csv"
df.to_csv(tujuan, index=False)
return tujuan
def main() -> None:
tanggal = date.today() - timedelta(days=1)
logging.info("Mulai rekap untuk %s", tanggal)
df = ambil_data(tanggal)
if len(df) < AMBANG_MINIMAL_BARIS:
raise ValueError(f"Cuma {len(df)} baris, di bawah ambang wajar")
hasil = simpan(ringkas(bersihin(df)), tanggal)
logging.info("Selesai. Output di %s", hasil)
if __name__ == "__main__":
main()
Perhatiin pengecekan AMBANG_MINIMAL_BARIS. Ini penjaga sederhana yang bikin skrip berhenti kalau data sumbernya cuma sebagian. Tanpa itu, skrip bakal tetap sukses dan kirim laporan omzet yang salah.
Cek kewarasan kayak gini bagian dari jaga data quality. Lebih baik gagal berisik daripada berhasil dengan angka ngawur.
Simpan di file .env dan baca lewat os.environ. Password database, token API, dan path folder beda antara laptop kamu dan server. Kalau nilainya ketik langsung di kode, skripmu cuma jalan di satu tempat dan rahasianya ikut ke-push ke git.
# file .env
DB_HOST=10.20.30.40
DB_USER=analis_readonly
DB_PASSWORD=rahasia-banget
FOLDER_DATA=/srv/data/penjualan
FOLDER_OUTPUT=/srv/output/rekap
from dotenv import load_dotenv
import os
load_dotenv()
DB_HOST = os.environ["DB_HOST"]
DB_PASSWORD = os.environ["DB_PASSWORD"]
Install dulu pakai pip install python-dotenv. Lalu tambahin .env ke file .gitignore kamu, ini langkah yang paling sering kelupaan.
Pakai os.environ["NAMA"] dengan kurung siku buat nilai yang wajib ada. Kalau variabelnya kosong, skrip langsung gagal di awal, bukan setengah jalan.
Ganti semua print() jadi logging. Bedanya, logging otomatis nyatet waktu kejadian, tingkat keparahan, dan bisa nulis ke file sekaligus ke layar. Waktu skrip jalan jam 6 pagi tanpa ada yang nonton, file log itu satu-satunya jejak.
import logging
from logging.handlers import RotatingFileHandler
def siapin_log(path_log: str = "rekap.log") -> None:
berkas = RotatingFileHandler(path_log, maxBytes=1_000_000, backupCount=5)
logging.basicConfig(
level=logging.INFO,
format="%(asctime)s | %(levelname)s | %(message)s",
handlers=[berkas, logging.StreamHandler()],
)
RotatingFileHandler bikin file log dipotong tiap 1 MB dan nyimpen 5 file terakhir. Tanpa itu, log kamu bisa jadi 4 GB dalam setahun.
Bungkus main() supaya error kecatat lengkap dan exit code-nya bener:
import sys
if __name__ == "__main__":
siapin_log()
try:
main()
except Exception:
logging.exception("Rekap gagal")
sys.exit(1)
sys.exit(1) itu yang bikin cron dan tool monitoring tau skripnya gagal. Tanpa baris ini, skrip yang error tetap dianggap sukses.
Penjelasan lengkap tiap level dan handler ada di panduan logging resmi Python.
Pilihannya tergantung tempat skripmu jalan. Di Linux dan macOS pakai cron, di Windows pakai Task Scheduler, di cloud pakai GitHub Actions atau layanan sejenis. Tiga-tiganya butuh satu hal yang sama: path lengkap ke interpreter Python di dalam virtual environment kamu.
| Cara | Cocok buat | Kelebihan | Yang perlu diperhatiin |
|---|---|---|---|
| cron | Server Linux, Mac | Bawaan sistem, ringan | Environment minim, path harus lengkap |
| Task Scheduler | Laptop atau server Windows | Ada antarmuka grafis | Laptop harus nyala |
| GitHub Actions | Skrip yang cuma butuh internet | Gak perlu server sendiri | Jadwalnya pakai zona waktu UTC |
| Airflow | Banyak skrip yang saling nunggu | Retry dan pantauan lengkap | Berat buat 1-2 skrip doang |
Buka daftar jadwal pakai crontab -e, lalu tambahin satu baris:
0 6 * * * cd /srv/rekap && /srv/rekap/venv/bin/python /srv/rekap/rekap_harian.py >> /srv/rekap/cron.log 2>&1
Lima angka di depan artinya menit, jam, tanggal, bulan, hari. Contoh di atas jalan tiap hari jam 6 pagi.
Bagian cd /srv/rekap penting kalau skripmu baca file pakai path relatif. Bagian 2>&1 bikin pesan error ikut kesimpan ke log, bukan hilang.
Lewat command prompt, satu baris ini bikin tugas harian jam 6 pagi:
schtasks /create /tn "Rekap Harian" /tr "C:\rekap\venv\Scripts\python.exe C:\rekap\rekap_harian.py" /sc daily /st 06:00
Simpan sebagai .github/workflows/rekap.yml:
name: Rekap Harian
on:
schedule:
- cron: "0 23 * * *" # 23:00 UTC = 06:00 WIB
workflow_dispatch:
jobs:
rekap:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: "3.12"
- run: pip install -r requirements.txt
- run: python rekap_harian.py
env:
DB_PASSWORD: ${{ secrets.DB_PASSWORD }}
Jadwal di GitHub Actions selalu dihitung pakai UTC. WIB itu UTC plus 7, jadi jam 6 pagi WIB ditulis sebagai 23:00 hari sebelumnya. Ini jebakan yang bikin banyak orang bingung kenapa skripnya jalan sore.
Toko Berkah punya 4 cabang. Tiap pagi, supervisor butuh rekap omzet kemarin sebelum briefing jam 7.
Sebelumnya, satu staf datang jam 6 pagi buat buka notebook, jalanin semua sel, screenshot tabelnya, lalu kirim ke grup. Rata-rata 38 menit termasuk nunggu laptop nyala.
Setelah dijadwalin, skripnya jalan jam 5.30 dan hasilnya masuk grup jam 5.31. Waktu eksekusinya 11 detik buat 4 cabang dengan rata-rata 640 transaksi per hari.
Efek samping yang gak diduga: karena skripnya berhenti waktu data kurang dari 50 baris, mereka nemu masalah sinkronisasi kasir cabang Godean yang selama 3 bulan diam-diam kehilangan transaksi sore. Rata-rata 22 transaksi per hari gak kecatat.
Masalah itu gak pernah kelihatan waktu rekapnya manual, soalnya orang cuma lihat angka totalnya dan ngerasa wajar.
Cron gak jalan dari folder skripmu. Tulis Path(__file__).parent buat dapetin lokasi file, atau pindah direktori dulu di perintah cron-nya.
Perintah python di cron nunjuk ke Python sistem yang gak punya pandas. Tulis path lengkap ke venv/bin/python.
Skrip yang gagal diam-diam lebih berbahaya dari skrip yang gak pernah dibuat. Minimal kirim satu pesan ke Slack atau email waktu terjadi error.
Kalau skrip jalan dua kali, versi lamanya hilang. Sisipin tanggal di nama file, kayak rekap-2025-09-28.csv.
Kalau proses ETL di hulu selesai jam 5.15, jangan jadwalin skripmu jam 5.00. Kasih jarak, atau bikin skripmu nunggu sampai file sumbernya muncul.
Bisa pakai papermill atau nbconvert dengan flag execute, dan itu masuk akal kalau outputnya memang notebook berisi grafik buat dibagikan. Tapi buat kerjaan rutin, file .py lebih gampang di-debug, lebih ringan dijalanin, dan riwayat perubahannya kebaca jelas di git. Notebook nyimpen output sekalian, jadi diff-nya berantakan.
Hampir selalu karena path. Cron jalan dengan environment yang minim, jadi perintah python biasa belum tentu nunjuk ke virtual environment kamu. Tulis path lengkap ke interpreter di dalam venv dan path lengkap ke file skripnya. Tambahin juga baris pindah direktori kerja kalau skripmu baca file dengan path relatif.
Jangan pernah tulis langsung di kode. Simpan di file .env yang gak ikut masuk git, lalu baca pakai os.environ. Di server produksi, isi variabelnya lewat environment variable sistem atau secret manager. Kalau skripnya jalan di GitHub Actions, taruh di menu Secrets repositori kamu.
Ikutin ritme keputusannya, bukan ritme datanya. Kalau tim baca laporan tiap pagi sebelum briefing, jadwalin jam 6 pagi sekali sehari. Jadwal tiap jam cuma masuk akal kalau ada orang yang beneran ambil tindakan tiap jam. Frekuensi berlebihan cuma nambah beban server dan bikin notifikasi gagal jadi diabaikan.
Pasang logging ke file dan kirim notifikasi saat terjadi error. Bungkus proses utama pakai try except, catat pesan lengkapnya pakai logging.exception, lalu kirim ke email atau webhook Slack. Kasih juga notifikasi sukses ringkas biar kamu bisa bedain antara skrip berhasil dan skrip yang gak pernah jalan sama sekali.
Empat hal yang bikin skrip kamu tahan jalan tanpa dipantengin: struktur fungsi dengan satu entry point, konfigurasi di file .env, logging ke file, dan pengecekan kewarasan data sebelum hasilnya dipakai.
Mulai dari satu notebook yang paling sering kamu jalanin ulang. Ekspor, rapikan, jadwalin. Butuh sekitar dua jam buat yang pertama, dan 20 menit buat yang kedua.
Kalau sumber datamu masih berupa file PDF, gabungin dulu sama teknik di artikel ekstrak tabel PDF dengan Python. Dan buat bikin tahap eksplorasi di notebook lebih cepat, ada 28 trik Jupyter Notebook yang layak dicoba.
Skrip pertama kamu jadwalin buat kerjaan yang mana? Ceritain di komentar.
Mau praktek langsung? Mulai latihan SQL gratis
Latihan interaktif, langsung di browser.
Kolom datetime nyimpen tanggal, jam, hari, semuanya nempel jadi satu. Accessor .dt di pandas ngeluarin tiap bagian jadi kolom sendiri buat dianalisa.
Nama toko ketik campur huruf besar-kecil, spasi nyasar di ujung, atau kode produk nempel jadi satu. Accessor .str di pandas ngerapiin semua itu tanpa loop.
Model machine learning cuma ngerti angka, bukan teks kayak merah atau biru. get_dummies ngubah kolom kategori jadi kolom 0/1 dalam satu baris kode.