用NumPy讀懂智慧養殖感測資料的數學基礎
把一串水溫、pH與溶氧讀值,轉成可計算、可比較、也知道限制的資料判讀。
完成本篇後,你能建立與檢查NumPy陣列、使用索引與布林遮罩、理解向量化與broadcasting、沿正確axis計算統計量、處理NaN、計算移動平均與變化率、建立異常候選遮罩,並對相關係數與標準化結果作負責任解讀。
一、為什麼智慧養殖需要NumPy?
Python串列可以保存數值,但NumPy的 ndarray 能以一致資料型別組成一維或多維陣列,並進行向量化運算。它是pandas、SciPy、影像處理與許多機器學習工具的數值基礎。
| 資料問題 | NumPy工具 | 仍需人工確認 |
|---|---|---|
| 每小時水溫的平均與波動 | mean、std、min、max | 探頭是否校正、取樣是否完整 |
| 設備離線造成缺值 | NaN與nan系列函式 | 缺值原因與補傳紀錄 |
| 短期趨勢太雜亂 | 移動平均、diff | 平滑是否掩蓋突發事件 |
| 某筆數值特別不同 | IQR、z-score候選 | 設備錯誤還是真實場域變化 |
二、安裝與確認NumPy
import subprocess
import sys
subprocess.run(
[sys.executable, "-m", "pip", "install", "NumPy"],
check=True,
)請在虛擬環境安裝,並記錄課程驗證版本。程式匯入時慣例使用 import numpy as np。
三、第一個水溫陣列
import numpy as np
temperature = np.array(
[27.2, 27.5, 28.0, 28.4, 29.1, 29.4],
dtype=np.float64,
)
print("資料:", temperature)
print("形狀:", temperature.shape)
print("維度:", temperature.ndim)
print("筆數:", temperature.size)
print("型別:", temperature.dtype)shape=(6,)代表包含6個元素的一維陣列。感測資料通常使用浮點型別;若需要缺值NaN,也必須使用可表示NaN的型別。
四、索引與切片:查看特定時段
print("第一筆:", temperature[0])
print("最後一筆:", temperature[-1])
print("第2到第4筆:", temperature[1:4])
morning = temperature[:3].copy()
morning[0] = 99.0
print("複製後修改:", morning)
print("原始資料不變:", temperature)基本切片常得到原陣列的view,修改時可能影響原資料。當你需要獨立資料時明確使用 .copy()。
五、向量化:一次換算全部感測值
temperature_f = temperature * 9 / 5 + 32
change_from_first = temperature - temperature[0]
print("華氏:", np.round(temperature_f, 1))
print("相對第一筆變化:", change_from_first)不必逐筆寫for迴圈,算術運算會套用到每個元素。這種向量化表達通常更簡潔,也能利用NumPy底層實作。
六、Broadcasting:每個感測器套用不同校正值
raw = np.array([
[27.2, 7.3, 5.8],
[27.5, 7.4, 5.6],
[28.0, 7.2, 5.4],
[28.4, 7.1, 5.2],
])
# 三欄依序是水溫、pH、溶氧
offset = np.array([0.2, -0.1, 0.0])
calibrated = raw + offset
print(calibrated)形狀為 (4,3) 的資料可和形狀 (3,) 的校正值broadcasting:每一列都套用三個欄位校正。若形狀無法相容,NumPy會報錯;不能靠猜測哪個方向被套用。
七、理解axis:沿時間還是沿指標?
column_mean = calibrated.mean(axis=0)
row_mean = calibrated.mean(axis=1)
print("各指標跨時間平均:", column_mean)
print("每個時間點跨欄平均:", row_mean)axis=0壓縮列,得到每一欄跨時間的平均;axis=1壓縮欄。但水溫、pH、溶氧單位不同,直接算跨欄平均通常沒有合理物理意義。程式算得出來,不代表應該這樣算。
八、基本統計:中心與離散程度
summary = {
"count": temperature.size,
"mean": float(np.mean(temperature)),
"median": float(np.median(temperature)),
"minimum": float(np.min(temperature)),
"maximum": float(np.max(temperature)),
"std_population": float(np.std(temperature, ddof=0)),
"std_sample": float(np.std(temperature, ddof=1)),
}
for name, value in summary.items():
print(name, value)ddof=0計算這批資料本身的母體標準差;ddof=1常用於由樣本估計母體變異。選哪一個取決於問題,不是固定答案。
九、NaN:未知不等於0
temperature_with_missing = np.array(
[27.2, 27.5, np.nan, 28.4, 29.1, 29.4],
dtype=float,
)
missing_mask = np.isnan(temperature_with_missing)
valid_count = np.count_nonzero(~missing_mask)
print("缺值位置:", missing_mask)
print("有效筆數:", valid_count)
print("忽略NaN平均:", np.nanmean(temperature_with_missing))
print("忽略NaN標準差:", np.nanstd(
temperature_with_missing, ddof=1
))np.mean()遇到NaN通常會得到NaN;np.nanmean()忽略NaN,但不能假裝缺值不存在。報告平均時要同時呈現有效筆數、缺值率與時間範圍。
十、資料值與品質標記分開
values = np.array([27.2, 27.5, 99.0, np.nan, 28.4])
quality = np.array([
"valid", "valid", "invalid", "unknown", "valid"
])
usable_mask = (quality == "valid") & np.isfinite(values)
usable_values = values[usable_mask]
print("可用遮罩:", usable_mask)
print("可納入一般統計:", usable_values)
print("平均:", usable_values.mean())invalid保留原始數值供追查,但不納入一般統計;unknown通常搭配NaN。只靠NaN無法表達「為何不可用」,品質標記仍要保留。
十一、布林遮罩:找出教材門檻外讀值
low, high = 26.0, 30.0
valid = np.isfinite(temperature_with_missing)
outside = valid & (
(temperature_with_missing < low)
| (temperature_with_missing > high)
)
print("門檻外位置:", np.flatnonzero(outside))
print("門檻外數值:", temperature_with_missing[outside])26~30°C只是程式示範。門檻必須附上物種、成長階段、場域、季節、資料來源、核定者與版本日期,不能把網路上找到的一個數字直接寫進控制系統。
十二、差分:觀察相鄰時間的變化
change = np.diff(temperature)
rapid_change = np.abs(change) >= 0.6
print("相鄰變化:", change)
print("快速變化區段:", np.flatnonzero(rapid_change))np.diff()的結果比原陣列少一筆,第0個差分代表原始第0筆到第1筆。若取樣間隔不固定,還要除以實際時間差,不能直接比較。
十三、移動平均:平滑短期波動
from numpy.lib.stride_tricks import sliding_window_view
def moving_average(values, window):
values = np.asarray(values, dtype=float)
if window < 1 or window > values.size:
raise ValueError("window超出資料範圍")
windows = sliding_window_view(values, window_shape=window)
return np.nanmean(windows, axis=-1)
smoothed = moving_average(temperature_with_missing, 3)
print(smoothed)3筆移動平均輸出會少2筆,時間應對齊視窗尾端或中心並清楚說明。平滑可以看趨勢,也可能遮住短暫但重要的急變;原始資料不可被覆蓋。
十四、IQR:找統計上的異常候選
candidate_data = np.array(
[27.1, 27.2, 27.3, 27.4, 27.5, 31.8],
dtype=float,
)
q1, q3 = np.quantile(candidate_data, [0.25, 0.75])
iqr = q3 - q1
lower = q1 - 1.5 * iqr
upper = q3 + 1.5 * iqr
candidate_mask = (
(candidate_data < lower) | (candidate_data > upper)
)
print("IQR範圍:", lower, upper)
print("異常候選:", candidate_data[candidate_mask])IQR規則只是探索方法。資料有日夜週期、季節變化、餵食或換水事件時,整批混算可能把正常情境標成異常。
十五、z-score:先檢查樣本量與標準差
def z_scores(values):
values = np.asarray(values, dtype=float)
valid = values[np.isfinite(values)]
if valid.size < 3:
raise ValueError("有效資料不足")
mean = valid.mean()
std = valid.std(ddof=1)
if np.isclose(std, 0.0):
raise ValueError("標準差接近0,無法計算z-score")
return (values - mean) / std
scores = z_scores(candidate_data)
print(np.round(scores, 2))z-score適合近似穩定分布的探索,不應機械式把 |z|>3當成通用場域告警。樣本小、分布偏斜或有時間趨勢時尤其要小心。
十六、標準化:不同量尺可比較,但單位意義消失
sensor_matrix = np.array([
[27.2, 7.3, 5.8],
[27.5, 7.4, 5.6],
[28.0, 7.2, 5.4],
[28.4, 7.1, 5.2],
])
mean = sensor_matrix.mean(axis=0, keepdims=True)
std = sensor_matrix.std(axis=0, ddof=1, keepdims=True)
if np.any(np.isclose(std, 0.0)):
raise ValueError("至少一欄沒有變化,無法標準化")
standardized = (sensor_matrix - mean) / std
print(np.round(standardized, 2))keepdims=True保留形狀為 (1,3),可安全broadcast回原矩陣。標準化結果沒有°C、pH或mg/L的原單位,不能拿來直接對照養殖門檻。
十七、相關係數:相關不代表因果
temperature_pair = np.array(
[27.2, 27.5, 28.0, 28.4, 29.1, np.nan]
)
dissolved_oxygen = np.array(
[5.8, 5.6, 5.4, 5.2, 4.9, 5.0]
)
pair_mask = (
np.isfinite(temperature_pair)
& np.isfinite(dissolved_oxygen)
)
if np.count_nonzero(pair_mask) < 3:
raise ValueError("成對有效資料不足")
correlation = np.corrcoef(
temperature_pair[pair_mask],
dissolved_oxygen[pair_mask],
)[0, 1]
print("Pearson相關係數:", correlation)高度正相關或負相關都不證明因果。共同時間趨勢、日夜循環、曝氣、餵食與感測器位置都可能影響結果;要回到實驗設計及場域紀錄。
十八、綜合函式:產生可追溯摘要
def sensor_summary(values, quality):
values = np.asarray(values, dtype=float)
quality = np.asarray(quality)
if values.shape != quality.shape:
raise ValueError("values與quality形狀不同")
usable = (quality == "valid") & np.isfinite(values)
selected = values[usable]
if selected.size == 0:
return {
"total": int(values.size),
"valid": 0,
"missing_or_invalid": int(values.size),
"mean": None,
"minimum": None,
"maximum": None,
}
return {
"total": int(values.size),
"valid": int(selected.size),
"missing_or_invalid": int(values.size - selected.size),
"mean": float(selected.mean()),
"minimum": float(selected.min()),
"maximum": float(selected.max()),
}
print(sensor_summary(values, quality))轉成Python的int與float,較方便輸出JSON。摘要仍應附感測器代碼、單位、起訖時間、取樣間隔、品質規則版本及產生時間。
十九、不要犯的五個錯誤
| 錯誤 | 後果 | 改善 |
|---|---|---|
| 把缺值補成0 | 平均與告警失真 | 保留NaN及quality |
| 混合不同單位直接平均 | 得到沒有物理意義的數字 | 依欄位與單位分別統計 |
| 忘記axis方向 | 跨時間與跨指標算反 | 每步印shape並用小資料驗證 |
| 平滑後覆蓋原始值 | 失去稽核與急變資訊 | 原始值與衍生值分開保存 |
| 把統計候選當成場域異常 | 錯誤巡查或控制 | 結合校正、事件及專業判讀 |
二十、課堂挑戰
二十一、用AI協助檢查數學,而不是代替場域判斷
請擔任NumPy與智慧養殖資料分析助教。
資料包含水溫、pH、溶氧、時間與quality標記。
請先確認每個陣列的shape、dtype、axis、單位、缺值與取樣間隔,
再檢查平均、標準差、移動平均、IQR與相關係數程式。
不得把NaN自動補成0,也不得把統計異常直接稱為養殖異常。
請為每個結果列出:數學意義、資料前提、場域限制、
最小測試資料,以及需要養殖者確認的問題。
沒有留言:
張貼留言