- KFS × Navera
- IoT & AI for Smart Agriculture
- السيشن 14
تنظيف البيانات ومعالجتها
Data Cleaning & Preprocessing
البيانات الحقيقية من الحقل «متسخة»: رسائل مكررة، أخطاء حساسات (−999)، قفزات، انقطاع شبكة لساعات، وقيم مفقودة. نتعلم كشف كل نوع ومعالجته: الحدود الفيزيائية، فلتر Hampel، الشبكة الزمنية المنتظمة، الاستكمال الزمني المحدود، التطبيع، وتحويل السلاسل الزمنية — لنحصل على بيانات جاهزة للنمذجة.
نظرة عامة على السيشن
بنهاية السيشن ستكون قادراً على
- تكتشف التكرار والقيم المستحيلة والقيم الشاذة والفجوات الزمنية في بيانات IoT.
- تطبق Hampel filter (median + MAD) لكشف القفزات بشكل متين.
- تبني شبكة زمنية منتظمة وتستكمل الفجوات القصيرة فقط بـ Time interpolation مع حد أقصى.
- تطبق Normalization (Min-Max) وStandardization وتعرف متى تستخدم كلاً منهما.
- تحول السلسلة الزمنية (Resample، Rolling، Lags) وتوثق جودة البيانات بأرقام.
التطبيق في مزرعة الكلية
معالجة سجل 3 عقد من المزرعة لمدة 3 أسابيع (يونيو) فيه كل أعطال الحقل الحقيقية، ثم تطبيق نفس الخطوات على بيانات عقد فريقك من farm.db، وكتابة «تقرير جودة البيانات» لكل عقدة.
المُخرج (Deliverable)
مجموعة بيانات نظيفة جاهزة للنمذجة (node_log_clean.csv + hourly) مع تقرير تنظيف يوثق كل خطوة وعدد القيم المتأثرة.
خطة السيشن (180 دقيقة)
- 0:00 مراجعة مراجعة: التقرير الإحصائي 10 د
- 0:10 شرح أنواع البيانات المتسخة في إنترنت الأشياء الزراعي 25 د
- 0:35 تطبيق Lab: التكرار، الحدود، الشبكة الزمنية، Hampel، الاستكمال 35 د
- 1:10 استراحة استراحة 10 د
- 1:20 تطبيق Lab: التطبيع وتحويل السلاسل الزمنية 30 د
- 1:50 مشروع المشروع الجاهز: s14_clean_node_log.py + تقرير التنظيف 30 د
- 2:20 ميداني تطبيقي: تنظيف بيانات عقد فريقك من المزرعة 30 د
- 2:50 تقييم اختبار سريع + الواجب 10 د
الأدوات والمكونات
قاموس المصطلحات (Glossary)
غياب القيمة — لا نستبدلها بصفر.
قيمة بعيدة جداً عن جيرانها — خطأ أو حدث حقيقي نادر.
مدى مستحيل تجاوزه (رطوبة نسبية 0–100%).
Median Absolute Deviation — مقياس تشتت لا يتأثر بالقيم الشاذة.
يعتبر القيمة شاذة إذا بعدت عن الوسيط المحلي > k × MAD.
فرض شبكة زمنية منتظمة (كل 15 د) لتظهر الفجوات كـ NaN.
تقدير القيم المفقودة من الجيران (خطي، زمني).
ملء القيم المفقودة بأي طريقة.
تحويل القيم إلى المدى 0–1.
(x − المتوسط) / الانحراف المعياري.
قيمة المتغير قبل ساعة/يوم كعمود جديد.
استخدام معلومات من المستقبل أثناء التدريب — خطأ قاتل.
البيانات المتسخة في الحقل
25 دقيقةnode_log_raw.csv — 3 farm nodes, June 2024, 15-min
سجل «عقد» الصوبة والحقلين A و B لمدة 3 أسابيع، مشتق من بيانات الطقس الحقيقية لموقع المزرعة (فروق الصوبة، ضوضاء حساسات، تحويل ADC لحساس سعوي) ثم حُقنت فيه أعطال الحقل عمداً لأغراض التدريب: 3% قيم مفقودة، 40 خطأ قراءة (−999 / 999)، انقطاع Wi-Fi 7 ساعات للعقدة field_B، و 60 رسالة مكررة.
- الحجم6,071 صف × 8 أعمدة
- نوع البياناتمشتقة من بيانات حقيقية + أعطال محقونة (موثّقة)
الأعمدة (Columns)
timestamp | وقت القراءة |
node_id | greenhouse_01 / field_A / field_B |
air_temp_c | حرارة الهواء |
air_humidity_pct | رطوبة الهواء |
soil_adc | قراءة ADC الخام للحساس السعوي |
soil_moisture_pct | رطوبة التربة بعد المعايرة |
light_adc | الإضاءة |
battery_v | جهد البطارية |
| العيب | السبب الحقيقي | المعالجة |
|---|---|---|
| صفوف مكررة | MQTT QoS 1 أعاد الإرسال | drop_duplicates(["timestamp","node_id"]) |
| −999 / 999 | فشل قراءة DHT22 | خارج الحدود الفيزيائية ⇒ NaN |
| قفزات معزولة | تشويش المضخة، ماء على الحساس | Hampel ⇒ NaN |
| فجوة 7 ساعات | انقطاع Wi-Fi / بطارية | Reindex تظهرها، ولا نملأها (أطول من ساعتين) |
| قيم مفقودة متفرقة | رسالة ضاعت | استكمال زمني لحد ساعتين |
| انجراف تدريجي (Drift) | تآكل الحساس، ملح التربة | إعادة معايرة — لا يكتشفه التنظيف الآلي! |
ليست كل قيمة شاذة خطأ
قفزة رطوبة التربة من 20% إلى 45% خلال 15 دقيقة قد تكون حدث ري حقيقياً! قبل حذف أي قيمة اسأل: هل لها تفسير زراعي؟ لهذا نضبط نافذة Hampel ومعامل k بحذر، ونراجع سجل الري (جدول commands في المنصة).
Lab: خطوات التنظيف
35 دقيقةraw = pd.read_csv("datasets/node_log_raw.csv", parse_dates=["timestamp"])
print("rows:", len(raw), "| duplicates:", raw.duplicated(["timestamp", "node_id"]).sum())
df = raw.drop_duplicates(["timestamp", "node_id"]).copy()
limits = {"air_temp_c": (-10, 60), "air_humidity_pct": (0, 100), "soil_moisture_pct": (0, 100)}
for col, (lo, hi) in limits.items():
bad = ~df[col].between(lo, hi) & df[col].notna()
print(f"{col}: {bad.sum()} impossible values -> NaN")
df.loc[bad, col] = np.nan
g = df[df.node_id == "field_B"].set_index("timestamp").sort_index()
grid = pd.date_range(g.index.min(), g.index.max(), freq="15min")
g = g.reindex(grid) # the 7-hour outage now appears as NaN rows
print("missing rows after reindex:", g["air_temp_c"].isna().sum())
def hampel(s, window=9, k=3.0):
med = s.rolling(window, center=True, min_periods=1).median()
mad = (s - med).abs().rolling(window, center=True, min_periods=1).median()
return (s - med).abs() > k * 1.4826 * mad.replace(0, np.nan)
spikes = hampel(g["air_temp_c"])
print("Hampel spikes in air_temp_c:", int(spikes.sum()))
g.loc[spikes, "air_temp_c"] = np.nan
g["air_temp_c"] = g["air_temp_c"].interpolate(method="time", limit=8) # fill gaps up to 2 h only
print("still missing (long outage kept on purpose):", g["air_temp_c"].isna().sum())
لماذا limit=8؟
ملء 15 دقيقة مفقودة بين قراءتين آمن. ملء 7 ساعات (من الليل للظهر!) بخط مستقيم يخترع بيانات غير حقيقية ويضلل النموذج. القاعدة: املأ الفجوة فقط إذا كانت أقصر من المدة التي يتغير فيها المتغير بشكل ملحوظ.
Lab: التطبيع وتحويل السلاسل
30 دقيقة| Min-Max (0–1) | Z-score | |
|---|---|---|
| المعادلة | (x − min) / (max − min) | (x − mean) / std |
| يتأثر بالقيم الشاذة | جداً | أقل |
| متى | شبكات عصبية، رسوم مقارنة | Linear / Logistic regression، KNN، SVM |
| تحتاجه Random Forest؟ | لا | لا |
h = g[["air_temp_c", "soil_moisture_pct"]].resample("h").mean() # 15-min -> hourly
h["soil_norm"] = (h.soil_moisture_pct - h.soil_moisture_pct.min()) / (h.soil_moisture_pct.max() - h.soil_moisture_pct.min())
h["temp_z"] = (h.air_temp_c - h.air_temp_c.mean()) / h.air_temp_c.std()
h["temp_roll_6h"] = h.air_temp_c.rolling(6, min_periods=3).mean() # smoothed trend
h["soil_lag_24h"] = h.soil_moisture_pct.shift(24) # same hour yesterday
h["soil_change_24h"] = h.soil_moisture_pct - h.soil_lag_24h # drying rate per day
print(h.dropna().head(3).round(3))
print("mean daily drying rate (%/day):", round(h.soil_change_24h.mean(), 2))
تسرب البيانات (Data leakage) — الخطأ الأشهر
إذا حسبت min/max أو المتوسط للتطبيع من كل البيانات ثم قسمتها تدريب/اختبار، فقد «رأى» النموذج المستقبل. الصحيح: احسب معاملات التطبيع من بيانات التدريب فقط وطبقها على الاختبار (StandardScaler.fit على train فقط) — سنطبق هذا في السيشن 15.
المشروع الجاهز
30 دقيقةخط تنظيف كامل لكل العقد + تقرير
يطبق كل الخطوات على العقد الثلاث، ويحفظ node_log_clean.csv (15 دقيقة + أعمدة مطبّعة + عمود still_missing) و node_log_hourly.csv، ويطبع تقرير التنظيف.
cd projects/ai
python s14_clean_node_log.py
المخرجات الفعلية عند التشغيل (Real output)
Raw rows: 6071 == Cleaning report == duplicates 60 out_of_range_air_temp_c 19 out_of_range_air_humidity_pct 20 out_of_range_soil_moisture_pct 0 missing_rows_field_A 32 hampel_field_A_air_temp_c 111 ... missing_rows_field_B 70 ... Remaining NaN (long outages kept as NaN on purpose): air_temp_c 20 air_humidity_pct 20 soil_moisture_pct 20
"""Session 14 - Data cleaning & preprocessing of the farm nodes log.
Input : datasets/node_log_raw.csv (3 nodes, 15-min, with real-world faults:
missing values, -999/999 sensor errors, a 7-hour Wi-Fi outage, duplicated MQTT messages)
Output: outputs/node_log_clean.csv + a cleaning report printed to the console
"""
import numpy as np
import pandas as pd
from common import DATA, OUT
raw = pd.read_csv(DATA / "node_log_raw.csv", parse_dates=["timestamp"])
print("Raw rows:", len(raw))
report = {}
# 1) Duplicates (MQTT QoS 1 can deliver the same message twice)
report["duplicates"] = int(raw.duplicated(["timestamp", "node_id"]).sum())
df = raw.drop_duplicates(["timestamp", "node_id"]).copy()
# 2) Physically impossible values -> NaN (DHT22 returns -999 / 999 on read errors)
limits = {"air_temp_c": (-10, 60), "air_humidity_pct": (0, 100), "soil_moisture_pct": (0, 100)}
for col, (lo, hi) in limits.items():
bad = ~df[col].between(lo, hi) & df[col].notna()
report[f"out_of_range_{col}"] = int(bad.sum())
df.loc[bad, col] = np.nan
# 3) Statistical outliers with a rolling median + MAD (robust to spikes)
def hampel(s: pd.Series, window: int = 9, k: float = 3.0) -> pd.Series:
med = s.rolling(window, center=True, min_periods=1).median()
mad = (s - med).abs().rolling(window, center=True, min_periods=1).median()
return (s - med).abs() > k * 1.4826 * mad.replace(0, np.nan)
clean_parts = []
for node, g in df.groupby("node_id"):
g = g.set_index("timestamp").sort_index()
# 4) Regular time grid: reveals the gaps (outage) as NaN rows
g = g.reindex(pd.date_range(g.index.min(), g.index.max(), freq="15min"))
g["node_id"] = node
report[f"missing_rows_{node}"] = int(g["air_temp_c"].isna().sum())
for col in ["air_temp_c", "air_humidity_pct", "soil_moisture_pct"]:
out = hampel(g[col])
report[f"hampel_{node}_{col}"] = int(out.sum())
g.loc[out, col] = np.nan
# 5) Temporal interpolation - only for short gaps (<= 2 hours = 8 samples)
g[col] = g[col].interpolate(method="time", limit=8)
g["still_missing"] = g["air_temp_c"].isna() # still NaN after the 2-hour limit -> real outage
clean_parts.append(g)
clean = pd.concat(clean_parts).rename_axis("timestamp").reset_index()
# 6) Normalisation (min-max per variable) - ready for ML
for col in ["air_temp_c", "air_humidity_pct", "soil_moisture_pct"]:
lo, hi = clean[col].min(), clean[col].max()
clean[f"{col}_norm"] = ((clean[col] - lo) / (hi - lo)).round(4)
# 7) Time-series transformation: hourly means per node + daily features
hourly = (clean.set_index("timestamp").groupby("node_id")
.resample("h")[["air_temp_c", "air_humidity_pct", "soil_moisture_pct"]].mean().round(2))
clean.to_csv(OUT / "node_log_clean.csv", index=False)
hourly.to_csv(OUT / "node_log_hourly.csv")
print("\n== Cleaning report ==")
for k, v in report.items():
print(f"{k:45s} {v}")
print("\nRemaining NaN (long outages kept as NaN on purpose):")
print(clean[["air_temp_c", "air_humidity_pct", "soil_moisture_pct"]].isna().sum())
print("\nSaved:", OUT / "node_log_clean.csv")
فكّر وناقش
كشف السكربت 60 تكراراً — وهو بالضبط عدد التكرارات المحقونة. لكن Hampel كشف ~100 قفزة لكل عقدة رغم أننا حقنّا 40 خطأ فقط للكل. ماذا يعني ذلك؟ (تلميح: هل k=3 صارم أكثر من اللازم مع ضوضاء الحساس الطبيعية؟)
جرّب k = 4 و k = 5: كم قفزة تُكتشف؟ وما القيمة المناسبة في رأيك؟ برّر.
تطبيقي: بيانات فريقك
30 دقيقةتقرير جودة البيانات لعقد المزرعة
بيانات farm.db الحقيقية لعقدكم منذ السيشن 8 — مع مراجعة سجل الري والأحداث الميدانية
- التصدير
اقرأ farm.db في Pandas (كود السيشن 13).
- المقاييس
لكل عقدة احسب: نسبة الاكتمال (القراءات الفعلية ÷ المتوقعة)، عدد NULL، عدد القفزات، أطول فجوة.
- المطابقة
قارن الفجوات بسجل الأحداث: هل فجوة الثلاثاء هي يوم فصلنا البطارية؟ هل القفزة الكبيرة يوم تجربة الري في السيشن 12؟
- القرار
لكل عيب: هل هو عطل (نصلحه في الحقل) أم حدث زراعي حقيقي (نحتفظ به)؟
| العقدة | الاكتمال % | NULL | قفزات | أطول فجوة | السبب الميداني | الإجراء |
|---|---|---|---|---|---|---|
- أي عقدة اكتمالها < 80%: افحصها في الحقل (البطارية، العلبة، الهوائي).
اختبر فهمك
5 أسئلة · نتيجة فوريةالواجب والتسليم
الواجب الأسبوعي (Weekly Assignment)
جرّب k = 3، 4، 5 في Hampel وارسم عدد القفزات المكتشفة مقابل k. اختر قيمة وبرّرها.
أكمل تقرير جودة البيانات لعقد فريقك (الجدول + 5 أسطر استنتاج).
أضف لسكربت التنظيف كشف «الانجراف»: متوسط متحرك 7 أيام لعقدة يبتعد تدريجياً عن العقد المجاورة.
قائمة التسليم (Submission Checklist)
مراجع ومصادر
استعد للسيشن القادمة
السيشن القادمة: أول نماذج تعلم آلي! ثبّت scikit-learn (pip install scikit-learn joblib).