إنتقل إلى المحتوى الرئيسي

🌍 درّب أول نموذج تعلّم آلي لك

نُشر في يوليو 2026.

مرحبًا ضيفنا — عمل رائع لوصولك إلى هنا. قد يبدو تثبيت Python فعليًا وبناء شيء بمفردك خطوة كبيرة بعد بيئة البرمجة في المتصفح، لكن لديك ما تحتاجه لذلك. لنبدأ.

يفترض هذا المشروع أنك مرتاح مع pandas على مستوى المسار العادي تقريبًا من تحليل البيانات — التصفية، .groupby()، التعامل مع القيم المفقودة. في الواقع يفترض أنك أنجزت تحديدًا التحليل الاستكشافي الموجَّه لبيانات تايتانك في الأسبوع 10: حمّلت تلك المجموعة بالفعل، نظّفتها، وطرحت أسئلة مثل "هل اختلف معدل النجاة حسب الدرجة أو الجنس؟" هذا المشروع هو التتمة المباشرة. لقد وصفت هذه المجموعة بالفعل. الآن ستتنبأ منها — بتدريب نموذج ينظر إلى راكب لم يره من قبل ويخمّن هل نجا أم لا.

هذا اختياري وغير مُقيَّم. راجع مشاريع من العالم الحقيقي للاطلاع على القائمة الكاملة، والتي تنمو باستمرار.

🎯 ما ستفعله

  1. تثبيت uv وإعداد مشروع محلي بـ scikit-learn وpandas.
  2. تحميل نفس مجموعة بيانات تايتانك من الأسبوع 10، وترميز أعمدتها الفئوية إلى أرقام.
  3. تقسيم البيانات إلى مجموعة تدريب ومجموعة اختبار، وفهم لماذا يهم ذلك التقسيم.
  4. تدريب مصنِّف LogisticRegression واستخدامه للتنبؤ بالنجاة.
  5. تقييمه بشكل صحيح، ثم تدريب نموذج ثانٍ (RandomForestClassifier) والمقارنة بينهما.

أين تشغّل هذا

ثلاث طرق معقولة لإنجاز هذا المشروع — اختر ما يناسب إعدادك:

  • محليًا بـ uv (موصى به). هذا المشروع صغير ولا يحتاج وحدة معالجة رسومية، لذا فهو مرشَّح جيد لتثبيت Python فعليًا على جهازك الخاص، تمامًا كباقي مشاريع العالم الحقيقي. تفترض الخطوات 1-5 أدناه هذا المسار.
  • GitHub Codespaces. افتح codespaces.new/abderrahim-lectures/python-data-analysis-course للحصول على بيئة تطوير سحابية بـ Node وPython وuv مثبّتين مسبقًا (انظر .devcontainer/devcontainer.json) — نفس الأوامر أدناه بالضبط تعمل من تبويب متصفح، دون أي تثبيت محلي إطلاقًا.
  • Google Colab أو Kaggle Notebooks. خيار جيد فعلاً هنا: تدريب LogisticRegression أو RandomForestClassifier على مجموعة بيانات بهذا الصغر (بضع مئات من الصفوف على الأكثر) لا يحتاج وحدة معالجة رسومية، لذا فإن بيئة دفتر ملاحظات مجانية أكثر من كافية. شغّل !pip install scikit-learn pandas في خلية، ثم الصق وكيّف الكود من الخطوات أدناه. Kaggle Notebooks تحديدًا خيار جميل يُغلق الدائرة — مجموعة بيانات تايتانك نفسها هي إحدى مسابقات Kaggle الأصلية والأشهر للمبتدئين، لذا ستكون تدرِّب نموذجًا على منصة Kaggle نفسها، على مجموعة بيانات Kaggle نفسها.

الخطوة 1: تثبيت uv

uv أداة واحدة تحل محل سلسلة "ثبّت Python، ثم ثبّت pip، ثم ثبّت أداة بيئة افتراضية، ثم ثبّت الحزم" المعتادة — تستطيع تثبيت وإدارة إصدارات Python بنفسها، إلى جانب اعتماديات مشروعك.

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 ml-classifier
cd ml-classifier
uv add scikit-learn pandas

الخطوة 2: حمّل البيانات وجهّزها

نفس المجموعة، نفس أعمدة التحليل الاستكشافي في الأسبوع 10 — هذه المرة مُحمَّلة من ملف مجموعة البيانات الخام للدورة بدلاً من بيئة البرمجة داخل المتصفح:

import pandas as pd

url = "https://raw.githubusercontent.com/abderrahim-lectures/python-data-analysis-course/main/static/datasets/titanic.csv"
df = pd.read_csv(url)
df.head()

ملخّص سريع للتنظيف الذي شرحه الأسبوع 10 بالتفصيل بالفعل — فقط بالقدر الكافي هنا للحصول على DataFrame نظيف، لا لإعادة تدريسه:

df["Age"] = df["Age"].fillna(df["Age"].median())
df["Embarked"] = df["Embarked"].fillna(df["Embarked"].mode()[0])
df = df.drop(columns=["PassengerId", "Name"]) # identifiers, not predictive signal

ترميز الأعمدة الفئوية

هذا الجزء جديد. Sex وEmbarked سلاسل نصية ("male"/"female"، "S"/"C"/"Q") — كان .groupby() في الأسبوع 10 سعيدًا تمامًا بالتجميع حسب عمود نصي، لكن نماذج scikit-learn ليست كذلك: كل نموذج في هذا المشروع، في الأساس، يقوم بحسابات على أرقام، لذا كل عمود يدخل يجب أن يكون رقميًا بالفعل. تتعامل pd.get_dummies مع هذا بتحويل عمود فئوي واحد إلى عدة أعمدة 0/1، عمود واحد لكل فئة:

df = pd.get_dummies(df, columns=["Sex", "Embarked"], drop_first=True)
df.head()

يُسقِط drop_first=True فئة واحدة لكل عمود (مثلًا يبقي Sex_male لكن لا يبقي Sex_female) لأن الفئة المُسقَطة تُستنتَج بالكامل من كون البقية 0 — الاحتفاظ بكلتيهما سيكون تكرارًا زائدًا. يصبح Sex عمودًا واحدًا (Sex_male، 1 أو 0)؛ ويصبح Embarked عمودين (Embarked_Q، Embarked_S، وكلاهما 0 يعني "C"). هذا نفس شكل التحويل الذي رأيته مع pd.cut في الأسبوع 10 — تحويل عمود إلى شكل أسهل استهلاكًا للخطوة التالية — فقط من نص إلى أرقام بدلاً من من مستمر إلى مُصنَّف في فئات.

أخيرًا، افصل الأعمدة التي تتنبأ منها (السمات، X) عن العمود الذي تتنبأ به (الهدف، y):

X = df.drop(columns=["Survived"])
y = df["Survived"]

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

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

طُبِّقت pd.get_dummies على Sex وEmbarked، لكن ليس على Pclass (1 أو 2 أو 3) — تُرِك كعمود رقمي واحد. Pclass فئة أيضًا (لا معنى مفيد لكون الدرجة 2 هي "ضعف" الدرجة 1)، ومع ذلك فإن تركه كما هو خيار مدافَع عنه في بعض التحليلات الحقيقية. هل تستطيع التفكير في حجة لترميز Pclass بنفس طريقة Sex، وحجة لتركه كما هو؟

الخطوة 3: قسّم إلى مجموعتَي تدريب واختبار

إليك الفكرة الجوهرية التي تُبنى عليها هذه الخطوة: درجة نموذج على البيانات التي دُرِّب عليها لا تخبرك تقريبًا بشيء عن أدائه على بيانات لم يرَها. يستطيع النموذج — بل سيفعل ذلك، إن أُعطي حرية كافية — أن يحفظ صفوف التدريب ببساطة بدلاً من تعلّم نمط حقيقي. تخيّل تقييم طالب باستخدام الأسئلة بالضبط التي سُلِّم إليه سلفًا ورقة إجاباتها: علامة كاملة لن تخبرك إن كان قد فهم المادة أم حفظ فقط تلك الإجابات المحددة. تقييم نموذج على بيانات تدريبه الخاصة له نفس العيب. للحصول على مقياس صادق لأداء النموذج على ركّاب لم يرَهم أبدًا، عليك حجب بعض البيانات وعدم السماح للنموذج أبدًا بالتدرب عليها.

from sklearn.model_selection import train_test_split

X_train, X_test, y_train, y_test = train_test_split(
X, y, test_size=0.2, random_state=42
)

يحجب test_size=0.2 20% من الصفوف للاختبار، ويدرِّب على الـ80% المتبقية. يُثبِّت random_state=42 الخلط العشوائي المستخدم لاختيار أي الصفوف تذهب إلى أين — بدونه، ستحصل على تقسيم مختلف (وبالتالي درجة دقة مختلفة قليلاً) في كل مرة تُعيد فيها تشغيل السكربت، مما يجعل من الصعب معرفة ما إذا كان تغيير في كودك قد ساعد فعلاً أم أنك حصلت فقط على تقسيم أكثر حظًا.

تسرّب البيانات: جهّز، ثم قسّم — لا العكس

تم ترميز الخطوة 2 على مجموعة البيانات بأكملها، قبل هذا التقسيم، وهذا مقبول هنا لأن pd.get_dummies تنظر فقط إلى فئة كل صف الخاصة به، لا إلى أي صف آخر. لكن من السهل أن يُخطأ في هذا مع تحويلات تنظر عبر الصفوف — مثلًا، توسيع نطاق عمود باستخدام متوسطه وانحرافه المعياري. إن حسبت ذلك المتوسط/الانحراف على مجموعة البيانات الكاملة ثم قسّمت، تكون مجموعة التدريب قد "رأت" بصمت معلومات من مجموعة الاختبار (ساهمت صفوفها في ذلك المتوسط). يُسمى هذا تسرّب البيانات (data leakage)، وهو أحد أكثر الأخطاء الواقعية شيوعًا في التعلّم الآلي التطبيقي — الحل هو دائمًا حساب أي شيء يلخّص البيانات (المتوسطات، الانحرافات المعيارية، قوائم الفئات) باستخدام مجموعة التدريب فقط، ثم تطبيق نفس التحويل على مجموعة الاختبار.

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

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

لو دربت نموذجًا وقيّمته على X_train/y_train بدلاً من X_test/y_test بالخطأ، هل تتوقع أن تبدو الدقة أفضل أم أسوأ من الرقم الصادق — ولماذا؟

الخطوة 4: درّب مصنِّفًا

LogisticRegression، رغم اسمها، هي مصنِّف، لا نموذج انحدار بالمعنى المعتاد. الفكرة: لكل راكب، تحسب مجموعًا مرجَّحًا لسماته (العمر، الأجرة، الجنس، الدرجة، ...) — نفس شكل الحساب لمعادلة خطية عادية — ثم تعصر ذلك المجموع عبر دالة (الدالة اللوجستية/السيجمويد) تُعيد أي رقم إلى قيمة بين 0 و1. يُفسَّر ذلك الناتج كـاحتمال مُقدَّر للنجاة. "ضبط النموذج" (fitting) يعني إيجاد مجموعة الأوزان التي تجعل تلك الاحتمالات المُقدَّرة تتوافق بأقرب ما يمكن مع النتائج الفعلية 0/1 في بيانات التدريب. التنبؤ حينها هو ببساطة "احتمال ≥ 0.5 ← تنبّأ بالنجاة."

from sklearn.linear_model import LogisticRegression

model = LogisticRegression(max_iter=1000)
model.fit(X_train, y_train)

predictions = model.predict(X_test)

.fit(X_train, y_train) هي حيث يحدث التعلّم — لا ترى أبدًا X_test أو y_test. يرفع max_iter=1000 الحد الأقصى لعدد خطوات التحسين التي يتخذها الحلّال للتقارب؛ القيمة الافتراضية أحيانًا لا تكفي لهذه البيانات وستحذرك scikit-learn إن توقفت مبكرًا.

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

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

قد تُعيد predict_proba شيئًا مثل 0.51 لراكب و0.98 لآخر — كلاهما يُقرَّب إلى نفس التنبؤ النهائي (1)، لكنهما يمثلان مستويي ثقة مختلفين جدًا. أي قرار واقعي قد يتغيّر لو كان لديك وصول إلى ذلك الاحتمال، بدلاً من مجرد التنبؤ النهائي بنعم/لا؟

الخطوة 5: قيِّم وقارن النماذج

الرقم الوحيد للبدء به هو الدقة (accuracy) — نسبة تنبؤات مجموعة الاختبار التي طابقت النتيجة الحقيقية:

from sklearn.metrics import accuracy_score, confusion_matrix

accuracy = accuracy_score(y_test, predictions)
print(f"Logistic Regression accuracy: {accuracy:.1%}")

تخفي الدقة وحدها نوع الأخطاء التي يرتكبها النموذج. تفصّل مصفوفة الالتباس (confusion matrix) ذلك:

cm = confusion_matrix(y_test, predictions)
print(cm)

النتيجة شبكة 2×2. بقراءتها بعبارات بسيطة: تعدّ، بشكل منفصل، كم راكبًا مات فعلاً وتنبَّأ النموذج بموته بشكل صحيح، وكم راكبًا مات فعلاً وتنبَّأ النموذج خطأً بنجاته (إيجابي زائف لـ"نجا")، وكم راكبًا نجا فعلاً وتنبَّأ النموذج خطأً بموته (سلبي زائف)، وكم راكبًا نجا فعلاً وتنبَّأ النموذج بنجاته بشكل صحيح. يستطيع نموذجان بنفس الدقة أن يرتكبا أنواعًا مختلفة جدًا من الأخطاء — يستحق معرفة ذلك، خصوصًا في مجالات يكون فيها نوع واحد من الخطأ (تشخيص طبي فائت، مثلًا) أعلى تكلفة بكثير من الآخر.

الآن درّب نموذجًا ثانيًا، من نوع مختلف، على نفس التقسيم بالضبط، وقارن بصدق:

from sklearn.ensemble import RandomForestClassifier

rf_model = RandomForestClassifier(n_estimators=100, random_state=42)
rf_model.fit(X_train, y_train)
rf_predictions = rf_model.predict(X_test)

rf_accuracy = accuracy_score(y_test, rf_predictions)
print(f"Random Forest accuracy: {rf_accuracy:.1%}")
print(confusion_matrix(y_test, rf_predictions))

تدرِّب الغابة العشوائية (random forest) العديد من أشجار قرار صغيرة، كل واحدة على مجموعة فرعية عشوائية مختلفة قليلاً من البيانات والسمات، وتجعلها تصوّت على التنبؤ النهائي — فكرة جوهرية مختلفة عن أسلوب الانحدار اللوجستي القائم على مجموع مرجَّح واحد زائد احتمال. قارن رقمَي الدقة اللذين لديك الآن. لا تفترض أن الأعلى هو "النموذج الأفضل" تلقائيًا — انظر الخطأ الشائع أدناه.

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

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

  • لو سجّل نموذجك دقة 95% لكن مجموعة البيانات 95% فئة واحدة، ماذا يخبرك ذلك الرقم فعليًا؟ (تحقق: ما نسبة ركاب تايتانك الذين نجوا فعلاً — قريبة من 50/50، أم منحرفة؟)
  • من المرجح أن يختلف رقما دقة النموذجين ببضع نقاط مئوية فقط، محسوبين على مجموعة اختبار من حوالي 20 صفًا فقط (نسخة هذه الدورة من المجموعة تحتوي حوالي 100 صف إجمالاً، أصغر من مجموعة تايتانك الأصلية على Kaggle البالغة ~900). ما مدى ثقتك بأن هذه الفجوة المحددة ستصمد على تقسيم اختبار عشوائي مختلف بنسبة 20%؟

⚠️ أخطاء شائعة

  • ترميز مجموعتَي التدريب والاختبار بشكل غير متسق. إن قسّمت أولاً ثم شغّلت pd.get_dummies بشكل منفصل على كل نصف، فإن فئة موجودة في التدريب وغائبة في الاختبار (أو العكس) قد تُنتج أعمدة غير متطابقة بين X_train وX_test، مما يكسر .fit()/.predict() أو ينتج نتائج خاطئة بصمت. رمّز قبل التقسيم عندما ينظر الترميز فقط إلى قيم كل صف الخاصة به (كما هنا)، أو درّب المُرمِّز على بيانات التدريب فقط وطبّقه على بيانات الاختبار، لا العكس أبدًا.
  • تسرّب البيانات — ضبط أي تحويل يلخّص مجموعة البيانات بأكملها (مُوسِّع نطاق، مُرمِّز بإحصاءات عبر الصفوف) قبل التقسيم، بدلاً من بعده. انظر التلميح في الخطوة 3؛ هذا من أكثر الأخطاء الواقعية شيوعًا في التعلّم الآلي التطبيقي، ويضخّم بصمت دقة اختبارك إلى رقم متفائل أكثر من اللازم.
  • المبالغة في تفسير فرق دقة صغير. بمجموعة اختبار بهذا الصغر (حوالي 20 صفًا، لأن مجموعة بيانات هذه الدورة تحتوي حوالي 100 صف إجمالاً فقط)، فجوة 2-3 نقاط مئوية — غالبًا مجرد تنبؤ أو تنبؤين مقلوبين — تقع تمامًا ضمن النطاق المتوقَّع من الضجيج العشوائي في أي صفوف انتهى بها المطاف في تقسيم الاختبار، وليست بالضرورة دليلاً على أن نموذجًا أفضل فعلاً من الآخر. التحقق المتقاطع (انظر أدناه) هو الطريقة المعيارية للحصول على مقارنة أكثر موثوقية، ويهم أكثر على مجموعة بيانات بهذا الحجم.
  • نسيان random_state. بدونه، يتغيّر تقسيمك (وعشوائية بعض النماذج الداخلية) في كل تشغيل، مما يجعل من المستحيل معرفة ما إذا كان تغيير أجريته قد حسّن شيئًا فعلاً أم أنك حصلت فقط على تقسيم عشوائي مختلف.

ما بنيته للتو

أخذت مجموعة بيانات كنت قد استكشفتها ولخّصتها بالفعل بـ pandas، ودفعت خطوة أخرى للأمام: نموذج يعمّم من أمثلة رآها إلى تنبؤ حول أمثلة لم يرَها. لا شيء هنا غريب — LogisticRegression وRandomForestClassifier من أكثر المصنِّفات استخدامًا في الممارسة العملية — لكن شكل سير العمل (تجهيز البيانات، التقسيم بصدق، الضبط، التقييم، المقارنة) هو نفس الشكل المستخدم لنماذج أكثر تطورًا بكثير.

تحقق من وثائق scikit-learn الحالية

scikit-learn مكتبة ناضجة ومستقرة، لكن واجهتها البرمجية تتغيّر أحيانًا بين الإصدارات الرئيسية — تتغيّر القيم الافتراضية للمعاملات، وتُهجَر دوال لصالح أخرى أحدث. قبل الاعتماد على هذا الكود لما يتجاوز مشروع دورة دراسية، تصفّح وثائق scikit-learn الحالية للإصدار الذي لديك فعليًا مثبَّتًا (uv pip show scikit-learn).

إلى أين من هنا

  • هندسة السمات (feature engineering). أُسقِط عمود Name في الخطوة 2، لكنه ليس عديم الفائدة — ألقاب مثل "Mr."، "Mrs."، "Miss."، و"Master." (مُضمَّنة في سلسلة الاسم) ترتبط ارتباطًا قويًا بالعمر والجنس، واستخراجها كعمود فئوي جديد تحسين كلاسيكي لهذه المجموعة بالذات.
  • التحقق المتقاطع (cross-validation). يمنحك تقسيم تدريب/اختبار واحد رقم دقة واحدًا يعتمد جزئيًا على الحظ (أي صفوف انتهى بها المطاف أين). تكرر sklearn.model_selection.cross_val_score دورة التقسيم-التدريب-التقييم عدة مرات على شرائح مختلفة وتحسب متوسط النتيجة — طريقة أكثر موثوقية لمقارنة نموذجين من المقارنة الوحيدة في الخطوة 5.
  • مجموعة بيانات مختلفة تمامًا. تستضيف Kaggle مئات مجموعات البيانات الصغيرة والموثَّقة جيدًا للمبتدئين، مشابهة في روحها لتايتانك — خطوة تالية جيدة بمجرد أن يصبح سير العمل هذا روتينيًا.

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

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

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