تحسين التعميم في نماذج البقاء | بواسطة نيكولاس لوبي
نهج تقليدي
تبدأ العديد من التطبيقات الحالية لتحليل البقاء بمجموعة بيانات تحتوي على ملاحظة واحدة لكل فرد (المرضى في دراسة صحية، الموظفون في حالة الاستنزاف، العملاء في حالة توقف العميل، وما إلى ذلك). بالنسبة لهؤلاء الأفراد، لدينا عادةً متغيران رئيسيان: أحدهما يشير إلى الحدث الذي يهمك (استقالة الموظف) والآخر لقياس الوقت (مدة بقائهم في الشركة، حتى اليوم أو مغادرتهم). ومع هذين المتغيرين، لدينا بعد ذلك متغيرات توضيحية نهدف من خلالها إلى التنبؤ بالمخاطر التي يتعرض لها كل فرد. يمكن أن تشمل هذه الميزات الدور الوظيفي أو العمر أو التعويض الذي يحصل عليه الموظف، على سبيل المثال.
من الآن فصاعدا، تأخذ معظم التطبيقات نموذج البقاء (من المقدرات الأبسط مثل كابلان ماير إلى المقدرات الأكثر تعقيدا مثل نماذج المجموعة أو حتى الشبكات العصبية)، وتناسبها مع مجموعة قطار ثم تقيمها على مجموعة اختبار. عادةً ما يتم إجراء هذا التقسيم لاختبار التدريب على الملاحظات الفردية، مما يؤدي بشكل عام إلى تقسيم طبقي.
في حالتي، بدأت بمجموعة بيانات تتبع العديد من الموظفين في الشركة شهريًا حتى ديسمبر 2023 (في حالة ما إذا كان الموظف لا يزال في الشركة)، أو حتى الشهر الذي تركوا فيه الشركة – تاريخ الحدث:
ومن أجل تكييف بياناتي مع حالة البقاء، أخذت الملاحظة الأخيرة لكل موظف كما هو موضح في الصورة أعلاه (النقاط الزرقاء للموظفين النشطين، والعلامات الحمراء للموظفين الذين غادروا). في تلك المرحلة، لكل موظف، قمت بتسجيل ما إذا كان الحدث قد وقع في ذلك التاريخ أم لا (إذا كانوا نشطين أو إذا كانوا قد غادروا)، ومدة خدمتهم بالأشهر في ذلك الوقت، وجميع المتغيرات التوضيحية الخاصة بهم. بعد ذلك، أجريت تقسيمًا طبقيًا لاختبار التدريب على هذه البيانات، على النحو التالي:
import numpy as np
import pandas as pd
from sklearn.model_selection import train_test_split# We load our dataset with several observations (record_date) per employee (employee_id)
# The event column indicates if the employee left on that given month (1) or if the employee was still active (0)
df = pd.read_csv(f'{FILE_NAME}.csv')
# Creating a label where positive events have tenure and negative events have negative tenure - required by Random Survival Forest
df_model['label'] = np.where(df_model['event'], df_model['tenure_in_months'], - df_model['tenure_in_months'])
df_train, df_test = train_test_split(df_model, test_size=0.2, stratify=df_model['event'], random_state=42)
بعد إجراء التقسيم، شرعت في ملاءمة النموذج. في هذه الحالة، اخترت تجربة أ غابة البقاء العشوائية باستخدام مكتبة scikit-survival.
from sklearn.preprocessing import OrdinalEncoder
from sksurv.datasets import get_x_y
from sksurv.ensemble import RandomSurvivalForestcat_features = [] # list of all the categorical features
features = [] # list of all the features (both categorical and numeric)
# Categorical Encoding
encoder = OrdinalEncoder(handle_unknown='use_encoded_value', unknown_value=-1)
encoder.fit(df_train[cat_features])
df_train[cat_features] = encoder.transform(df_train[cat_features])
df_test[cat_features] = encoder.transform(df_test[cat_features])
# X & y
X_train, y_train = get_x_y(df_train, attr_labels=['event','tenure_in_months'], pos_label=1)
X_test, y_test = get_x_y(df_test, attr_labels=['event','tenure_in_months'], pos_label=1)
# Fit the model
estimator = RandomSurvivalForest(random_state=RANDOM_STATE)
estimator.fit(X_train[features], y_train)
# Store predictions
y_pred = estimator.predict(X_test[features])
بعد تشغيل سريع باستخدام الإعدادات الافتراضية للنموذج، شعرت بسعادة غامرة بمقاييس الاختبار التي رأيتها. بادئ ذي بدء ، كنت أحصل على مؤشر التوافق فوق 0.90 في مجموعة الاختبار. يعد مؤشر التوافق مقياسًا لمدى نجاح النموذج في توقع ترتيب الأحداث: فهو يعكس ما إذا كان الموظفون المتوقع تعرضهم لمخاطر عالية هم بالفعل أولئك الذين يغادرون الشركة أولاً. يتوافق المؤشر 1 مع دقة التنبؤ المثالية، بينما يشير المؤشر 0.5 إلى توقع ليس أفضل من الصدفة العشوائية.
لقد كنت مهتمًا بشكل خاص بمعرفة ما إذا كان الموظفون الذين غادروا في مجموعة الاختبار متطابقين مع الموظفين الأكثر خطورة وفقًا للنموذج. في حالة غابة البقاء العشوائية، يقوم النموذج بإرجاع درجات المخاطر لكل ملاحظة. لقد أخذت النسبة المئوية للموظفين الذين تركوا الشركة في مجموعة الاختبار، واستخدمتها لتصفية الموظفين الأكثر خطورة وفقًا للنموذج. وكانت النتائج قوية للغاية، حيث كان الموظفون الذين تم تصنيفهم على أنهم الأكثر تعرضًا للخطر يتطابقون تمامًا تقريبًا مع المغادرين الفعليين، مع درجة F1 أعلى من 0.90 في فئة الأقلية.
from lifelines.utils import concordance_index
from sklearn.metrics import classification_report# Concordance Index
ci_test = concordance_index(df_test['tenure_in_months'], -y_pred, df_test['event'])
print(f'Concordance index:{ci_test:0.5f}\n')
# Match the most risky employees (according to the model) with the employees who left
q_test = 1 - df_test['event'].mean()
thr = np.quantile(y_pred, q_test)
risky_employees = (y_pred >= thr) * 1
print(classification_report(df_test['event'], risky_employees))
يجب أن يطلق الحصول على مقاييس +0.9 في الجولة الأولى إنذارًا: هل كان النموذج قادرًا حقًا على التنبؤ بما إذا كان الموظف سيبقى أو يغادر بهذه الثقة؟ تخيل هذا: نحن نقدم توقعاتنا لنحدد الموظفين الذين من المرجح أن يغادروا. ومع ذلك، تمر بضعة أشهر، ثم يصل إلينا قسم الموارد البشرية قلقًا، قائلًا إن الأشخاص الذين غادروا خلال الفترة الماضية، لم يتطابقوا تمامًا مع توقعاتنا، على الأقل بالمعدل المتوقع من مقاييس الاختبار لدينا.
لدينا مشكلتان رئيسيتان هنا: الأولى هي أن نموذجنا لا يستقرئ تمامًا كما كنا نعتقد. أما الأمر الثاني، والأسوأ من ذلك، فهو أننا لم نتمكن من قياس هذا النقص في الأداء. أولاً، سأعرض طريقة بسيطة يمكننا من خلالها تقدير مدى نجاح نموذجنا في الاستقراء، ثم سأتحدث عن أحد الأسباب المحتملة لفشله في القيام بذلك، وكيفية تخفيفه.
تقدير قدرات التعميم
المفتاح هنا هو الوصول إلى بيانات اللوحة، أي عدة سجلات لأفرادنا بمرور الوقت، حتى وقت الحدث أو وقت انتهاء الدراسة (تاريخ اللقطة لدينا، في حالة تناقص الموظفين). بدلاً من التخلص من كل هذه المعلومات والاحتفاظ فقط بالسجل الأخير لكل موظف، يمكننا استخدامه لإنشاء مجموعة اختبار تعكس بشكل أفضل كيفية أداء النموذج في المستقبل. الفكرة بسيطة للغاية: لنفترض أن لدينا سجلات شهرية لموظفينا حتى ديسمبر 2023. يمكننا الرجوع إلى الوراء، على سبيل المثال، 6 أشهر، والتظاهر بأننا التقطنا اللقطة في يونيو بدلاً من ديسمبر. بعد ذلك، سنعتبر الملاحظة الأخيرة للموظفين الذين تركوا الشركة قبل يونيو 2023 كأحداث إيجابية، وسجل يونيو 2023 للموظفين الذين بقوا على قيد الحياة بعد ذلك التاريخ كأحداث سلبية، حتى لو كنا نعرف بالفعل أن بعضهم غادر في النهاية بعد ذلك. نحن نتظاهر بأننا لا نعرف هذا بعد.
كما توضح الصورة أعلاه، قمت بالتقاط لقطة في شهر يونيو، وتم اعتبار جميع الموظفين الذين كانوا نشطين في ذلك الوقت نشطين. تأخذ مجموعة بيانات الاختبار جميع هؤلاء الموظفين النشطين في شهر يونيو مع المتغيرات التوضيحية الخاصة بهم كما كانت في ذلك التاريخ، وتأخذ آخر فترة عمل حققوها بحلول شهر ديسمبر:
test_date="2023-07-01"# Selecting training data from records before the test date and taking the last observation per employee
df_train = df[df.record_date < test_date].reset_index(drop=True).copy()
df_train = df_train.groupby('employee_id').tail(1).reset_index(drop=True)
df_train['label'] = np.where(df_train['event'], df_train['tenure_in_months'], - df_train['tenure_in_months'])
# Preparing test data with records of active employees at the test date
df_test = df[(df.record_date == test_date) & (df['event']==0)].reset_index(drop=True).copy()
df_test = df_test.groupby('employee_id').tail(1).reset_index(drop=True)
df_test = df_test.drop(columns = ['tenure_in_months','event'])
# Fetching the last tenure and event status for employees in the test dataset
df_last_tenure = df[df.employee_id.isin(df_test.employee_id.unique())].reset_index(drop=True).copy()
df_last_tenure = df_last_tenure.groupby('employee_id').tail(1).reset_index(drop=True)
df_test = df_test.merge(df_last_tenure[['employee_id','tenure_in_months','event']], how='left')
df_test['label'] = np.where(df_test['event'], df_test['tenure_in_months'], - df_test['tenure_in_months'])
لقد قمنا بمطابقة نموذجنا مرة أخرى مع بيانات القطار الجديد، وبمجرد الانتهاء، نقوم بعمل توقعاتنا لجميع الموظفين الذين كانوا نشطين في شهر يونيو. ثم نقارن هذه التوقعات بالنتيجة الفعلية لشهر يوليو – ديسمبر 2023 – وهذه هي مجموعة الاختبار الخاصة بنا. إذا كان هؤلاء الموظفون الذين وضعنا علامة عليهم على أنهم يواجهون أكبر قدر من المخاطر خلال الفصل الدراسي، وأولئك الذين وضعنا علامة على أنهم يواجهون أقل المخاطر لم يغادروا، أو غادروا في وقت متأخر إلى حد ما خلال الفترة، فإن نموذجنا يستقرئ بشكل جيد. ومن خلال إعادة تحليلنا إلى الوراء وترك الفترة الأخيرة للتقييم، يمكننا أن نحصل على فهم أفضل لمدى تعميم نموذجنا. بالطبع، يمكننا أن نأخذ هذه الخطوة إلى الأمام ونقوم بنوع من التحقق من صحة السلاسل الزمنية. على سبيل المثال، يمكننا تكرار هذه العملية عدة مرات، وفي كل مرة نرجع بالزمن 6 أشهر إلى الوراء، ونقيم دقة النموذج على عدة أطر زمنية.
وبعد تدريب نموذجنا مرة أخرى، نرى الآن انخفاضًا حادًا في الأداء. أولًا، يبلغ مؤشر التوافق الآن حوالي 0.5، وهو ما يعادل مؤشر المتنبئ العشوائي. أيضًا، إذا حاولنا مطابقة الموظفين “n” الأكثر خطورة وفقًا للنموذج مع الموظفين “n” الذين غادروا في مجموعة الاختبار، فإننا نرى تصنيفًا سيئًا للغاية مع 0.15 F1 لفئة الأقلية:
من الواضح أن هناك شيئًا خاطئًا، ولكن على الأقل أصبحنا الآن قادرين على اكتشافه بدلاً من تضليلنا. الفكرة الرئيسية هنا هي أن نموذجنا يعمل بشكل جيد مع التقسيم التقليدي، لكنه لا يستقرئ عند إجراء التقسيم على أساس الوقت. هذه علامة واضحة على احتمال وجود تحيز لبعض الوقت. باختصار، يتم تسريب المعلومات المعتمدة على الوقت، كما أن نموذجنا يفرط في ملاءمتها. يعد هذا أمرًا شائعًا في حالات مثل مشكلة تناقص الموظفين لدينا، عندما تأتي مجموعة البيانات من لقطة تم التقاطها في تاريخ ما.
التحيز الزمني
تتلخص المشكلة في ما يلي: جميع ملاحظاتنا الإيجابية (الموظفون الذين غادروا) تنتمي إلى تواريخ سابقة، وجميع ملاحظاتنا السلبية (الموظفون النشطون حاليًا) يتم قياسها جميعًا في نفس التاريخ – اليوم. إذا كانت هناك ميزة واحدة تكشف ذلك للنموذج، إذن فبدلاً من التنبؤ بالمخاطر، سنتوقع ما إذا تم تسجيل الموظف في ديسمبر 2023 أو قبل ذلك. قد يكون هذا دقيقًا جدًا. على سبيل المثال، إحدى الميزات التي يمكننا استخدامها هي درجة مشاركة الموظفين. يمكن أن تظهر هذه الميزة بعض الأنماط الموسمية، ومن المؤكد أن قياسها في نفس الوقت للموظفين النشطين سيقدم بعض التحيز في النموذج. ربما في شهر ديسمبر، خلال موسم العطلات، تميل درجة التفاعل هذه إلى الانخفاض. سيرى النموذج درجة منخفضة مرتبطة بجميع الموظفين النشطين، لذلك قد يتعلم التنبؤ بأنه كلما انخفضت المشاركة، تنخفض أيضًا مخاطر التوقف عن العمل، في حين أنه في الواقع يجب أن يكون العكس!
في الوقت الحالي، يجب أن يكون الحل البسيط والفعال لهذه المشكلة واضحًا: فبدلاً من أخذ الملاحظة الأخيرة لكل موظف نشط، يمكننا فقط اختيار شهر عشوائي من تاريخه بالكامل داخل الشركة. سيؤدي هذا إلى تقليل فرص اختيار النموذج لأي أنماط زمنية لا نرغب في تجاوزها:
في الصورة أعلاه يمكننا أن نرى أننا نغطي الآن مجموعة أوسع من التواريخ للموظفين النشطين. بدلاً من استخدام النقاط الزرقاء في يونيو 2023، نأخذ النقاط البرتقالية العشوائية بدلاً من ذلك، ونسجل متغيراتها في ذلك الوقت، ومدة خدمتهم حتى الآن في الشركة:
np.random.seed(0)# Select training data before the test date
df_train = df[df.record_date < test_date].reset_index(drop=True).copy()
# Create an indicator for whether an employee eventually churns within the train set
df_train['indicator'] = df_train.groupby('employee_id').event.transform(max)
# Isolate records of employees who left, and store their last observation
churn = df_train[df_train.indicator==1].reset_index(drop=True).copy()
churn = churn.groupby('employee_id').tail(1).reset_index(drop=True)
# For employees who stayed, randomly pick one observation from their historic records
stay = df_train[df_train.indicator==0].reset_index(drop=True).copy()
stay = stay.groupby('employee_id').apply(lambda x: x.sample(1)).reset_index(drop=True)
# Combine churn and stay samples into the new training dataset
df_train = pd.concat([churn,stay], ignore_index=True).copy()
df_train['label'] = np.where(df_train['event'], df_train['tenure_in_months'], - df_train['tenure_in_months'])
del df_train['indicator']
# Prepare the test dataset similarly, using only the snapshot from the test date
df_test = df[(df.record_date == test_date) & (df.event==0)].reset_index(drop=True).copy()
df_test = df_test.groupby('employee_id').tail(1).reset_index(drop=True)
df_test = df_test.drop(columns = ['tenure_in_months','event'])
# Get the last known tenure and event status for employees in the test set
df_last_tenure = df[df.employee_id.isin(df_test.employee_id.unique())].reset_index(drop=True).copy()
df_last_tenure = df_last_tenure.groupby('employee_id').tail(1).reset_index(drop=True)
df_test = df_test.merge(df_last_tenure[['employee_id','tenure_in_months','event']], how='left')
df_test['label'] = np.where(df_test['event'], df_test['tenure_in_months'], - df_test['tenure_in_months'])
نقوم بعد ذلك بتدريب نموذجنا مرة أخرى، وتقييمه باستخدام نفس مجموعة الاختبار التي أجريناها من قبل. نرى الآن مؤشر توافق يبلغ حوالي 0.80. هذا ليس +0.90 الذي كان لدينا سابقًا، ولكنه بالتأكيد خطوة أعلى من مستوى الفرصة العشوائية البالغ 0.5. فيما يتعلق باهتمامنا بتصنيف الموظفين، ما زلنا بعيدين جدًا عن +0.9 F1 الذي كان لدينا من قبل، لكننا نرى زيادة طفيفة مقارنة بالنهج السابق، خاصة بالنسبة لفئة الأقليات.
اكتشاف المزيد من موقع علم
اشترك للحصول على أحدث التدوينات المرسلة إلى بريدك الإلكتروني.