علم البيانات ·10 دقائق قراءة

بناء نموذج تنبؤي بلغة بايثون: دليل عملي للتقييم والتفسير مع scikit-learn

دليل عملي لبناء نموذج تنبؤي بلغة بايثون وتقييمه باستخدام scikit-learn: خطوط المعالجة، والتحقق المتقاطع، ومقاييس الأداء، وعدم توازن الفئات، وتسرب البيانات، وتفسير النموذج.

في هذه المقالة
  1. صياغة المشكلة قبل كتابة أي كود
  2. البيانات وتقسيمها
  3. خط المعالجة: Pipeline وColumnTransformer
  4. التحقق المتقاطع ومقارنة النماذج
  5. مقاييس الأداء: اختر ما يناسب القرار
  6. حين يكون المتغير التابع رقميًا
  7. عدم توازن الفئات
  8. تسرب البيانات: الخطأ الذي يصنع نتائج مستحيلة
  9. تفسير النموذج: أهمية التبديل وقيم SHAP
  10. الاستخدام المسؤول في المؤسسات
  11. أخطاء شائعة
  12. أسئلة شائعة
  13. كم حجم البيانات الذي أحتاجه لبناء نموذج تنبؤي؟
  14. ما الفرق بين النموذج التنبؤي والانحدار الذي أستخدمه في رسالتي؟
  15. هل أعتمد على ROC-AUC أم على F1؟
  16. هل يناسب النموذج التنبؤي رسالة دكتوراه؟

تريد مؤسسة أن تعرف أي المستفيدين من برنامج تدريبي معرضون للانسحاب قبل إكماله، لتتدخل مبكرًا بدل أن تكتشف ذلك في التقرير السنوي. بناء نموذج تنبؤي بلغة بايثون لهذا الغرض يبدأ بأسطر قليلة في scikit-learn، وهنا بالضبط يكمن الخطر: الكود القصير يعطي رقمًا جميلًا بسهولة، لكن الرقم قد يكون متضخمًا بسبب تسرب البيانات، أو مضللًا لأن المقياس لا يناسب القرار، أو منحازًا ضد فئة لم يفحص أحد أداء النموذج عليها. يعرض هذا المقال خط العمل كاملًا على بيانات اصطناعية واضحة: صياغة المشكلة، والتقسيم، وخطوط المعالجة، والتحقق المتقاطع، والمقاييس، وعدم توازن الفئات، وتجنب التسرب، والتفسير، ثم متطلبات الاستخدام المسؤول داخل المؤسسات.

صياغة المشكلة قبل كتابة أي كود

أغلب المشاريع التنبؤية التي تفشل لا تفشل في الخوارزمية، بل في السؤال. قبل فتح المحرر أجب كتابيًا عن خمسة أسئلة:

  1. ما الذي نتنبأ به بالضبط؟ «الانسحاب» يحتاج تعريفًا تشغيليًا: انقطاع أربعة أسابيع متتالية؟ عدم حضور الاختبار النهائي؟ التعريف يغير النموذج كله.
  2. متى يصدر التنبؤ؟ إذا كان التنبؤ يصدر في الأسبوع الثالث من البرنامج، فلا يجوز أن يدخل النموذج أي معلومة لا تعرف إلا بعد الأسبوع الثالث.
  3. ما القرار الذي يتبع التنبؤ؟ اتصال هاتفي من مرشد؟ منحة إضافية؟ طبيعة القرار تحدد كلفة كل نوع من الأخطاء.
  4. أيهما أغلى: الإنذار الكاذب أم الحالة الفائتة؟ هذا السؤال إداري قبل أن يكون إحصائيًا، وجوابه يحدد العتبة والمقياس.
  5. ما خط الأساس؟ ما أداء القاعدة الحالية أو التخمين البسيط؟ النموذج الذي لا يتفوق على خط الأساس لا مبرر لتشغيله.

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

البيانات وتقسيمها

نستخدم في هذا المقال بيانات اصطناعية يولدها الكود نفسه، تحاكي سجلات برنامج تدريبي: العمر، والدخل، ونسبة الحضور، والمنطقة، ومتغير الانسحاب. وأضفنا قيمًا مفقودة في الدخل لأن السجلات الإدارية الحقيقية نادرًا ما تكون كاملة. الأمثلة كلها ببايثون، والخطوات نفسها ممكنة في R عبر tidymodels، والمقارنة بين اللغتين في مقال بايثون أم R لتحليل البيانات.

import numpy as np
import pandas as pd

# Clearly synthetic data: program participants and whether they dropped out
rng = np.random.default_rng(42)
n = 3000
df = pd.DataFrame({
    "age": rng.integers(18, 65, n),
    "income": rng.lognormal(mean=6.5, sigma=0.5, size=n),
    "attendance": rng.uniform(0.3, 1.0, n),
    "region": rng.choice(["north", "center", "south"], n),
})
logit = (-2.2 - 5.0 * (df["attendance"] - 0.65)
         - 0.001 * (df["income"] - 700) + 0.5 * (df["region"] == "south"))
df["dropout"] = rng.binomial(1, 1 / (1 + np.exp(-logit)))

# Mimic incomplete administrative records
df.loc[rng.choice(n, 150, replace=False), "income"] = np.nan
print(df["dropout"].mean().round(3))

أول قرار منهجي هو عزل مجموعة اختبار لا تلمسها حتى النهاية. نستخدم التقسيم الطبقي (stratify=y) حتى تبقى نسبة الحالات المنسحبة متقاربة في المجموعتين. وإذا كانت بياناتك زمنية، كسجلات دفعات متتالية، فالتقسيم الأصدق أن تدرب على الدفعات السابقة وتختبر على أحدث دفعة، لأن هذا ما سيحدث فعلًا عند التشغيل.

خط المعالجة: Pipeline وColumnTransformer

المعالجة المسبقة (تعويض القيم المفقودة، والتوحيد القياسي، وترميز المتغيرات الفئوية) يجب أن تتعلم معاملاتها من بيانات التدريب وحدها. أسهل طريقة لضمان ذلك أن تضع المعالجة والنموذج في كائن واحد:

from sklearn.model_selection import train_test_split
from sklearn.compose import ColumnTransformer
from sklearn.pipeline import Pipeline
from sklearn.impute import SimpleImputer
from sklearn.preprocessing import StandardScaler, OneHotEncoder
from sklearn.linear_model import LogisticRegression

X = df.drop(columns="dropout")
y = df["dropout"]
X_train, X_test, y_train, y_test = train_test_split(
    X, y, test_size=0.2, stratify=y, random_state=42)

num_cols = ["age", "income", "attendance"]
cat_cols = ["region"]
preprocess = ColumnTransformer([
    ("num", Pipeline([("impute", SimpleImputer(strategy="median")),
                      ("scale", StandardScaler())]), num_cols),
    ("cat", OneHotEncoder(handle_unknown="ignore"), cat_cols),
])
model = Pipeline([
    ("prep", preprocess),
    ("clf", LogisticRegression(max_iter=1000, class_weight="balanced")),
])

بهذا الشكل، حين يمرر الكائن إلى التحقق المتقاطع، يعاد حساب الوسيط والمتوسط والانحراف المعياري داخل كل طية تدريب، ولا تتسرب معلومة من طية التحقق. ويمكن حفظ الكائن كاملًا بمكتبة joblib واستخدامه لاحقًا على بيانات جديدة بالمعالجة نفسها. والمعالجة هنا لا تغني عن تنظيف البيانات قبلها: القيم المستحيلة والمكررات والترميز الخاطئ تعالج أولًا كما في خطوات تنظيف البيانات قبل التحليل.

التحقق المتقاطع ومقارنة النماذج

نقارن ثلاثة مرشحين على بيانات التدريب فقط: خط أساس يتنبأ بالنسبة العامة، وانحدارًا لوجستيًا، ونموذج تعزيز متدرج.

from sklearn.model_selection import StratifiedKFold, cross_validate
from sklearn.ensemble import HistGradientBoostingClassifier
from sklearn.dummy import DummyClassifier

cv = StratifiedKFold(n_splits=5, shuffle=True, random_state=42)
candidates = {
    "baseline": DummyClassifier(strategy="prior"),
    "logistic": model,
    "boosting": Pipeline([("prep", preprocess),
                          ("clf", HistGradientBoostingClassifier(random_state=42))]),
}
for name, est in candidates.items():
    s = cross_validate(est, X_train, y_train, cv=cv,
                       scoring=["roc_auc", "average_precision"])
    print(f"{name:9s} ROC-AUC={s['test_roc_auc'].mean():.3f} "
          f"PR-AUC={s['test_average_precision'].mean():.3f}")

في هذه البيانات الاصطناعية تفوق الانحدار اللوجستي على نموذج التعزيز، لأن العلاقة التي ولدنا بها البيانات خطية في الأصل. النتائج توضيحية وتختلف حسب البيانات، لكن الدرس عام: النموذج الأعقد ليس أفضل تلقائيًا، وحين يتقارب الأداء فالنموذج الأبسط أسهل في الشرح والصيانة. وإذا ضبطت المعاملات الفائقة باستخدام GridSearchCV، فاجعل الضبط داخل التحقق المتقاطع نفسه، أو استخدم تحققًا متداخلًا (nested CV) إذا أردت تقديرًا غير متحيز للأداء بعد الضبط.

مقاييس الأداء: اختر ما يناسب القرار

المقياس ما الذي يجيب عنه متى يناسب تحفظ
ROC-AUC احتمال أن يعطي النموذج حالة إيجابية درجة أعلى من حالة سلبية مقارنة النماذج في ترتيب الحالات قد يبدو مرتفعًا مع فئة نادرة جدًا
PR-AUC (Average Precision) جودة الترتيب مع التركيز على الفئة الإيجابية الفئات غير المتوازنة يقارن بالنسبة الأساسية للفئة الإيجابية لا بالقيمة 0.5
الدقة النوعية (Precision) من بين من أنذرنا بهم، كم نسبة الصحيح منهم؟ حين يكون التدخل مكلفًا تتغير مع العتبة
الاستدعاء (Recall) من بين الحالات الفعلية، كم نسبة ما التقطناه؟ حين تكون الحالة الفائتة مكلفة تتغير مع العتبة
F1 توازن بين الدقة النوعية والاستدعاء حين تتقارب كلفة نوعي الخطأ يخفي أي الجانبين هو الضعيف
RMSE متوسط حجم الخطأ في التنبؤ بقيمة رقمية، بوحدة المتغير نفسه مشكلات الانحدار حساس للأخطاء الكبيرة، فقارنه مع MAE

بعد اختيار النموذج ندربه على كامل بيانات التدريب، ونقيمه مرة واحدة على مجموعة الاختبار:

from sklearn.metrics import (roc_auc_score, average_precision_score,
                             precision_score, recall_score)

model.fit(X_train, y_train)
proba = model.predict_proba(X_test)[:, 1]
print("Test ROC-AUC:", round(roc_auc_score(y_test, proba), 3))
print("Test PR-AUC :", round(average_precision_score(y_test, proba), 3))

# The decision threshold is a policy choice: compare a few options
for t in [0.3, 0.5, 0.7]:
    pred = (proba >= t).astype(int)
    print(f"threshold={t}: precision={precision_score(y_test, pred):.2f}, "
          f"recall={recall_score(y_test, pred):.2f}, flagged={pred.sum()}")

عمود flagged هو ما يهم الإدارة: كم شخصًا سيتصل بهم المرشدون عند كل عتبة؟ إذا كانت طاقة الفريق محدودة، فالعتبة تختار بناءً على هذه الطاقة وعلى كلفة الحالة الفائتة، لا لأن 0.5 هي القيمة الافتراضية.

حين يكون المتغير التابع رقميًا

في مشكلات الانحدار، كالتنبؤ بمدة معاملة أو بحجم طلب، يكون RMSE مقياسًا شائعًا. ولا معنى له دون مقارنة بخط أساس يتنبأ بالمتوسط دائمًا. المثال التالي يستخدم بيانات diabetes المضمنة في scikit-learn:

from sklearn.datasets import load_diabetes
from sklearn.dummy import DummyRegressor
from sklearn.linear_model import RidgeCV
from sklearn.model_selection import cross_val_score

X_r, y_r = load_diabetes(return_X_y=True)
for name, reg in [("mean baseline", DummyRegressor()),
                  ("ridge", RidgeCV(alphas=np.logspace(-3, 3, 13)))]:
    rmse = -cross_val_score(reg, X_r, y_r, cv=5,
                            scoring="neg_root_mean_squared_error")
    print(f"{name}: RMSE={rmse.mean():.1f}")

عدم توازن الفئات

إذا كانت نسبة المنسحبين 10% فقط، فنموذج يقول «لن ينسحب أحد» يحقق دقة كلية (accuracy) قدرها 90% دون أن يلتقط حالة واحدة. لهذا لا تعتمد على الدقة الكلية في الفئات غير المتوازنة. الأدوات المتاحة:

  • التقسيم الطبقي في التدريب والاختبار والتحقق المتقاطع.
  • أوزان الفئات عبر class_weight="balanced"، كما فعلنا، فيعطي النموذج وزنًا أكبر لأخطاء الفئة النادرة.
  • اختيار العتبة وفق كلفة الأخطاء، وهو غالبًا أبسط وأوضح من أي تقنية أخرى.
  • إعادة المعاينة مثل SMOTE من حزمة imbalanced-learn، بشرط أن تطبق داخل طيات التدريب فقط عبر خط المعالجة الذي توفره الحزمة نفسها (imblearn.pipeline.Pipeline)، لا على البيانات كلها قبل التقسيم.

انتبه إلى أن أوزان الفئات وإعادة المعاينة تشوهان الاحتمالات المتوقعة، فلا تقرأ 0.7 على أنها احتمال انسحاب قدره 70%. إذا احتاجت المؤسسة إلى احتمالات معايرة، فاستخدم CalibratedClassifierCV وافحص منحنى المعايرة.

تسرب البيانات: الخطأ الذي يصنع نتائج مستحيلة

يحدث التسرب حين يصل إلى النموذج أثناء التدريب ما لن يكون متاحًا له عند الاستخدام الفعلي. أشهر صوره:

  • معالجة قبل التقسيم: توحيد قياسي أو اختيار متغيرات أو تعويض قيم مفقودة على البيانات كلها، ثم التقسيم.
  • تسرب الهدف: متغير يسجل بعد وقوع النتيجة، مثل «تاريخ إغلاق الملف» أو «مقابلة الخروج»، فيكشف النتيجة بدل أن يتنبأ بها.
  • تسرب المجموعات: الشخص نفسه أو الأسرة نفسها في التدريب والاختبار. الحل التقسيم بالمجموعات عبر GroupKFold.
  • تسرب الزمن: تدريب على بيانات لاحقة واختبار على بيانات سابقة. الحل TimeSeriesSplit أو تقسيم زمني صريح.

المثال التالي يوضح الصورة الأولى بأوضح شكل ممكن: بيانات ضوضاء خالصة لا علاقة فيها بين المتغيرات والهدف.

from sklearn.feature_selection import SelectKBest, f_classif
from sklearn.pipeline import make_pipeline

# Pure noise: 200 rows, 5,000 random features, random labels
rng = np.random.default_rng(0)
X_noise = rng.normal(size=(200, 5000))
y_noise = rng.integers(0, 2, 200)
clf = LogisticRegression(max_iter=1000)

# Wrong: feature selection sees every label before cross-validation
X_leaky = SelectKBest(f_classif, k=20).fit_transform(X_noise, y_noise)
wrong = cross_val_score(clf, X_leaky, y_noise, cv=5, scoring="roc_auc")

# Right: selection is refit inside each training fold
pipe = make_pipeline(SelectKBest(f_classif, k=20), clf)
right = cross_val_score(pipe, X_noise, y_noise, cv=5, scoring="roc_auc")
print(f"leaky AUC={wrong.mean():.2f}  honest AUC={right.mean():.2f}")

الطريقة الخاطئة تعطي ROC-AUC مرتفعًا لبيانات لا يمكن التنبؤ بها أصلًا، لأن اختيار المتغيرات رأى كل الأهداف قبل التحقق. الطريقة الصحيحة تعطي قيمة قريبة من 0.5، وهي الحقيقة. القيم الدقيقة توضيحية وتختلف حسب البذرة العشوائية، لكن الفجوة بين الطريقتين هي المتوقعة في مثل هذا الإعداد. وكل نتيجة تبدو أفضل من المتوقع تستحق بحثًا عن التسرب قبل الاحتفال بها.

تفسير النموذج: أهمية التبديل وقيم SHAP

أهمية التبديل (Permutation Importance) تقيس كم ينخفض الأداء على بيانات الاختبار إذا خلطنا قيم متغير واحد عشوائيًا. ميزتها أنها تعمل على الأعمدة الأصلية وعلى أي نموذج. أما قيم SHAP فتوزع تنبؤ كل حالة على المتغيرات، فتجيب عن سؤال: لماذا حصل هذا الشخص تحديدًا على هذه الدرجة؟

import shap
from sklearn.inspection import permutation_importance

# Global view: drop in test ROC-AUC when each original column is shuffled
result = permutation_importance(model, X_test, y_test, scoring="roc_auc",
                                n_repeats=20, random_state=42)
importance = pd.Series(result.importances_mean, index=X_test.columns)
print(importance.sort_values(ascending=False).round(3))

# Local view: SHAP values for the fitted logistic model on transformed features
prep, clf_fitted = model[:-1], model[-1]
explainer = shap.Explainer(clf_fitted, prep.transform(X_train),
                           feature_names=prep.get_feature_names_out())
shap_values = explainer(prep.transform(X_test))
shap.plots.beeswarm(shap_values)

تحفظان مهمان. الأول أن الأهمية تصف سلوك النموذج لا العالم: إذا ظهر الحضور أهم متغير، فهذا لا يعني أن رفع الحضور سيخفض الانسحاب. الثاني أن المتغيرات المترابطة تتقاسم الأهمية، فقد يبدو متغير ضعيفًا لأن متغيرًا آخر يحمل المعلومة نفسها.

الاستخدام المسؤول في المؤسسات

حين يؤثر النموذج في حصول شخص على خدمة أو منحة أو متابعة، يصبح التقييم الإجمالي غير كافٍ. افحص الأداء لكل فئة على حدة:

# Performance by subgroup at the chosen threshold
threshold = 0.5
res = X_test.assign(y=y_test.values, p=proba)
for region, g in res.groupby("region"):
    auc = roc_auc_score(g["y"], g["p"])
    rec = recall_score(g["y"], g["p"] >= threshold)
    print(f"{region:7s} n={len(g)}  ROC-AUC={auc:.3f}  recall={rec:.3f}")

قد يتشابه ROC-AUC بين المناطق بينما يختلف الاستدعاء عند العتبة نفسها، أي أن النموذج يلتقط الحالات في منطقة أكثر من غيرها. ومصادر التحيز في المؤسسات معروفة: بيانات تاريخية تعكس قرارات سابقة غير منصفة، ومتغيرات تعمل بديلًا غير مباشر لخصائص حساسة، وجودة بيانات تختلف بين الفروع. وإلى جانب الفحص، تحتاج المؤسسة إلى:

  • توثيق مكتوب للنموذج على طريقة بطاقات النماذج (Model Cards): الغرض، والبيانات، وفترة التدريب، والمقاييس العامة ولكل فئة، والاستخدامات غير المناسبة.
  • إنسان في حلقة القرار: النموذج يرتب الحالات، والموظف يقرر بعد مراجعتها.
  • مراقبة بعد التشغيل: البيانات تتغير مع الوقت، والأداء الذي قيس عند البناء قد لا يستمر. حدد موعدًا لإعادة التقييم.
  • حماية البيانات: أقل قدر لازم من البيانات الشخصية، وصلاحيات وصول محددة، والتزام بالأنظمة المحلية لحماية البيانات.

أخطاء شائعة

  • تقييم النموذج على البيانات نفسها التي درب عليها، أو العودة إلى مجموعة الاختبار مرارًا حتى يتحسن الرقم.
  • الاكتفاء بالدقة الكلية مع فئة نادرة.
  • اعتماد العتبة 0.5 دون نقاش كلفة الأخطاء وطاقة الفريق.
  • معالجة البيانات أو موازنة الفئات قبل التقسيم.
  • إدخال متغيرات لا تكون متاحة لحظة التنبؤ.
  • غياب خط الأساس، فلا يعرف أحد هل يضيف النموذج شيئًا.
  • تفسير أهمية المتغيرات على أنها علاقات سببية.
  • تشغيل النموذج دون توثيق أو مراقبة أو فحص للأداء حسب الفئات.

أسئلة شائعة

كم حجم البيانات الذي أحتاجه لبناء نموذج تنبؤي؟

لا يوجد رقم واحد. الحاجة تزيد مع ندرة الفئة الإيجابية وعدد المتغيرات وتعقيد النموذج. ارسم منحنى التعلم عبر learning_curve في scikit-learn: إذا كان الأداء ما زال يتحسن مع زيادة البيانات، فالمزيد منها سيفيد.

ما الفرق بين النموذج التنبؤي والانحدار الذي أستخدمه في رسالتي؟

قد تكون الخوارزمية واحدة والهدف مختلفًا. في الانحدار التفسيري تهتم بالمعاملات وأخطائها المعيارية واختبار الفرضيات. وفي النموذج التنبؤي تهتم بدقة التنبؤ على بيانات لم يرها النموذج، فيصبح التحقق المتقاطع ومجموعة الاختبار هما معيار الحكم.

هل أعتمد على ROC-AUC أم على F1؟

ROC-AUC يقيم الترتيب على كل العتبات، وهو مناسب لمقارنة النماذج. وF1 يقيم عتبة واحدة بعينها. في التقرير اعرض الاثنين مع PR-AUC إذا كانت الفئات غير متوازنة، واعرض الدقة النوعية والاستدعاء عند العتبة التي ستستخدم فعلًا.

هل يناسب النموذج التنبؤي رسالة دكتوراه؟

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

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

آخر تحديث:
ESC
اكتب كلمة مفتاحية ثم اضغط Enter — مثل: «منهجية»، «إحصاء»، «اقتباس»، «نشر».