- KFS × Navera
- IoT & AI for Smart Agriculture
- السيشن 19
التصور المتقدم وتحليل الاتجاهات
Advanced Visualization & Trend Analysis
تحويل البيانات إلى قرارات يفهمها الباحث والمزارع: خرائط حرارية لتوزيع الرطوبة في الحقل، مقارنات موسمية بين سنتين، مؤشرات أداء المحصول (درجات الحرارة النامية GDD، ساعات الإجهاد، الاحتياج المائي)، وتصدير تقارير CSV/PDF — مع خريطة تفاعلية للمزرعة على Leaflet.
نظرة عامة على السيشن
بنهاية السيشن ستكون قادراً على
- تختار الرسم المناسب للسؤال: Heatmap للتوزيع، Line للاتجاه، Calendar heatmap للموسمية.
- تبني خريطة توزيع مكاني بـ IDW interpolation من نقاط عقد قليلة وتعرف حدودها.
- تقارن الفصول والسنوات وتحسب شذوذ (Anomaly) عن المتوسط.
- تحسب مؤشرات أداء المحصول: GDD، ساعات الإجهاد الحراري، ETc الموسمي.
- تصدّر التقارير (CSV / PNG / PDF / GeoJSON) وتعرض الخريطة تفاعلياً بـ Leaflet.
التطبيق في مزرعة الكلية
خريطة توزيع رطوبة التربة على مواقع العقد في المزرعة (بإحداثيات GPS حقيقية)، ومقارنة صيف 2023 بصيف 2024 لموقع الكلية، وحساب مؤشرات أداء موسم الذرة في تقرير جاهز للباحثين والمزارعين.
المُخرج (Deliverable)
تقارير مرئية جاهزة: خريطة IDW + GeoJSON، رسم مقارنة موسمية، تقويم حراري للبخر-نتح، جدول مؤشرات المحصول، وصفحة خريطة تفاعلية في المنصة.
خطة السيشن (180 دقيقة)
- 0:00 مراجعة مراجعة: نتائج تجربة الري الذكي 10 د
- 0:10 شرح مبادئ التصور للقرار + مؤشرات أداء المحصول 25 د
- 0:35 تطبيق Lab: مقارنة موسمية + GDD + تقويم حراري 35 د
- 1:10 استراحة استراحة 10 د
- 1:20 مشروع المشروع الجاهز: خريطة IDW + GeoJSON 30 د
- 1:50 تطبيق Lab: الخريطة التفاعلية في المنصة + تصدير التقارير 30 د
- 2:20 ميداني ميداني: GPS للعقد وخريطة الرطوبة الفعلية للحقل 35 د
- 2:55 تقييم اختبار سريع + الواجب 5 د
الأدوات والمكونات
قاموس المصطلحات (Glossary)
ألوان تمثل قيمة متغير على شبكة أو مكان.
Inverse Distance Weighting — النقطة الأقرب لها تأثير أكبر.
تقدير القيمة بين نقاط القياس.
JSON للنقاط والمضلعات تفتحه Leaflet و QGIS.
خرائط تفاعلية في المتصفح.
الفرق عن المتوسط طويل المدى لنفس الفترة.
Growing Degree Days = Σ max(0, Tmean − Tbase) — تتنبأ بمراحل النمو.
تحتها لا ينمو المحصول (الذرة ~10°C).
ساعات فوق عتبة ضارة (الذرة ~35°C).
Σ ET0 × Kc خلال الموسم.
شبكة أيام × أشهر بألوان القيمة.
حفظ النتائج بصيغ يستخدمها الآخرون.
التصور للقرار ومؤشرات المحصول
25 دقيقة| السؤال | أفضل رسم | مثال في المزرعة |
|---|---|---|
| أين الجفاف في الحقل؟ | خريطة حرارية مكانية | IDW لرطوبة التربة من العقد |
| هل هذا الصيف أحر من الماضي؟ | خطان على نفس المحور + منطقة الفرق | 2023 مقابل 2024 (حرارة / ET0) |
| متى تبلغ الذرة التزهير؟ | منحنى تراكمي | GDD التراكمي مع خط العتبة |
| ما الأشهر الأعلى احتياجاً؟ | تقويم حراري / أعمدة | ET0 اليومي على مدار السنة |
| هل الأداء مقبول؟ | بطاقات KPI | ساعات الإجهاد، م³/فدان، أيام بدون إجهاد مائي |
درجات الحرارة النامية (GDD)
نمو النبات يعتمد على الحرارة المتراكمة أكثر من عدد الأيام. للذرة: GDD اليوم = max(0, (Tmax + Tmin)/2 − 10) (مع قص Tmax عند 30°C). تحتاج الذرة الهجين عادةً ~1300–1500 GDD من الزراعة حتى التزهير (حسب الصنف — راجع بيانات الصنف المزروع في الكلية).
حدود IDW
4 عقد لا تكفي لخريطة دقيقة لحقل كامل! IDW يفترض أن الرطوبة تتغير بسلاسة مع المسافة، بينما الواقع يتأثر بخطوط التنقيط ونوع التربة والميول. استخدم الخريطة كـ «اتجاه عام» وزد الكثافة في المناطق المهمة.
Lab: المقارنة الموسمية و GDD
35 دقيقةimport matplotlib.pyplot as plt
daily = pd.read_csv("datasets/kfs_farm_daily_2023_2024.csv", parse_dates=["date"]).set_index("date")
s23 = daily.loc["2023-05-01":"2023-09-30"]; s24 = daily.loc["2024-05-01":"2024-09-30"]
cmp = pd.DataFrame({"t_mean_2023": s23.air_temp_mean.values, "t_mean_2024": s24.air_temp_mean.values[:len(s23)],
"et0_2023": s23.et0_mm.values, "et0_2024": s24.et0_mm.values[:len(s23)]}, index=s23.index.strftime("%m-%d"))
print("mean temp May-Sep: 2023 =", round(s23.air_temp_mean.mean(), 2), "| 2024 =", round(s24.air_temp_mean.mean(), 2))
print("total ET0 May-Sep: 2023 =", round(s23.et0_mm.sum()), "mm | 2024 =", round(s24.et0_mm.sum()), "mm")
print("days with Tmax >= 38 C: 2023 =", int((s23.air_temp_max >= 38).sum()), "| 2024 =", int((s24.air_temp_max >= 38).sum()))
fig, ax = plt.subplots(figsize=(11, 4))
ax.plot(cmp.index, cmp.t_mean_2023.rolling(7).mean(), label="2023")
ax.plot(cmp.index, cmp.t_mean_2024.rolling(7).mean(), label="2024")
ax.set_xticks(cmp.index[::15]); ax.legend(); ax.set_title("KFS farm - 7-day mean air temperature, May-Sep")
fig.savefig("projects/ai/outputs/s19_season_compare.png", dpi=120, bbox_inches="tight")
season = daily.loc["2024-05-15":"2024-09-01"] # a typical summer maize season
tmax = season.air_temp_max.clip(upper=30); tmin = season.air_temp_min.clip(lower=10)
gdd = ((tmax + tmin) / 2 - 10).clip(lower=0)
gdd_cum = gdd.cumsum()
hourly = pd.read_csv("datasets/kfs_farm_hourly_2023_2024.csv", parse_dates=["time"]).set_index("time")
heat_hours = int((hourly.loc["2024-05-15":"2024-09-01", "air_temp_c"] >= 35).sum())
kc = pd.Series(0.3, index=season.index) # FAO-56 maize Kc curve (simplified)
kc.loc["2024-06-05":"2024-07-05"] = np.linspace(0.3, 1.2, len(kc.loc["2024-06-05":"2024-07-05"]))
kc.loc["2024-07-06":"2024-08-10"] = 1.2
kc.loc["2024-08-11":] = np.linspace(1.2, 0.6, len(kc.loc["2024-08-11":]))
etc = (season.et0_mm * kc).sum()
kpi = {"season GDD": round(gdd_cum.iloc[-1]), "date GDD >= 1400": str(gdd_cum[gdd_cum >= 1400].index.min().date()),
"heat-stress hours (>=35 C)": heat_hours, "seasonal ETc (mm)": round(etc),
"irrigation need (m3/feddan, 75% efficiency)": round(max(0, etc - season.precipitation_mm.sum()) * 4.2 / 0.75)}
print(pd.Series(kpi))
y = daily.loc["2024", "et0_mm"]
cal = pd.DataFrame({"month": y.index.month, "day": y.index.day, "et0": y.values}).pivot(index="month", columns="day", values="et0")
fig, ax = plt.subplots(figsize=(12, 4))
im = ax.imshow(cal, aspect="auto", cmap="YlOrRd")
ax.set_yticks(range(12)); ax.set_yticklabels(["Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec"])
ax.set_xlabel("day of month"); ax.set_title("KFS farm - daily ET0 2024 (mm/day)")
fig.colorbar(im, label="mm/day")
fig.savefig("projects/ai/outputs/s19_et0_calendar.png", dpi=120, bbox_inches="tight")
print("peak ET0 day:", y.idxmax().date(), round(y.max(), 2), "mm")
ماذا قالت بيانات موقع الكلية؟
صيف 2024 كان أحر من 2023 بحوالي درجة، مع 21 يوماً إضافياً بحرارة عظمى ≥ 38°C — ورغم ذلك زاد ET0 الموسمي 2% فقط. ناقش: لماذا؟ (الرطوبة والرياح والإشعاع تدخل في معادلة Penman-Monteith).
المخرجات الفعلية عند التشغيل (Real output)
mean temp May-Sep: 2023 = 27.5 | 2024 = 28.48 total ET0 May-Sep: 2023 = 1037 mm | 2024 = 1060 mm days with Tmax >= 38 C: 2023 = 41 | 2024 = 62 season GDD 1797 date GDD >= 1400 2024-08-09 heat-stress hours (>=35 C) 600 seasonal ETc (mm) 676 irrigation need (m3/feddan, 75% efficiency) 3784 peak ET0 day: 2024-05-20 8.69 mm
المشروع الجاهز: خريطة IDW
30 دقيقةs19_moisture_heatmap.py
يقرأ متوسط رطوبة كل عقدة من السجل النظيف (السيشن 14)، يستوفي شبكة 160×160 بـ IDW، يرسم الخريطة مع أسماء العقد، ويصدّر GeoJSON تفتحه Leaflet و QGIS و geojson.io. عدّل NODES بإحداثياتكم الحقيقية. المخرجات الفعلية:
cd projects/ai
python s19_moisture_heatmap.py
المخرجات الفعلية عند التشغيل (Real output)
Mean soil moisture per node (%): node_id greenhouse_01 34.3 field_A 22.3 field_B 16.2 field_C 24.3 Saved map + GeoJSON to ...\projects\ai\outputs
لاحظ field_C
لا توجد قراءات لعقدة field_C في السجل، فأعطاها السكربت متوسط العقد الأخرى (24.3%) — هذا افتراض يجب توضيحه في أي تقرير، أو حذف النقطة. الخرائط الجميلة قد تخفي بيانات مخترعة!
"""Session 19 - Moisture distribution map across node locations (IDW interpolation).
Edit NODES with the GPS coordinates you measured at the college farm (Google Maps -> long press).
Values come from the cleaned node log (session 14) - or type the latest readings by hand.
Output: outputs/s19_moisture_map.png + outputs/s19_nodes.geojson (open it in Leaflet / QGIS)
"""
import json
import matplotlib
matplotlib.use("Agg")
import matplotlib.pyplot as plt
import numpy as np
import pandas as pd
from common import OUT
# Approximate positions inside the KFS Faculty of Agriculture farm - replace with real GPS points.
NODES = {
"greenhouse_01": (31.1052, 30.9431),
"field_A": (31.1047, 30.9442),
"field_B": (31.1041, 30.9436),
"field_C": (31.1045, 30.9428),
}
clean_path = OUT / "node_log_clean.csv"
if clean_path.exists():
log = pd.read_csv(clean_path)
latest = log.dropna(subset=["soil_moisture_pct"]).groupby("node_id")["soil_moisture_pct"].mean()
else:
latest = pd.Series({"greenhouse_01": 38.0, "field_A": 24.0, "field_B": 17.0})
latest = latest.reindex(NODES.keys()).fillna(latest.mean()).round(1)
print("Mean soil moisture per node (%):\n", latest)
lat = np.array([NODES[n][0] for n in latest.index]); lon = np.array([NODES[n][1] for n in latest.index])
val = latest.values
# Inverse Distance Weighting on a grid
gx, gy = np.meshgrid(np.linspace(lon.min() - 4e-4, lon.max() + 4e-4, 160),
np.linspace(lat.min() - 4e-4, lat.max() + 4e-4, 160))
d = np.sqrt((gx[..., None] - lon) ** 2 + (gy[..., None] - lat) ** 2) + 1e-9
w = 1 / d ** 2
grid = (w * val).sum(-1) / w.sum(-1)
fig, ax = plt.subplots(figsize=(7, 6))
im = ax.contourf(gx, gy, grid, levels=20, cmap="RdYlBu")
ax.scatter(lon, lat, c="k", s=40)
for n, x, y_, v in zip(latest.index, lon, lat, val):
ax.annotate(f"{n}\n{v}%", (x, y_), textcoords="offset points", xytext=(6, 6), fontsize=8)
fig.colorbar(im, label="Soil moisture %")
ax.set_title("KFS farm - soil moisture distribution (IDW)")
ax.set_xlabel("Longitude"); ax.set_ylabel("Latitude")
fig.tight_layout(); fig.savefig(OUT / "s19_moisture_map.png", dpi=130)
geo = {"type": "FeatureCollection", "features": [
{"type": "Feature", "properties": {"node": n, "soil_moisture_pct": float(v)},
"geometry": {"type": "Point", "coordinates": [float(NODES[n][1]), float(NODES[n][0])]}}
for n, v in latest.items()]}
(OUT / "s19_nodes.geojson").write_text(json.dumps(geo, indent=2))
print("Saved map + GeoJSON to", OUT)
Lab: الخريطة التفاعلية والتصدير
30 دقيقة{% extends "base.html" %}
{% block title %}خريطة المزرعة — KFS Smart Farm{% endblock %}
{% block head %}
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css">
{% endblock %}
{% block content %}
<section class="card">
<div class="card-head">
<h2>توزيع رطوبة التربة على عقد المزرعة</h2>
<span class="legend"><i style="background:#c0392b"></i> جاف <i style="background:#f5a524"></i> متوسط <i style="background:#13787b"></i> رطب</span>
</div>
<div id="map" style="height:70vh;border-radius:12px"></div>
</section>
{% endblock %}
{% block scripts %}
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"></script>
<script src="https://unpkg.com/leaflet.heat@0.2.0/dist/leaflet-heat.js"></script>
<script>
const map = L.map('map').setView([{{ lat }}, {{ lon }}], 17);
L.tileLayer('https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}',
{maxZoom: 20, attribution: 'Imagery © Esri'}).addTo(map);
const color = v => v == null ? '#888' : v < 30 ? '#c0392b' : v < 55 ? '#f5a524' : '#13787b';
fetch('/api/v1/latest').then(r => r.json()).then(({nodes}) => {
const heat = [];
nodes.filter(n => n.lat && n.lon).forEach(n => {
L.circleMarker([n.lat, n.lon], {radius: 14, color: '#fff', weight: 2, fillColor: color(n.soil_moisture_pct), fillOpacity: .9})
.addTo(map)
.bindPopup(`<b>${n.name || n.node_id}</b><br>رطوبة التربة: ${n.soil_moisture_pct ?? '—'}%<br>الحرارة: ${n.air_temp_c ?? '—'} °C<br>${n.online ? '🟢 متصل' : '🔴 غير متصل'}`);
if (n.soil_moisture_pct != null) heat.push([n.lat, n.lon, 1 - n.soil_moisture_pct / 100]);
});
if (heat.length) L.heatLayer(heat, {radius: 45, blur: 30, gradient: {0.2: '#13787b', 0.5: '#f5a524', 0.9: '#c0392b'}}).addTo(map);
});
</script>
{% endblock %}
صفحة /map في المنصة: صور أقمار صناعية (Esri) + دائرة لكل عقدة بلون رطوبتها + طبقة حرارية (leaflet.heat) تتحدث من /api/v1/latest. الإحداثيات من جدول nodes — حدّثها بـ GPS الحقيقي.
تحديث مواقع العقد وتصدير التقرير
- المواقع
حدّث إحداثيات العقد في قاعدة البيانات:
UPDATE nodes SET lat=31.1049, lon=30.9440 WHERE node_id='field_A';(أو عدّل DEFAULT_NODES في db.py قبل أول تشغيل). - الخريطة
افتح
/mapواضغط على كل عقدة لترى قراءتها. - GeoJSON
افتح
outputs/s19_nodes.geojsonعلى geojson.io أو في QGIS فوق صورة القمر الصناعي. - تقرير PDF
في matplotlib يكفي
fig.savefig("report.pdf")— اجمع الرسوم الثلاثة في PDF واحد بـmatplotlib.backends.backend_pdf.PdfPages. - CSV للباحثين
من المنصة:
/export.csv?days=30— ملف بترميز UTF-8 مع BOM يفتح في Excel بالعربية مباشرة.
from matplotlib.backends.backend_pdf import PdfPages
with PdfPages("projects/ai/outputs/kfs_farm_report.pdf") as pdf:
for title, series in [("Daily mean air temperature 2024", daily.loc["2024", "air_temp_mean"]),
("Daily ET0 2024 (mm)", daily.loc["2024", "et0_mm"]),
("Cumulative GDD - maize season 2024", gdd_cum)]:
fig, ax = plt.subplots(figsize=(10, 4))
series.plot(ax=ax); ax.set_title(title); ax.grid(alpha=.3)
pdf.savefig(fig); plt.close(fig)
print("report saved: kfs_farm_report.pdf (3 pages)")
ميداني: خريطة رطوبة الحقل
35 دقيقةتوزيع الرطوبة عبر مواقع العقد في الحقل
حقل الخضر — العقد الثابتة + مسح يدوي بحساس محمول
- GPS دقيق
سجّل إحداثيات كل عقدة بـ Google Maps (اضغط مطولاً، 5 أرقام عشرية ≈ 1 م).
- مسح يدوي
بعقدة محمولة (ESP32 + حساس + Power bank) قس الرطوبة على 15 سم في شبكة 10 × 10 م (حوالي 12 نقطة) وسجّل GPS لكل نقطة.
- الخريطة
أضف النقاط اليدوية إلى NODES في s19 وأعد رسم الخريطة.
- المقارنة
هل خريطة 4 عقد تشبه خريطة 16 نقطة؟ أين الاختلاف الأكبر؟ هل له تفسير (نهاية خط التنقيط، ميل الأرض، ظل)؟
- التوصية
أين يجب إضافة عقدة خامسة؟ برّر من الخريطة.
| النقطة | Lat | Lon | رطوبة 15 سم % | ملاحظة (قرب نقاط التنقيط؟ ظل؟) |
|---|---|---|---|---|
- امشِ بين الخطوط لا فوقها.
اختبر فهمك
5 أسئلة · نتيجة فوريةالواجب والتسليم
الواجب الأسبوعي (Weekly Assignment)
أنتج «تقرير الموسم» PDF: مقارنة 2023/2024، تقويم ET0، جدول KPI لموسم الذرة، وخريطة الرطوبة.
حدّث إحداثيات العقد الحقيقية في المنصة وأرفق لقطة من صفحة /map.
اختياري: نفذ الخريطة بـ folium (
pip install folium) وصدّرها HTML تفاعلية.
قائمة التسليم (Submission Checklist)
مراجع ومصادر
استعد للسيشن القادمة
السيشن القادمة الصيانة والتوسع: أحضر كابل USB لكل عقدة (للرفع الأول لـ OTA) وأعد قائمة بكل عقد فريقك وإصدارات الـ firmware.