Spaces:
Runtime error
Runtime error
File size: 8,276 Bytes
c97e168 4e48d40 3239ea2 4e48d40 3239ea2 e3188b7 3239ea2 c97e168 4e48d40 3239ea2 4e48d40 3239ea2 4e48d40 3239ea2 4e48d40 3239ea2 4e48d40 c97e168 3239ea2 c97e168 3239ea2 4e48d40 c97e168 4e48d40 c97e168 4e48d40 c97e168 3239ea2 c97e168 4e48d40 3239ea2 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 | import pandas as pd
import numpy as np
from sklearn.model_selection import train_test_split
from sklearn.preprocessing import StandardScaler
BURNOUT_SYMPTOM_COLS = ['DAILY_STRESS', 'DAILY_SHOUTING', 'LOST_VACATION']
TARGET_ADJACENT_COLS = BURNOUT_SYMPTOM_COLS + ['WORK_LIFE_BALANCE_SCORE', 'BURNOUT_RISK']
# Features with valid range [1, 10] β 0 is an impossible response
LIKERT_1_10 = [
'PERSONAL_AWARDS', 'WEEKLY_MEDITATION', 'SLEEP_HOURS',
'SOCIAL_NETWORK', 'TODO_COMPLETED', 'CORE_CIRCLE', 'SUPPORTING_OTHERS',
'SUFFICIENT_INCOME', 'BMI_RANGE',
]
# Features with valid range [0, 10] β 0 is a valid response
LIKERT_0_10 = [
'TIME_FOR_PASSION', 'ACHIEVEMENT', 'DONATION', 'FLOW',
'LIVE_VISION', 'PLACES_VISITED', 'FRUITS_VEGGIES',
]
MAX_OOR_RATE = 0.05
def load_data(path='data/unified_dataset.csv',
use_unified=False, force_rebuild=False):
"""
Load training data. Two modes:
use_unified=False (default):
Load the Kaggle wellness survey CSV directly.
Fast, no API calls needed. preprocess() will encode categoricals,
derive the burnout label, and engineer features.
use_unified=True:
Build the unified dataset from all three API sources:
- Kaggle API (structured wellness survey)
- Groq API (LLM-generated synthetic profiles)
- HuggingFace (annotated mental health posts)
The unified dataset is pre-processed (numeric features, BURNOUT_RISK
already computed). preprocess() detects this and skips those steps.
Results cached to data/unified_dataset.csv after first build.
Pass force_rebuild=True to re-fetch from APIs.
"""
if use_unified:
from data.data_pipeline import build_unified_dataset
df = build_unified_dataset(force_rebuild=force_rebuild)
return df.drop(columns=['source'], errors='ignore')
df = pd.read_csv(path)
return df
def preprocess(df, use_domain_cleaning=False):
"""
Preprocessing pipeline β handles both Kaggle raw data and unified dataset.
Detects which source it's working with:
- Kaggle raw: has 'Timestamp', string GENDER/AGE, symptom columns
- Unified: already numeric, BURNOUT_RISK already computed
Intervention 1 β adaptive domain-bounds validation
βββββββββββββββββββββββββββββββββββββββββββββββββββ
Likert-scale columns clamped to valid ranges [0,10] or [1,10].
Adaptive guard skips columns where >5% OOR (different natural scale).
Intervention 2 β threshold tuning for class imbalance
ββββββββββββββββββββββββββββββββββββββββββββββββββββββ
Applied in preprocessing_experiment.py on the val set only.
"""
# ββ Detect source and normalise accordingly βββββββββββββββββββββββββ #
# Kaggle raw: has Timestamp column, string GENDER/AGE, symptom columns
# Unified: already numeric, BURNOUT_RISK already present, no Timestamp
is_kaggle_raw = (
'Timestamp' in df.columns or
('GENDER' in df.columns and df['GENDER'].dtype == object) or
('BURNOUT_RISK' not in df.columns and
all(c in df.columns for c in BURNOUT_SYMPTOM_COLS))
)
if is_kaggle_raw:
# ββ Kaggle raw data: needs full encoding + label derivation ββββ #
df = df.drop(columns=['Timestamp'], errors='ignore')
if 'GENDER' in df.columns and df['GENDER'].dtype == object:
df['GENDER'] = df['GENDER'].map({'Female': 0, 'Male': 1})
if 'AGE' in df.columns and df['AGE'].dtype == object:
age_map = {'Less than 20': 0, '21 to 35': 1,
'36 to 50': 2, '51 or more': 3}
df['AGE'] = df['AGE'].map(age_map)
df = df.apply(pd.to_numeric, errors='coerce')
df = df.dropna()
# Domain cleaning on raw survey data
if use_domain_cleaning:
_apply_domain_cleaning(df)
# Derive burnout label from symptom composite
burnout_index = df[BURNOUT_SYMPTOM_COLS].sum(axis=1)
threshold = burnout_index.quantile(0.70)
df['BURNOUT_RISK'] = (burnout_index >= threshold).astype(int)
pos_rate = df['BURNOUT_RISK'].mean()
print(f"[target] threshold={threshold:.1f} | "
f"positive rate={pos_rate:.1%} | "
f"class ratio 1:{(1-pos_rate)/pos_rate:.1f}")
else:
# ββ Unified dataset: already numeric, BURNOUT_RISK already set ββ #
df = df.drop(columns=['source'], errors='ignore') # text col β drop before coerce
df = df.apply(pd.to_numeric, errors='coerce')
df = df.dropna()
# Domain cleaning still valid β synthetic/HF values may drift
if use_domain_cleaning:
_apply_domain_cleaning(df)
pos_rate = df['BURNOUT_RISK'].mean()
print(f"[unified dataset] {len(df)} rows | "
f"positive rate={pos_rate:.1%} | "
f"class ratio 1:{(1-pos_rate)/pos_rate:.1f}")
# ββ Feature engineering (same for both sources) βββββββββββββββββββββ #
df['RECOVERY_SCORE'] = (df['SLEEP_HOURS']
+ df['TIME_FOR_PASSION']
+ df['WEEKLY_MEDITATION'])
df['SOCIAL_SUPPORT_SCORE'] = df['SOCIAL_NETWORK'] + df['CORE_CIRCLE']
df['LIFESTYLE_SCORE'] = (df['FLOW'] + df['ACHIEVEMENT']
+ df['LIVE_VISION'] + df['TIME_FOR_PASSION'])
df['HEALTH_HABITS'] = (df['FRUITS_VEGGIES']
+ df['SLEEP_HOURS']
+ df['TODO_COMPLETED'])
feature_cols = [c for c in df.columns if c not in TARGET_ADJACENT_COLS]
return df[feature_cols], df['BURNOUT_RISK'], feature_cols
def _apply_domain_cleaning(df):
"""
Adaptive domain-bounds validation β modifies df in place.
Only clamps columns where OOR rate < MAX_OOR_RATE (genuine errors,
not a different natural scale).
"""
total_fixed, clamped_cols, skipped_cols = 0, [], []
for col, lo, hi in (
[(c, 1, 10) for c in LIKERT_1_10] +
[(c, 0, 10) for c in LIKERT_0_10]
):
if col not in df.columns:
continue
oor_mask = (df[col] < lo) | (df[col] > hi)
oor_rate = oor_mask.mean()
if oor_rate == 0:
continue
if oor_rate > MAX_OOR_RATE:
skipped_cols.append(f"{col} ({oor_rate:.1%} OOR)")
continue
n = int(oor_mask.sum())
df[col] = df[col].clip(lo, hi)
total_fixed += n
clamped_cols.append(f"{col} ({n} values β [{lo},{hi}])")
print(f"[domain cleaning] Fixed {total_fixed} errors: "
f"{', '.join(clamped_cols) or 'none'}")
if skipped_cols:
print(f"[domain cleaning] Skipped: {'; '.join(skipped_cols)}")
def split_and_scale(X, y):
X_train, X_temp, y_train, y_temp = train_test_split(
X, y, test_size=0.3, random_state=42)
X_val, X_test, y_val, y_test = train_test_split(
X_temp, y_temp, test_size=0.5, random_state=42)
scaler = StandardScaler()
X_train = scaler.fit_transform(X_train)
X_val = scaler.transform(X_val)
X_test = scaler.transform(X_test)
print(f"Train: {X_train.shape}, Val: {X_val.shape}, Test: {X_test.shape}")
return X_train, X_val, X_test, y_train, y_val, y_test, scaler
if __name__ == '__main__':
print("=== Testing with Kaggle source ===")
df = load_data()
X, y, cols = preprocess(df, use_domain_cleaning=True)
split_and_scale(X, y)
print(f"Features ({len(cols)}): {cols}")
print("\n=== Testing with unified source ===")
if __import__('os').path.exists('data/unified_dataset.csv'):
df2 = load_data(use_unified=True)
X2, y2, cols2 = preprocess(df2, use_domain_cleaning=True)
split_and_scale(X2, y2)
else:
print("unified_dataset.csv not found β run src/data_pipeline.py first") |