المحطّة ٥: تقييم النموذج — الدقّة ومصفوفة الالتباس
قِس لا تظنّ: الدقّة ومصفوفة الالتباس، وأيّ خطأٍ يؤذي قرارك أكثر. ⏱ نحو 28 دقيقة.
- تقيس جودة النموذج بالمقارنة بالحقيقة لا بالظنّ.
- تبني مصفوفة الالتباس وتميّز نوعي الخطأ.
- تحسب الاستدعاء وتفهم متى يهمّ.
- تربط اختيار المقياس بتبعات القرار.
١هل نموذجك جيّد؟ لا تصدّق، قِس
بنينا نموذجًا — لكن هل هو جيّد؟ السؤال لا يُجاب بالظنّ بل بالقياس: نقارن تنبّؤاته بالنتائج الحقيقيّة التي نعرفها (at_risk). أبسط مقياسٍ هو الدقّة: نسبة التنبّؤات الصحيحة.
students = [ {"name": "سعود", "grades": [70, 65, 80], "attendance": 0.9, "at_risk": False}, {"name": "نورة", "grades": [55, 40, 60], "attendance": 0.6, "at_risk": True}, {"name": "خالد", "grades": [90, 85, 88], "attendance": 0.95, "at_risk": False}, {"name": "ريم", "grades": [50, 45], "attendance": 0.5, "at_risk": True}, {"name": "فهد", "grades": [60, 62], "attendance": 0.8, "at_risk": True},] def predict(avg, attendance): return avg < 60 or attendance < 0.70 for s in students: avg = round(sum(s["grades"]) / len(s["grades"]), 1) s["pred"] = predict(avg, s["attendance"]) correct = sum(1 for s in students if s["pred"] == s["at_risk"])print("صحيحة:", correct, "من", len(students))print("الدقّة:", round(correct / len(students), 2))
٢لماذا لا تكفي الدقّة؟ مصفوفة الالتباس
الدقّة وحدها تخدع: قد تخطئ في النوع المهمّ من الخطأ. نفصّلها بأربع خاناتٍ — مصفوفة الالتباس — نعدّ فيها نوعي الصواب ونوعي الخطأ. «الإيجابيّ» هنا يعني «متعثّر»:
students = [ {"name": "سعود", "grades": [70, 65, 80], "attendance": 0.9, "at_risk": False}, {"name": "نورة", "grades": [55, 40, 60], "attendance": 0.6, "at_risk": True}, {"name": "خالد", "grades": [90, 85, 88], "attendance": 0.95, "at_risk": False}, {"name": "ريم", "grades": [50, 45], "attendance": 0.5, "at_risk": True}, {"name": "فهد", "grades": [60, 62], "attendance": 0.8, "at_risk": True},] def predict(avg, attendance): return avg < 60 or attendance < 0.70 for s in students: avg = round(sum(s["grades"]) / len(s["grades"]), 1) s["pred"] = predict(avg, s["attendance"]) tp = fp = fn = tn = 0for s in students: if s["pred"] and s["at_risk"]: tp += 1 # تنبّأنا متعثّر وهو متعثّر elif s["pred"] and not s["at_risk"]: fp += 1 # تنبّأنا متعثّر وهو سليم (إنذار كاذب) elif not s["pred"] and s["at_risk"]: fn += 1 # قلنا سليم وهو متعثّر (فاتنا!) else: tn += 1 # تنبّأنا سليم وهو سليم print("صحيح-إيجابي TP:", tp)print("خاطئ-إيجابي FP:", fp)print("خاطئ-سلبي FN:", fn)print("صحيح-سلبي TN:", tn)
٣أيّ خطأٍ أخطر؟ يعتمد على القرار
في مشروعنا الخطآن ليسا سواءً:
| الخطأ | معناه | أثره |
|---|---|---|
| خاطئ-سلبي FN | طالبٌ متعثّرٌ ظنّه النموذج سليمًا | الأخطر: فاتنا طالبٌ يحتاج دعمًا |
| خاطئ-إيجابي FP | طالبٌ سليمٌ ظنّه متعثّرًا | أخفّ: دعمٌ زائدٌ لمن لا يحتاجه |
لذلك نهتمّ بـ الاستدعاء (Recall = TP/(TP+FN)): كم نسبة المتعثّرين الحقيقيّين التقطهم النموذج. نموذجنا فوّت «فهد» (FN=1)، فاستدعاؤه ليس تامًّا — إشارةٌ إلى أنّ قاعدتنا قد تحتاج تحسينًا (خفض عتبة الدرجة مثلًا).
توقّع وتحقّق
ما الدقّة (Accuracy)؟
«قلنا سليم وهو متعثّر» هو:
في التنبّؤ بالتعثّر، أيّ خطأٍ أخطر عادةً؟
الاستدعاء Recall يقيس:
TP/(TP+FN).الخلاصة
- لا تصدّق النموذج؛ قِسه بمقارنة التنبّؤ بالحقيقة.
- الدقّة = الصحيحة/الكلّ، لكنها تخفي نوع الخطأ.
- مصفوفة الالتباس:
TPوFPوFNوTNتفصّل الصواب والخطأ. FN(تفويت متعثّر) أخطر منFP(إنذار كاذب) في مشروعنا.- أفضل مقياسٍ يتبع تبعات القرار؛ الاستدعاء يهمّ حين يكون التفويت خطِرًا.
تحدٍّ برمجي: تقييم النموذج
متوسّطقيّم نموذجك: احسب الدقّة ومصفوفة الالتباس الأربع، ثمّ الاستدعاء. قارن التنبّؤ بالنتيجة الحقيقيّة at_risk.
- احسب الدقّة = الصحيحة / الكلّ (مدوّرةً لخانتين).
- احسب
tp, fp, fn, tnبمقارنةpredبـat_risk. - اطبع
الدقّة: ...ثم الخانات الأربع. - اطبع
الاستدعاء: ...=tp / (tp + fn)مدوّرًا لخانتين.
ابدأ من هذا الكود، واضغط ▶ شغّل هنا لتكتب حلّك وتجرّبه مباشرة:
students = [ {"name": "سعود", "grades": [70, 65, 80], "attendance": 0.9, "at_risk": False}, {"name": "نورة", "grades": [55, 40, 60], "attendance": 0.6, "at_risk": True}, {"name": "خالد", "grades": [90, 85, 88], "attendance": 0.95, "at_risk": False}, {"name": "ريم", "grades": [50, 45], "attendance": 0.5, "at_risk": True}, {"name": "فهد", "grades": [60, 62], "attendance": 0.8, "at_risk": True},] def predict(avg, attendance): return avg < 60 or attendance < 0.70 for s in students: avg = round(sum(s["grades"]) / len(s["grades"]), 1) s["pred"] = predict(avg, s["attendance"]) # أكمل: احسب الدقّة والمصفوفة والاستدعاء واطبعها
💡 تلميح 1
sum(1 for s in students if s["pred"] == s["at_risk"]) / len(students).💡 تلميح 2
pred وat_risk، ثم Recall = tp/(tp+fn).حاول بنفسك أولًا؛ للمسألة أكثر من حلّ صحيح.
students = [ {"name": "سعود", "grades": [70, 65, 80], "attendance": 0.9, "at_risk": False}, {"name": "نورة", "grades": [55, 40, 60], "attendance": 0.6, "at_risk": True}, {"name": "خالد", "grades": [90, 85, 88], "attendance": 0.95, "at_risk": False}, {"name": "ريم", "grades": [50, 45], "attendance": 0.5, "at_risk": True}, {"name": "فهد", "grades": [60, 62], "attendance": 0.8, "at_risk": True},] def predict(avg, attendance): return avg < 60 or attendance < 0.70 for s in students: avg = round(sum(s["grades"]) / len(s["grades"]), 1) s["pred"] = predict(avg, s["attendance"]) correct = sum(1 for s in students if s["pred"] == s["at_risk"])print("الدقّة:", round(correct / len(students), 2)) tp = fp = fn = tn = 0for s in students: if s["pred"] and s["at_risk"]: tp += 1 elif s["pred"] and not s["at_risk"]: fp += 1 elif not s["pred"] and s["at_risk"]: fn += 1 else: tn += 1 print("TP:", tp, "FP:", fp, "FN:", fn, "TN:", tn)print("الاستدعاء:", round(tp / (tp + fn), 2))