Topik ini mengerjakan tahap implementasi proyek analisis kisi suhu ruangan, yaitu membangun seluruh perhitungannya dalam bentuk tervektorisasi tahap demi tahap. Tahapan yang dibangun meliputi sintesis data, normalisasi per sensor, ringkasan per jam, dan jarak kesamaan antar sensor, dan setiap perhitungan inti disertai versi perulangan Python sebagai pembanding kebenaran. Kesamaan kedua versi diverifikasi dengan np.allclose pada data yang sama, sebagaimana ditetapkan definisi selesai. Bagian akhir menyatukan semuanya menjadi satu berkas proyek yang dapat dijalankan dari terminal.
Sintesis data kisi suhu#
Data proyek dibangkitkan dari tiga komponen yang masing-masing punya peran. Komponen pertama adalah gelombang harian berbentuk sinus yang menaikkan suhu dari dini hari sampai tengah hari, komponen kedua adalah offset kalibrasi yang berbeda pada setiap sensor, dan komponen ketiga adalah derau acak berdistribusi normal. Ketiganya dijumlahkan lalu dibulatkan satu desimal, sehingga keluarannya mudah dibaca. Kode berikut membangun data (24, 8) dari seed tetap 42.
import numpy as np
JAM, SENSOR = 24, 8
rng = np.random.default_rng(42)
offset = rng.uniform(-1.5, 1.5, size=SENSOR)
derau = rng.normal(0, 0.8, size=(JAM, SENSOR))
jam = np.arange(JAM)[:, np.newaxis]
suhu = np.round(22.0 + 3.0 * np.sin((jam - 6) * np.pi / 12) + offset + derau, 1)
print("bentuk data:", suhu.shape)
print("offset kalibrasi per sensor:")
print(np.round(offset, 2))
print("suhu pada jam ke-12 (tengah hari):")
print(suhu[12])
print("rata-rata seluruh kisi:", round(float(suhu.mean()), 2))bentuk data: (24, 8)
offset kalibrasi per sensor:
[ 0.82 -0.18 1.08 0.59 -1.22 1.43 0.78 0.86]
suhu pada jam ke-12 (tengah hari):
[25.1 24.7 25.3 25.3 24.5 25. 26.1 26. ]
rata-rata seluruh kisi: 22.5Bentuk data sesuai peta rencana, dan offset kalibrasinya terlihat berbeda jauh antar sensor, misalnya -1.22 pada sensor 4 berbanding 1.43 pada sensor 5. Perbedaan inilah alasan normalisasi dilakukan per sensor, karena tanpa normalisasi jarak antar sensor lebih banyak mengukur selisih kalibrasi daripada pola suhunya. Suhu tengah hari pada jam 12 berkisar 24.5 sampai 26.1 derajat Celsius, sesuai basis gelombang harian yang puncaknya 25.0.
Normalisasi per sensor dengan keepdims#
Normalisasi per sensor mengurangi setiap kolom dengan rata-rata kolomnya, lalu membaginya dengan simpangan baku kolom yang sama, sehingga setiap sensor memiliki profil dengan skala yang sebanding. Hasilnya disebut z-skor (z-score), yaitu selisih suatu nilai dari rata-rata kolomnya yang dinyatakan dalam satuan simpangan baku. Selisih itu bernilai negatif apabila suhu berada di bawah rata-rata sensornya dan positif apabila berada di atasnya. Notasi menyatakan suhu pada jam ke-j untuk sensor ke-s, sedangkan dan menyatakan rata-rata dan simpangan baku sensor ke-s.
Agregasi dilakukan pada axis=0 karena sensor tersusun sebagai kolom, dan keepdims=True menjaga hasilnya tetap berbentuk (1, 8) sehingga siap disiarkan melawan data (24, 8).
import numpy as np
JAM, SENSOR = 24, 8
rng = np.random.default_rng(42)
offset = rng.uniform(-1.5, 1.5, size=SENSOR)
derau = rng.normal(0, 0.8, size=(JAM, SENSOR))
jam = np.arange(JAM)[:, np.newaxis]
suhu = np.round(22.0 + 3.0 * np.sin((jam - 6) * np.pi / 12) + offset + derau, 1)
rata = suhu.mean(axis=0, keepdims=True)
simpangan = suhu.std(axis=0, keepdims=True)
z = (suhu - rata) / simpangan
print("bentuk rata-rata:", rata.shape, "| bentuk z:", z.shape)
print("z tiga jam pertama:")
print(np.round(z[:3], 2))
print("rata-rata z per sensor mendekati nol :", np.allclose(z.mean(axis=0), 0))
print("simpangan z per sensor mendekati satu:", np.allclose(z.std(axis=0), 1))bentuk rata-rata: (1, 8) | bentuk z: (24, 8)
z tiga jam pertama:
[[-1.44 -1.75 -1.09 -1.14 -1.32 -0.97 -1.26 -1.67]
[-1.24 -1.7 -1.05 -1.36 -1.37 -1.56 -0.92 -1.38]
[-1.39 -1.37 -1.05 -1.09 -1.01 -1.01 -0.44 -1.33]]
rata-rata z per sensor mendekati nol : True
simpangan z per sensor mendekati satu: TrueDua pemeriksaan terakhir mengembalikan True, sehingga syarat definisi selesai untuk tahap ini terpenuhi. Nilai z pada dini hari negatif seluruhnya, misalnya -1.44 pada sensor 0, karena suhu dini hari berada di bawah rata-rata harian sensornya. Sebagai pembanding kebenaran, versi perulangan berikut menghitung hasil yang sama dengan operasi skalar, terdiri atas tiga putaran per sensor yang masing-masing menjumlahkan, menghitung simpangan, lalu menormalisasi. Pembaginya sengaja memakai banyaknya jam sesuai perilaku bawaan std, sehingga keduanya benar-benar setara.
import numpy as np
JAM, SENSOR = 24, 8
rng = np.random.default_rng(42)
offset = rng.uniform(-1.5, 1.5, size=SENSOR)
derau = rng.normal(0, 0.8, size=(JAM, SENSOR))
jam = np.arange(JAM)[:, np.newaxis]
suhu = np.round(22.0 + 3.0 * np.sin((jam - 6) * np.pi / 12) + offset + derau, 1)
z = (suhu - suhu.mean(axis=0, keepdims=True)) / suhu.std(axis=0, keepdims=True)
z_loop = np.zeros((JAM, SENSOR))
for s in range(SENSOR):
total = 0.0
for j in range(JAM):
total += suhu[j, s]
rata_s = total / JAM
kuadrat = 0.0
for j in range(JAM):
kuadrat += (suhu[j, s] - rata_s) ** 2
simpangan_s = (kuadrat / JAM) ** 0.5
for j in range(JAM):
z_loop[j, s] = (suhu[j, s] - rata_s) / simpangan_s
print("allclose z perulangan vs tervektorisasi:", np.allclose(z_loop, z))allclose z perulangan vs tervektorisasi: TrueRingkasan per jam dengan axis#
Peringkasan per jam mereduksi sumbu sensor, sehingga axis=1 yang dipakai dan hasilnya berbentuk (24,), yaitu satu baris ringkasan untuk setiap jam. Tiga agregasi dihitung sekaligus, yaitu rata-rata, terendah, dan tertinggi, sehingga setiap jam dapat dibaca sebagai rentang suhu ruangannya.
import numpy as np
JAM, SENSOR = 24, 8
rng = np.random.default_rng(42)
offset = rng.uniform(-1.5, 1.5, size=SENSOR)
derau = rng.normal(0, 0.8, size=(JAM, SENSOR))
jam = np.arange(JAM)[:, np.newaxis]
suhu = np.round(22.0 + 3.0 * np.sin((jam - 6) * np.pi / 12) + offset + derau, 1)
rata_jam = suhu.mean(axis=1)
terendah = suhu.min(axis=1)
tertinggi = suhu.max(axis=1)
print("bentuk ringkasan:", rata_jam.shape)
print("jam rata terendah tertinggi")
for j in range(5):
print(f"{j:3d} {rata_jam[j]:5.2f} {terendah[j]:8.2f} {tertinggi[j]:9.2f}")
rata_loop = np.zeros(JAM)
for j in range(JAM):
total = 0.0
for s in range(SENSOR):
total += suhu[j, s]
rata_loop[j] = total / SENSOR
print("allclose ringkasan perulangan vs tervektorisasi:", np.allclose(rata_loop, rata_jam))bentuk ringkasan: (24,)
jam rata terendah tertinggi
0 19.68 17.80 21.30
1 19.68 17.70 20.90
2 20.19 18.50 21.90
3 20.32 18.60 21.40
4 21.25 19.40 22.10
allclose ringkasan perulangan vs tervektorisasi: TrueLima jam pertama memperlihatkan suhu rata-rata yang naik dari 19.68 menuju 21.25, sesuai gelombang harian yang masih menanjak pada dini hari menuju pagi. Selisih antara suhu terendah dan tertinggi pada setiap jam sekitar tiga derajat, yang berasal dari perbedaan offset dan derau antar sensor. Versi perulangan pada blok yang sama menghitung rata-rata per jam dengan penjumlahan skalar, dan pemeriksaan np.allclose mengembalikan True.
Jarak antar sensor dengan broadcasting#
Jarak kesamaan antar sensor dihitung pada profil z-skor, sehingga yang dibandingkan adalah bentuk pola suhunya, bukan kalibrasinya. Setiap sensor dipandang sebagai vektor berisi 24 nilai z, dan jaraknya dihitung dengan jarak Euclidean (Euclidean distance), yaitu akar dari jumlah kuadrat selisih komponennya. Notasi menyatakan jarak antara sensor ke-i dan sensor ke-k.
Penyusunan semua pasangan dilakukan dengan broadcasting tiga dimensi, yaitu (8, 1, 24) dikurangi (1, 8, 24), sehingga terbentuk array selisih (8, 8, 24) yang direduksi pada sumbu terakhirnya menjadi matriks jarak (8, 8).
import numpy as np
JAM, SENSOR = 24, 8
rng = np.random.default_rng(42)
offset = rng.uniform(-1.5, 1.5, size=SENSOR)
derau = rng.normal(0, 0.8, size=(JAM, SENSOR))
jam = np.arange(JAM)[:, np.newaxis]
suhu = np.round(22.0 + 3.0 * np.sin((jam - 6) * np.pi / 12) + offset + derau, 1)
z = (suhu - suhu.mean(axis=0, keepdims=True)) / suhu.std(axis=0, keepdims=True)
selisih = z.T[:, np.newaxis, :] - z.T[np.newaxis, :, :]
jarak = np.sqrt((selisih ** 2).sum(axis=-1))
jarak_norm = np.linalg.norm(z.T[:, np.newaxis, :] - z.T[np.newaxis, :, :], axis=-1)
print("bentuk selisih:", selisih.shape, "| bentuk jarak:", jarak.shape)
print(np.round(jarak, 2))
print("simetris :", np.allclose(jarak, jarak.T))
print("diagonal :", np.allclose(np.diag(jarak), 0))
print("versi norma sama:", np.allclose(jarak, jarak_norm))bentuk selisih: (8, 8, 24) | bentuk jarak: (8, 8)
[[0. 1.72 2.05 2.28 2.17 2.08 2.9 1.8 ]
[1.72 0. 2.38 2.15 2.29 2.12 2.76 2. ]
[2.05 2.38 0. 1.89 1.97 2.08 2.58 1.88]
[2.28 2.15 1.89 0. 2.27 2.4 2.99 2.13]
[2.17 2.29 1.97 2.27 0. 2.25 2.53 1.98]
[2.08 2.12 2.08 2.4 2.25 0. 2.19 2.17]
[2.9 2.76 2.58 2.99 2.53 2.19 0. 2.47]
[1.8 2. 1.88 2.13 1.98 2.17 2.47 0. ]]
simetris : True
diagonal : True
versi norma sama: TrueVersi np.linalg.norm dengan axis=-1 menghasilkan matriks yang sama, dan pemeriksaan pada blok tersebut mengembalikan True. Matriks jarak simetris dengan diagonal nol sesuai definisi jarak, dan kedua sifat itu diperiksa secara eksplisit. Nilai terkecil di luar diagonal adalah 1.72 pada pasangan sensor 0 dan 1, sedangkan yang terbesar adalah 2.99 pada pasangan sensor 3 dan 6, sehingga pasangan pertama paling mirip dan pasangan kedua paling berbeda polanya.
Versi perulangan menghitung setiap sel matriks dengan tiga perulangan bersarang, yaitu dua perulangan untuk pasangan sensor dan satu untuk profil jam.
import numpy as np
JAM, SENSOR = 24, 8
rng = np.random.default_rng(42)
offset = rng.uniform(-1.5, 1.5, size=SENSOR)
derau = rng.normal(0, 0.8, size=(JAM, SENSOR))
jam = np.arange(JAM)[:, np.newaxis]
suhu = np.round(22.0 + 3.0 * np.sin((jam - 6) * np.pi / 12) + offset + derau, 1)
z = (suhu - suhu.mean(axis=0, keepdims=True)) / suhu.std(axis=0, keepdims=True)
selisih = z.T[:, np.newaxis, :] - z.T[np.newaxis, :, :]
jarak = np.sqrt((selisih ** 2).sum(axis=-1))
jarak_loop = np.zeros((SENSOR, SENSOR))
for i in range(SENSOR):
for k in range(SENSOR):
total = 0.0
for j in range(JAM):
total += (z[j, i] - z[j, k]) ** 2
jarak_loop[i, k] = total ** 0.5
print("allclose jarak perulangan vs tervektorisasi:", np.allclose(jarak_loop, jarak))allclose jarak perulangan vs tervektorisasi: TrueBerkas proyek utuh#
Seluruh tahap di atas disatukan dalam satu berkas analisis_suhu.py, yang memuat fungsi bangkitan data, tiga fungsi tervektorisasi, tiga fungsi perulangan pembanding, dan rangkaian verifikasi definisi selesai. Berkas berikut dapat disalin apa adanya ke dalam folder kerja, lalu dijalankan dari terminal.
"""Analisis kisi suhu ruangan.
Kisi suhu 24 jam x 8 sensor dibangkitkan dari seed tetap, dinormalisasi per
sensor, diringkas per jam, lalu dihitung jarak kesamaan antar sensornya.
Setiap tahap memiliki versi tervektorisasi dan versi perulangan pembanding.
"""
import numpy as np
JAM, SENSOR = 24, 8
def buat_suhu():
rng = np.random.default_rng(42)
offset = rng.uniform(-1.5, 1.5, size=SENSOR)
derau = rng.normal(0, 0.8, size=(JAM, SENSOR))
jam = np.arange(JAM)[:, np.newaxis]
return np.round(22.0 + 3.0 * np.sin((jam - 6) * np.pi / 12) + offset + derau, 1)
def normalisasi(suhu):
rata = suhu.mean(axis=0, keepdims=True)
simpangan = suhu.std(axis=0, keepdims=True)
return (suhu - rata) / simpangan
def ringkasan(suhu):
return suhu.mean(axis=1), suhu.min(axis=1), suhu.max(axis=1)
def jarak(z):
selisih = z.T[:, np.newaxis, :] - z.T[np.newaxis, :, :]
return np.sqrt((selisih ** 2).sum(axis=-1))
def normalisasi_perulangan(suhu):
z = np.zeros((JAM, SENSOR))
for s in range(SENSOR):
total = 0.0
for j in range(JAM):
total += suhu[j, s]
rata_s = total / JAM
kuadrat = 0.0
for j in range(JAM):
kuadrat += (suhu[j, s] - rata_s) ** 2
simpangan_s = (kuadrat / JAM) ** 0.5
for j in range(JAM):
z[j, s] = (suhu[j, s] - rata_s) / simpangan_s
return z
def ringkasan_perulangan(suhu):
rata = np.zeros(JAM)
for j in range(JAM):
total = 0.0
for s in range(SENSOR):
total += suhu[j, s]
rata[j] = total / SENSOR
return rata
def jarak_perulangan(z):
d = np.zeros((SENSOR, SENSOR))
for i in range(SENSOR):
for k in range(SENSOR):
total = 0.0
for j in range(JAM):
total += (z[j, i] - z[j, k]) ** 2
d[i, k] = total ** 0.5
return d
suhu = buat_suhu()
z = normalisasi(suhu)
rata_jam, terendah, tertinggi = ringkasan(suhu)
d = jarak(z)
print("bentuk : suhu", suhu.shape, "| z", z.shape,
"| ringkasan", rata_jam.shape, "| jarak", d.shape)
print("z per sensor : rata 0?", np.allclose(z.mean(axis=0), 0),
"| simpangan 1?", np.allclose(z.std(axis=0), 1))
print("jarak : simetris", np.allclose(d, d.T),
"| diagonal 0", np.allclose(np.diag(d), 0))
print("vs perulangan : normalisasi", np.allclose(z, normalisasi_perulangan(suhu)),
"| ringkasan", np.allclose(rata_jam, ringkasan_perulangan(suhu)),
"| jarak", np.allclose(d, jarak_perulangan(z)))
tanpa_diagonal = d.copy()
np.fill_diagonal(tanpa_diagonal, np.inf)
i, k = np.unravel_index(tanpa_diagonal.argmin(), tanpa_diagonal.shape)
print(f"pasangan sensor paling mirip : {i} dan {k} (jarak {tanpa_diagonal[i, k]:.2f})")
i, k = np.unravel_index(d.argmax(), d.shape)
print(f"pasangan sensor paling beda : {i} dan {k} (jarak {d[i, k]:.2f})")bentuk : suhu (24, 8) | z (24, 8) | ringkasan (24,) | jarak (8, 8)
z per sensor : rata 0? True | simpangan 1? True
jarak : simetris True | diagonal 0 True
vs perulangan : normalisasi True | ringkasan True | jarak True
pasangan sensor paling mirip : 0 dan 1 (jarak 1.72)
pasangan sensor paling beda : 3 dan 6 (jarak 2.99)Berkasnya dijalankan dengan perintah berikut, dan keluarannya identik dengan blok di atas karena isi berkasnya memang sama.
# menjalankan berkas proyek dari folder yang sama
python3 analisis_suhu.pybentuk : suhu (24, 8) | z (24, 8) | ringkasan (24,) | jarak (8, 8)
z per sensor : rata 0? True | simpangan 1? True
jarak : simetris True | diagonal 0 True
vs perulangan : normalisasi True | ringkasan True | jarak True
pasangan sensor paling mirip : 0 dan 1 (jarak 1.72)
pasangan sensor paling beda : 3 dan 6 (jarak 2.99)Baris verifikasi pada keluaran menutup lima syarat pertama definisi selesai sekaligus, yaitu bentuk setiap keluaran, sifat z per sensor, sifat matriks jarak, dan kesamaan ketiga versi perulangannya. Dua baris terakhir memberi jawaban atas pertanyaan isi proyek, yaitu pasangan sensor yang paling mirip dan paling berbeda polanya. Syarat keenam mengenai pengukuran waktu menjadi isi laporan benchmark modul ini.
Kesalahan umum#
Kesalahan yang paling berbahaya pada normalisasi per sensor adalah menukar axis=0 dengan axis=1. Kode dengan sumbu yang salah tetap berjalan tanpa error karena bentuknya kebetulan cocok, tetapi hasilnya ternormalisasi per jam alih-alih per sensor. Keliru semacam ini tidak menimbulkan pesan apa pun, sehingga satu-satunya pertahanan adalah pemeriksaan definisi selesai.
import numpy as np
JAM, SENSOR = 24, 8
rng = np.random.default_rng(42)
offset = rng.uniform(-1.5, 1.5, size=SENSOR)
derau = rng.normal(0, 0.8, size=(JAM, SENSOR))
jam = np.arange(JAM)[:, np.newaxis]
suhu = np.round(22.0 + 3.0 * np.sin((jam - 6) * np.pi / 12) + offset + derau, 1)
z_salah = (suhu - suhu.mean(axis=1, keepdims=True)) / suhu.std(axis=1, keepdims=True)
print("rata-rata z_salah per sensor:")
print(np.round(z_salah.mean(axis=0), 2))
print("pemeriksaan rata-rata nol :", np.allclose(z_salah.mean(axis=0), 0))
print("pemeriksaan simpangan satu:", np.allclose(z_salah.std(axis=0), 1))rata-rata z_salah per sensor:
[ 0.18 -0.72 0.63 0.2 -1.7 0.91 0.31 0.18]
pemeriksaan rata-rata nol : False
pemeriksaan simpangan satu: FalseRata-rata z_salah per sensor bukan nol dan kedua pemeriksaan mengembalikan False, sehingga kesalahannya tertangkap sebelum hasilnya dipakai. Kesalahan kedua muncul ketika arah agregasinya per jam tanpa keepdims, misalnya ketika setiap sensor ingin diselisihkan terhadap rata-rata ruangan pada jam yang sama. Hasil suhu.mean(axis=1) berbentuk (24,), dan bentuk tersebut tidak dapat disiarkan melawan dimensi terakhir data (24, 8), sehingga NumPy menolaknya dengan error.
import numpy as np
JAM, SENSOR = 24, 8
rng = np.random.default_rng(42)
offset = rng.uniform(-1.5, 1.5, size=SENSOR)
derau = rng.normal(0, 0.8, size=(JAM, SENSOR))
jam = np.arange(JAM)[:, np.newaxis]
suhu = np.round(22.0 + 3.0 * np.sin((jam - 6) * np.pi / 12) + offset + derau, 1)
try:
deviasi = suhu - suhu.mean(axis=1)
except ValueError as e:
print(f"ValueError: {e}")
print("dengan keepdims, bentuknya:", (suhu - suhu.mean(axis=1, keepdims=True)).shape)ValueError: operands could not be broadcast together with shapes (24,8) (24,)
dengan keepdims, bentuknya: (24, 8)Perbaikannya adalah menambahkan keepdims=True agar hasil agregasinya berbentuk (24, 1) dan siap disiarkan. Perlu diperhatikan pula bahwa versi perulangan pembanding harus memakai pembagi yang sama dengan perilaku bawaan std, yaitu banyaknya elemen tanpa koreksi ddof, karena pembeda kecil semacam ini membuat pemeriksaan np.allclose gagal meskipun kodenya tampak benar.
Ringkasan#
- Data proyek
(24, 8)dibangkitkan dari gelombang harian, offset kalibrasi per sensor, dan derau normal, semuanya dari seed 42. - Normalisasi per sensor memakai
axis=0dengankeepdims=True, ringkasan per jam memakaiaxis=1, dan jarak antar sensor memakai broadcasting(8, 8, 24)yang direduksi pada sumbu terakhir. - Setiap tahap memiliki versi perulangan pembanding, dan kesamaannya diverifikasi
np.allcloseyang mengembalikanTrue. - Berkas
analisis_suhu.pymenyatukan seluruh tahap dan menutup lima syarat teknis definisi selesai dalam satu keluaran.
Referensi#
- numpy.allclose — NumPy — Dokumentasi resmi fungsi verifikasi kesamaan yang dipakai pada setiap tahap.
- numpy.mean — NumPy
— Dokumentasi agregasi rata-rata beserta perilaku parameter
axisdankeepdims. - Broadcasting — NumPy — Aturan penyiaran bentuk yang dipakai normalisasi dan penyusunan matriks jarak.
- numpy.linalg.norm — NumPy — Dokumentasi fungsi norma yang dipakai sebagai versi alternatif penghitungan jarak.