تقسيم البيانات (Train/Test Split): لماذا يجب أن نختبر النموذج على بيانات لم يرها من قبل؟

دقائق القراءة: 6

مقدمة: لماذا لا يكفي أن ينجح النموذج على نفس البيانات التي تعلم منها؟

في أي مشروع يعتمد على Machine Learning، لا تكون المشكلة الحقيقية هي جعل النموذج يحفظ البيانات، بل التأكد من قدرته على التعميم عند مواجهة سجلات جديدة لم يشاهدها أثناء التدريب. هنا تظهر أهمية Train/Test Split كخطوة أساسية في بناء أي نظام تنبؤي موثوق.

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

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

ما هو تقسيم البيانات Train/Test Split؟

فكرة التقسيم بسيطة ظاهرياً لكنها عميقة جداً من الناحية المنهجية. نقوم بفصل البيانات إلى مجموعتين رئيسيتين:

  • بيانات التدريب: تُستخدم لتعليم النموذج واستخراج العلاقات بين المتغيرات.
  • بيانات الاختبار: تُحجب عن النموذج أثناء التدريب، ثم تُستخدم لاحقاً لقياس الأداء الحقيقي.

في كثير من المشاريع تكون النسبة الشائعة هي 80/20 أو 70/30، لكن الاختيار ليس قانوناً ثابتاً. حجم البيانات، نوع المسألة، وتوازن الفئات كلها عوامل تؤثر في قرار التقسيم.

قبل هذه الخطوة، يجب أن تكون البيانات قد مرت بعمليات تجهيز سليمة مثل تنظيف البيانات (Data Cleaning): اكتشاف ومعالجة القيم المفقودة (Missing Values)، ومعالجة البيانات المكررة والمشوهة (Duplicates & Outliers) باستخدام بايثون، ثم تنظيم البنية الجدولية بدقة عبر فهم DataFrame و Series.

لماذا يجب أن تكون بيانات الاختبار غير مرئية تماماً؟

1. قياس القدرة على التعميم

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

2. كشف مشكلة Overfitting

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

3. منع تسرب البيانات Data Leakage

من أخطر الأخطاء في مشاريع الذكاء الاصطناعي أن تصل معلومات من مجموعة الاختبار إلى النموذج بشكل مباشر أو غير مباشر. قد يحدث ذلك عند تنفيذ التحجيم، الترميز، أو اختيار الميزات على كامل البيانات قبل التقسيم. لذلك يجب أن تُطبق عمليات إعداد البيانات للتدريب (Data Preprocessing): تحجيم البيانات (Scaling & Normalization) بعد الفصل، وليس قبله.

في بيئات Big Data، قد يحدث Data Leakage داخل مراحل ETL Pipeline نفسها، خصوصاً عند بناء جداول مشتقة تحتوي على مؤشرات زمنية أو نتائج مستقبلية. لهذا يجب تصميم طبقات البيانات التحليلية بحيث تُحترم الحدود الزمنية والمنطقية بين التدريب والاختبار.

كيف نختار طريقة التقسيم الصحيحة؟

التقسيم العشوائي

يصلح عندما تكون البيانات مستقلة نسبياً ولا توجد علاقة زمنية قوية بين السجلات. في هذه الحالة يمكن استخدام تقسيم عشوائي يضمن تمثيلاً مناسباً للعينات داخل مجموعتي التدريب والاختبار.

التقسيم الطبقي Stratified Split

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

التقسيم الزمني

في بيانات السلاسل الزمنية لا يجوز خلط الماضي بالمستقبل. يجب تدريب النموذج على الفترات الأقدم، ثم اختباره على الفترات الأحدث. هذا المبدأ مهم جداً عند تحليل الطلب، المبيعات، أو حركة المستخدمين عبر الزمن، كما في التعامل مع التواريخ والوقت (Datetime): تحليل التوجهات الزمنية (Time Series).

مثال عملي باستخدام Pandas وscikit-learn

في المشاريع المصغرة أو التحليل الأولي، يمكن تنفيذ التقسيم بسهولة بعد قراءة البيانات عبر مكتبة Pandas (1): قراءة واستدعاء البيانات من ملفات CSV و Excel برمجياً، ثم تجهيز المتغيرات الهدف والميزات.

import pandas as pd
from sklearn.model_selection import train_test_split
from sklearn.preprocessing import StandardScaler
from sklearn.linear_model import LogisticRegression
from sklearn.metrics import accuracy_score, classification_report

# Load dataset
df = pd.read_csv("customer_churn.csv")

# Select features and target
X = df[["age", "monthly_bill", "tenure_months", "support_calls"]]
y = df["churn"]

# Split data before scaling
X_train, X_test, y_train, y_test = train_test_split(
    X, y, test_size=0.2, random_state=42, stratify=y
)

# Fit scaler on training data only
scaler = StandardScaler()
X_train_scaled = scaler.fit_transform(X_train)
X_test_scaled = scaler.transform(X_test)

# Train model
model = LogisticRegression(max_iter=1000)
model.fit(X_train_scaled, y_train)

# Predict on unseen data
y_pred = model.predict(X_test_scaled)

# Evaluate
print("Accuracy:", accuracy_score(y_test, y_pred))
print(classification_report(y_test, y_pred))

لاحظ أن StandardScaler تم تدريبه على X_train فقط، ثم استُخدم لتحويل X_test. هذه نقطة محورية لمنع تسرب المعلومات الإحصائية من الاختبار إلى التدريب.

تقسيم البيانات في البيئات الموزعة باستخدام PySpark

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

from pyspark.sql import SparkSession
from pyspark.ml.feature import VectorAssembler
from pyspark.ml.classification import LogisticRegression
from pyspark.ml.evaluation import BinaryClassificationEvaluator

spark = SparkSession.builder.appName("TrainTestSplitExample").getOrCreate()

df = spark.read.csv("hdfs:///data/customer_churn.csv", header=True, inferSchema=True)

feature_cols = ["age", "monthly_bill", "tenure_months", "support_calls"]
assembler = VectorAssembler(inputCols=feature_cols, outputCol="features")
data = assembler.transform(df).select("features", "churn")

train_df, test_df = data.randomSplit([0.8, 0.2], seed=42)

lr = LogisticRegression(featuresCol="features", labelCol="churn", maxIter=20)
model = lr.fit(train_df)

predictions = model.transform(test_df)

evaluator = BinaryClassificationEvaluator(labelCol="churn")
auc = evaluator.evaluate(predictions)

print("Test AUC:", auc)

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

أخطاء شائعة تفسد تقييم النموذج

  • إجراء التنظيف أو التحويل الإحصائي على كامل البيانات قبل التقسيم.
  • إدخال متغيرات مشتقة من الهدف نفسه داخل مجموعة الميزات.
  • تكرار السجلات بين التدريب والاختبار بعد عمليات دمج وتوحيد الجداول (Merge, Join, Concat).
  • استخدام تقسيم عشوائي مع بيانات زمنية تتطلب ترتيباً تاريخياً.
  • الاعتماد على مقياس واحد مثل Accuracy في مسائل غير متوازنة.

متى لا يكون Train/Test Split وحده كافياً؟

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

كما أن مرحلة التقسيم لا تُغني عن أهمية هندسة الميزات (Feature Engineering): كيف تستخرج بيانات جديدة من البيانات الحالية؟، ولا عن فهم العلاقات من خلال الارتباط (Correlation): كيف تكتشف العلاقة الخفية بين المتغيرات؟، لأن جودة النموذج تبدأ من جودة التمثيل البنيوي للبيانات نفسها.

خاتمة

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

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

اترك تعليقاً

لن يتم نشر عنوان بريدك الإلكتروني. الحقول الإلزامية مشار إليها بـ *