الوحدة 4 · العمل بالطريقة الصحيحة
الـ Pipelines وColumnTransformer
22 دقراءة 4 دتمرين Python
الدرس في لمحة
ابنِ Pipeline واحدًا يملأ القيم المفقودة ويوحّد المقاييس ويطبّق one-hot encoding على الأعمدة الصحيحة باستخدام ColumnTransformer، فتُعاد المعالجة المسبقة داخل كل طيّة تحقق متقاطع وترافق النموذج أينما ذهب.
لا فيديو هنا، بل الشيء الحقيقي: درس مكتوب وأمثلة قابلة للتشغيل واختبار وتدريب.
ظلّ القسم 3 يترك بيانات على الطاولة. فـ country وsegment نصوص، ولا يقبلها أي نموذج في scikit-learn مباشرة. وavg_satisfaction فيه فجوات يرفضها الانحدار اللوجستي. وكنت توحّد المقاييس يدويًا، متتبّعًا X_train_s وX_test_s. كل خطوة من هذه الخطوات يجب أن تُتعلَّم من بيانات التدريب وحدها، وتُطبَّق بالطريقة نفسها على البيانات الجديدة، وتُعاد داخل كل طيّة تحقق متقاطع. وفعل ذلك يدويًا هو الطريقة التي يحدث بها التسرّب والأخطاء. الـ pipeline يفعله نيابةً عنك.
الـ Pipelines تجمع الخطوات في estimator واحد
الـ Pipeline قائمة من الخطوات: أي عدد من المحوّلات (transformers) (كائنات لها fit وtransform، مثل StandardScaler) يليها estimator نهائي واحد. ويتصرّف الـ pipeline كنموذج واحد:
- يدرّب
pipe.fit(X, y)المحوّل الأول، ويحوّل البيانات، ويمرّرها إلى الخطوة التالية، وهكذا، ثم يدرّب النموذج على الناتج. - يمرّر
pipe.predict(X_new)البياناتX_newعبر المحوّلات المدرَّبة مسبقًا، ثم يتنبأ.
استخدمت make_pipeline، التي تسمّي الخطوات تلقائيًا ("standardscaler" و"logisticregression"). أما Pipeline([("prep", ...), ("model", ...)]) فيتيح لك اختيار الأسماء، وستحتاج إلى ذلك في الدرس القادم حين تضبط المعاملات بأسمائها.
المكسب الحقيقي يظهر في التحقق المتقاطع. تستنسخ cross_validate الـ pipeline بأكمله وتعيد تدريبه في كل طيّة، فتُتعلَّم متوسطات الـ scaler ووسيطات الـ imputer وقائمة فئات الـ encoder من صفوف تدريب تلك الطيّة وحدها.
أعمدة مختلفة، ومعالجة مختلفة
ليس كل عمود بحاجة إلى المعالجة المسبقة نفسها. يوجّه ColumnTransformer كل مجموعة من الأعمدة عبر محوّلها الخاص، ثم يلصق المخرجات جنبًا إلى جنب.
ثلاثة محوّلات تقوم بالعمل:
OneHotEncoderينشئ عمود 0/1 لكل فئة:country_Egyptوcountry_Germanyوهكذا. اضبطhandle_unknown="ignore"؛ فالقيمة الافتراضية ترفع خطأً في أول مرة يصل فيها عميل من دولة لم تتضمّنها بيانات التدريب، وهذا يعني في بيئة الإنتاج خدمة تنبؤ منهارة.SimpleImputerيملأ القيم المفقودة بالمتوسط أو الوسيط أو القيمة الأكثر تكرارًا أو قيمة ثابتة، متعلَّمة من بيانات التدريب. ومعadd_indicator=Trueيضيف أيضًا عمودًا قيمته 1 حيث كانت القيمة مفقودة. نصف عملاء Cartwheel لم يجيبوا عن الاستبيان قط، وقد تكون هذه الحقيقة أهم من الدرجة نفسها.StandardScaler، كما سبق. ويتيحmake_pipelineصغير لمجموعة أعمدة واحدة أن تمرّ عبر خطوتين بالترتيب.
import pandas as pd
from sklearn.compose import ColumnTransformer
from sklearn.impute import SimpleImputer
from sklearn.linear_model import LogisticRegression
from sklearn.model_selection import StratifiedKFold, cross_val_score, train_test_split
from sklearn.pipeline import Pipeline, make_pipeline
from sklearn.preprocessing import OneHotEncoder, StandardScaler
churn = pd.read_csv("churn.csv")
X = churn.drop(columns=["customer_id", "churned"])
y = churn["churned"]
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.25, random_state=42, stratify=y)
numeric = ["tenure_days", "orders", "total_spent", "avg_order_value",
"days_since_last_order", "used_coupon", "support_tickets", "mobile_share"]
preprocess = ColumnTransformer([
("num", StandardScaler(), numeric),
("sat", make_pipeline(SimpleImputer(strategy="median", add_indicator=True), StandardScaler()),
["avg_satisfaction"]),
("cat", OneHotEncoder(handle_unknown="ignore"), ["country", "segment"]),
])
pipe = Pipeline([("prep", preprocess), ("model", LogisticRegression())])
pipe.fit(X_train, y_train)
names = pipe.named_steps["prep"].get_feature_names_out()
print(len(names), "model inputs, e.g.", list(names[8:12]))
cv = StratifiedKFold(n_splits=5, shuffle=True, random_state=42)
print("all columns ROC AUC:", cross_val_score(pipe, X_train, y_train, cv=cv, scoring="roc_auc").mean().round(3))
numeric_only = make_pipeline(StandardScaler(), LogisticRegression())
print("numeric only ROC AUC:", cross_val_score(numeric_only, X_train[numeric], y_train, cv=cv, scoring="roc_auc").mean().round(3))يقبل الـ pipeline الـ DataFrame الخام، بما فيه من نصوص وفجوات، وينتج 21 مُدخلًا للنموذج: ثمانية أرقام موحّدة المقاييس، ودرجة الرضا المملوءة ومؤشّر غيابها، وثمانية أعمدة للدول وثلاثة للشرائح. وتسردها get_feature_names_out مع بادئة تسمّي المحوّل الذي أنتج كلًّا منها، وهكذا تطابق المعاملات بالأعمدة لاحقًا.
أعمدة أكثر، ونموذج أسوأ
ثم تأتي المفاجأة: الـ pipeline الكامل يرتّب العملاء أسوأ من الأعمدة الرقمية الثمانية وحدها، 0.776 مقابل 0.807. أحد عشر عمود one-hot ودرجة رضا نصف قيمها مملوءة تعطي الانحدار اللوجستي معاملات أكثر ليلائمها بـ 298 صفًا فقط، وعلى هذه البيانات تحمل الأعمدة الإضافية ضجيجًا أكثر من الإشارة. صحيح أن نسبة الانقطاع في مصر تبدو أعلى في بيانات التدريب، لكن مع 44 عميلًا مصريًا في التدريب، هذا أضعف من أن يُراهَن عليه.
هذا ليس فشلًا للـ pipeline. بل هو الـ pipeline يؤدي عمله: جعل اختبار الأعمدة الإضافية تغييرًا من خمسة أسطر، وأعطى التحقق المتقاطع إجابة واضحة. يبقى النموذج الرقمي البطل حاليًا. والأعمدة الفئوية تستحق العودة إليها حين يصبح لدى Cartwheel بضعة آلاف من العملاء، أو مع regularization أقوى.
ويعطي الـ pipeline أيضًا كل إعداد عنوانًا: model__C هو C في الخطوة المسمّاة model، وprep__sat__simpleimputer__strategy يصل إلى الـ imputer. يستخدم الدرس القادم هذه العناوين للبحث عن أفضل الإعدادات تلقائيًا.
أهم النقاط
- يسلسل الـ
Pipelineمحوّلات (transformers) ونموذجًا نهائيًا في estimator واحد: يدرّبfitكل خطوة بالترتيب، ويمرّرpredictالبيانات الجديدة عبر الخطوات نفسها. - يطبّق
ColumnTransformerمعالجة مسبقة مختلفة على أعمدة مختلفة ثم يضمّ النتائج جنبًا إلى جنب. - يحوّل
OneHotEncoder(handle_unknown="ignore")الفئات إلى أعمدة 0/1 ولا ينهار أمام فئة لم يرَها في التدريب. - يملأ
SimpleImputer(add_indicator=True)الفجوات ويضيف عمودًا يسجّل القيم التي كانت مفقودة. - لأن الـ pipeline كله يُعاد تدريبه في كل طيّة، لا تتعلّم المعالجة المسبقة إلا من صفوف التدريب، وهذا يُبقي التحقق المتقاطع أمينًا.