شرح train_test_split في scikit-learn: تقسيم بيانات التدريب والاختبار للمبتدئين

شرح train_test_split وتقسيم بيانات التدريب والاختبار في scikit-learn

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

لهذا نُقسّم البيانات عادةً إلى جزأين: جزء يتعلم منه النموذج يسمى Training Data، وجزء يبقى بعيدًا عن التدريب ثم نستخدمه لاحقًا لقياس الأداء ويسمى Test Data.

مكتبة scikit-learn توفر الدالة train_test_split() لتنفيذ هذا التقسيم بسهولة. لكن استخدامها لا يقتصر على كتابة سطر واحد؛ من المهم أن تفهم معنى test_size وrandom_state وstratify، ومتى تستخدم كل خيار، وما الأخطاء التي قد تجعل نتيجة النموذج تبدو أفضل من حقيقتها.

{alertInfo} الفكرة ببساطة: درّب النموذج على جزء من البيانات، ثم اختبره على جزء لم يره أثناء التدريب. إذا اختبرته على البيانات نفسها التي تعلم منها، فلن تعرف بشكل عادل هل يستطيع التعميم على أمثلة جديدة.

{getToc} $title={محتوى المقال}

ما هي train_test_split في scikit-learn؟

train_test_split() هي دالة من وحدة sklearn.model_selection تساعدك على تقسيم مجموعة أو أكثر من البيانات إلى أجزاء عشوائية للتدريب والاختبار.

أبسط استيراد لها:

from sklearn.model_selection import train_test_split

وفي تعلم الآلة نستخدمها غالبًا مع:

  • X: الميزات Features التي يدخلها النموذج.
  • y: الهدف Target الذي نريد توقعه.

ثم نقسمهما معًا:

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

ما معنى Training Data و Test Data؟

لنعتبر أن لديك بيانات طلاب، وتريد تدريب نموذج يتوقع الدرجة اعتمادًا على عدد ساعات الدراسة.

إذا كانت لديك 100 حالة، تستطيع مثلًا استخدام 80 حالة للتدريب و20 حالة للاختبار.

الجزء وظيفته
Training Data البيانات التي يتعلم منها النموذج أثناء التدريب.
Test Data بيانات لا تستخدم أثناء التدريب، ونقيس عليها الأداء بعد انتهاء التدريب.

المهم أن تكون بيانات الاختبار غير مستخدمة في تدريب النموذج نفسه.

شرح الفرق بين بيانات التدريب وبيانات الاختبار في تعلم الآلة

إنشاء بيانات بسيطة للتجربة

سنستخدم مثالًا تعليميًا صغيرًا جدًا حتى نركز على التقسيم نفسه.

import pandas as pd

data = pd.DataFrame({
    "hours": [
        1, 2, 3, 4, 5,
        6, 7, 8, 9, 10
    ],
    "score": [
        48, 52, 58, 64, 68,
        73, 79, 84, 89, 94
    ]
})

print(data)

تحديد Features و Target

نريد استخدام عدد ساعات الدراسة لتوقع الدرجة.

X = data[["hours"]]
y = data["score"]

استخدمنا قوسين داخل X:

data[["hours"]]

حتى تكون X على شكل جدول ثنائي الأبعاد، وهو الشكل الذي تتعامل معه كثير من نماذج scikit-learn.

أول تقسيم باستخدام train_test_split

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
)

الناتج ليس قيمة واحدة، بل أربع مجموعات:

  • X_train: ميزات التدريب.
  • X_test: ميزات الاختبار.
  • y_train: القيم الصحيحة المقابلة لبيانات التدريب.
  • y_test: القيم الصحيحة المقابلة لبيانات الاختبار.

فحص حجم البيانات بعد التقسيم

print("X_train:", X_train.shape)
print("X_test:", X_test.shape)
print("y_train:", y_train.shape)
print("y_test:", y_test.shape)

بما أن لدينا 10 صفوف وtest_size=0.2، سيكون الاختبار مكوّنًا من صفين والتدريب من 8 صفوف.

X_train: (8, 1)
X_test: (2, 1)
y_train: (8,)
y_test: (2,)

ما معنى test_size؟

الخيار test_size يحدد حجم بيانات الاختبار.

يمكنك كتابة نسبة عشرية:

test_size=0.2

وهذا يعني تقريبًا 20% للاختبار و80% للتدريب.

أو:

test_size=0.25

أي 25% للاختبار.

كما يمكن تمرير عدد صحيح ليكون عدد عينات الاختبار مباشرةً:

test_size=2

في هذه الحالة سيحجز صفين للاختبار.

ما النسبة المناسبة للتدريب والاختبار؟

لا توجد نسبة واحدة صحيحة لكل مشروع. نسب مثل 80/20 أو 75/25 شائعة في الأمثلة التعليمية، لكن الاختيار الفعلي يعتمد على حجم البيانات وطبيعة المشكلة وطريقة التقييم.

إذا كانت البيانات قليلة جدًا، فإن حجز جزء كبير للاختبار يقلل أمثلة التدريب. وإذا كانت البيانات كثيرة، يمكن تخصيص عدد مناسب للاختبار مع بقاء كمية كبيرة للتدريب.

{alertInfo} لا تختار test_size لأن رقم 0.2 مشهور فقط. فكر في عدد الصفوف الفعلي الذي سيبقى للتدريب والاختبار.

ما معنى random_state؟

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 و random_state في train_test_split

هل الرقم 42 له معنى خاص؟

لا تحتاج إلى استخدام 42 تحديدًا. يمكنك اختيار عدد صحيح آخر.

random_state=7

أو:

random_state=123

الفكرة الأساسية هي تثبيت seed للتقسيم حتى تحصل على نفس النتيجة عند إعادة التجربة، وليس أن للرقم 42 دقة أعلى من غيره.

ما معنى shuffle؟

الخيار shuffle يحدد هل يتم خلط البيانات قبل التقسيم.

القيمة الافتراضية تكون:

shuffle=True

أي أن الصفوف يتم خلطها قبل تكوين التدريب والاختبار.

يمكن تعطيل الخلط:

X_train, X_test, y_train, y_test = train_test_split(
    X,
    y,
    test_size=0.2,
    shuffle=False
)

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

تدريب نموذج بعد التقسيم

الآن نستطيع تدريب نموذج انحدار خطي بسيط على X_train وy_train فقط.

from sklearn.linear_model import LinearRegression

model = LinearRegression()

model.fit(X_train, y_train)

بعد انتهاء التدريب، نستخدم X_test للتنبؤ:

predictions = model.predict(X_test)

print(predictions)

ثم نقارن التوقعات مع y_test، وهي القيم الصحيحة التي احتفظنا بها بعيدًا عن التدريب.

تقييم النموذج باستخدام MAE

في مشكلة توقع قيم رقمية، يمكن استخدام mean_absolute_error كمقياس بسيط.

from sklearn.metrics import mean_absolute_error

mae = mean_absolute_error(
    y_test,
    predictions
)

print("MAE:", mae)

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

مثال كامل من التقسيم إلى التقييم

import pandas as pd

from sklearn.linear_model import LinearRegression
from sklearn.metrics import mean_absolute_error
from sklearn.model_selection import train_test_split


data = pd.DataFrame({
    "hours": [
        1, 2, 3, 4, 5,
        6, 7, 8, 9, 10
    ],
    "score": [
        48, 52, 58, 64, 68,
        73, 79, 84, 89, 94
    ]
})

X = data[["hours"]]
y = data["score"]

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

model = LinearRegression()
model.fit(X_train, y_train)

predictions = model.predict(X_test)

mae = mean_absolute_error(
    y_test,
    predictions
)

print("القيم الحقيقية:")
print(y_test.to_list())

print("التوقعات:")
print(predictions)

print("MAE:", mae)
{alertWarning} بيانات هذا المثال صغيرة ومصطنعة للتعلم فقط. لا تعتبر نتيجة نموذج على عشر حالات دليلًا على صلاحيته لاتخاذ قرار حقيقي.

استخدام train_test_split في التصنيف

الدالة نفسها تعمل مع مسائل التصنيف. لننشئ مثالًا بسيطًا يحتوي على ميزة وعدة تصنيفات.

import pandas as pd

data = pd.DataFrame({
    "message_length": [
        12, 18, 10, 50, 55,
        60, 14, 17, 48, 53
    ],
    "label": [
        "normal",
        "normal",
        "normal",
        "spam",
        "spam",
        "spam",
        "normal",
        "normal",
        "spam",
        "spam"
    ]
})

ثم:

X = data[["message_length"]]
y = data["label"]

ما هي stratify في train_test_split؟

في مسائل التصنيف قد تكون بعض الفئات أكثر من غيرها. الخيار stratify يساعد على جعل نسب الفئات في التقسيم قريبة من نسبها في البيانات الأصلية.

استخدم عادةً الهدف y:

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

هذه الفكرة مفيدة خصوصًا عندما يكون الحفاظ على تمثيل الفئات مهمًا أثناء التقسيم.

استخدام stratify مع train_test_split للحفاظ على نسب الفئات

فحص نسب الفئات قبل وبعد التقسيم

يمكنك استخدام value_counts(normalize=True):

print("النسب الأصلية:")
print(
    y.value_counts(normalize=True)
)

print("\nنسب التدريب:")
print(
    y_train.value_counts(normalize=True)
)

print("\nنسب الاختبار:")
print(
    y_test.value_counts(normalize=True)
)

هل أستخدم stratify دائمًا؟

ليست مطلوبة لكل نوع من المشكلات. تستخدم غالبًا عند تقسيم بيانات تصنيف وتريد الحفاظ على توزيع الفئات، بينما مسائل الانحدار التي يكون الهدف فيها قيمة رقمية مستمرة لا تستخدم stratify=y بالطريقة نفسها عادةً.

كما أن البيانات الصغيرة جدًا أو الفئات التي تحتوي على عدد قليل جدًا من العينات قد تمنع إجراء تقسيم stratified مناسب.

التأكد من تطابق X و y

يجب أن تحتوي X وy على عدد العينات نفسه.

print(len(X))
print(len(y))

إذا كان العدد مختلفًا، فلن تستطيع الدالة معرفة أي Target ينتمي إلى أي صف من Features.

أين أضع تنظيف البيانات بالنسبة للتقسيم؟

هذه نقطة تحتاج إلى تمييز بين نوعين من الخطوات.

بعض عمليات التنظيف العامة التي لا تتعلم معلومات من توزيع البيانات يمكن إجراؤها قبل التقسيم، مثل إزالة صف تالف واضح وفق قاعدة ثابتة.

لكن العمليات التي تتعلم قيمًا من البيانات، مثل حساب المتوسط لتعويض القيم الناقصة، أو StandardScaler، أو تعلم قائمة Features من البيانات، يجب أن تنتبه لها لأن حسابها على بيانات التدريب والاختبار معًا قد يؤدي إلى Data Leakage.

لهذا تستخدم scikit-learn كثيرًا أسلوب Pipeline حتى يتم تعلم خطوات preprocessing من بيانات التدريب فقط ثم تطبيقها على بيانات الاختبار.

ما هو Data Leakage؟

تسرب البيانات يحدث عندما تصل معلومات من بيانات الاختبار أو من المستقبل إلى مرحلة تدريب النموذج بصورة غير مناسبة، فيحصل النموذج أو preprocessing على معلومات لم يكن يجب أن يعرفها.

مثال مبسط غير مفضل:

from sklearn.preprocessing import StandardScaler

scaler = StandardScaler()

# غير مفضل: تعلم scaler من كل البيانات قبل التقسيم
X_scaled = scaler.fit_transform(X)

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

السبب أن scaler تعلم المتوسط والانحراف المعياري من كامل X، بما في ذلك الصفوف التي ستصبح Test Data.

الطريقة الأفضل باستخدام Pipeline

في مثال يحتاج Scaling، يمكن ربط preprocessing والنموذج داخل Pipeline:

from sklearn.linear_model import LinearRegression
from sklearn.pipeline import Pipeline
from sklearn.preprocessing import StandardScaler


model = Pipeline([
    ("scaler", StandardScaler()),
    ("model", LinearRegression())
])

model.fit(X_train, y_train)

predictions = model.predict(X_test)

هنا يتعلم StandardScaler داخل fit() باستخدام بيانات التدريب، ثم يطبق التحويل المناسب عند التنبؤ على بيانات الاختبار.

أخطاء train_test_split وتسرب البيانات Data Leakage في تعلم الآلة

خطأ شائع: تقييم النموذج على بيانات التدريب

هذا المثال يقيس أداء النموذج على البيانات التي تدرب عليها:

train_predictions = model.predict(X_train)

print(
    mean_absolute_error(
        y_train,
        train_predictions
    )
)

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

خطأ شائع: تقسيم X و y بشكل منفصل

لا تفعل:

X_train, X_test = train_test_split(X)
y_train, y_test = train_test_split(y)

لأنك قد تحصل على ترتيب مختلف للعينات، وبالتالي لا يعود كل صف من X مرتبطًا بالهدف الصحيح في y.

مرر X وy معًا في الاستدعاء نفسه:

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

خطأ شائع: نسيان أن التقسيم عشوائي

إذا لم تثبت random_state، قد تختلف النتيجة بين تشغيل وآخر. هذا ليس خطأ في scikit-learn، بل نتيجة للخلط العشوائي.

أثناء تجربة الكود أو شرح درس أو مقارنة نموذجين، تثبيت random_state يجعل المقارنة أوضح.

خطأ شائع: Dataset صغيرة جدًا

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

الأمثلة الصغيرة ممتازة لفهم الكود، لكنها ليست بديلًا عن بيانات كافية وتمثيلية للمشكلة الحقيقية.

خطأ شائع: استخدام تقسيم عشوائي لبيانات زمنية

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

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

هل Train/Test Split يكفي دائمًا؟

ليس دائمًا. تقسيم Train/Test مناسب جدًا للتعلم والمشاريع البسيطة، لكن أثناء اختيار النموذج وضبط الإعدادات قد تحتاج إلى:

  • Training Set للتدريب.
  • Validation أو Cross-Validation للمقارنة والضبط.
  • Test Set نهائي للتقييم بعد اتخاذ القرارات.

من الأدوات الشائعة لاحقًا cross_val_score وKFold وStratifiedKFold.

تقسيم Training وValidation وTest يدويًا

يمكنك تنفيذ تقسيمين متتاليين. مثلًا نريد تقريبًا 60% تدريب و20% Validation و20% Test.

X_train, X_temp, y_train, y_temp = train_test_split(
    X,
    y,
    test_size=0.4,
    random_state=42
)

X_valid, X_test, y_valid, y_test = train_test_split(
    X_temp,
    y_temp,
    test_size=0.5,
    random_state=42
)

في التقسيم الأول احتفظنا بـ40% داخل X_temp. ثم قسمنا هذه الـ40% إلى نصفين، فيصبح كل نصف تقريبًا 20% من البيانات الأصلية.

قالب عملي لمشروع Regression

from sklearn.model_selection import train_test_split


X = df[["feature_1", "feature_2"]]
y = df["target"]

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

print(X_train.shape)
print(X_test.shape)

قالب عملي لمشروع Classification

from sklearn.model_selection import train_test_split


X = df[["feature_1", "feature_2"]]
y = df["label"]

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

متى لا أستخدم stratify؟

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

متى لا أستخدم train_test_split العشوائية؟

قد تحتاج طريقة أخرى عندما:

  • تتعامل مع بيانات زمنية.
  • لديك عينات مرتبطة في مجموعات لا يجب فصل أفرادها عشوائيًا.
  • تحتاج Cross-Validation أكثر موثوقية من تقسيم واحد.
  • تعمل على Dataset صغيرة جدًا.

الفكرة ليست أن train_test_split() سيئة، بل أن طريقة التقسيم يجب أن تحترم بنية البيانات والمشكلة.

تمرين عملي

أنشئ DataFrame يحتوي على 20 طالبًا مع الأعمدة:

  • hours: ساعات الدراسة.
  • attendance: نسبة الحضور.
  • passed: 0 أو 1.

ثم نفذ التالي:

  1. ضع hours وattendance داخل X.
  2. ضع passed داخل y.
  3. قسم البيانات بنسبة 75% تدريب و25% اختبار.
  4. استخدم random_state=42.
  5. استخدم stratify=y.
  6. اطبع عدد الحالات في التدريب والاختبار.
  7. اطبع توزيع الفئات داخل y_train وy_test.

حل مختصر

from sklearn.model_selection import train_test_split


X = df[["hours", "attendance"]]
y = df["passed"]

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

print("Training:", len(X_train))
print("Test:", len(X_test))

print(
    y_train.value_counts(
        normalize=True
    )
)

print(
    y_test.value_counts(
        normalize=True
    )
)

قائمة فحص قبل تدريب أي نموذج

  • هل حددت Features وTarget بشكل صحيح؟
  • هل X وy لهما العدد نفسه من العينات؟
  • هل يوجد Test Set لم يدخل في التدريب؟
  • هل اخترت test_size بناءً على حجم البيانات؟
  • هل ثبتت random_state إذا أردت تجربة قابلة للتكرار؟
  • هل تحتاج stratify في مسألة التصنيف؟
  • هل تعلم preprocessing من Training Data فقط؟
  • هل بياناتك زمنية أو مجمعة وتحتاج طريقة تقسيم أخرى؟

روابط داخلية مفيدة من بايثون العرب

مصادر خارجية رسمية للتوسع

الخلاصة

train_test_split() هي واحدة من أول الأدوات التي تحتاج إلى فهمها عند بناء نماذج تعلم الآلة باستخدام scikit-learn. وظيفتها الأساسية هي فصل جزء من البيانات للتدريب وجزء آخر للاختبار.

تعلمنا أن test_size يحدد حجم Test Data، وأن random_state يساعد على جعل التقسيم قابلًا للتكرار، وأن stratify مفيد في كثير من مسائل التصنيف للحفاظ على تمثيل الفئات.

كما شرحنا لماذا يجب تجنب تدريب النموذج أو preprocessing على معلومات من بيانات الاختبار، ولماذا تختلف طريقة التقسيم عند التعامل مع بيانات زمنية أو مجموعات مترابطة أو Dataset صغيرة.

{alertSuccess} القاعدة المهمة: بيانات الاختبار ليست جزءًا من التدريب. احتفظ بها لقياس قدرة النموذج على التعامل مع أمثلة لم يرها من قبل، وتجنب أي خطوة تجعل معلومات Test Data تتسرب إلى التدريب.

أسئلة شائعة

ما هي train_test_split؟

هي دالة في scikit-learn تُستخدم لتقسيم البيانات إلى أجزاء للتدريب والاختبار، ويمكنها تقسيم Features وTarget معًا مع الحفاظ على تطابق العينات.

ما معنى test_size=0.2؟

يعني تخصيص حوالي 20% من العينات لبيانات الاختبار، وتبقى النسبة الأخرى للتدريب إذا لم تحدد train_size بصورة أخرى.

ما فائدة random_state=42؟

يساعد على الحصول على نفس التقسيم عند إعادة تشغيل الكود بالبيانات والإعدادات نفسها، مما يجعل التجارب قابلة لإعادة الإنتاج بصورة أسهل.

هل يجب استخدام الرقم 42؟

لا. يمكنك استخدام أي عدد صحيح مناسب. المهم هو تثبيت القيمة عندما تريد تكرار التقسيم نفسه.

ما فائدة stratify=y؟

تساعد في مسائل التصنيف على إنشاء تقسيمات تحافظ تقريبًا على نسب الفئات الموجودة في y.

هل أستخدم stratify في Regression؟

ليس بالشكل المعتاد مع Target رقمية مستمرة. stratify تستخدم بصورة طبيعية أكثر مع فئات تصنيفية.

هل 80% تدريب و20% اختبار هي النسبة الأفضل دائمًا؟

لا. هي نسبة شائعة، لكن النسبة المناسبة تعتمد على حجم البيانات والمشكلة وطريقة التقييم.

هل أعمل StandardScaler قبل train_test_split؟

لا تجعل الـ scaler يتعلم من بيانات الاختبار. الأفضل التقسيم أولًا ثم تعلم preprocessing من بيانات التدريب، أو استخدام Pipeline.

لماذا لا أختبر النموذج على Training Data فقط؟

لأن الأداء على البيانات التي تعلم منها لا يقيس بشكل كافٍ قدرته على التعميم على بيانات جديدة.

هل train_test_split مناسبة للبيانات الزمنية؟

التقسيم العشوائي قد لا يكون مناسبًا للبيانات الزمنية. تحتاج غالبًا إلى احترام ترتيب الزمن أو استخدام أدوات مخصصة للتقييم الزمني.

إرسال تعليق

أحدث أقدم