سجل التنوع البيولوجي
لا يراقب علماء البيئة كل فرد ، بل يراقبون إشارات. انخفاض 30% أو أكثر في الطيور المرصودة عبر موسم محفّز للمسح؛ وتذبذب قرب خط الأساس يستحق المراقبة؛ وعدّ مستقر معناه «اتركه وشأنه». يبني هذا المشروع حلقة القرار تلك كـعامل مسح صغير: يحمل سجل ملاحظات بطول موسم (اصطناعي، فقابل للتكرار)، ويحسب خط الأساس ونافذة كل نوع حديثة، ويطبق قاعدة عتبات لعلم كل نوع بـSURVEY / WATCH / OK، ويمتص دفعات أسبوعية جديدة ويسجل كل قرار إلى CSV، ثم يرسم مخطط أعمدة ASCII لإجماليات الأنواع ويطبع قائمة مسح مرتبة بالأولوية. كل شيء يعمل في pandas والمكتبة القياسية، ببذرة ثابتة ، نفس الجري يعلم نفس الأنواع في كل مرة، وعلى أي جهاز.
هذا يفترض groupby وترشيحًا ودمجًا في pandas. إنه مشروع اختياري غير مُقيَّم ، راجع مشاريع من العالم الحقيقي للاطلاع على القائمة الكاملة والنامية. تركيب واحد: pandas.
🎯 ما ستفعله
- توليد سجل ملاحظات حتمي بطول 111 يومًا لخمسة أنواع عبر ثلاثة مواقع.
- كتابة دماغ العامل: نافذتا أساس وحالية زائد قاعدة عتبات من ثلاث مستويات.
- تشغيل حلقة الامتصاص: ابتلاع ثلاث دفعات أسبوعية، وإلحاق صف قرار واحد لكل نوع.
- تصوير إجماليات الأنواع كمخطط أعمدة ASCII.
- ترتيب الأولويات: طلب الأنواع للمسح التالي، SURVEY أولًا.
أين تُشغّل هذا
محليًا باستخدام uv هو المسار الموصى به:
uv init biodiversity-logger && cd biodiversity-logger
uv add pandas
Google Colab وKaggle Notebooks وBinder يشغّلون كل خطوة دون تعديل ، pandas مثبتة مسبقًا على المنصتين، وdefault_rng(11) الثابتة تجعل الناتج متطابقًا في كل مكان.
الإعداد
كل ما تحتاجه قبل أول مشاهدة.
جهّز المشروع
uv init biodiversity-logger
cd biodiversity-logger
uv add pandas
الاستيرادات التي ستستخدمها طوال الوقت:
import numpy as np
import pandas as pd
✅ قائمة التحقق
- ✅ ينجح
uv run python3 -c "import pandas, numpy". - ✅ تستطيع تخيّل صف واحد كـ
date | site | species | countقبل كتابة أي شيء.
🤔 سؤال (أسئلة) سقراطي(ة) :
- «سجل» يخزّن صفوفًا فقط هو جدول بيانات. ماذا يضيف هذا المشروع فيجعله عاملًا ، صانع قرار يتفاعل مع البيانات لا مسجّلها فقط؟
- عدّادات الأنواع اصطناعية، بأعطي نوعان منحنى انحدارًا عمدًا. لو انخفض كل نوع 10% في آنٍ واحد، أكانت نسبة الحالي-إلى-الأساس حتى تكتشفه ، وما البقعة العمياء التي يكشفها ذلك عن المراقبة المبنية على النسب؟
الخطوة 1: ولّد سجل الملاحظات
كل رقم لاحق يأتي من هذه الكتلة، فهي حتمية: مولِّد RNG واحد، وبذرة واحدة، ومدى يوم واحد.
1.1 إطار الموسم
👟 تلميح البداية : ابنِ إطارًا بطول 111 يومًا بـpd.date_range؛ ثلاثة مواقع؛ خمسة أنواع بعدّادات أساس لكل نوع ومضاعفات موقع وانحدارين نزوليين.
# main.py
import numpy as np
import pandas as pd
rng = np.random.default_rng(11)
species = ["acorn_woodpecker", "blue_jay", "eastern_bluebird",
"northern_cardinal", "tree_swallow"]
sites = ["riverside", "meadow", "forest"]
base = {"acorn_woodpecker": 48, "blue_jay": 62, "eastern_bluebird": 70,
"northern_cardinal": 55, "tree_swallow": 60}
trend = {"acorn_woodpecker": -2.0, "blue_jay": 0.0, "eastern_bluebird": 0.0,
"northern_cardinal": 0.0, "tree_swallow": -1.5}
site_mult = {"riverside": 1.2, "meadow": 0.8, "forest": 1.4}
day = pd.date_range("2025-04-01", periods=111, freq="D")
rows = []
for s in species:
for site in sites:
for i, d in enumerate(day):
week = i // 7
mu = base[s] * site_mult[site] + trend[s] * week
rows.append({"date": d, "site": site, "species": s,
"count": max(0, round(mu + rng.normal(0, 4)))})
full = pd.DataFrame(rows)
print(full.head(3).to_string(index=False))
print("shape:", full.shape)
الإدخالان في trend ، نقّار البلوط الأكورن −2 في الأسبوع، وخطّاف الرقبة −1.5 ، هما النوعان اللذان يجب أن يلاحظهما العامل في النهاية. حدّ rng.normal(0, 4) ضجيج يومي واقعي: تتمايل العدّادات ±4 حتى مع اتجاه مستقر، فقاعدة «انخفض عدد اليوم» الصارمة كانت ستطلق إنذارًا في كل وقت. منطق النسبة في الخطوة 2 يبدّد ذلك الضجيج وسطًا.
🎯 الناتج المتوقع:
date site species count
0 2025-04-01 riverside acorn_woodpecker 58
1 2025-04-02 riverside acorn_woodpecker 63
2 2025-04-03 riverside acorn_woodpecker 62
shape: (1665, 4)
🩹 إذا لم يعمل : إذا اختلف أول عدّ للرأس عن 58، فبذرة RNG أو ترتيب الاستدعاء تغيّر. إذا لم تكن shape هي (1665, 4)، فالحلقة تراتبت خطأً 5 أنواع × 3 مواقع × 111 يومًا = 1665.
1.2 احتفظ بأسابيع «القادمة»
👟 تلميح البداية : أبقِ الأيام قبل 2025-06-30 (<2025-06-30) كالموسم المرصود؛ ستُسلَّم الأسابيع الثلاثة الأخيرة إلى العامل كـ«دفعات استشعار جديدة» في الخطوة 3.
# main.py (continued)
observed = full[full["date"] < pd.Timestamp("2025-06-30")].copy()
print("observed rows (days 1–90):", len(observed))
تحتاج الخطوة 3 دفعات طازجة لم يرها العامل. بدل إعادة توليدها من الصفر (تيّار RNG ثانٍ)، ولّد السكربت موسمًا بطول 111 يومًا مقدمًا واحتجز الذيل فقط: الإطار نفسه هو الطلبية، والتقطيع هو وهم «وصول بيانات جديدة». هذا يُبقي كل شيء على بذرة واحدة ، لا تيّار عشوائي ثانٍ لتوثيقه.
🎯 الناتج المتوقع : observed rows (days 1–90): 1350 ، 90 يومًا × 30 صفًا/يومًا.
🩹 إذا لم يعمل : إذا ظهر 1350 كـ1665، فقد أصبح < هو <= (شمل اليوم 91) أو أضاعت النسخة الفلتر. إذا ظهر عدّ مختلف، فالبلاغ بين التواريخ يخلط المناطق الزمنية ، قارن كائنات Timestamp، لا سلاسل.
1.3 تحقّق من طبقة البيانات
✅ قائمة التحقق
- ✅
full.shape == (1665, 4)؛ لكل نوع270صفًا (أنواعspeciesالمميزة معدودة فيgroupby). - ✅ صفوف الرأس والعدّادات حتمية (نفس الجري، نفس الأرقام).
- ✅
observedهي أول 90 يومًا بالضبط ،1350صفًا.
🤔 سؤال (أسئلة) سقراطي(ة) :
- تضاعف ثلاثة مواقع عدّاد الأساس بشكل مختلف (
forest1.4×،meadow0.8×). عندما يقارن العامل إجمالي الأنواع, هل يعدّ طيورًا خام أم طيورًا لكل موقع؟ ماذا يحدث لنسبة نوع كثيف في الغابة إذا قارنت عدّادات خام عبر مناطق بجهد غير متساوٍ؟ - يعني ضجيج
rng.normal(0, 4)اليومي أن يومًا واحدًا قد يسقط 8 طيور صدفة. كم يومًا من التوسيط يلزم قبل أن يبدأ اتجاه −2/أسبوع بالهيمنة على ±4 ضجيج ، وماذا يقول ذلك عن لماذا يستخدم العامل نوافذ, لا أيامًا مفردة؟
الخطوة 2: دماغ العامل
قاعدة القرار هي كل العامل. تعرّف الخطوة 2 النوافذ وعتبة المستويات الثلاث.
2.1 نافذتا الأساس والحالية
👟 تلميح البداية : احسب متوسطات عدّ كل نوع لخط الأساس (أول 28 يومًا) والنافذة الحالية (من 1 يونيو فصاعدًا)، ثم نسبتها.
# main.py (continued)
BASE_END = pd.Timestamp("2025-04-29")
RECENT_START = pd.Timestamp("2025-06-01")
base_mean = observed[observed["date"] < BASE_END].groupby("species")["count"].mean()
recent_mean = observed[observed["date"] >= RECENT_START].groupby("species")["count"].mean()
ratio = recent_mean / base_mean
print(ratio.round(3))
خط الأساس هو «الطبيعي» للأنواع ، عدّادات الربيع من الأسابيع الأربعة الأولى. النافذة الحالية هي «ما يحدث الآن» ، العدّادات من يونيو فصاعدًا. قسمة الحالي على الأساس تعطي نسبة بلا أبعاد: 0.66 تعني «المشاهدات الحالية أقل بثلث من الأساس»، و1.0 تعني «على قدم المساواة»، و1.2 تعني «يزدهر». النسب تمحو المقياس، فتعمل قاعدة عتبات واحدة عبر الأنواع مهما كانت شيوعها.
🎯 الناتج المتوقع:
acorn_woodpecker 0.659
blue_jay 1.003
eastern_bluebird 1.004
northern_cardinal 1.003
tree_swallow 0.804
Name: count, dtype: float64
🩹 إذا لم يعمل : إذا أظهرت الأنواع المستقرة نسبًا مثل 1.20، فقد التقطت النافذة الحالية ذروة الموسم بينما التقط الأساس القاع ، حدود النوافذ مهمة. إذا أظهر الأكورن ~1.0، فقاموس trend لم يُطبَّق (تحقق من trend[s] * week).
2.2 القاعدة المدرّجة
👟 تلميح البداية : حوّل النسبة إلى طبقة بـalert_level(r) ، < 0.7 SURVEY، و< 1.0 WATCH، وإلا OK.
# main.py (continued)
def alert_level(r):
if r < 0.7:
return "SURVEY"
if r < 1.0:
return "WATCH"
return "OK"
for s in species:
print(f"{s:<20} {ratio[s]:.3f} {alert_level(ratio[s])}")
العتبات هي سياسة العامل: نقصٌ بثلث عن الأساس يبرر إرسال مسح ميداني؛ وأي انخفاض دون المستوى يستحق المراقبة؛ وما هو عند المستوى أو فوقه يُترك وشأنه. ينخفض نوعان عن 1.0: نقّار البلوط الأكورن عند 0.659 (عميق بما يكفي لـSURVEY) وخطّاف الرقبة عند 0.804 (WATCH). المستقرّون يجلسون عند 1.00 ، أرضية الضجيج، لا إشارة حقيقية (لاحظ أن العتبة لا تكترث بأن الفرق بين 1.003 و0.999 ضجيج خالص).
🎯 الناتج المتوقع:
acorn_woodpecker 0.659 SURVEY
blue_jay 1.003 OK
eastern_bluebird 1.004 OK
northern_cardinal 1.003 OK
tree_swallow 0.804 WATCH
🩹 إذا لم يعمل : إذا كان عمود الطبقة كله OK، فقارن alert_level str بـfloat (مرِّر النسبة، لا الملصق). إذا أظهرت إحصاءات SURVEY WATCH، فحد 0.7 هو < مقابل <= ، اختر واحدًا وكن ثابتًا.
2.3 تحقّق من الدماغ
✅ قائمة التحقق
- ✅ النسب مقارنات بلا أبعاد للحالي/الأساس؛ الأنواع المستقرة ≈ 1.00.
- ✅ الطبقات: الأكورن SURVEY (0.659)، الخطّاف WATCH (0.804)، ثلاثة OK.
- ✅
alert_levelدالة صافية ، نفس النسبة، نفس الطبقة، كل استدعاء.
🤔 سؤال (أسئلة) سقراطي(ة) :
- تحوم نسب الأنواع المستقرة ضمن ±0.005 من 1.00 ، ضجيج قياس لا اتجاه. يرى مستخدم
alert_levelWATCHعند 0.999 وOKعند 1.001. ما النطاق الميت (مثل معاملة 0.95–1.05 كـ«لا تغيير») الذي كان سيقلل الإنذارات الكاذبة، وكيف تنفذه دون تغيير روح المستويات الثلاث؟ - تقسم
ratioالحالي على الأساس. إذا كان نوع غائبًا في الأساس (أساس = 0)، فتنفجر النسبة إلىinf. ما الحارس الذي تضيفه ، وماذا سيفعل إنذار حصيف لـ«نوع ظهر جديدًا»؟
الخطوة 3: حلقة الامتصاص
لا يقيّم العامل مرة واحدة ، بل يستقبل بيانات جديدة ويعيد القرار. تغذّيه الخطوة 3 بثلاثة أسابيع من الدفعات «الجديدة» وتسجل كل قرار.
3.1 غذِّ دفعة واحدة، أعد القرار
👟 تلميح البداية : لكل من الأسابيع الثلاثة المحتجزة، ادمج الشريحة في observed, وأعد احتساب النسبة/الطبقة لكل نوع، وألحق صف (checked, species, ratio, level) واحدًا لكل نوع.
# main.py (continued)
BASE_END = pd.Timestamp("2025-04-29")
RECENT_START = pd.Timestamp("2025-06-01")
def alert_level(r):
return "SURVEY" if r < 0.7 else ("WATCH" if r < 1.0 else "OK")
def status_of(obs, checked):
base_mean = obs[obs["date"] < BASE_END].groupby("species")["count"].mean()
recent_mean = obs[obs["date"] >= RECENT_START].groupby("species")["count"].mean()
rows = []
for s in species:
r = recent_mean[s] / base_mean[s]
rows.append({"checked_after_days": checked, "species": s,
"ratio": round(r, 3), "level": alert_level(r)})
return pd.DataFrame(rows)
decisions = pd.DataFrame()
weeks = [(pd.Timestamp("2025-06-30"), pd.Timestamp("2025-07-06")),
(pd.Timestamp("2025-07-07"), pd.Timestamp("2025-07-13")),
(pd.Timestamp("2025-07-14"), pd.Timestamp("2025-07-20"))]
for week, (start, end) in enumerate(weeks, start=1):
chunk = full[(full["date"] >= start) & (full["date"] <= end)]
observed = pd.concat([observed, chunk], ignore_index=True)
status = status_of(observed, checked=90 + 7 * week)
decisions = pd.concat([decisions, status], ignore_index=True)
print(decisions.head(10).to_string(index=False))
كل تمريرة إعادة قرار: يبقى الأساس الـمثبّت على الأسابيع الأربعة الأولى (عقد تاريخي)، بينما تمتص النافذة الحالية الدفعة الجديدة ، فتتحرك النسبة بسلاسة مع وصول أسابيع جديدة. القرارات جدول نامٍ، صف واحد لكل نوع لكل فحص: 5 أنواع × 3 فحوص = 15 صفًا في النهاية.
🎯 الناتج المتوقع:
checked_after_days species ratio level
97 acorn_woodpecker 0.644 SURVEY
97 blue_jay 1.001 OK
97 eastern_bluebird 1.003 OK
97 northern_cardinal 1.003 OK
97 tree_swallow 0.789 WATCH
104 acorn_woodpecker 0.626 SURVEY
104 blue_jay 1.000 WATCH
104 eastern_bluebird 1.001 OK
104 northern_cardinal 0.998 WATCH
104 tree_swallow 0.779 WATCH
🩹 إذا لم يعمل : إذا اُدمجت chunk لكن الأرقام لم تتحرك، فاستخدم status_of نسبةً عالمية مخزنة مؤقتًا ، أعد الاحتساب من obs في كل تمريرة. إذا كان للقرارات 10 صفوف في الرأس لا 5، فتشغّل status_of أسبوعيًا و لكل نوع مرتين.
3.2 ثبّت القرارات
👟 تلميح البداية : to_csv("decisions.csv", index=False) ثم أعد قراءته لإثبات أن الطلبية تنجو الجلسة.
# main.py (continued)
decisions.to_csv("decisions.csv", index=False)
print(pd.read_csv("decisions.csv").shape)
print(pd.read_csv("decisions.csv").tail(5).to_string(index=False))
CSV قرارات هو ما يستهلكه فعلًا صاحب مصلحة غير Python: 15 صفًا، خمسة لكل فحص، كل منها بـchecked_after_days ونوع ونسبة ومستوى. إعادة قراءته بـpandas تعيد ترتيب الطلبية ، ستقرأ قائمة الأولويات في الخطوة التالية من هذا الملف نفسه.
🎯 الناتج المتوقع:
(15, 4)
checked_after_days species ratio level
111 acorn_woodpecker 0.607 SURVEY
111 blue_jay 0.999 WATCH
111 eastern_bluebird 0.999 WATCH
111 northern_cardinal 0.996 WATCH
111 tree_swallow 0.767 WATCH
🩹 إذا لم يعمل : إذا لم تكن شكل الذهاب والعودة (15, 4)، فأسقط to_csv/read_csv عمودًا (عَلَم index كتب عمودًا مشرقًا بلا اسم). إذا أظهر الذيل صفوفًا قديمة من جري سابق، فامسح الـCSV قبل الحلقة.
3.3 تحقّق من الحلقة
✅ قائمة التحقق
- ✅ ثلاث دمجات → 15 صف قرار؛ أيام الفحص
97, 104, 111. - ✅ نقّار البلوط الأكورن
SURVEYفي كل فحص؛ نسبته تسقط 0.644 → 0.626 → 0.607 (تباطؤ متسارع). - ✅
decisions.csvتعيد التردد(15, 4).
🤔 سؤال (أسئلة) سقراطي(ة) :
- سقطت نسبة الأكورن عبر الفحوص الثلاثة بينما تذبذب الأنواع المستقرة
OK ↔ WATCHقرب 1.00. أي نمط هو الإشارة وأيها الضجيج ، وماذا يخبرك الانخفاض الرتيب للنسبة لا تستطيع لقطة مفردة عند يوم 97 إخبارك به إطلاقًا؟ - الأساس مثبّت على الأسابيع الأربعة الأولى إلى الأبد. نوع تعافى إلى 2.0× الأساس ما زال يقارن بالربيع. متى يكون الأساس المتحرك (يُعاد حسابه من آخر 90 يومًا) أفضل من الثابت ، وما الخطر الجديد (تزحلق إلى انحدار يحقق نفسه) الذي يقدمه؟
الخطوة 4: تصوير الإجماليات
القرارات تخبرك أي الأنواع؛ والمخطط يخبرك كمالك كل منها. تعرض الخطوة 4 إجماليات الأنواع كمخطط أعمدة ASCII ، تصوير ودود للطرفية.
4.1 إجمالي المشاهدات لكل نوع
👟 تلميح البداية : groupby("species")["count"].sum() فوق موسم الـ111 يومًا، مصنفة تصاعديًا للمخطط.
# main.py (continued)
totals = full.groupby("species")["count"].sum().sort_values()
print(totals.to_dict())
الإجماليات تجيب «مَن وفير ومَن نادر» ، غريزة تقريرية، لا قاعدة قرار. النوعان المعلمان (الأكورن 13,223 والخطّاف 18,954) في منتصف الترتيب: ليسا الأندر، وهذا بالضبط سبب أهمية النسبة ، النوع النادر والنوع الشائع كلاهما يستحق مسحًا حين تنخفض عدّاداته عن الأساس.
🎯 الناتج المتوقع:
{'acorn_woodpecker': 13223, 'tree_swallow': 18954, 'northern_cardinal': 20705, 'blue_jay': 23398, 'eastern_bluebird': 26435}
🩹 إذا لم يعمل : إذا جمعت القيم بعنف فوق 1665×~50، فـconcat كرّر الشظايا (تشغّل الخطوة 4 قبل الحلقة ، احسب فوق full, لا observed في منتصف الامتصاص).
4.2 اعرض أعمدة ASCII
👟 تلميح البداية : اطبق كل إجمالي على "#" * round(v / max * 40) لمخطط أعمدة 40 محرفًا بثابت عرض.
# main.py (continued)
mx = totals.max()
for s, v in totals.items():
bar = "#" * round(v / mx * 40)
print(f" {s:<20} {v:6d} {bar}")
يعيد v / mx * 40 قياس أكبر الأنواع (الزرزور الشرقي الأزرق، 26,435) إلى 40 محرفًا والباقي تناسبيًا ، مخطط أعمدة لا يعتمد على الضخامة المطلقة. بدائية تصوير تعمل في أي طرفية وأي دفتر وأي منصة، وتجعل الوفرة النسبية مرئية في لمحة.
🎯 الناتج المتوقع:
acorn_woodpecker 13223 ####################
tree_swallow 18954 #############################
northern_cardinal 20705 ###############################
blue_jay 23398 ###################################
eastern_bluebird 26435 ########################################
🩹 إذا لم يعمل : إذا كان عمود فارغًا (""), فإجمالي النوع هبط إلى 0 (حارس النسبة من سؤال الخطوة 2 السقراطي كان ليحذر). إذا تجاوزت الأعمدة السطر, فـround(v / mx * 40) تبلغ ذروتها عند 40 فقط إذا v <= mx ، وهي كذلك, بتعريف max.
4.3 تحقّق من المخطط
✅ قائمة التحقق
- ✅ الإجماليات تطابق القاموس المصنف: الأكورن 13,223 … الزرزور الأزرق 26,435.
- ✅ أطول عمود (40
#) ينتمي إلى أكبر إجمالي (الزرزور الشرقي الأزرق). - ✅ العلمات والأعمدة تأتي من نفس الإطار الحتمي ، المخطط والقرارات لا يمكن أن يتناقضا.
🤔 سؤال (أسئلة) سقراطي(ة) :
- يرتب المخطط بالـإجمالي، والعامل بالـنسبة. الزرزور الشرقي الأزرق يصدّر المخطط (26,435) لكنه
OK؛ والأكورن ثاني الأقل (13,223) لكنهSURVEY. إلى أين كان سيودي مسحٌ يعرض الإجماليات فقط ، وماذا يعلّمك ذلك عن «أكثر الطيور» مقابل «أكثر تعرضًا للخطر»؟ - 40
#تعطي دقة 2.6% لكل محرف ، فلا يستطيع 13,223 و13,223+300 أن يتمايزا. للإشارة القرارية تريد مقياسًا أكبر أو مقياسًا لوغاريتميًا. متى يكون مخطط أعمدة ASCII التصوير الخاطئ لتضعه بجوار قائمة الأولويات؟
الخطوة 5: قائمة المسح ذات الأولوية
القرارات + الإجماليات ليست خطة عمل؛ يجب أن يقول العامل مَن يبدأ. أنواع SURVEY أولًا، ثم أنواع WATCH بالخطورة (أدنى نسبة)، ثم OK.
5.1 رتّب الأنواع
👟 تلميح البداية : اقرأ آخر القرارات، واقرن المستويات برقم أولوية (SURVEY=0 < WATCH=1 < OK=2)، ورتّب بـ(priority, ratio, species).
# main.py (continued)
latest = pd.read_csv("decisions.csv")
latest = latest[latest["checked_after_days"] == latest["checked_after_days"].max()]
prio = {"SURVEY": 0, "WATCH": 1, "OK": 2}
latest["priority"] = latest["level"].map(prio)
latest = latest.sort_values(["priority", "ratio", "species"])
for i, row in latest.iterrows():
print(f"{i+1:>2}. {row['level']:<6} {row['species']:<20} ratio {row['ratio']:.3f}")
الترشيح لأحدث فحص (checked_after_days == max) يُبقي الصورة الحالية فقط ، قرارات الأسبوع 1 تاريخ، لا أولويات. قرن المستوى برقم يسمح لـsort_values بعمل سياسة العمل: كل SURVEY قبل كل WATCH قبل كل OK، وتُحسم المتعادلات بالخطورة (نسبة أدنى = أسوأ) ثم بالاسم للحتمية.
🎯 الناتج المتوقع:
1. SURVEY acorn_woodpecker ratio 0.607
2. WATCH tree_swallow ratio 0.767
3. WATCH northern_cardinal ratio 0.996
4. WATCH blue_jay ratio 0.999
5. WATCH eastern_bluebird ratio 0.999
🩹 إذا لم يعمل : إذا لم يكن الأكورن أولًا، فتقران الأولوية أو مفاتيح الفرز معكوسان (رتّب بـ("priority", "ratio"), لا بالاسم). إذا طُبع أكثر من 5 صفوف، ففلتر الأقصى checked_after_days لم يعمل.
5.2 أشحن الخطة
👟 تلميح البداية : أصدر سلسلة خطة بسطر واحد كي تتضاعف الطلبية كرسالة قابلة للتنفيذ.
# main.py (continued)
plan = "; ".join(f"{row['level']}:{row['species']}"
for _, row in latest.iterrows())
print("SURVEY PLAN ->", plan)
سطر الخطة هو ما يقرؤه عالم البيئة فعليًا: SURVEY:acorn_woodpecker; WATCH:tree_swallow; …. دورة العامل الكاملة ، امتصاص → قرار → تسجيل → أولوية → رسالة ، الآن ناتج خط واحد يستطيع إنسان التصرف عليه.
🎯 الناتج المتوقع : SURVEY PLAN -> SURVEY:acorn_woodpecker; WATCH:tree_swallow; WATCH:northern_cardinal; WATCH:blue_jay; WATCH:eastern_bluebird
🩹 إذا لم يعمل : إذا أدرجت الخطة الأنواع بترتيب الملف، فـsort_values المسبق لسطر الخطة حُذف. عدم تطابق أسماء الأعمدة (ratio مقابل Ratios) يكسر الربط صمتًا ، أبقِ مخطط CSV من 3.2 كما هو تمامًا.
5.3 تحقّق من الطابور
✅ قائمة التحقق
- ✅ ترتيب الأولوية: SURVEY (الأكورن) قبل كل WATCH؛ وWATCH مصنف تصاعديًا بالنسبة.
- ✅ يخيط سطر
planكل نوع في نفس ترتيب القائمة المطبوعة. - ✅ كل شيء يُقرأ من
decisions.csv، الملف هو سجل الحقيقة.
🤔 سؤال (أسئلة) سقراطي(ة) :
- يرتّب الطابور
WATCHبالنسبة، فسبق الخطّاف (0.767) الكاردينال الشمالي (0.996). لكن نوعًا نادرًا عند 0.996 قد يكون أهش من شائع عند 0.767. ما الوزن الذي يجمع النسبة و الوفرة المطلقة في درجة أولوية واحدة ، وماذا يكلف ذلك في قابلية الشرح؟ - في هذا العامل قررت عتباتٌ اختارها إنسان (0.7/1.0). خطٌّ «آلي» بعتبات منتقاة يدويًا أتمتةٌ بإنسان في الحلقة. أين في هذا المشروع كنت تسجل اختيار العتبة كي لا يكون جري عامل مستقبلي صندوقًا أسود صامتًا؟
⚠️ مآزق شائعة
- تيّارا عشوائية اثنان. إذا أُعيد توليد الشظايا بآلية RNG خاصة بها، فتكسر البيانات «الجديدة» قابليةَ التكرار ولا يمكن شرح سجل القرارات. أبقِ الموسم كله على
default_rng(11)واحدة واقطع الشرائح. - نوافذ تشمل المستقبل.
date >= 2025-06-01حيث ينتهي إطار المرصود عند 06-29 جيد؛ لكن الفلترة بـ<=على نافذة الشريحة قد تحسب أيامًا مشتركة بين الدمج والحالة مضاعفةً. استخدم مقارنات نصف-مفتوحة (>= start & < end_next). - نسبة على أساس صفري. نوع غائب في الربيع يعطي نسبة
infوطبقة خاطئة. احرس بفرعbaseline == 0(فدائمًاSURVEYلأساس اختفى). concatمقابل التحوير.pd.concat([observed, chunk])يعيد ربط الاسم ، عادة.appendفي المكان تعيد بصمت لعب الشظايا القديمة وتضخّم النسب. أعِد الربط صراحةً دائمًا واحذف المكررات عند إعادة التشغيل.- قراءة
decisions.csvفي منتصف الحلقة. إذا كان الملف موجودًا من جري سابق، فـto_csvدون دلالات استبدال يضاعف الصفوف. اقتطع أو أعد البناء قبل كل حلقة. - فرز مفاتيح خاطئة. ترتيب الأولوية بـ
ratioوحده يضع OK عند 0.60 قبل SURVEY عند 0.90. الترتيب السياسي هوlevelأولًا، ثمratio, ثم اسم النوع.
ما بنيته للتو
عامل مسح يحوّل تيّارَ ملاحظات إلى خطة حفظ قابلة للتنفيذ: سجلات موسم اصطناعية، وخطوط أساس ونوافذ حالية لكل نوع، وسياسة عتبات من ثلاث طبقات، وحلقة امتصاص تعيد القرار مع وصول أسابيع جديدة وتلحق كل قرار بـCSV، ومخطط أعمدة ASCII يرتب الوفرة، وطابور أولوية يقول مَن يفحص أولًا. الأفكار الجوهرية تنتقل إلى أي مكان تظهر فيه عتبات + نوافذ زمنية: قارن السلوك الحديث بخط أساس مثبّت، واقرر بسياسة صغيرة قابلة للقراءة البشرية، وسجّل كل قرار كبيانات، واقرن دائمًا إشارةً (النسبة) بضخامتها (الإجماليات) ، لأن «انخفض 40%» لا يعني شيئًا حتى تعرف أنه هو نقّار البلوط الأكورن، والأنواع المستقرة سبب للنظر في مكان آخر، لا سبب للنظر بعيدًا.
شغّل نسخة أكمل دون أي إعداد محلي
examples/biodiversity-logger/ في مستودع الدورة العامل كاملًا كدفتر ، توليد الموسم، والنوافذ، وحلقة امتصاص ثلاثة أسابيع، ومخطط ASCII، وخطة الأولويات، قابل للتشغيل في Colab/Kaggle/Binder. استنسخ المستودع أو افتحه في Codespace.
إلى أين تذهب من هنا
- أضف نطاقًا ميتًا إلى
alert_level(WATCHفقط للنسب في[0.7, 0.95), وعامل0.95–1.05كـOK) كي يتوقف ضجيج الحدود عن قلب الطابور. - أضف تفصيلًا على مستوى الموقع: بدل نسبة واحدة لكل نوع، عَلِم أزواج موقع×نوع (مثل
tree_swallow@meadow)، ورقّع مخطط ASCII بالموقع. - صوّر بمكتبة رسم حقيقية:
totals.plot.barh()أوsevplot، نفس بيانات groupby تغذّي مخطط ASCII وfigure من matplotlib معًا. - جدول الحلقة: لفّ الخطوات 3–5 في دالة
run_check(observed, new_chunk)ونَادِها ليلًا، محمّلًاobservedمن الـCSV السابق بدل إعادة التوليد.
شارك مشروعك مع الصف
بنيت شيئًا تفخر به؟ examples/student-projects/ معرض لمشاريع قدّمها طلاب آخرون ، وREADME الخاص به يحوي شرحًا كاملًا وودودًا للمبتدئين لإضافة مشروعك عبر pull request، حتى لو لم تستخدم git من قبل قط: عمل fork للمستودع، وإنشاء فرع، وتثبيت ملفاتك، وفتح الـ PR، خطوة بخطوة. لا يُفترض أي خبرة سابقة بـ git.
مرحبًا بك في كتابة Python خارج المتصفح. 🎓
أكمل كل خطوة ثم حدد المشروع كمكتمل لجمع نقاطه.