| import numpy as np |
| import pandas as pd |
| from sklearn.model_selection import train_test_split |
| from sklearn.preprocessing import StandardScaler |
|
|
| def generate_synthetic_data(n_samples: int = 1000, random_state: int = 42) -> pd.DataFrame: |
| """ |
| Generates robust synthetic customer churn data. |
| |
| Features: |
| - age: customer age (int, years) |
| - monthly_charges: monthly subscription charge (float, USD) |
| - contract_length: months of commitment (int: 1, 12, 24) |
| - support_calls: number of customer support calls (int) |
| - tech_support: whether customer has tech support (str: 'yes', 'no') |
| - churn: label (int: 0, 1) |
| """ |
| np.random.seed(random_state) |
| |
| age = np.random.randint(18, 70, size=n_samples) |
| monthly_charges = np.random.uniform(20.0, 120.0, size=n_samples) |
| contract_length = np.random.choice([1, 12, 24], size=n_samples, p=[0.4, 0.4, 0.2]) |
| support_calls = np.random.poisson(lam=1.5, size=n_samples) |
| tech_support = np.random.choice(['yes', 'no'], size=n_samples, p=[0.3, 0.7]) |
| |
| |
| |
| |
| |
| |
| churn_logit = ( |
| 0.5 * support_calls |
| - 0.02 * age |
| + 0.01 * monthly_charges |
| - 1.2 * (contract_length > 1) |
| - 0.8 * (tech_support == 'yes') |
| - 0.5 |
| ) |
| |
| churn_prob = 1 / (1 + np.exp(-churn_logit)) |
| churn = (np.random.rand(n_samples) < churn_prob).astype(int) |
| |
| df = pd.DataFrame({ |
| 'age': age, |
| 'monthly_charges': monthly_charges, |
| 'contract_length': contract_length, |
| 'support_calls': support_calls, |
| 'tech_support': tech_support, |
| 'churn': churn |
| }) |
| |
| return df |
|
|
| class Preprocessor: |
| """ |
| Handles preprocessing: scaling numerical columns and converting categorical values. |
| Uses scikit-learn standard Scaler internally and handles one-hot encoding manual/safe mapping. |
| """ |
| def __init__(self): |
| self.scaler = StandardScaler() |
| self.numerical_cols = ['age', 'monthly_charges', 'contract_length', 'support_calls'] |
| self.categorical_cols = ['tech_support'] |
| self.is_fitted = False |
| |
| def fit(self, df: pd.DataFrame): |
| |
| self.scaler.fit(df[self.numerical_cols]) |
| self.is_fitted = True |
| return self |
| |
| def transform(self, df: pd.DataFrame) -> np.ndarray: |
| if not self.is_fitted: |
| raise ValueError("Preprocessor has not been fitted yet!") |
| |
| |
| num_scaled = self.scaler.transform(df[self.numerical_cols]) |
| |
| |
| tech_support_encoded = (df['tech_support'] == 'yes').astype(float).values.reshape(-1, 1) |
| |
| |
| features = np.hstack([num_scaled, tech_support_encoded]) |
| return features |
|
|
| def fit_transform(self, df: pd.DataFrame) -> np.ndarray: |
| return self.fit(df).transform(df) |
|
|
| def prepare_data(df: pd.DataFrame, test_size: float = 0.2, random_state: int = 42): |
| """ |
| Splits the data, preprocesses features and returns train/test splits. |
| """ |
| X = df.drop(columns=['churn']) |
| y = df['churn'] |
| |
| X_train_raw, X_test_raw, y_train, y_test = train_test_split( |
| X, y, test_size=test_size, random_state=random_state, stratify=y |
| ) |
| |
| preprocessor = Preprocessor() |
| X_train = preprocessor.fit_transform(X_train_raw) |
| X_test = preprocessor.transform(X_test_raw) |
| |
| return X_train, X_test, y_train.values, y_test.values, preprocessor |
|
|