PyDA Course
متوسط نصف موجّه ⏱ 90 دقيقة ⚡ +100 XP +15 نقطة لكل خطوة

سجل التنوع البيولوجي

EnvironmentAI AgentsData Visualization

لا يراقب علماء البيئة كل فرد ، بل يراقبون إشارات. انخفاض 30% أو أكثر في الطيور المرصودة عبر موسم محفّز للمسح؛ وتذبذب قرب خط الأساس يستحق المراقبة؛ وعدّ مستقر معناه «اتركه وشأنه». يبني هذا المشروع حلقة القرار تلك كـعامل مسح صغير: يحمل سجل ملاحظات بطول موسم (اصطناعي، فقابل للتكرار)، ويحسب خط الأساس ونافذة كل نوع حديثة، ويطبق قاعدة عتبات لعلم كل نوع بـSURVEY / WATCH / OK، ويمتص دفعات أسبوعية جديدة ويسجل كل قرار إلى CSV، ثم يرسم مخطط أعمدة ASCII لإجماليات الأنواع ويطبع قائمة مسح مرتبة بالأولوية. كل شيء يعمل في pandas والمكتبة القياسية، ببذرة ثابتة ، نفس الجري يعلم نفس الأنواع في كل مرة، وعلى أي جهاز.

هذا يفترض groupby وترشيحًا ودمجًا في pandas. إنه مشروع اختياري غير مُقيَّم ، راجع مشاريع من العالم الحقيقي للاطلاع على القائمة الكاملة والنامية. تركيب واحد: pandas.

🎯 ما ستفعله

  1. توليد سجل ملاحظات حتمي بطول 111 يومًا لخمسة أنواع عبر ثلاثة مواقع.
  2. كتابة دماغ العامل: نافذتا أساس وحالية زائد قاعدة عتبات من ثلاث مستويات.
  3. تشغيل حلقة الامتصاص: ابتلاع ثلاث دفعات أسبوعية، وإلحاق صف قرار واحد لكل نوع.
  4. تصوير إجماليات الأنواع كمخطط أعمدة ASCII.
  5. ترتيب الأولويات: طلب الأنواع للمسح التالي، SURVEY أولًا.

أين تُشغّل هذا

محليًا باستخدام uv هو المسار الموصى به:

uv init biodiversity-logger && cd biodiversity-logger
uv add pandas

Google Colab وKaggle Notebooks وBinder يشغّلون كل خطوة دون تعديل ، pandas مثبتة مسبقًا على المنصتين، وdefault_rng(11) الثابتة تجعل الناتج متطابقًا في كل مكان.

Open In Colab Open In Kaggle Binder

الإعداد

كل ما تحتاجه قبل أول مشاهدة.

جهّز المشروع

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 صفًا.

🤔 سؤال (أسئلة) سقراطي(ة) :

  • تضاعف ثلاثة مواقع عدّاد الأساس بشكل مختلف (forest ‏1.4×، meadow ‏0.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_level WATCH عند 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 خارج المتصفح. 🎓

أكمل كل خطوة ثم حدد المشروع كمكتمل لجمع نقاطه.