"""Project configuration and default hyperparameters.""" from __future__ import annotations import os from pathlib import Path ROOT = Path(__file__).resolve().parent.parent DATA_RAW = ROOT / "data" / "raw" DATA_ANONYMISED = ROOT / "anonymisedData" DATA_PROCESSED = ROOT / "data" / "processed" RESULTS = ROOT / "results" FIGURES = RESULTS / "figures" TABLES = RESULTS / "tables" DEMO_CACHE = ROOT / "demo" / "demo_cache" PRESENTATION = ("AAA", "2014J") PRESENTATION_LENGTH = 270 MIN_ENGAGEMENT_DAYS = 30 USE_REGION_ONEHOT = False ACTIVITY_TYPES_TOP_N = 9 DEFAULT_ACTIVITY_TYPES = [ "oucontent", "subpage", "homepage", "resource", "url", "quiz", "forumng", "oucollaborate", "ouwiki", ] PCA_N_COMPONENTS = 10 UMAP_N_COMPONENTS = 10 UMAP_2D_COMPONENTS = 2 UMAP_N_NEIGHBORS = 15 UMAP_MIN_DIST = 0.1 K_SWEEP = list(range(3, 11)) HDBSCAN_MIN_CLUSTER_SIZE = 30 HDBSCAN_MIN_SAMPLES = 10 BOOTSTRAP_B = int(os.getenv("INT396_BOOTSTRAP_B", "30")) BOOTSTRAP_FRAC = float(os.getenv("INT396_BOOTSTRAP_FRAC", "0.80")) N_JOBS = int(os.getenv("INT396_N_JOBS", "-1")) GROUP_SIZE = 4 SIZE_TOLERANCE = 1 FAIRNESS_ATTR = "imd_band_ord" FAIRNESS_TVD_MAX = 0.20 COMPLEMENTARITY_PERCENTILE = 60 ENGAGEMENT_BALANCE_SIGMA = 1.0 MAX_SWAP_ITERS = 500 MIN_STABILITY = 0.40 SEED = 42 def ensure_dirs() -> None: """Create runtime output directories.""" for path in [DATA_RAW, DATA_PROCESSED, DEMO_CACHE, RESULTS, FIGURES, TABLES]: path.mkdir(parents=True, exist_ok=True) def raw_data_dir() -> Path: """Return the directory containing OULAD CSV files. The documented project uses data/raw, but this workspace ships the dataset in anonymisedData. Prefer data/raw if populated, otherwise use the bundled path. """ expected = [ "studentInfo.csv", "studentVle.csv", "studentAssessment.csv", "studentRegistration.csv", "vle.csv", "assessments.csv", ] if all((DATA_RAW / name).exists() for name in expected): return DATA_RAW if all((DATA_ANONYMISED / name).exists() for name in expected): return DATA_ANONYMISED missing = [name for name in expected if not (DATA_RAW / name).exists()] raise FileNotFoundError( "Missing OULAD CSVs in data/raw. Also checked anonymisedData. " f"Missing from data/raw: {missing}" ) def parse_presentation(value: str | None) -> tuple[str, str]: if not value: return PRESENTATION parts = value.replace("-", "_").split("_") if len(parts) != 2: raise ValueError("Presentation must look like AAA_2014J") return parts[0], parts[1]