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

مراقب استهلاك الطاقة المنزلي

clicsvcalculations

فاتورة الكهرباء الخاصة بك صندوق أسود: رقم واحد كل شهر ورفع أكتاف. هذا المشروع يكسرها. ستقرأ أرقام أجهزة حقيقية ، واط، ساعات في اليوم ، من ملف CSV، وتحسب الطاقة بالوحدة التي تُفوَّر بها المرافق فعلًا (كيلوواط-ساعة)، وترتب الأجهزة بحصة كل منها من الإجمالي، وتسعّر تعرفة متدرجة (الزيادة على الحد تكلف أكثر)، وتدقق ما تحرقه الأجهزة وهي جالسة في وضع الاستعداد فقط، وتقيّم سيناريو “ماذا لو قلّلت استخدام المدفأة” بالدولار. الرياضيات أربع معادلات حسابية؛ والمهارة تحويل التصنيفات المتناثرة إلى تقرير صادق جاهز للقرار.

هذا يفترض إنهاء Python 101 ، القوائم، القواميس، الحلقات، الدوال ، مع إلمام مريح بقراءة csv. لا شيء هنا يحتاج pandas. هذا اختياري وغير مُقيَّم؛ راجع مشاريع من العالم الحقيقي للاطلاع على القائمة الكاملة والنامية.

🎯 ما ستفعله

  1. تحمّل تصنيفات الأجهزة وتحسب كيلوواط-ساعة في اليوم والشهر لكل جهاز.
  2. تجمع الشهر وترتب كل جهاز بحصته من الفاتورة.
  3. تسعّر الإجمالي بتعرفة متدرجة من شريحتين ، فوق 250 كيلوواط-ساعة يكلف أكثر.
  4. تدقق استهلاك وضع الاستعداد وتجعل الكود يقترح ما يستحق فصله من الكهرباء.
  5. تقارن استخدام “الحالي” مقابل “المحسَّن” وتُبلّغ عن الدولارات الموفَّرة.

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

محليًا باستخدام uv هو المسار الموصى به ، مراقب الطاقة CLI ملفٌّ داخِل/ملفٌّ خارج (CSV داخلة، تقرير مطبوع خارج)، والملفات تنتمي إلى طرفيتك.

GitHub Codespaces بديل بلا إعداد: افتح مستودع الدورة كاملًا في Codespace مجاني (Node وPython مثبّتان بالفعل) وشغّل الأوامر نفسها من طرفية في المتصفح.

Google Colab أو Kaggle Notebooks أو Binder تعمل ، الدفتر في examples/energy-monitor/notebook.ar.ipynb يشغّل التقرير نفسه على أجهزة العينة المرفقة في الذاكرة.

Open In Colab Open In Kaggle Binder

الإعداد

uv أداة واحدة تحل محل سلسلة “ثبّت Python، ثم pip، ثم أداة بيئة افتراضية” ، وهذا المشروع بالمكتبة القياسية النقية.

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

ثم أعدّ المشروع:

uv init energy-monitor
cd energy-monitor

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

  • ✅ يطبع uv --version رقم إصدار.
  • ✅ يوجد energy-monitor/ مع pyproject.toml.
  • ✅ ينجح python -c "import csv" ، لا حزم طرف ثالث.

الخطوة 1: كيلوواط-ساعة لكل جهاز

تُفوَّر الطاقة بوحدة كيلوواط-ساعة (kWh) ، طاقة جهاز بقدرة 1000 واط يعمل ساعة واحدة. ملصقات الأجهزة تعطي الواط وعاداتك تعطي الساعات، فالتحويل watts / 1000 * hours. مدفأة 1500 واط تعمل 3 ساعات تستهلك 1.5 × 3 = 4.5 كيلوواط-ساعة يوميًا ، ولن تخمّن ذلك أبدًا من الملصق وحده. هذه الخطوة تحوّل التصنيفات إلى الرقم الواحد الذي يهم.

1.1 اكتب مُحمِّل الأجهزة والمحوِّل

👟 تلميح البداية : أبقِ أعمدة CSV الخام، ثم اشتق kwh_per_day وkwh_per_month في الحلقة نفسها:

cat > appliances.csv <<'EOF'
device,device_type,watts,avg_hours_per_day
fridge,kitchen,150,24
tv,living_room,120,5
router,network,10,24
heater,bedroom,1500,3
ps5,gaming,200,2
EOF
# energy.py
import csv

def load_appliances(path: str = "appliances.csv") -> list[dict]:
    with open(path, newline="") as f:
        rows = list(csv.DictReader(f))
    for row in rows:
        row["watts"] = float(row["watts"])
        row["avg_hours_per_day"] = float(row["avg_hours_per_day"])
        row["kwh_per_day"] = row["watts"] / 1000 * row["avg_hours_per_day"]
        row["kwh_per_month"] = row["kwh_per_day"] * 30
    return rows

if __name__ == "__main__":
    for a in load_appliances():
        print(f"{a['device']:<9} {a['device_type']:<11} {a['watts']:>5.0f} W "
              f"{a['avg_hours_per_day']:>5.1f} h/d  {a['kwh_per_day']:>6.2f} kWh/d "
              f"{a['kwh_per_month']:>7.2f} kWh/mo")

list(csv.DictReader(f)) يلتقط كل الصفوف دفعة واحدة ، الحلقة التي تُثريها لا تعيد قراءة الملف أبدًا. كل حقل مشتق دالة نقية للأعمدة الخام داخل الصف نفسه، مُضاف حيث يولد الصف. 30 كشهر وكيل متعمَّد؛ صراحةً شهر مدوَّر، لا 31 ولا كسري، لذا تكون الأرقام مستقرة وقابلة لإعادة الإنتاج ، مدقق طاقة يقول “شهر 30 يومًا” بصوت عالٍ بدلًا من الادعاء أن التقويم منتظم.

🎯 الناتج المتوقع :

fridge    kitchen       150 W  24.0 h/d    3.60 kWh/d   108.00 kWh/mo
tv        living_room   120 W   5.0 h/d    0.60 kWh/d    18.00 kWh/mo
router    network        10 W  24.0 h/d    0.24 kWh/d     7.20 kWh/mo
heater    bedroom      1500 W   3.0 h/d    4.50 kWh/d   135.00 kWh/mo
ps5       gaming        200 W   2.0 h/d    0.40 kWh/d    12.00 kWh/mo

🩹 إذا لم يعمل : إذا طبع جهاز 0.00 kWh، فـ watts أو avg_hours_per_day ما زال سلسلة نصية عند القسمة ، float() ناقصة على أحدهما. إذا أظهر المدفأة 1500 W لكن 4.50 لم تزد أبدًا، فخطأ إملائي في عنوان عمود (watts مقابل watt) جعل row["watts"] مفتاح سلسلة جديدًا ، اطبع row.keys() لمقارنته بصف العناوين.

1.2 تحقّق من التحويل

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

  • ✅ كل kwh_per_day مشتق يساوي watts / 1000 × hours يدويًا (الثلاجة: 150/1000×24 = 3.6).
  • kwh_per_month هي بالضبط 30× kwh_per_day ، لا تقويم نباتي، لا ارتخاء.
  • ✅ إثراء float() يغطي العمودين العدديين الخام، لذا sum(...) لا يدمج سلاسل أبدًا.

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

  • avg_hours_per_day للثلاجة هو 24 ، تعمل دائمًا. كيف سيبدو فرق فريزر جديد مقابل ثلاجة بيرة قديمة في هذا النموذج، وما المدخل الثاني الذي سيضيفه مراقب حقيقي بدلًا من متوسط واحد؟
  • المدفأة 1500 واط على الملصق. سمِّ رقمًا واقعيًا أقل من الملصق في المتوسط (تدور، لا تعمل دائمًا) وواحدًا أعلى (حرارة مقاومة بمخفت تضاد النموذج). أي اتجاه يجعل kwh تُوخَّم فوق الواقع، وأيٌّ تحته؟

الخطوة 2: الإجمالي والحصة

جدول لكل جهاز قائمة طعام؛ والإجمالي وحصة كل جهاز هما القصة. مع total = sum(...)، تقفز المدفأة عند ~48% كـ”نصف فاتورتك”، بينما يُكشف الراوتر عند أقل من 3% بأنه لا يُذكر. هذه الخطوة تطبع النسبة التي تُتخذ على أساسها القرارات.

2.1 اكتب تقرير الحصة

👟 تلميح البداية : sum(a["kwh_per_day"] for a in appliances) مرة واحدة، ثم share = device_kwh / total * 100 داخل حلقة الطباعة:

# report.py
import energy
from pathlib import Path

appliances = energy.load_appliances()
total_day = sum(a["kwh_per_day"] for a in appliances)
total_month = total_day * 30

print(f"{'device':<9} {'type':<11} {'kWh/d':>6} {'kWh/mo':>8} {'share':>6}")
for a in sorted(appliances, key=lambda a: a["kwh_per_day"], reverse=True):
    share = a["kwh_per_day"] / total_day * 100
    print(f"{a['device']:<9} {a['device_type']:<11} {a['kwh_per_day']:>6.2f} "
          f"{a['kwh_per_month']:>8.2f} {share:>5.1f}%")

print(f"\ntotal: {total_day:.2f} kWh/day = {total_month:.2f} kWh/month")

sum(a["kwh_per_day"] for a in appliances) مولد ، لا قائمة وسيطة، تمريرة واحدة، والإجمالي لا يمكن أن ينحرف عن قيم الصفوف لأنه محسوب من الحقل نفسه. sorted(..., reverse=True) يعيد الترتيب للعرض فقط؛ القائمة الأساسية للقواميس تبقى كما هي، لذا الخطوة 3 تعيد استخدام نفس الصفوف. الحصة جزء / كل × 100، والمقام يأتي من البيانات لا من ثابت سحري أبدًا.

🎯 الناتج المتوقع :

device    type        kWh/d   kWh/mo   share
heater    bedroom      4.50   135.00   48.2%
fridge    kitchen      3.60   108.00   38.5%
tv        living_room  0.60    18.00    6.4%
ps5       gaming       0.40    12.00    4.3%
router    network      0.24     7.20    2.6%

total: 9.34 kWh/day = 280.20 kWh/month

🩹 إذا لم يعمل : إذا خرج الترتيب تصاعديًا سحريًا، فـ reverse=True ناقصة في sorted. إذا أظهرت كل حصة 100.0% (كل جهاز مُقسَّم على نفسه)، فمتغير الحلقة a يُستخدم كلا الأمرين كصفًا وtotal_day ، يجب أن يُحسب تعبير المجموع قبل الحلقة، خارجها.

2.2 تحقّق من الترتيب

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

  • ✅ الإجمالي صفوف: 9.34 × 30 = 280.20 ، متسق مع صفوف الخطوة 1.
  • ✅ الحصص تصل إلى 100.0% (الكل إما على الفاتورة أو ليس عليها؛ لا انجراف تقريب فوق العُشر).
  • ✅ المدفأة أولًا والراوتر أخيرًا، والفجوة تبدو كما يتصرف السلوك (الثرموستات ≠ معدات شبكة تعمل دائمًا).

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

  • الحصص نسبة من الطاقة، لا نسبة من الفاتورة ، الاثنان يتساويان فقط تحت تعرفة ثابتة. الخطوة 3 تقدم تعرفة متدرجة. أي جهاز ستتقَلّص حصته تحت التدرج، ولماذا ، آخر ~30 كيلوواط-ساعة تُفوَّر بالسعر المميز، لكن ليس كل جهاز ولّدها؟
  • الإجمالي 280.20 كيلوواط-ساعة/شهر، رقم منزلي معقول. أين سيختلف رسم منزل حقيقي عن CSV هذا (ذروة مقابل ليل، موسم تدفئة، شحن سيارة كهربائية)؟ وما أول عمود يجعل هذا النموذج موسميًا؟

الخطوة 3: سعّر تعرفة متدرجة

نادرًا ما تُفوِّر المرافق سعرًا ثابتًا لكل كيلوواط-ساعة: تحت الشريحة، الطاقة رخيصة؛ فوقها، كل كيلوواط-ساعة إضافي يكلف أكثر. هذه الخطوة تسعّر 280.2 كيلوواط-ساعة بتعرفة أول 250 كيلوواط-ساعة بـ 0.20،وكلمافوقهبـ0.20، وكل ما فوقه بـ 0.35 ، وتُظهر العلاوة التي تضيفها آخر 30.2 كيلوواط-ساعة بهدوء.

3.1 اكتب حاسبة الفاتورة المتدرجة

👟 تلميح البداية : bill_for(kwh) تُرجع التكلفة الأساسية لـ kwh <= 250 وتقسّم ما فوقه إلى شريحتين:

# tariff.py
import energy

BASE_RATE = 0.20      # $/kWh for the first 250 kWh
BRACKET = 250         # kWh
HIGH_RATE = 0.35      # $/kWh above the bracket

def bill_for(kwh_month: float) -> float:
    if kwh_month <= BRACKET:
        return kwh_month * BASE_RATE
    base_cost = BRACKET * BASE_RATE
    high_cost = (kwh_month - BRACKET) * HIGH_RATE
    return base_cost + high_cost

if __name__ == "__main__":
    appliances = energy.load_appliances()
    total_month = sum(a["kwh_per_day"] for a in appliances) * 30

    for a in sorted(appliances, key=lambda a: a["kwh_per_day"], reverse=True):
        flat = a["kwh_per_day"] * 30 * BASE_RATE
        print(f"{a['device']:<9} flat-rate cost {flat:>6.2f} $/mo")

    flat_total = total_month * BASE_RATE
    tiered = bill_for(total_month)
    print(f"\nflat rate:  {total_month:.2f} kWh @ ${BASE_RATE:.2f} -> ${flat_total:.2f}")
    print(f"tiered:     first {BRACKET} kWh @ ${BASE_RATE:.2f}, then ${HIGH_RATE:.2f} -> ${tiered:.2f}")
    print(f"tiering premium: ${tiered - flat_total:.2f}")

bill_for دالة بفرعين: تحت الشريحة، ضرب واحد؛ فوقها، أول 250 يسعَّر بالسعر الأساسي والباقي بالمميز. الثوابت تعيش أعلى الملف، لذا “غيّر التعرفة إلى 275 كيلوواط-ساعة” هو تعديل ثلاثة أرقام، لا مطاردة معادلة. العرض التجريبي يطبع كلفة الجهاز المسطحة (لرؤية “أي جهاز يستحق المطاردة”) والعلاوة ، وهي بالضبط الـ $4.53 التي تضيفها بنية السعر إلى فاتورة بدا أن تسعيرها كان يجب أن يكون ثابتًا.

🎯 الناتج المتوقع :

heater      flat-rate cost  27.00 $/mo
fridge      flat-rate cost  21.60 $/mo
tv          flat-rate cost   3.60 $/mo
ps5         flat-rate cost   2.40 $/mo
router      flat-rate cost   1.44 $/mo

flat rate:  280.20 kWh @ $0.20 -> $56.04
tiered:     first 250 kWh @ $0.20, then $0.35 -> $60.57
tiering premium: $4.53

🩹 إذا لم يعمل : إذا تساوت الفاتورة المتدرجة مع المسطحة، فـ kwh_month <= BRACKET يقارن عشريًا مقابل عدد صحيح مدوَّر في الاتجاه الخاطئ ، تحقق أن bill_for(280.2) تُرجع 60.57 لا 56.04. إذا خرجت فواتير الاستخدام المرتفع أرخص من المنخفض، فالمجموع الفرعي - BRACKET ناقص ، فوق الشريحة أنت تفرض الشهر كاملًا بالسعر المميز.

3.2 تحقّق من التعرفة

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

  • bill_for(280.2) == 250×0.20 + 30.2×0.35 == 60.57 يدويًا.
  • bill_for(249.9) == 49.98 وأرخص لكل كيلوواط-ساعة من bill_for(280.2) ، الشريحة تعض فعلًا.
  • ✅ سطر كل جهاز ما زال يستخدم السعر الثابت، مُوسمًا بصدق ، ترتيب الأجهزة وتسعير التعرفة سؤالان منفصلان.

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

  • العلاوة $4.53 (8% من الفاتورة) لكن كيلوواط-ساعة الشريحة 10.8% من الاستخدام. لماذا تظل الأجهزة تحت الشريحة ضمنيًا “على السعر الأساسي”، وما الذي سيغيّر أسطر تكلفة الجهاز لو سعّرت حصة الزيادة لكل جهاز بدلًا من ذلك؟
  • التعرفة تنشر عتبتين. مرافق حقيقي له نوافذ استخدام بالوقت (الليل أرخص من المساء). كيف ستتغير bill_for لو صار السعر price(hour) ، وما الذي يفعله ذلك بنموذج avg_hours_per_day؟

الخطوة 4: دقّق هدر وضع الاستعداد

معظم الفاتورة ليس أجهزة تعمل ، بل أجهزة مطفأة لكن موصولة: مؤشر LED الخاص بالتلفاز، وإلكترونيات المدفأة، ووحدة التحكم تنتظر إشارة. استهلاك وضع الاستعداد صغير لكل جهاز وضخم في الإجمالي، وتدقيق هذه الخطوة يجده: حمّل واط الاستعداد، وحوّله إلى كيلوواط-ساعة شهرية، واقترح ما يستحق الفصل بقاعدة عتبة.

4.1 اكتب تدقيق وضع الاستعداد

👟 تلميح البداية : وضع الاستعداد هو watts/1000 × 24 (اليوم لا يتوقف)؛ قائمة التلميحات تنطلق فقط عندما يتجاوز جهاز WASTE_THRESHOLD_KWH:

cat > standby.csv <<'EOF'
device,standby_watts
tv,4
heater,20
ps5,7
router,8
EOF
# standby.py
import csv

WASTE_THRESHOLD_KWH = 5.0  # monthly alert level

def load_standby(path: str = "standby.csv") -> list[dict]:
    with open(path, newline="") as f:
        rows = list(csv.DictReader(f))
    for row in rows:
        row["standby_watts"] = float(row["standby_watts"])
        row["kwh_per_day"] = row["standby_watts"] / 1000 * 24
        row["kwh_per_month"] = row["kwh_per_day"] * 30
    return rows

if __name__ == "__main__":
    standby = load_standby()
    for s in sorted(standby, key=lambda s: s["kwh_per_month"], reverse=True):
        print(f"{s['device']:<9} standby {s['standby_watts']:>5.1f} W  "
              f"waste {s['kwh_per_month']:>6.2f} kWh/mo")

    total_month = sum(s["kwh_per_month"] for s in standby)
    print(f"\ntotal standby waste: {total_month:.2f} kWh/mo")
    print(f"cost at $0.20/kWh: ${total_month * 0.20:.2f}/mo")

    print("\n== savings hints ==")
    for s in standby:
        if s["kwh_per_month"] >= WASTE_THRESHOLD_KWH:
            print(f" - unplug {s['device']} at night "
                  f"(saves {s['kwh_per_month']:.1f} kWh/mo)")

السمة المميزة لوضع الاستعداد هي 24 ، لا مدخل ساعات، لأن “مطفأ وموصول” لا ينام أبدًا. العتبة تقوم بعمل الحكم: جهاز يهدر 5+ كيلوواط-ساعة/شهر يطفو على السطح كفعل (“افصل…”)، بينما يبقى التلفاز عند 2.88 كيلوواط-ساعة حاشية بدلًا من كونها إزعاجًا عبقريًا. تقرير التدقيق يفصل القياس عن النصيحة: جدول الأرقام حقيقة خام، والتلميحات قاعدة يمكن ضبطها إلى 10 كيلوواط-ساعة أو إلى 1 ، نفس البيانات، عتبة مختلفة، قائمة مختلفة.

🎯 الناتج المتوقع :

heater    standby  20.0 W  waste  14.40 kWh/mo
router    standby   8.0 W  waste   5.76 kWh/mo
ps5       standby   7.0 W  waste   5.04 kWh/mo
tv        standby   4.0 W  waste   2.88 kWh/mo

total standby waste: 28.08 kWh/mo
cost at $0.20/kWh: $5.62/mo

== savings hints ==
 - unplug heater at night (saves 14.4 kWh/mo)
 - unplug ps5 at night (saves 5.0 kWh/mo)
 - unplug router at night (saves 5.8 kWh/mo)

🩹 إذا لم يعمل : إذا كانت قائمة التلميحات فارغة، فـ >= أصبحت > وانزلقت ps5 ذات الـ 5.04 كيلوواط-ساعة تحت العارضة ، أو WASTE_THRESHOLD_KWH سلسلة من إعداد ما، قوبلَت بشكل خاطئ مع أعداد عشرية. إذا طبع هدر جهازًا بالـواط (0.20 kWh/mo للمدفأة)، فـ /1000 ناقصة ، الواط ليست كيلوواط-ساعة بعد.

4.2 تحقّق من التدقيق

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

  • ✅ المدفأة تتقدم عند 14.4 كيلوواط-ساعة/شهر، والتلفاز يتراجع عند 2.88 ، الترتيب العكسي في الفرز يطابق الواقع.
  • ✅ العتبة 5.0 تقبل 3 أجهزة من 4 بالضبط؛ تغييرها إلى 6 يستبعد ps5 ويغيّر القائمة لا الرياضيات.
  • ✅ الإجمالي = 28.08 كيلوواط-ساعة/شهر، مسعَّرًا عند $5.62 ، نفس السعر الثابت الذي استخدمته الخطوة 3، عمدًا.

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

  • “افصل المدفأة ليلًا” نصيحة القاعدة ، لكن إلكترونيات استعداد المدفأة موجودة لإبقاء جدولها وساعة الأمان حيين. ما المفاضلة التي لا يراها رقم $5.62، وماذا سيضيف عمود تكلفة-فائدة قبل أن تنتزع القابس؟
  • الراوتر يهدر 5.76 كيلوواط-ساعة/شهر، وهو يستحق التشغيل حدًا أدنى (إنترنت منزلك يعتمد عليه). ما الخطر في جعل عتبة التدقيق الصوت الوحيد ، وما المدخل الثاني (أولوية الجهاز، الأمان، الضرورة) الذي تحتاجه أداة بقرار قوي؟

الخطوة 5: قارن سيناريوهات الاستخدام

المهارة الأخيرة هي ماذا لو: “إذا خفّضت المدفأة من 3 إلى 2 ساعات والتلفاز من 5 إلى 3، ماذا يحدث للفاتورة؟” دالة scenario(hours_map) تأخذ CSV الأجهزة، تتجاوز الساعات للأجهزة المختارة، وتعاود حساب كيلوواط-ساعة الشهرية، وتسعّر العالمَين الحالي والمحسَّن بنفس التعرفة المتدرجة. المخرجات ، توفير $14.97، خفض 18.6% ، هي غاية المراقب كلها: أسئلة الطاقة تصبح أسئلة دولار.

5.1 اكتب مقارن السيناريوهات

👟 تلميح البداية : hours_map.get(device, avg_hours_per_day) يتراجع إلى ساعات CSV لأي شيء ليس في الخريطة:

# scenarios.py
import csv
from tariff import bill_for

def scenario(hours_map: dict) -> float:
    total_kwh = 0.0
    with open("appliances.csv", newline="") as f:
        for a in csv.DictReader(f):
            watts = float(a["watts"])
            hours = hours_map.get(a["device"], float(a["avg_hours_per_day"]))
            total_kwh += watts / 1000 * hours
    return total_kwh * 30

if __name__ == "__main__":
    current = scenario({})
    optimized = scenario({"heater": 2.0, "tv": 3.0})

    print(f"current:    {current:.1f} kWh/mo -> ${bill_for(current):.2f}")
    print(f"optimized:  {optimized:.1f} kWh/mo -> ${bill_for(optimized):.2f}")
    saved_kwh = current - optimized
    print(f"savings:    {saved_kwh:.1f} kWh/mo = ${bill_for(current) - bill_for(optimized):.2f} "
          f"({100 * saved_kwh / current:.1f}% cut)")

scenario({}) يرسل خريطة فارغة → كل جهاز يحتفظ بساعات CSV → هذا هو “الحالي”، معيدًا استخدام الدالة نفسها بدلًا من ترميز 280.2. خريطة التجاوز إضافية لا تفرّع: heater وtv فقط يتغيران، وكل ما عداهما يعيد قراءة ساعات CSV، لذا لا يمكن للنموذج نسيان الثلاجة. المقارنة تعيد تسعير العالمَين عبر bill_for، وهذا ما يجعل رقم التوفير واعيًا بالتعرفة: فاتورة مسطحة كانت “ستوفر” 10.44،وتحتالشريحة،الدولاراتالحقيقية10.44 ، وتحت الشريحة، الدولارات الحقيقية 14.97، لأن كيلوواط-ساعات رخيصة عُصرت من الزيادة.

🎯 الناتج المتوقع :

current:    280.2 kWh/mo -> $60.57
optimized:  228.0 kWh/mo -> $45.60
savings:    52.2 kWh/mo = $14.97 (18.6% cut)

🩹 إذا لم يعمل : إذا تساوى optimized مع current، فمفاتيح التجاوز تفوّت قيم device الدقيقة في CSV ، "Heater" (بـ H كبيرة) لا يطابق "heater" أبدًا، فيبتلعه الاحتياطي. إذا بدا معدل التوفير وكأنه خفض كيلوواط-ساعة لا 18.6%، فالطباعة تقسم saved_kwh على current بشكل صحيح أصلًا ، لكن تحقق من أنك تقسم الحدين الصحيحين، لا optimized/current.

5.2 تحقّق من السيناريو

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

  • optimized(228.0) < current(280.2) وكلا الرقمين يمر عبر bill_for المتدرجة (228 يبقى تحت الشريحة؛ و280.2 يدفع العلاوة).
  • ✅ حساب التوفير: إزالة 52.2 kWh$60.57 − $45.60 = $14.97؛ و52.2/280.2 = 18.6%.
  • ✅ إدخال خريطة إضافي افتراضي (مثلًا {"router": 0}) يندمج بسلاسة ، الخريطة هي المقبض الوحيد الذي يتغير.

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

  • الشريحة تعض الاستثناءات: قطع كيلوواط-ساعات الزيادة بالضبط يوفر 0.35لكلمنها،بينماقطعكيلوواطساعاتالسعرالأساسييوفر0.35 لكل منها، بينما قطع كيلوواط-ساعات السعر الأساسي يوفر 0.20. مع optimized = 228 kWh، هل اختار هذا السيناريو بالفعل أي كيلوواط-ساعة يقطع ، وكيف يختلف تنويع “وفّر أعلى 52 كيلوواط-ساعة بغض النظر عن الجهاز” في النتيجة؟
  • خرائط الساعات لا تحكم على الراحة ، “المدفأة عند ساعتين” فرضية غير مُسعَّرة. ما الطريقة الصادقة لتقديم اقتراح يوفر المال لكنه يبرّد الغرفة: اطبع المفاضلة كثنائي، أم ادفن الفرضية؟

⚠️ مآزق شائعة

  • واط دون /1000. كيلوواط-ساعة هي watts/1000 × hours. تخطي تحويل الكيلو يسعّر مدفأة 1500 واط على أنها 45 كيلوواط-ساعة/يوم بدلًا من 4.5 ، شبح بعشرة أضعاف على الفاتورة.
  • قسمة سلسلة. خلايا CSV تصل كنص؛ float(...) قبل الحساب وإلا تدمج sum سلاسل بصمت وتصيب التقرير بـ NaN. أثْرِ الصف مرة واحدة، عند التحميل، لا عند كل مستهلك.
  • 30 سحري يؤدي عملين. يضرب مرة واحدة في load_appliances. إذا تسلّل * 30 ثانٍ إلى تقرير، يتحول الشهر إلى 900 يوم. عرّفه مرة واحدة، وعلّق عليه “شهر 30 يومًا”.
  • حساب الإجماليات داخل حلقة. total = sum(...) يُعاد حسابه لكل صف هو ‎O(n²) ، والأسوأ، حصة كل صف تقسم على إجمالي جزئي. اجمع مرة واحدة، خارج الحلقة.
  • انجراف حالة الحروف أو المسافات في أسماء الأجهزة. خريطة مفاتيحها اسم CSV يختلف بمسافة ("heater " مقابل "heater") تتراجع بصمت إلى ساعات افتراضية ، السيناريو “لا يرى” التغيير. طابق حالة أحرف CSV بدقة.

ما بنيته للتو

مراقب طاقة يُقرأ كتقرير كان ليدفع عنه مالك العقار: واط → كيلوواط-ساعة، وإجماليات + حصص، وتعرفة تعض عند الزيادة، وتدقيق وضع استعداد بعتبات قابلة للضبط، ومقارنة سيناريوهات تُظهر دولارات حقيقية. الخيط الداخلي انضباط الاشتقاق: كل رقم دالة نقية لمدخلات CSV زائد ثوابت صريحة (30, 0.20, 250, 5.0)؛ لا تتناقض عبارتا طباعة أبدًا؛ والقصة ، المدفأة نصف الفاتورة؛ ووضع الاستعداد 5.62؛وخفْضساعاتالمدفأةوالتلفاز5.62؛ وخفْض ساعات المدفأة والتلفاز 14.97 ، تأتي من البيانات لا من الانطباع.

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

examples/energy-monitor/ في مستودع الدورة يحتوي السكربتات الكاملة بالإضافة إلى appliances.csv وstandby.csv النموذجيتين. أو افتح المستودع كاملًا في GitHub Codespaces.

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

  • أضف تسعيرًا حسب وقت الاستخدام: استبدل الشرائح الثابتة بنطاقات price(hour) ومرِّر hours_map لكل جهاز لكل ساعة (جدول نهار، جدول ليل) ، يصبح النموذج موسميًا مجانًا.
  • اجعل scenario تُرجع الدولارات لا كيلوواط-ساعة: أعد هيكلة الخطوة 5 لمقارنة bill_for(scenario(map_a)) مقابل bill_for(scenario(map_b)) واطبع فرق الكيلوواط-ساعة وفرق الدولار معًا، كدالة compare(map_a, map_b).
  • حمّل ملف قراءات حقيقي: استبدل avg_hours_per_day بأرقام طاقة فعلية لكل ساعة (من عداد قابس أو بوابة المرافق) ودع kwh_per_day يأتي من الملف بدلًا من واط×ساعات ، نفس التقرير، بيانات حقيقية.
  • ثبّت التقرير: سلسّل جدول الحصص إلى monthly_report.csv وأعده تحميلًا في جدول markdown ، يصبح التدقيق قطعة أثرية يمكنك إرفاقها بخيط بريد المالك.

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

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

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

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