Skip to content

Folders and files

NameName
Last commit message
Last commit date

Latest commit

ย 

History

7 Commits
ย 
ย 
ย 
ย 

Repository files navigation

๐Ÿ“Š HR Analytics โ€” Job Change of Data Scientists

End-to-End Machine Learning Preprocessing Pipeline

Course: Machine Learning ย ยทย  Author: Youssef Emad ย ยทย  University: Benha University โ€” Computer Science, AI Track


๐Ÿ“Œ Problem Statement

Given candidate background data, predict whether a data scientist is actively seeking a new job after training โ€” enabling companies to optimize recruitment costs and design retention strategies.

Target Meaning
0 Not looking for a job change
1 Looking for a job change

Binary Classification with a ~75:25 class imbalance.


๐Ÿ“ฆ Dataset

HR Analytics: Job Change of Data Scientists โ€” Kaggle

File Rows Description
aug_train.csv ~19,158 Labeled training data
aug_test.csv ~2,129 Unlabeled test data

Key Features:

Feature Type Description
city_development_index Numerical Urbanisation level of city (0โ€“1)
experience Numerical Years of work experience
training_hours Numerical Hours of training completed
education_level Ordinal Primary โ†’ High School โ†’ Graduate โ†’ Masters โ†’ PhD
company_size Ordinal Headcount band of current employer
gender Categorical Gender of candidate
relevent_experience Categorical Whether candidate has relevant experience
enrolled_university Categorical Type of university enrollment
major_discipline Categorical Field of study
company_type Categorical Type of current company

๐Ÿ—บ๏ธ Pipeline Overview

Raw Data  (aug_train.csv ยท aug_test.csv)
   โ”‚
   โ”œโ”€โ”€ 01. Load & Explore          โ†’ shape, dtypes, target distribution
   โ”œโ”€โ”€ 02. Missing Value Analysis  โ†’ heatmap + smart imputation
   โ”œโ”€โ”€ 03. Outlier Detection       โ†’ box plots + 99th percentile capping
   โ”œโ”€โ”€ 04. Feature Cleaning        โ†’ fix string-encoded numerics
   โ”œโ”€โ”€ 05. Ordinal Encoding        โ†’ education_level, company_size
   โ”œโ”€โ”€ 06. One-Hot Encoding        โ†’ gender, major_discipline, company_typeโ€ฆ
   โ”œโ”€โ”€ 07. Feature Engineering     โ†’ 3 new derived interaction features
   โ”œโ”€โ”€ 08. Drop Irrelevant Cols    โ†’ enrollee_id, city, redundant raw cols
   โ”œโ”€โ”€ 09. Scaling                 โ†’ StandardScaler (fit on train only)
   โ””โ”€โ”€ 10. Train / Val Split       โ†’ 80/20 stratified split
         โ”‚
         โ”œโ”€โ”€ train_clean.csv  โœ…
         โ”œโ”€โ”€ val_clean.csv    โœ…
         โ””โ”€โ”€ test_clean.csv   โœ…

๐Ÿ› ๏ธ Preprocessing Steps in Detail

1 โ€” Missing Value Imputation

Strategy Applied To Reason
Fill with 'Unknown' All categorical columns Preserves missingness as a signal
Fill with median All numerical columns Robust to skew and outliers

Both train and test are imputed with the same function to prevent any data leakage.


2 โ€” Outlier Handling

Feature Action Reason
training_hours Capped at 99th percentile Right-skewed with extreme high values
city_development_index No capping Bounded [0, 1] by nature

3 โ€” Feature Cleaning

Raw string values converted to clean integers:

Feature Raw โ†’ Cleaned
experience '>20' โ†’ 21 ยท '<1' โ†’ 0
last_new_job '>4' โ†’ 5 ยท 'never' โ†’ 0

4 โ€” Encoding

Ordinal Encoding โ€” natural order preserved:

Feature Scale
education_level Unknown=0 ยท Primary School=1 ยท High School=2 ยท Graduate=3 ยท Masters=4 ยท PhD=5
company_size Unknown=0 ยท <10=1 ยท 10/49=2 ยท 50-99=3 ยท 100-500=4 ยท 500-999=5 ยท 1000-4999=6 ยท 5000-9999=7 ยท 10000+=8

One-Hot Encoding (drop_first=True) applied to: gender ยท relevent_experience ยท enrolled_university ยท major_discipline ยท company_type

Train and test columns are aligned after OHE to handle any category present in one but not the other.


5 โ€” Feature Engineering

3 new interaction features capturing signals not present in raw columns:

New Feature Formula Intuition
hours_per_exp training_hours / (experience + 1) Training intensity relative to seniority
cdi_x_exp city_development_index ร— experience Combined urbanisation & experience signal
high_cdi CDI โ‰ฅ 0.9 โ†’ 1, else 0 Binary flag for highly developed cities

6 โ€” Scaling

StandardScaler applied to 6 continuous features:

city_development_index ยท training_hours ยท experience
last_new_job ยท hours_per_exp ยท cdi_x_exp

โš ๏ธ Scaler is fit only on the training set then applied to val and test โ€” zero leakage.


7 โ€” Train / Validation Split

Split Size Method
Train 80% Stratified by target
Validation 20% Stratified by target

๐Ÿ’ป Code

๐Ÿ“ฅ 01. Setup & Download Dataset
!pip install kagglehub -q

import kagglehub, shutil, os

path = kagglehub.dataset_download('arashnic/hr-analytics-job-change-of-data-scientists')
dest = '/content/hr_data'
os.makedirs(dest, exist_ok=True)
for f in os.listdir(path):
    shutil.copy(os.path.join(path, f), dest)

print('โœ… Files:', os.listdir(dest))
๐Ÿ“ฆ 02. Imports & Load Data
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import seaborn as sns
from sklearn.model_selection import train_test_split
from sklearn.preprocessing import StandardScaler

pd.set_option('display.max_columns', None)
sns.set_theme(style='whitegrid')
SEED = 42

train = pd.read_csv('/content/hr_data/aug_train.csv')
test  = pd.read_csv('/content/hr_data/aug_test.csv')

print(f'Train shape: {train.shape}')
print(f'Test  shape: {test.shape}')
train.head()
๐Ÿ” 03. Exploratory Overview
train.info()
train.describe(include='all').T

# Target distribution
counts = train['target'].value_counts()
pct    = counts / len(train) * 100

fig, axes = plt.subplots(1, 2, figsize=(11, 4))
axes[0].bar(['Not Changing (0)', 'Changing (1)'], counts.values,
            color=['#4C72B0', '#DD8452'], edgecolor='white')
for i, (v, p) in enumerate(zip(counts.values, pct.values)):
    axes[0].text(i, v + 40, f'{v}\n({p:.1f}%)', ha='center', fontweight='bold')
axes[0].set_title('Target Distribution (Count)', fontweight='bold')

axes[1].pie(counts.values, labels=['Not Changing', 'Changing'],
            autopct='%1.1f%%', colors=['#4C72B0','#DD8452'],
            startangle=90, wedgeprops=dict(edgecolor='white'))
axes[1].set_title('Target Distribution (Proportion)', fontweight='bold')

plt.suptitle('โš ๏ธ Class Imbalance Detected (~75% vs ~25%)', color='firebrick')
plt.tight_layout()
plt.show()
๐Ÿงน 04. Missing Value Analysis & Imputation
missing     = train.isnull().sum().sort_values(ascending=False)
missing_pct = (missing / len(train) * 100).round(2)
print(pd.DataFrame({'Missing': missing, 'Pct (%)': missing_pct})[missing > 0])

fig, axes = plt.subplots(1, 2, figsize=(14, 4))
(missing_pct[missing_pct > 0].sort_values()
 .plot(kind='barh', ax=axes[0], color='#DD8452', edgecolor='white'))
axes[0].set_title('Missing Values (%) per Column', fontweight='bold')
sns.heatmap(train[missing[missing > 0].index].isnull(),
            cbar=False, yticklabels=False, cmap='Oranges', ax=axes[1])
axes[1].set_title('Missing Value Heatmap', fontweight='bold')
plt.tight_layout(); plt.show()

def impute_df(df):
    df = df.copy()
    for col in df.select_dtypes(include='object').columns:
        df[col] = df[col].fillna('Unknown')
    for col in [c for c in df.select_dtypes(include=np.number).columns
                if c not in ['enrollee_id', 'target']]:
        df[col] = df[col].fillna(df[col].median())
    return df

train = impute_df(train)
test  = impute_df(test)
print(f'โœ… Missing after imputation โ€” Train: {train.isnull().sum().sum()} | Test: {test.isnull().sum().sum()}')
๐Ÿ“ฆ 05. Outlier Detection & Capping
fig, axes = plt.subplots(1, 2, figsize=(12, 4))
for ax, col in zip(axes, ['city_development_index', 'training_hours']):
    sns.boxplot(y=train[col], ax=ax, color='#4C72B0',
                flierprops=dict(marker='o', markerfacecolor='red', markersize=4))
    ax.set_title(f'{col}', fontweight='bold')
plt.suptitle('Outlier Detection โ€” Box Plots', fontweight='bold')
plt.tight_layout(); plt.show()

cap_99 = train['training_hours'].quantile(0.99)
train['training_hours'] = train['training_hours'].clip(upper=cap_99)
test['training_hours']  = test['training_hours'].clip(upper=cap_99)
print(f'โœ… training_hours capped at {cap_99:.0f} hrs (99th percentile)')
๐Ÿ”ง 06. Feature Cleaning
def clean_experience(val):
    if val == '>20': return 21
    if val == '<1':  return 0
    try: return int(val)
    except: return np.nan

def clean_last_new_job(val):
    if val == '>4':    return 5
    if val == 'never': return 0
    try: return int(val)
    except: return np.nan

for df in [train, test]:
    df['experience']   = df['experience'].apply(clean_experience).fillna(train['experience'].median())
    df['last_new_job'] = df['last_new_job'].apply(clean_last_new_job).fillna(train['last_new_job'].median())

print('โœ… experience unique values:   ', sorted(train['experience'].unique()))
print('โœ… last_new_job unique values: ', sorted(train['last_new_job'].unique()))
๐Ÿ”ข 07. Feature Encoding
# Ordinal encoding
edu_order  = {'Unknown':0,'Primary School':1,'High School':2,'Graduate':3,'Masters':4,'Phd':5}
size_order = {'Unknown':0,'<10':1,'10/49':2,'50-99':3,'100-500':4,
              '500-999':5,'1000-4999':6,'5000-9999':7,'10000+':8}

for df in [train, test]:
    df['education_level_enc'] = df['education_level'].map(edu_order).fillna(0).astype(int)
    df['company_size_enc']    = df['company_size'].map(size_order).fillna(0).astype(int)

# One-hot encoding
ohe_cols = ['gender', 'relevent_experience', 'enrolled_university',
            'major_discipline', 'company_type']
train_enc = pd.get_dummies(train, columns=ohe_cols, drop_first=True)
test_enc  = pd.get_dummies(test,  columns=ohe_cols, drop_first=True)
train_enc, test_enc = train_enc.align(test_enc, join='left', axis=1, fill_value=0)

print(f'โœ… Shape after OHE โ€” Train: {train_enc.shape} | Test: {test_enc.shape}')
โš—๏ธ 08. Feature Engineering
for df in [train_enc, test_enc]:
    df['hours_per_exp'] = df['training_hours'] / (df['experience'] + 1)
    df['cdi_x_exp']     = df['city_development_index'] * df['experience']
    df['high_cdi']      = (df['city_development_index'] >= 0.9).astype(int)

print('โœ… New features added: hours_per_exp ยท cdi_x_exp ยท high_cdi')
๐Ÿ—‘๏ธ 09. Drop Irrelevant Columns
drop_cols   = ['enrollee_id', 'city', 'education_level', 'company_size']
train_clean = train_enc.drop(columns=[c for c in drop_cols if c in train_enc.columns])
test_clean  = test_enc.drop(columns=[c for c in drop_cols if c in test_enc.columns])

if 'target' in test_clean.columns:
    test_clean = test_clean.drop(columns=['target'])

print(f'โœ… Train columns kept: {train_clean.shape[1]} | Test: {test_clean.shape[1]}')
๐Ÿ“ 10. Scaling
scale_cols = [c for c in ['city_development_index', 'training_hours', 'experience',
                           'last_new_job', 'hours_per_exp', 'cdi_x_exp']
              if c in train_clean.columns]

scaler = StandardScaler()
train_clean[scale_cols] = scaler.fit_transform(train_clean[scale_cols])
test_clean[scale_cols]  = scaler.transform(test_clean[scale_cols])

print('โœ… Scaling done โ€” mean โ‰ˆ 0, std โ‰ˆ 1')
print(train_clean[scale_cols].describe().T[['mean', 'std']].round(3))
โœ‚๏ธ 11. Train / Validation Split
X = train_clean.drop(columns=['target'])
y = train_clean['target']

X_train, X_val, y_train, y_val = train_test_split(
    X, y, test_size=0.2, random_state=SEED, stratify=y
)

print(f'X_train: {X_train.shape}  |  X_val: {X_val.shape}')
print(f'Train class balance โ†’ {y_train.value_counts(normalize=True).round(3).to_dict()}')
print(f'Val   class balance โ†’ {y_val.value_counts(normalize=True).round(3).to_dict()}')
๐Ÿ“Š 12. Correlation Heatmap
corr_cols = scale_cols + ['education_level_enc', 'company_size_enc', 'high_cdi', 'target']
corr_cols = [c for c in corr_cols if c in train_clean.columns]

plt.figure(figsize=(11, 8))
mask = np.triu(np.ones_like(train_clean[corr_cols].corr(), dtype=bool))
sns.heatmap(train_clean[corr_cols].corr(), annot=True, fmt='.2f',
            cmap='coolwarm', center=0, mask=mask,
            linewidths=0.5, annot_kws={'size': 9})
plt.title('Correlation Matrix โ€” Numerical & Ordinal Features', fontweight='bold')
plt.tight_layout(); plt.show()

print('\nTop features correlated with target:')
print(train_clean[corr_cols].corr()['target'].drop('target')
      .abs().sort_values(ascending=False).round(3).to_string())
๐Ÿ’พ 13. Save Cleaned Files
os.makedirs('/content/hr_data/cleaned', exist_ok=True)

X_train.assign(target=y_train.values).to_csv('/content/hr_data/cleaned/train_clean.csv', index=False)
X_val.assign(target=y_val.values).to_csv('/content/hr_data/cleaned/val_clean.csv',       index=False)
test_clean.to_csv('/content/hr_data/cleaned/test_clean.csv',                              index=False)

print('โœ… Saved: train_clean.csv | val_clean.csv | test_clean.csv')

โœ… Preprocessing Summary

Step Action Detail
Missing โ€” categorical Fill 'Unknown' Treats missingness as its own category
Missing โ€” numerical Fill median Robust to skewed distributions
Outliers Cap training_hours at 99th pct Reduces right-tail distortion
experience '>20'โ†’21 ยท '<1'โ†’0 Enables numerical treatment
last_new_job '>4'โ†’5 ยท 'never'โ†’0 Enables numerical treatment
education_level Ordinal encoded (0โ€“5) Preserves natural order
company_size Ordinal encoded (0โ€“8) Preserves natural order
Categorical cols One-Hot (drop_first=True) No ordinal assumption
New features hours_per_exp ยท cdi_x_exp ยท high_cdi Capture interaction signals
Scaling StandardScaler on 6 numeric cols Fit on train only โ€” no leakage
Split 80/20 stratified Preserves class ratio in both sets

๐Ÿ“ Repository Structure

HR-Analytics-Job-Change/
โ”‚
โ”œโ”€โ”€ ๐Ÿ“ Documentation/                    โ† Report & docs
โ”œโ”€โ”€ ๐Ÿ“ Presentation/                     โ† Slides
โ””โ”€โ”€ ๐Ÿ“ code/
    โ”œโ”€โ”€ ML_Project.ipynb                 โ† Full preprocessing notebook
    โ”œโ”€โ”€ data/
    โ”‚   โ”œโ”€โ”€ aug_train.csv                โ† Raw training data
    โ”‚   โ”œโ”€โ”€ aug_test.csv                 โ† Raw test data
    โ”‚   โ””โ”€โ”€ cleaned/
    โ”‚       โ”œโ”€โ”€ train_clean.csv          โ† Ready for modeling โœ…
    โ”‚       โ”œโ”€โ”€ val_clean.csv            โœ…
    โ”‚       โ””โ”€โ”€ test_clean.csv           โœ…
    โ””โ”€โ”€ README.md

๐Ÿ› ๏ธ Requirements

pip install pandas numpy matplotlib seaborn scikit-learn kagglehub

๐Ÿ“š Concepts Used

Exploratory Data Analysis ยท Missing Value Imputation ยท Outlier Detection & Capping ยท Ordinal Encoding ยท One-Hot Encoding ยท Feature Engineering ยท StandardScaler ยท Stratified Train/Val Split ยท Correlation Analysis ยท Binary Classification


๐Ÿ”ฎ Next Steps

# What Why
1 SMOTE oversampling Fix ~75:25 class imbalance before modeling
2 Model training Logistic Regression โ†’ Random Forest โ†’ XGBoost
3 Hyperparameter tuning GridSearchCV or Optuna
4 SHAP explainability Understand per-prediction feature impact
5 Streamlit deployment Interactive prediction app

๐Ÿ“Š Built with Python ยท pandas ยท scikit-learn ยท seaborn ย ยทย  Benha University 2025

About

๐Ÿ“Š Full ML preprocessing pipeline for HR Analytics โ€” EDA, missing value imputation, outlier capping, ordinal + one-hot encoding, feature engineering & stratified splitting. pandas ยท scikit-learn ยท seaborn.

Topics

Resources

Stars

2 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages