PyDA Course
مبتدئ موجّه ⏱ 60 دقيقة ⚡ +100 XP +15 نقطة لكل خطوة

محلل النوم

clicsvdatetimestatisticsscripting

كل متتبع نوم في جوهره جدول بيانات صغير مع حكم: سجّل متى ذهبت إلى الفراش ومتى استيقظت، واطرح للحصول على المدة، وقارنها بهدف، ولاحظ ما إذا كان متوسطك يزحف نحو الصحة أو بعيدًا عنها على مدار أسبوع. يبني هذا المشروع ذلك ، واجهة أوامر تسجّل ليلة، وتلتقط المصيدة الكلاسيكية للنوم بعد منتصف الليل، وتقيّم كل ليلة على المدة والاتساق، وتقرأ السجل كاملًا للمقارنة أسبوعًا بأسبوع. لا سوار لياقة و لا EEG: «المستشعر» هو أنت تكتب وقتين، والتحليل هو datetime وحسابات بايثون خالصة. إنه أصغر شيء في هذه الدورة ما زال يبدو كأداة حقيقية ستستخدمها فعلًا.

يفترض هذا أساسيات بايثون ، النصوص، datetime، إدخال/إخراج الملفات، الدوال. اختياري وغير مُقيَّم؛ راجع المشاريع الواقعية للقائمة الكاملة.

🎯 ما ستفعله

  1. اطلب وقت نوم ووقت استيقاظ وحوّلهما إلى كائنات datetime.
  2. احسب مدة النوم التي تعبر منتصف الليل بشكل صحيح.
  3. قيّم الليلة مقابل مدة هدف وقاعدة اتساق.
  4. احفظ كل ليلة مسجّلة في سجل CSV.
  5. اقرأ السجل مرة أخرى واطبع مقارنة المتوسطات أسبوعًا بأسبوع.

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

التشغيل محليًا عبر uv هو المسار الأساسي ، الأداة كلها سكربت تشغّله (uv run python sleep.py --log)، وسجل CSV ملف حقيقي تفتحه في أي محرر. المكسب هو الصدق: أنت تسجّل لياليك الخاصة، لذلك أنت الذي تختار المكان الذي تكتب فيه موعد نومك فعلًا، حاسوبك المحمول أو داخل متصفح Codespaces.

GitHub Codespaces يشغّل نفس واجهة الأوامر تمامًا: افتح codespaces.new/abderrahim-lectures/python-data-analysis-course، ويتصرف كل استدعاء لـ sleep.py تمامًا كما محليًا، مع sleep_log.csv يقطن في شجرة الملفات.

تعالج Google Colab ودفاتر Kaggle وBinder التحليل بأمانة ، حسابات المدة وتقييم الجودة ومتوسطات CSV هي بايثون خالصة وتعمل بشكل مماثل في دفتر، حيث يُعرض رسم الاتجاه أو جدول المتوسطات كنتاج خلية. الشيء الوحيد الذي لا يستطيع الدفتر فعله هو طلب المدخلات تفاعليًا بالطريقة التي تفعلها المحطة الطرفية ، لذلك في دفتر ستُغذّي قائمة إدخالات مكتوبة أو محمّلة من CSV بدلًا من input(). كلاهما نفس المحرك؛ يختلف مصدر الإدخال فقط.

Open In Colab Open In Kaggle Binder

الإعداد

فقط uv وملف CSV فارغ ينتظران ليلتك الأولى.

ثبّت uv

macOS / Linux (المحطة الطرفية):

curl -LsSf https://astral.sh/uv/install.sh | sh

Windows (PowerShell):

powershell -ExecutionPolicy ByPass -c "irm https://astral.sh/uv/install.ps1 | iex"

أغلق محطتك وأعد فتحها، ثم:

uv --version
mkdir sleep-analyzer && cd sleep-analyzer
uv init --bare

لا تبعيات من طرف ثالث ، يعمل المشروع كله على المكتبة القياسية.

جهّز عنوان CSV مسبقًا

mkdir -p data
printf "date,bedtime,waketime,hours,score\n" > data/sleep_log.csv

✅ قائمة التحقق

  • uv --version يطبع رقم إصدار.
  • ✅ يوجد data/sleep_log.csv بعنوان date,bedtime,waketime,hours,score.
  • ✅ لديك وقتان حقيقيان في ذهنك (نوم واستيقاظ الليلة الماضية) تجرب بهما.

الخطوة 1: تحليل ليلة والتحقق منها

يبدأ كل سجل نوم بأن يُكتشف خطأ كتابة قبل أن يتحول إلى صف مسموم. يقرأ المحلل وقت نوم ووقت استيقاظ بصيغة "HH:MM"، ويحوّلهما إلى datetime.time، ويرفض أي شيء ليس وقت ساعة صالحًا برسالة واضحة بدلًا من انهيار صامت. هذا هو انضباط «المدخلات الرديئة، المخرجات الصاخبة» المطبَّق على طلب من سطرين.

👟 تلميح البداية : ابدأ بكتابة parse_time(label): حلقة while True تستدعي input() على نص HH:MM، تعيد datetime.strptime(raw, "%H:%M").time() عند النجاح، وتطبع إعادة طلب ودية عند ValueError.

# sleep.py
from datetime import datetime, time, timedelta
import csv, sys
from pathlib import Path

LOG = Path("data/sleep_log.csv")
TARGET_H = 7.5

def parse_time(label: str) -> time:
    while True:
        raw = input(f"{label} (HH:MM): ").strip()
        try:
            return datetime.strptime(raw, "%H:%M").time()
        except ValueError:
            print(f"  '{raw}' isn't a valid clock time. Try e.g. 22:30 or 06:15.")

def log_night() -> dict:
    date = datetime.now().strftime("%Y-%m-%d")
    bed = parse_time("Bedtime")
    wake = parse_time("Wake time")
    hours, score = duration_and_score(bed, wake)
    return {"date": date, "bedtime": bed.strftime("%H:%M"),
            "waketime": wake.strftime("%H:%M"), "hours": round(hours, 1),
            "score": round(score, 1)}

تستمر parse_time في الحلقة حتى تحصل على "HH:MM" صالح ، datetime.strptime(raw, "%H:%M") ينتج وقتًا عند النجاح ويُطلق ValueError عند أي شيء آخر، وهذه هي التي تلتقطها while True بطباعة الإدخال المخالف وطلب الإدخال مجددًا. شكل الإرجاع قاموس واحد، بالأعمدة الخمسة كلها التي وعد بها عنوان CSV، محسوبة عبر دالة مساعدة duration_and_score ستكتبها تاليًا. التقاط المدخلات السيئة عند الباب (قبل أن تصل إلى الصف) هو الدرس كله: strptime واحدة وtry/except تُبقيان السجل نظيفًا للأبد.

🎯 الناتج المتوقع : لـ 22:30 و06:15 صالحين، قاموس مثل {'date': '2026-09-06', 'bedtime': '22:30', 'waketime': '06:15', 'hours': 7.8, 'score': 8.7}; لخطأ كتابة مثل 25:99، رسالة «ليست وقت ساعة صالحًا» وطلب إعادة إدخال.

🩹 إذا لم يعمل : إذا أطلق "23:00" صالح ValueError، فقد مرّرت التنسيق خطأً ، "%H:%M" بحرف H كبير، صيغة 24 ساعة؛ فـ %h الصغيرة ليست توجيهًا صالحًا. إذا كان مدخل رديء يُسقط السكربت بدلًا من إعادة الطلب، فحلقة while True لا تدور فعلًا (الـ return داخل try فقط) ، يجب أن يترك except الحلقة تستمر. إذا كان التاريخ خاطئًا، فـ datetime.now() مناسب لعرض تجريبي لكنه يلاحظ التوقيت المحلي، لا UTC ، وهذا اختيار مقصود هنا لأن «اليوم» هو ما يعنيه المستخدم.

✅ قائمة التحقق

  • ✅ إدخال وقتين صالحين يعيد القاموس الكامل بخمسة حقول.
  • ✅ وقت غير سليم يعيد الطلب دون انهيار.
  • ✅ مفاتيح القاموس تطابق عنوان CSV من الإعداد تمامًا.

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

  • تستمر parse_time في الحلقة حتى يكون الإدخال صالحًا، لكن ماذا لو ضغط المستخدم Enter بإدخال فارغ؟ .strip() تحوّل "" إلى فشل strptime يعيد الطلب ، لكن وقت نوم فارغًا قد يكون إشارة «نسيت» شرعية. أي سلوك تختار أداة ودية ولماذا (إعادة الطلب للأبد مقابل تركها تُتجاوز)؟
  • يقبل هذا المحلل أي وقت من صيغة 24 ساعة، بما في ذلك 13:00. «وقت نوم» في 13:00 غفوة أو خطأ كتابة ، لكن الأداة لا يمكنها أن تعرف. ما فحص النطاق (مثل وقت نوم بين 18:00 و04:00) الذي يضيف معنى، وأين ينتمي: داخل parse_time أم بعده؟

الخطوة 2: حساب المدة عبر منتصف الليل

أول «مصيدة» في رياضيات النوم: إذا نمت في 23:00 واستيقظت في 06:30، فأنت لم تنم سالب 16.5 ساعة ، بل عبرت منتصف الليل. تحسب هذه الدالة المساعدة المدة صحيحًا باكتشاف الالتفاف وإضافة دقائق يوم كامل، ثم تحوّلها إلى ساعات. الطرح الساذج wake - bed هو الخطأ الذي يصطدم به كل مبتدئ؛ هذه الخطوة تسميه وتصلحه.

👟 تلميح البداية : ابدأ بكتابة تحويل الدقائق داخل duration_and_score: كشف التفاف منتصف الليل بـ if wake_min >= bed_min، وأضف 24 * 60 - bed_min + wake_min في فرع العبور، ثم حوّل إلى ساعات قبل التقييم.

# sleep.py (continued)

def duration_and_score(bed: time, wake: time) -> tuple[float, float]:
    bed_min = bed.hour * 60 + bed.minute
    wake_min = wake.hour * 60 + wake.minute
    if wake_min >= bed_min:           # same-day: went to bed and woke later the SAME day
        minutes = wake_min - bed_min
    else:                             # crossed midnight
        minutes = (24 * 60 - bed_min) + wake_min
    hours = minutes / 60.0

    duration_score = min(hours / TARGET_H, 1.0)          # 100% when you hit target
    consistency = 0.8 if hours >= TARGET_H else 0.9      # small bonus for doctor nudge
    score = (100 * duration_score) * consistency
    if hours >= TARGET_H:
        score = 100.0 + min((hours - TARGET_H) * 10, 25)  # a little bonus for sleeping in
    return hours, score

محور المدة هو if wake_min >= bed_min: نفس اليوم (نوم في 06:00 واستيقاظ في 09:00، غفوة وردية ليلية) يُطرح بشكل طبيعي؛ وفرع عبور منتصف الليل يجمع الدقائق من وقت النوم إلى منتصف الليل (24*60 - bed_min) زائد الدقائق من منتصف الليل إلى الاستيقاظ (wake_min). يثني التقييم نسبة مئوية بسيطة بجانبين ، «إصابة الهدف» بدرجة حرفية (محدودة عند 100 من المدة، ومكافأة صغيرة للنوم في نطاق صحي) وخصم اتساق لقلة النوم ، لذلك تحصل ليلة 7.5 ساعات على نتيجة أعلى من ليلة 4 ساعات، وتحصل رتابة قصيرة قليلًا لكنها منتظمة على نتيجة أعلى من التذبذب.

🎯 الناتج المتوقع : bed=23:00, wake=06:30(7.5, 100.0); bed=23:00, wake=05:00(6.0, 72.0); bed=01:00, wake=01:30 (غفوة 30 دقيقة) → (0.5, 5.3…); عبور منتصف الليل يعطي دائمًا قيمة ساعات موجبة معقولة.

🩹 إذا لم يعمل : إذا أعادت ليلة تعبر منتصف الليل رقمًا سالبًا أو ضخمًا، فقد اشتعل فرع if wake_min >= bed_min حين كان يجب أن يسقط في فرع العبور ، شرط الالتفاف على الدقائق لا الساعات؛ تحقق أن المقارنة wake_min >= bed_min (كلاهما دقائق). إذا أنتج bed=23:00, wake=06:30 القيمة 16.5، فأنت طرحت دون التفاف ، هذا هو بالضبط خطأ «سالب 16.5»؛ أعد التشغيل عبر فرع else. إذا تجاوزت النتائج 100 أو هبطت بغرابة، فمنطق القناع/المكافأة عدّ مرتين ، اطبع duration_score وconsistency منفصلين لترى أي حدّ تجاوز.

✅ قائمة التحقق

  • ✅ عبور منتصف الليل يعطي دائمًا قيمة ساعات موجبة في نطاق معقول.
  • 7.5 ساعات → نتيجة 100 (إصابة الهدف)؛ 6.0 → أقل بوضوح؛ غفوة → منخفضة جدًا.
  • ✅ النوم بعد الهدف يكسب مكافأة متواضعة محدودة، لا تضخمًا جامحًا.

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

  • يعمل إصلاح عبور منتصف الليل لزوج HH:MM واحد. لكن عاملًا يمتد ورده بعد الفجر (نوم 08:00، استيقاظ 18:00) ، هل يتعامل فرع wake_min >= bed_min معه كنوم طويل 10 ساعات، أم يسمّيه تسمية خاطئة؟ تتبّع الحدود التي عندها يتوقف افتراض «نفس اليوم» عن الصحة (ماذا عن نوم 20 ساعة؟).
  • يحد التقييم حدّ المدة عند 100 ويضيف مكافأة للنوم بعد الهدف. هل ليلة 10 ساعات «أفضل» حقًا من ليلة 8 صحية، أم أن المكافأة اختراع مريح؟ ناقش كيف كان باحث نوم ليقيّم ليلة 5 ساعات مقابل ليلة 10، وكيف يخفي تبسيط صيغتك تلك الدقة الطبية.

الخطوة 3: الحفظ في سجل CSV

ليلة مسجلة واحدة حقيقة؛ أما السجل فمادةً تصنع منها الاتجاهات. تلحق هذه الخطوة القاموس المُحلَّل من الخطوة 1 بالملف، فيكبر CSV صفًا واحدًا لكل استدعاء. نمط «اكتب مرة، وألحق للأبد» هو نفسه الذي يُبقي سجل أسابيع متعددة رخيصًا ، وقد لمست العنوان مسبقًا في الإعداد.

👟 تلميح البداية : ابدأ بكتابة append_row(row) مع csv.DictWriter وقائمة fieldnames صريحة في وضع "a"، ثم اربط main_entry لتسجّل ليلة وتطبع الصف المحفوظ.

# sleep.py (continued)

def append_row(row: dict) -> None:
    header = ["date", "bedtime", "waketime", "hours", "score"]
    with LOG.open("a", newline="") as f:
        w = csv.DictWriter(f, fieldnames=header)
        w.writerow(row)

def main_entry() -> None:
    row = log_night()
    append_row(row)
    print(f"saved {row['date']}: {row['hours']}h, score {row['score']}")

if __name__ == "__main__":
    main_entry()

تفتح append_row السجل في وضع الإلحاق ("a") وتستخدم csv.DictWriter بقائمة fieldnames صريحة، فتُكتب كل صف جديد بترتيب العنوان بغض النظر عن ترتيب مفاتيح القاموس ، مكسب صحة صغير لكنه حقيقي (كان قاموس مبعثر سيلبّك الأعمدة). حارس if __name__ == "__main__" هو مفتاح السكربت/الاستيراد: شغّله فيسجّل ليلة؛ استورده فتصبح الدوال قابلة لإعادة الاستخدام دون آثار جانبية. سطر «اطبع ما حفظت» يغلق الحلقة ، يرى المستخدم الصف نفسه الذي استقر في CSV.

🎯 الناتج المتوقع : سطر طرفية saved 2026-09-06: 7.5h, score 100.0، وصف جديد يُلحق بـ data/sleep_log.csv بالأعمدة الخمسة كلها بترتيب العنوان، بعمود التاريخ معبأً.

🩹 إذا لم يعمل : إذا تكرر العنوان على كل صف، فـ append_row تكتب العنوان في كل مرة ، أنت تنادي w.writeheader() داخل الإلحاق؛ اكتب العناوين عند إنشاء الملف فقط (الإعداد)، لا عند كل إلحاق. إذا ظهرت الأعمدة مبعثرة، فـ fieldnames لا تطابق ترتيب سلسلة العنوان ، طابق القائمة مع عنوان الإعداد تمامًا. إذا ظهر TypeError: not enough fields، فالقاموس فيه مفتاح ليس ضمن fieldnames أو ناقص أحدها ، يجب أن يحتوي قاموس الخطوة 1 بالضبط هذه المفاتيح الخمسة.

✅ قائمة التحقق

  • ✅ إلحاق واحد يضيف سطرًا واحدًا بالضبط إلى CSV، مطابقًا للعنوان.
  • ✅ إعادة تشغيل السكربت تلحق بالسجل ولا تقتطعه.
  • ✅ الملخص المطبوع يطابق ما كتبه DictWriter فعلًا.

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

  • الإلحاق في وضع "a" يعني أن برنامجين يلحقان بنفس CSV (دفتر وواجهة أوامر) قد يتشابكا الصفوف. ماذا تتنازل عنه CSV مقارنة بمخزن صفوف مقفول، ومتى يستحق فقدان التكامل هذا التسامح في سجل شخصي؟
  • يخزن كل صف hours وscore رغم أن كليهما قابل للاشتقاق من bedtime/waketime. ما حجة تخزينهما (مُعاد إنتاجه بذاته، واصف لنفسه) مقابل إعادة حسابهما عند القراءة (مصدر حقيقة واحد)؟ اختر جانبًا سيدافع عنه محلل صغير.

الخطوة 4: قراءة السجل وحساب المتوسطات

السجل الذي لا تقرؤه يوميات لا تفتحها. تحمّل هذه الخطوة كل صف من CSV، وتحوّل عمود hours إلى أعداد عشرية، وتطبع متوسط المدة والنتيجة ، أول إجابة «كيف حالي» حقيقية تنتجها الأداة.

👟 تلميح البداية : ابدأ بكتابة summarize(history) التي تحرس حالتي الملف المفقود وCSV الفارغ برسائل ودية، ثم تحوّل عمودي hours وscore وتطبع متوسطيهما.

# sleep.py (continued)

def summarize(history: Path = LOG) -> None:
    if not history.exists():
        print("no log yet")
        return
    rows = list(csv.DictReader(history.open()))
    if not rows:
        print("no entries yet")
        return
    durations = [float(r["hours"]) for r in rows]
    scores = [float(r["score"]) for r in rows]
    avg_h = sum(durations) / len(durations)
    avg_s = sum(scores) / len(scores)
    print(f"{len(rows)} nights logged")
    print(f"avg duration: {avg_h:.2f} h   (target {TARGET_H})")
    print(f"avg score   : {avg_s:.1f} / 100")

if __name__ == "__main__":
    import sys
    if len(sys.argv) > 1 and sys.argv[1] == "--summary":
        summarize()
    else:
        main_entry()

يعيد csv.DictReader كل صف كقاموس مفاتيحه من العنوان، فيتحول نفس fieldnames من الخطوة 3 إلى مفاتيح عند القراءة ، الرحلة ذهابًا وإيابًا متماثلة. سطرا المتوسط (sum(durations)/len(durations)) هما «التحليل» كله لهذه الخطوة، لكن الانضباط في الحراس: ملف غائب («no log yet») وجسم فارغ («no entries yet») يعيدان رسائل صاخبة ودية بدلًا من ZeroDivisionError الناجم عن القسمة على صفر. علم --summary بوابة CLI صغيرة ، شغّل معه للقراءة وبدونه للتسجيل ، تحوّل السكربت إلى أداة بنمطين.

🎯 الناتج المتوقع : بعد تسجيل ليلتين أو ثلاث، يطبع summary 3 nights logged، avg duration: 7.28 h (target 7.5)، avg score: 92.6 / 100.

🩹 إذا لم يعمل : إذا أطلق float(r["hours"]) ValueError، فخلية hours في صفٍّ تالفة (حرف شارد من تحرير يدوي) ، DictReader محلل رفيع؛ أضف try/except حول float(...) لتتخطى وتحذّر بدلًا من الموت. إذا أخطأ المقسم، فـ rows فارغ ولم يلتقطه حارس len(rows) ، يجب أن يأتي حارس القائمة الفارغة قبل القسمة. إذا تجاهل --summary، فتحليل sys.argv جرى بعد أن استدعت حزمة else دالة main_entry ، تحقق من ترتيب if __name__.

✅ قائمة التحقق

  • ✅ تشغيل --summary يطبع الأعداد والمتوسطات الصحيحة على الصفوف المسجلة.
  • ✅ حالات الملف المفقود وCSV الفارغ كلتاهما تعيدان رسائل ودية لا انهيارات.
  • --summary وحده يقرأ؛ بلا --summary يسجّل ، النمطان متميزان.

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

  • يخفي المتوسط الانتشار: متوسط 7.0 قد يكون سبع ليالٍ من 7.0 أو ثلاثًا من 10 وأربعًا من 4. ما إحصاء واحد (مثل نطاق max-min أو std للمدد) يكشف الفرق الذي يمحوه متوسط بسيط ، ولماذا التباين هو الإشارة الأكثر دلالة على النوم من المتوسط وحده؟
  • مستخدم يسجّل ليلة 5 ساعات وليلة 10 يحصل على متوسط 7.5 نفسه كمن يسجّل ليلتين من 7.5 ، لكن طبيب نوم يهتم بأيهما. ما المقياس الأسبوعي (أقل ساعات، أسوأ ليلة، أو التباين) الذي تحتاجه الأداة لمواجهة هذا الوهم؟

الخطوة 5: المقارنة أسبوعًا بأسبوع

ترتقي الخطوة الأخيرة من «كيف حالي» إلى «هل أحسنت أكثر من الأسبوع الماضي؟» تقسم السجل أسابيع، تحسب متوسط أحدث اثنين، وتُبلغ الفرق ، خط الاتجاه الذي وُجد تتبع العادات أصلاً ليستخلص منه. هنا يتحول التسجيل الخام إلى معرفة ذاتية.

👟 تلميح البداية : ابدأ بكتابة trend(history) التي تجمّع الصفوف في مجموعات شهر date[:7]، تحسب متوسط ساعات كل مجموعة، وتطرح آخر متوسطين لطبع فرق بعلامة إضافة إلى حكم improving/declining/steady.

# sleep.py (continued)
from collections import defaultdict

def trend(history: Path = LOG, weeks: int = 2) -> None:
    rows = list(csv.DictReader(history.open()))
    weekly: dict[str, list[float]] = defaultdict(list)
    for r in rows:
        week = r["date"][:7]                      # "2026-09" month-group as a cheap week proxy
        weekly[week].append(float(r["hours"]))
    keys = sorted(weekly)[-weeks:]
    avgs = {k: sum(v) / len(v) for k, v in weekly.items()}
    if len(avgs) < 2:
        print("need at least two distinct weeks to compare")
        return
    k0, k1 = keys[0], keys[1]
    delta = avgs[k1] - avgs[k0]
    print(f"week {k0}{k1}")
    print(f"avg hours {avgs[k0]:.2f}{avgs[k1]:.2f}  ({delta:+.2f})")
    print("improving" if delta > 0 else "declining" if delta < 0 else "steady")

if __name__ == "__main__":
    if len(sys.argv) > 1 and sys.argv[1] == "--trend":
        trend()
    elif len(sys.argv) > 1 and sys.argv[1] == "--summary":
        summarize()
    else:
        main_entry()

«التجميع بالشهر» هو البديل الصادق عن «الأسبوع»: يضع الصفوف في دلاء بأول 7 أحرف من تاريخ ISO ("2026-09")، فتصبح مجموعة ليالي الشهر دلوًا واحدًا. متوسط كل دلو وطرح آخر اثنين يعطي الفرق ، وحكم الطباعة +0.45/-0.30 (تحسّن، تراجع، ثبات) هو ملخص الاتجاه بكلمة. حارس len(avgs) < 2 يُبقي الدالة صادقة حين توجد بيانات شهر واحد فقط. إنه ساذج (الشهر ليس أسبوعًا)، لكنه شكل تحليل الاتجاه الحقيقي: قسم بدلًا، احسب المتوسط، اطرح، واحكم.

🎯 الناتج المتوقع : بصفوف موزعة على قيمتي YYYY-MM مختلفتين، يطبع trend المتوسطين وفرقًا بعلامة إضافة إلى تسمية improving/declining/steady; ببيانات شهر واحد، يطبع need at least two distinct weeks to compare.

🩹 إذا لم يعمل : إذا انهار كل تاريخ في مجموعة واحدة، فلا يوجد في CSV لديك تباين تواريخ ، trend على شهر واحد بلا أثر (need at least two)، وهو صحيح؛ أضف يدويًا صفًا بتاريخ أقدم للاختبار. إذا انقلبت علامة الفرق، فمفاتيح الفرز تنازلية (sorted(weekly)[-weeks:] تلتقط آخر اثنتين) ، أكّد أن keys بترتيب الأقدم→الأحدث حتى يعني k1 - k0 «الأحدث ناقص الأقدم». إذا انطلق KeyError، فقد اصطدم تاريخ صف بـ float() على قيمة غير رقمية ، بديل الأسبوع [:7] آمن، لكن خلية hours تالفة مجددًا؛ احرسها.

✅ قائمة التحقق

  • ✅ مع مجموعتي شهور مختلفتين على الأقل، يطبع الاتجاه المتوسطين وفرقًا بعلامة وحكمًا.
  • ✅ سجل دلو واحد يعيد رسالة need at least two الودية.
  • ✅ اتجاه الحكم (improving مقابل declining) يطابق علامة الفرق.

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

  • مفتاح التجميع date[:7] ، شهر، لا أسبوع حقيقي من 7 أيام. ما الخطأ الدقيق الذي يطفو إذا سجّلت كل يوم مدة 45 يومًا (مجموعة 15 يومًا ومجموعة 30 يومًا تُعالج متساويتين في المتوسط)؟ كيف يصلحه مرساة أسبوعية (date - datetime.timedelta(days=weekday))؟
  • الحكم علامة +/-/0 واحدة من المتوسطات، تخفي التباين مجددًا. صغ الجملة التي سيقولها مدرب نوم مستخدمًا كلًّا من متوسط الاتجاه و انتشاره (مثل «متوسطك ثابت، لكن أسوأ ليلة انخفضت») ، وسمِّ الرقمين اللذين تحتاجهما من الخطوة 5 لقولها.

⚠️ مآزق شائعة

  • خطأ «النوم السالب». wake - bed حين يعبر وقت النوم منتصف الليل يعطي سالبًا ضخمًا. ارتكز دائمًا على wake_min >= bed_min لإضافة دقائق اليوم ، الخطأ الأكثر شيوعًا في رياضيات النوم.
  • إلحاق العنوان بكل صف. كتابة writeheader() داخل append_row تكرر العنوان وتفسد DictReader عند القراءة. اكتب العنوان عند إنشاء الملف فقط (الإعداد)، ثم ألحق صفوف البيانات فقط.
  • خطأ %H:%M عارٍ. %h الصغيرة أو %I (12 ساعة) تُحلَّل بصمت أو تخطئ. "%H:%M" صيغة 24 ساعة؛ استخدمها باستمرار للإدخال والإخراج أو تتوقف مراتك ومتوسطاتك المخزنة عن التطابق.
  • ترميز مفاتيح قد تنجرف. يجب أن يظل fieldnames عند الكتابة، وعنوان CSV، ومفاتيح قاموس log_night متزامنة ، ثلاث نسخ من الأسماء الخمسة نفسها. عرّف العنوان مرة واحدة (ثابت وحدة) وأعد استخدامه للكتابة والقراءة معًا.
  • الثقة بـ avg فوق الانتشار. متوسط 7.0 قد يخفي تذبذبات 10+4. ما إن تملك أسابيع متعددة، بلّغ عن الفرق (الخطوة 5) وتَبِعَه بالتباين إن استطعت ، ولا تدع متوسطًا واحدًا يروي قصة العادة كلها.

ما بنيته للتو

محلل نوم صغير مفيد فعلًا: حللت وحققت من وقتين للساعة، حسبت المدة التي تعبر منتصف الليل صحيحًا، قيّمت كل ليلة مقابل هدف مع دفع اتساق ومكافأة نوم زائد، ألحقت كل ليلة في CSV، قرأت السجل مرة أخرى في متوسطات، وقارنت مجموعات شهرية لتسمي اتجاهًا. الكود القابل للنقل أوسع من النوم: حلقة إدخال parse_time بأخطاء صاخبة، ورياضيات لف منتصف الليل، وانضباط «عنوان مرة / إلحاق للأبد» في CSV، وشك «المتوسط ليس الانتشار» ، كلها عادات ستستخدمها في أي سكربت له شكل بيانات.

شغِّل نسخة أكمل دون أي إعداد محلي

examples/sleep-analyzer/ في مستودع الدورة يجمع وحدة المحلل، وsleep_log.csv مزروعًا، ودفترًا يحسب المدد والنتائج والمتوسطات والاتجاه سطرًا بسطر (مع رسوم نواتج خلايا). استنسخه، أو افتح المستودع كله في GitHub Codespaces، وابدأ التسجيل.

إلى أين تذهب من هنا

  • أسبوع حقيقي لا شهر: جمّع بـ (datetime - timedelta(days=date.weekday())) لمجموعات 7 أيام فعلية ، إصلاح الخطوة 5، مُدوَّنًا.
  • بلّغ الانتشار أيضًا: أضف min/max (أو statistics.stdev) للمدد أسبوعيًا ليحمل خط الاتجاه تباينه، مسدودًا فجوة «المتوسط يخفي التذبذب».
  • رسم بياني: حمّل CSV في Matplotlib وارسم الساعات عبر الوقت مع الهدف كخط متقطع ، الملف نفسه، صورة تُقرأ الآن بلمحة.
  • تحرير CSV متعدد الأسابيع: اربط مسار «حرر الليلة الماضية» صغيرًا يعيد كتابة صف مصحح في مكانه، مُبقيًا السجل صادقًا بعد خطأ تسجيل.

شارك مشروعك مع الصف

اكتشفت تحسن نومك، أو بنيت رسمًا لأسابيعك؟ examples/student-projects/ معرض مشاريع قدمها طلاب آخرون، وREADME يشرح إضافة مشروعك عبر طلب سحب (pull request) من البداية إلى النهاية: النسخ، التفريع، الالتزام، وفتح PR. لا تُفترض أي خبرة سابقة بـ git.

مرحبًا بك في كتابة بايثون خارج المتصفح. 🎓

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