work-sejal commited on
Commit
8ece6f3
·
1 Parent(s): 84a0358

Add models and dataset

Browse files
This view is limited to 50 files because it contains too many changes.   See raw diff
Files changed (50) hide show
  1. .gitattributes +1 -0
  2. data/artifacts/models/answer_scorer/metrics.json +45 -0
  3. data/artifacts/models/answer_scorer/model.joblib +3 -0
  4. data/artifacts/models/answer_scorer/model_card.md +51 -0
  5. data/artifacts/models/answer_scorer/training_config.json +26 -0
  6. data/artifacts/models/answer_scorer/vectorizer.joblib +3 -0
  7. data/artifacts/models/bloom_classifier/label_encoder.joblib +3 -0
  8. data/artifacts/models/bloom_classifier/metrics.json +204 -0
  9. data/artifacts/models/bloom_classifier/model.joblib +3 -0
  10. data/artifacts/models/bloom_classifier/model_card.md +47 -0
  11. data/artifacts/models/bloom_classifier/training_config.json +28 -0
  12. data/artifacts/models/bloom_classifier/vectorizer.joblib +3 -0
  13. data/artifacts/models/difficulty_model/encoder.joblib +3 -0
  14. data/artifacts/models/difficulty_model/feature_columns.json +6 -0
  15. data/artifacts/models/difficulty_model/metrics.json +38 -0
  16. data/artifacts/models/difficulty_model/model.joblib +3 -0
  17. data/artifacts/models/difficulty_model/model_card.md +50 -0
  18. data/artifacts/models/difficulty_model/training_config.json +33 -0
  19. data/artifacts/models/lo_tagger/label_encoder.joblib +3 -0
  20. data/artifacts/models/lo_tagger/metrics.json +0 -0
  21. data/artifacts/models/lo_tagger/model.joblib +3 -0
  22. data/artifacts/models/lo_tagger/model_card.md +50 -0
  23. data/artifacts/models/lo_tagger/training_config.json +27 -0
  24. data/artifacts/models/lo_tagger/vectorizer.joblib +3 -0
  25. data/artifacts/models/mastery_model/feature_columns.json +11 -0
  26. data/artifacts/models/mastery_model/metrics.json +132 -0
  27. data/artifacts/models/mastery_model/model.joblib +3 -0
  28. data/artifacts/models/mastery_model/model_card.md +56 -0
  29. data/artifacts/models/mastery_model/training_config.json +35 -0
  30. data/artifacts/models/recommender/encoder.joblib +3 -0
  31. data/artifacts/models/recommender/feature_columns.json +7 -0
  32. data/artifacts/models/recommender/metrics.json +33 -0
  33. data/artifacts/models/recommender/model.joblib +3 -0
  34. data/artifacts/models/recommender/model_card.md +53 -0
  35. data/artifacts/models/recommender/training_config.json +39 -0
  36. data/artifacts/models/risk_model/feature_columns.json +21 -0
  37. data/artifacts/models/risk_model/metrics.json +101 -0
  38. data/artifacts/models/risk_model/model.joblib +3 -0
  39. data/artifacts/models/risk_model/model_card.md +71 -0
  40. data/artifacts/models/risk_model/training_config.json +44 -0
  41. data/learning_outcome_os_dataset_v2/README.md +134 -0
  42. data/learning_outcome_os_dataset_v2/ai_prediction_logs.csv +3 -0
  43. data/learning_outcome_os_dataset_v2/assessment_questions.csv +3 -0
  44. data/learning_outcome_os_dataset_v2/assessments.csv +3 -0
  45. data/learning_outcome_os_dataset_v2/chapters.csv +3 -0
  46. data/learning_outcome_os_dataset_v2/classes.csv +3 -0
  47. data/learning_outcome_os_dataset_v2/content_catalog.csv +3 -0
  48. data/learning_outcome_os_dataset_v2/dataset_metadata.json +98 -0
  49. data/learning_outcome_os_dataset_v2/engagement_logs.csv +3 -0
  50. data/learning_outcome_os_dataset_v2/generation_script.py +354 -0
.gitattributes CHANGED
@@ -33,3 +33,4 @@ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
33
  *.zip filter=lfs diff=lfs merge=lfs -text
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
 
 
33
  *.zip filter=lfs diff=lfs merge=lfs -text
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
36
+ *.csv filter=lfs diff=lfs merge=lfs -text
data/artifacts/models/answer_scorer/metrics.json ADDED
@@ -0,0 +1,45 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "model_name": "answer_scorer",
3
+ "model_version": "answer_scorer_v2_baseline_001",
4
+ "dataset_version": "2.0.0",
5
+ "trained_at": "2026-05-21T05:59:12.590667+00:00",
6
+ "seed": 42,
7
+ "split_counts": {
8
+ "train": 8371,
9
+ "validation": 1820,
10
+ "test": 1809
11
+ },
12
+ "metrics": {
13
+ "validation": {
14
+ "mae": 0.4787,
15
+ "rmse": 0.773,
16
+ "r_squared": 0.718,
17
+ "distribution": {
18
+ "predicted_mean": 1.8303,
19
+ "predicted_std": 0.9736,
20
+ "actual_mean": 2.0455,
21
+ "actual_std": 1.4557
22
+ },
23
+ "pct_above_review_threshold": 16.87
24
+ },
25
+ "test": {
26
+ "mae": 0.5256,
27
+ "rmse": 0.8385,
28
+ "r_squared": 0.6791,
29
+ "distribution": {
30
+ "predicted_mean": 1.8985,
31
+ "predicted_std": 0.9581,
32
+ "actual_mean": 2.174,
33
+ "actual_std": 1.4803
34
+ },
35
+ "pct_above_review_threshold": 19.68
36
+ }
37
+ },
38
+ "limitations": [
39
+ "Trained on synthetic data only.",
40
+ "TF-IDF features do not capture deep semantic similarity.",
41
+ "Predictions clipped to [0, max_marks] range.",
42
+ "teacher_review_required is always True in V2 baseline.",
43
+ "MAE is the primary metric; individual predictions may deviate significantly."
44
+ ]
45
+ }
data/artifacts/models/answer_scorer/model.joblib ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:d79a6024682b2c0c756882dda8ab277c14afba6ebd18c50f76a791655a2046fb
3
+ size 8522
data/artifacts/models/answer_scorer/model_card.md ADDED
@@ -0,0 +1,51 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Model Card: Answer Scorer
2
+
3
+ ## Model Details
4
+
5
+ - **Model Name:** answer_scorer
6
+ - **Model Version:** answer_scorer_v2_baseline_001
7
+ - **Algorithm:** TF-IDF + Ridge Regression
8
+ - **Framework:** scikit-learn
9
+ - **Trained At:** 2026-05-21T05:59:12.590667+00:00
10
+ - **Seed:** 42
11
+
12
+ ## Intended Use
13
+
14
+ Score subjective student answers against a rubric and model answer.
15
+ Produces a predicted marks value clipped to [0, max_marks]. Always sets
16
+ teacher_review_required=True in V2 baseline — predictions are advisory only.
17
+
18
+ ## Training Data
19
+
20
+ - **Source:** training_answer_scoring.csv (synthetic dataset v2)
21
+ - **Split Counts:** train=8371, validation=1820, test=1809
22
+ - **Features:** student_answer (TF-IDF, max_features=5000, ngram_range=(1,2)) + rubric_match_score + concept_coverage_score
23
+ - **Target:** teacher_marks (continuous)
24
+
25
+ ## Metrics
26
+
27
+ ### Validation Set
28
+ - MAE: 0.4787
29
+ - RMSE: 0.773
30
+ - R-squared: 0.718
31
+ - % Above Review Threshold (>1.0): 16.87%
32
+
33
+ ### Test Set
34
+ - MAE: 0.5256
35
+ - RMSE: 0.8385
36
+ - R-squared: 0.6791
37
+ - % Above Review Threshold (>1.0): 19.68%
38
+
39
+ ## Known Limitations
40
+
41
+ - Trained on synthetic data only — performance on real student answers is unknown.
42
+ - TF-IDF features do not capture deep semantic similarity or paraphrasing.
43
+ - Predictions are clipped to [0, max_marks]; the model may predict outside this range before clipping.
44
+ - teacher_review_required is always True in V2 baseline.
45
+ - MAE is the primary metric; individual predictions may deviate significantly from teacher marks.
46
+
47
+ ## Fallback Behavior
48
+
49
+ When the model is not loaded or confidence is below threshold,
50
+ the system falls back to rubric keyword coverage + length heuristic,
51
+ always setting teacher_review_required=True.
data/artifacts/models/answer_scorer/training_config.json ADDED
@@ -0,0 +1,26 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "model_name": "answer_scorer",
3
+ "model_version": "answer_scorer_v2_baseline_001",
4
+ "dataset_version": "2.0.0",
5
+ "seed": 42,
6
+ "split_counts": {
7
+ "train": 8371,
8
+ "validation": 1820,
9
+ "test": 1809
10
+ },
11
+ "hyperparameters": {
12
+ "tfidf_max_features": 5000,
13
+ "ngram_range": [
14
+ 1,
15
+ 2
16
+ ],
17
+ "ridge_alpha": 1.0
18
+ },
19
+ "feature_columns": [
20
+ "student_answer (TF-IDF)",
21
+ "rubric_match_score",
22
+ "concept_coverage_score"
23
+ ],
24
+ "target_column": "teacher_marks",
25
+ "algorithm": "TF-IDF + Ridge regression"
26
+ }
data/artifacts/models/answer_scorer/vectorizer.joblib ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:bf5b83c246aacede1528d6c6b52986e36712363b5e1b85bad1e1b3495134a268
3
+ size 26983
data/artifacts/models/bloom_classifier/label_encoder.joblib ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:49cd11f5861283c0d8da24d21a2f021791eb64ea0d971e21e6d76ce16c3f2b7f
3
+ size 439
data/artifacts/models/bloom_classifier/metrics.json ADDED
@@ -0,0 +1,204 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "model_name": "bloom_classifier",
3
+ "model_version": "bloom_classifier_v2_baseline_001",
4
+ "dataset_version": "2.0.0",
5
+ "trained_at": "2026-05-21T05:59:09.626503+00:00",
6
+ "seed": 42,
7
+ "split_counts": {
8
+ "train": 3912,
9
+ "validation": 1033,
10
+ "test": 875
11
+ },
12
+ "metrics": {
13
+ "validation": {
14
+ "macro_f1": 0.6434,
15
+ "weighted_f1": 0.6634,
16
+ "per_class": {
17
+ "Analyze": {
18
+ "precision": 0.614,
19
+ "recall": 0.6774,
20
+ "f1": 0.6442,
21
+ "support": 155
22
+ },
23
+ "Apply": {
24
+ "precision": 0.65,
25
+ "recall": 0.5778,
26
+ "f1": 0.6118,
27
+ "support": 270
28
+ },
29
+ "Create": {
30
+ "precision": 0.8947,
31
+ "recall": 1.0,
32
+ "f1": 0.9444,
33
+ "support": 17
34
+ },
35
+ "Evaluate": {
36
+ "precision": 1.0,
37
+ "recall": 0.1522,
38
+ "f1": 0.2642,
39
+ "support": 46
40
+ },
41
+ "Remember": {
42
+ "precision": 0.7438,
43
+ "recall": 0.5732,
44
+ "f1": 0.6475,
45
+ "support": 157
46
+ },
47
+ "Understand": {
48
+ "precision": 0.68,
49
+ "recall": 0.8325,
50
+ "f1": 0.7486,
51
+ "support": 388
52
+ }
53
+ },
54
+ "confusion_matrix": [
55
+ [
56
+ 105,
57
+ 50,
58
+ 0,
59
+ 0,
60
+ 0,
61
+ 0
62
+ ],
63
+ [
64
+ 27,
65
+ 156,
66
+ 0,
67
+ 0,
68
+ 0,
69
+ 87
70
+ ],
71
+ [
72
+ 0,
73
+ 0,
74
+ 17,
75
+ 0,
76
+ 0,
77
+ 0
78
+ ],
79
+ [
80
+ 37,
81
+ 0,
82
+ 2,
83
+ 7,
84
+ 0,
85
+ 0
86
+ ],
87
+ [
88
+ 0,
89
+ 2,
90
+ 0,
91
+ 0,
92
+ 90,
93
+ 65
94
+ ],
95
+ [
96
+ 2,
97
+ 32,
98
+ 0,
99
+ 0,
100
+ 31,
101
+ 323
102
+ ]
103
+ ]
104
+ },
105
+ "test": {
106
+ "macro_f1": 0.6178,
107
+ "weighted_f1": 0.6654,
108
+ "per_class": {
109
+ "Analyze": {
110
+ "precision": 0.7,
111
+ "recall": 0.6853,
112
+ "f1": 0.6926,
113
+ "support": 143
114
+ },
115
+ "Apply": {
116
+ "precision": 0.6411,
117
+ "recall": 0.5678,
118
+ "f1": 0.6022,
119
+ "support": 236
120
+ },
121
+ "Create": {
122
+ "precision": 0.8333,
123
+ "recall": 0.9375,
124
+ "f1": 0.8824,
125
+ "support": 16
126
+ },
127
+ "Evaluate": {
128
+ "precision": 0.6667,
129
+ "recall": 0.0588,
130
+ "f1": 0.1081,
131
+ "support": 34
132
+ },
133
+ "Remember": {
134
+ "precision": 0.7677,
135
+ "recall": 0.608,
136
+ "f1": 0.6786,
137
+ "support": 125
138
+ },
139
+ "Understand": {
140
+ "precision": 0.665,
141
+ "recall": 0.8411,
142
+ "f1": 0.7428,
143
+ "support": 321
144
+ }
145
+ },
146
+ "confusion_matrix": [
147
+ [
148
+ 98,
149
+ 45,
150
+ 0,
151
+ 0,
152
+ 0,
153
+ 0
154
+ ],
155
+ [
156
+ 13,
157
+ 134,
158
+ 0,
159
+ 0,
160
+ 0,
161
+ 89
162
+ ],
163
+ [
164
+ 0,
165
+ 0,
166
+ 15,
167
+ 1,
168
+ 0,
169
+ 0
170
+ ],
171
+ [
172
+ 29,
173
+ 0,
174
+ 3,
175
+ 2,
176
+ 0,
177
+ 0
178
+ ],
179
+ [
180
+ 0,
181
+ 2,
182
+ 0,
183
+ 0,
184
+ 76,
185
+ 47
186
+ ],
187
+ [
188
+ 0,
189
+ 28,
190
+ 0,
191
+ 0,
192
+ 23,
193
+ 270
194
+ ]
195
+ ]
196
+ }
197
+ },
198
+ "limitations": [
199
+ "Trained on synthetic data only.",
200
+ "6 classes with imbalanced distribution \u2014 Create (~2%) and Evaluate (~4%) are rare.",
201
+ "Macro F1 is the primary metric; per-class recall may be low for rare classes.",
202
+ "TF-IDF features do not capture semantic similarity beyond n-gram overlap."
203
+ ]
204
+ }
data/artifacts/models/bloom_classifier/model.joblib ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:2cae60751163b66384f69e916fda7c5a302eb3b6e2a6b8117944a47a3fd70dfd
3
+ size 76776
data/artifacts/models/bloom_classifier/model_card.md ADDED
@@ -0,0 +1,47 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Model Card: Bloom Classifier
2
+
3
+ ## Model Details
4
+
5
+ - **Model Name:** bloom_classifier
6
+ - **Model Version:** bloom_classifier_v2_baseline_001
7
+ - **Algorithm:** TF-IDF + LogisticRegression (multinomial)
8
+ - **Framework:** scikit-learn
9
+ - **Trained At:** 2026-05-21T05:59:09.626503+00:00
10
+ - **Seed:** 42
11
+
12
+ ## Intended Use
13
+
14
+ Automatically classify questions by Bloom's taxonomy cognitive level.
15
+ Used in the Bloom classification endpoint to predict one of 6 levels:
16
+ Remember, Understand, Apply, Analyze, Evaluate, Create.
17
+
18
+ ## Training Data
19
+
20
+ - **Source:** training_bloom_classification.csv (synthetic dataset v2)
21
+ - **Split Counts:** train=3912, validation=1033, test=875
22
+ - **Feature:** question_text (TF-IDF vectorized, max_features=8000, ngram_range=(1,2))
23
+ - **Target:** bloom_level (6 classes)
24
+
25
+ ## Metrics
26
+
27
+ ### Validation Set
28
+ - Macro F1: 0.6434
29
+ - Weighted F1: 0.6634
30
+
31
+ ### Test Set
32
+ - Macro F1: 0.6178
33
+ - Weighted F1: 0.6654
34
+
35
+ ## Known Limitations
36
+
37
+ - Trained on synthetic data only — performance on real classroom questions is unknown.
38
+ - Class imbalance: Create (~2%) and Evaluate (~4%) are rare; recall on these classes may be low.
39
+ - TF-IDF features do not capture semantic similarity beyond n-gram overlap.
40
+ - Macro F1 is the primary metric; accuracy alone would mask poor performance on rare classes.
41
+
42
+ ## Fallback Behavior
43
+
44
+ When the model is not loaded or confidence is below the threshold (0.55),
45
+ the system falls back to keyword heuristic classification:
46
+ define/list → Remember; explain → Understand; calculate/use → Apply;
47
+ compare/contrast → Analyze; justify → Evaluate; design → Create.
data/artifacts/models/bloom_classifier/training_config.json ADDED
@@ -0,0 +1,28 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "model_name": "bloom_classifier",
3
+ "model_version": "bloom_classifier_v2_baseline_001",
4
+ "dataset_version": "2.0.0",
5
+ "seed": 42,
6
+ "split_counts": {
7
+ "train": 3912,
8
+ "validation": 1033,
9
+ "test": 875
10
+ },
11
+ "hyperparameters": {
12
+ "tfidf_max_features": 8000,
13
+ "ngram_range": [
14
+ 1,
15
+ 2
16
+ ],
17
+ "sublinear_tf": true,
18
+ "logreg_C": 1.0,
19
+ "logreg_solver": "lbfgs",
20
+ "logreg_max_iter": 1000,
21
+ "logreg_multi_class": "multinomial"
22
+ },
23
+ "feature_columns": [
24
+ "question_text"
25
+ ],
26
+ "target_column": "bloom_level",
27
+ "algorithm": "LogisticRegression(multinomial)"
28
+ }
data/artifacts/models/bloom_classifier/vectorizer.joblib ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:3ca74184cd96c1a424e09a6e35df2a89d27fd009eb77aa35886666d4a097ab96
3
+ size 31067
data/artifacts/models/difficulty_model/encoder.joblib ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:781bac207e91e216ba726703b113d6646eabe3f46567205b10088621e6404f47
3
+ size 704
data/artifacts/models/difficulty_model/feature_columns.json ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+ [
2
+ "bloom_score",
3
+ "grade",
4
+ "subject",
5
+ "question_type"
6
+ ]
data/artifacts/models/difficulty_model/metrics.json ADDED
@@ -0,0 +1,38 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "model_name": "difficulty_model",
3
+ "model_version": "difficulty_model_v2_baseline_001",
4
+ "dataset_version": "2.0.0",
5
+ "trained_at": "2026-05-21T05:59:09.943332+00:00",
6
+ "seed": 42,
7
+ "split_counts": {
8
+ "train": 3912,
9
+ "validation": 1033,
10
+ "test": 875
11
+ },
12
+ "metrics": {
13
+ "validation": {
14
+ "mae": 0.3475,
15
+ "r_squared": 0.5003,
16
+ "per_bucket_mae": {
17
+ "easy": 0.3058,
18
+ "medium": 0.2934,
19
+ "hard": 0.6563
20
+ }
21
+ },
22
+ "test": {
23
+ "mae": 0.3519,
24
+ "r_squared": 0.4685,
25
+ "per_bucket_mae": {
26
+ "easy": 0.325,
27
+ "medium": 0.2885,
28
+ "hard": 0.6797
29
+ }
30
+ }
31
+ },
32
+ "limitations": [
33
+ "Trained on synthetic data only.",
34
+ "difficulty_score distribution may not reflect real-world difficulty.",
35
+ "OrdinalEncoder assumes an ordering that may not be meaningful for subject/question_type.",
36
+ "Per-bucket MAE depends on the quality of the difficulty string labels."
37
+ ]
38
+ }
data/artifacts/models/difficulty_model/model.joblib ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:3d41ef6570c86bc6878a6cbe7de68aba85bebc996f96d2a33198c8de91165f3f
3
+ size 735351
data/artifacts/models/difficulty_model/model_card.md ADDED
@@ -0,0 +1,50 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Model Card: Difficulty Model
2
+
3
+ ## Model Details
4
+
5
+ - **Model Name:** difficulty_model
6
+ - **Model Version:** difficulty_model_v2_baseline_001
7
+ - **Algorithm:** RandomForestRegressor
8
+ - **Framework:** scikit-learn
9
+ - **Trained At:** 2026-05-21T05:59:09.943332+00:00
10
+ - **Seed:** 42
11
+
12
+ ## Intended Use
13
+
14
+ Estimate question difficulty as a continuous score in [0, 1] based on
15
+ question features (bloom_score, grade, subject, question_type). Used in
16
+ the difficulty estimation endpoint to predict how hard a question is for
17
+ a given grade level.
18
+
19
+ ## Training Data
20
+
21
+ - **Source:** training_lo_tagging.csv + questions.csv (for question_type)
22
+ - **Split Counts:** train=3912, validation=1033, test=875
23
+ - **Features:** bloom_score (numeric), grade (numeric), subject (OrdinalEncoded), question_type (OrdinalEncoded)
24
+ - **Target:** difficulty_score (continuous [0, 1])
25
+
26
+ ## Metrics
27
+
28
+ ### Validation Set
29
+ - MAE: 0.3475
30
+ - R-squared: 0.5003
31
+ - Per-bucket MAE: {'easy': 0.3058, 'medium': 0.2934, 'hard': 0.6563}
32
+
33
+ ### Test Set
34
+ - MAE: 0.3519
35
+ - R-squared: 0.4685
36
+ - Per-bucket MAE: {'easy': 0.325, 'medium': 0.2885, 'hard': 0.6797}
37
+
38
+ ## Known Limitations
39
+
40
+ - Trained on synthetic data only — performance on real questions is unknown.
41
+ - difficulty_score distribution may not reflect real-world difficulty.
42
+ - OrdinalEncoder assumes an ordering that may not be meaningful for subject/question_type.
43
+ - Per-bucket MAE depends on the quality of the difficulty string labels.
44
+ - Limited feature set (4 features); text-based features could improve performance.
45
+
46
+ ## Fallback Behavior
47
+
48
+ When the model is not loaded or confidence is below threshold, the system
49
+ falls back to a rule-based difficulty estimation using bloom_score and
50
+ grade-level heuristics.
data/artifacts/models/difficulty_model/training_config.json ADDED
@@ -0,0 +1,33 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "model_name": "difficulty_model",
3
+ "model_version": "difficulty_model_v2_baseline_001",
4
+ "dataset_version": "2.0.0",
5
+ "seed": 42,
6
+ "split_counts": {
7
+ "train": 3912,
8
+ "validation": 1033,
9
+ "test": 875
10
+ },
11
+ "hyperparameters": {
12
+ "n_estimators": 100,
13
+ "random_state": 42,
14
+ "algorithm": "RandomForestRegressor",
15
+ "encoder": "OrdinalEncoder"
16
+ },
17
+ "feature_columns": [
18
+ "bloom_score",
19
+ "grade",
20
+ "subject",
21
+ "question_type"
22
+ ],
23
+ "categorical_columns": [
24
+ "subject",
25
+ "question_type"
26
+ ],
27
+ "numeric_columns": [
28
+ "bloom_score",
29
+ "grade"
30
+ ],
31
+ "target_column": "difficulty_score",
32
+ "algorithm": "RandomForestRegressor"
33
+ }
data/artifacts/models/lo_tagger/label_encoder.joblib ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:42e9faea136d20cf19963170fc609d0f3a02b28b6d2e3e376b79739b3f043e43
3
+ size 1147
data/artifacts/models/lo_tagger/metrics.json ADDED
The diff for this file is too large to render. See raw diff
 
data/artifacts/models/lo_tagger/model.joblib ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:b30022c2f5a8b307b13dd7b433f6e0ea112297c0ec4cb69e5d2ed1fc6412996c
3
+ size 7427152
data/artifacts/models/lo_tagger/model_card.md ADDED
@@ -0,0 +1,50 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Model Card: LO Tagger
2
+
3
+ ## Model Details
4
+
5
+ - **Model Name:** lo_tagger
6
+ - **Model Version:** lo_tagger_v2_baseline_001
7
+ - **Algorithm:** TF-IDF + CalibratedClassifierCV(LinearSVC)
8
+ - **Framework:** scikit-learn
9
+ - **Trained At:** 2026-05-21T05:59:08.919894+00:00
10
+ - **Seed:** 42
11
+
12
+ ## Intended Use
13
+
14
+ Automatically tag questions to their most relevant Learning Outcome (LO).
15
+ Used in the LO tagging endpoint to classify question text into one of 194
16
+ learning outcome categories.
17
+
18
+ ## Training Data
19
+
20
+ - **Source:** training_lo_tagging.csv (synthetic dataset v2)
21
+ - **Split Counts:** train=3912, validation=1033, test=875
22
+ - **Feature:** question_text (TF-IDF vectorized, max_features=10000, ngram_range=(1,2))
23
+ - **Target:** lo_id (194 classes)
24
+
25
+ ## Metrics
26
+
27
+ ### Validation Set
28
+ - Top-1 Accuracy: 0.9245
29
+ - Top-3 Accuracy: 1.0
30
+ - Macro F1: 0.9042
31
+ - Weighted F1: 0.9249
32
+
33
+ ### Test Set
34
+ - Top-1 Accuracy: 0.9097
35
+ - Top-3 Accuracy: 1.0
36
+ - Macro F1: 0.8861
37
+ - Weighted F1: 0.9115
38
+
39
+ ## Known Limitations
40
+
41
+ - Trained on synthetic data only — performance on real classroom questions is unknown.
42
+ - 194 classes with imbalanced distribution; rare LOs may have low recall.
43
+ - TF-IDF features do not capture semantic similarity beyond n-gram overlap.
44
+ - Top-3 accuracy is the primary metric; top-1 may be low for ambiguous questions.
45
+
46
+ ## Fallback Behavior
47
+
48
+ When the model is not loaded or confidence is below the threshold (0.55),
49
+ the system falls back to keyword/embedding match against learning_outcomes.embedding_text
50
+ filtered by grade and subject, returning top-3 with rule-based confidence.
data/artifacts/models/lo_tagger/training_config.json ADDED
@@ -0,0 +1,27 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "model_name": "lo_tagger",
3
+ "model_version": "lo_tagger_v2_baseline_001",
4
+ "dataset_version": "2.0.0",
5
+ "seed": 42,
6
+ "split_counts": {
7
+ "train": 3912,
8
+ "validation": 1033,
9
+ "test": 875
10
+ },
11
+ "hyperparameters": {
12
+ "tfidf_max_features": 10000,
13
+ "ngram_range": [
14
+ 1,
15
+ 2
16
+ ],
17
+ "sublinear_tf": true,
18
+ "svc_max_iter": 5000,
19
+ "calibration_cv": 3,
20
+ "calibration_method": "sigmoid"
21
+ },
22
+ "feature_columns": [
23
+ "question_text"
24
+ ],
25
+ "target_column": "lo_id",
26
+ "algorithm": "CalibratedClassifierCV(LinearSVC)"
27
+ }
data/artifacts/models/lo_tagger/vectorizer.joblib ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:aa3bb1d853a64bbb2147a6f4039771b7bacddef6c99e730a59cc28f00fe1657c
3
+ size 31067
data/artifacts/models/mastery_model/feature_columns.json ADDED
@@ -0,0 +1,11 @@
 
 
 
 
 
 
 
 
 
 
 
 
1
+ [
2
+ "attempt_count",
3
+ "accuracy",
4
+ "average_marks_ratio",
5
+ "average_time_seconds",
6
+ "hint_usage_rate",
7
+ "attendance_percentage",
8
+ "assignment_completion_rate",
9
+ "average_login_per_week",
10
+ "inactive_days_last_14"
11
+ ]
data/artifacts/models/mastery_model/metrics.json ADDED
@@ -0,0 +1,132 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "model_name": "mastery_model",
3
+ "model_version": "mastery_model_v2_baseline_001",
4
+ "dataset_version": "2.0.0",
5
+ "trained_at": "2026-05-21T05:59:11.764140+00:00",
6
+ "seed": 42,
7
+ "split_counts": {
8
+ "train": 24515,
9
+ "validation": 5218,
10
+ "test": 5187
11
+ },
12
+ "metrics": {
13
+ "validation": {
14
+ "macro_f1": 0.8649,
15
+ "weighted_f1": 0.8952,
16
+ "per_class": {
17
+ "weak": {
18
+ "precision": 0.9661,
19
+ "recall": 0.9626,
20
+ "f1": 0.9643,
21
+ "support": 2247
22
+ },
23
+ "developing": {
24
+ "precision": 0.8884,
25
+ "recall": 0.905,
26
+ "f1": 0.8966,
27
+ "support": 1337
28
+ },
29
+ "proficient": {
30
+ "precision": 0.7831,
31
+ "recall": 0.7385,
32
+ "f1": 0.7601,
33
+ "support": 826
34
+ },
35
+ "mastered": {
36
+ "precision": 0.8234,
37
+ "recall": 0.854,
38
+ "f1": 0.8384,
39
+ "support": 808
40
+ }
41
+ },
42
+ "confusion_matrix": [
43
+ [
44
+ 2163,
45
+ 84,
46
+ 0,
47
+ 0
48
+ ],
49
+ [
50
+ 76,
51
+ 1210,
52
+ 51,
53
+ 0
54
+ ],
55
+ [
56
+ 0,
57
+ 68,
58
+ 610,
59
+ 148
60
+ ],
61
+ [
62
+ 0,
63
+ 0,
64
+ 118,
65
+ 690
66
+ ]
67
+ ]
68
+ },
69
+ "test": {
70
+ "macro_f1": 0.8754,
71
+ "weighted_f1": 0.9029,
72
+ "per_class": {
73
+ "weak": {
74
+ "precision": 0.9667,
75
+ "recall": 0.9717,
76
+ "f1": 0.9692,
77
+ "support": 2120
78
+ },
79
+ "developing": {
80
+ "precision": 0.9155,
81
+ "recall": 0.9089,
82
+ "f1": 0.9122,
83
+ "support": 1394
84
+ },
85
+ "proficient": {
86
+ "precision": 0.7814,
87
+ "recall": 0.7555,
88
+ "f1": 0.7683,
89
+ "support": 814
90
+ },
91
+ "mastered": {
92
+ "precision": 0.8395,
93
+ "recall": 0.865,
94
+ "f1": 0.8521,
95
+ "support": 859
96
+ }
97
+ },
98
+ "confusion_matrix": [
99
+ [
100
+ 2060,
101
+ 60,
102
+ 0,
103
+ 0
104
+ ],
105
+ [
106
+ 71,
107
+ 1267,
108
+ 56,
109
+ 0
110
+ ],
111
+ [
112
+ 0,
113
+ 57,
114
+ 615,
115
+ 142
116
+ ],
117
+ [
118
+ 0,
119
+ 0,
120
+ 116,
121
+ 743
122
+ ]
123
+ ]
124
+ }
125
+ },
126
+ "limitations": [
127
+ "Trained on synthetic data only.",
128
+ "4-class mastery labels derived from synthetic mastery_score thresholds.",
129
+ "All features are numeric; no text or contextual features used.",
130
+ "Class distribution may not reflect real-world mastery patterns."
131
+ ]
132
+ }
data/artifacts/models/mastery_model/model.joblib ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:51ba404df9382d85b82d30964ed4e1d2e58c5a98948ffc2427a7b105886e9ca5
3
+ size 6856082
data/artifacts/models/mastery_model/model_card.md ADDED
@@ -0,0 +1,56 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Model Card: Mastery Model
2
+
3
+ ## Model Details
4
+
5
+ - **Model Name:** mastery_model
6
+ - **Model Version:** mastery_model_v2_baseline_001
7
+ - **Algorithm:** RandomForestClassifier
8
+ - **Framework:** scikit-learn
9
+ - **Trained At:** 2026-05-21T05:59:11.764140+00:00
10
+ - **Seed:** 42
11
+
12
+ ## Intended Use
13
+
14
+ Predict per-student per-LO mastery label (weak, developing, proficient, mastered)
15
+ based on behavioral and performance features. Used in the mastery prediction
16
+ endpoint to classify student mastery level for a given learning outcome.
17
+
18
+ ## Training Data
19
+
20
+ - **Source:** training_mastery_prediction.csv (synthetic dataset v2)
21
+ - **Split Counts:** train=24515, validation=5218, test=5187
22
+ - **Features:** attempt_count, accuracy, average_marks_ratio, average_time_seconds, hint_usage_rate, attendance_percentage, assignment_completion_rate, average_login_per_week, inactive_days_last_14 (all numeric)
23
+ - **Target:** mastery_label (integer 0-3, mapped to weak/developing/proficient/mastered)
24
+
25
+ ## Metrics
26
+
27
+ ### Validation Set
28
+ - Macro F1: 0.8649
29
+ - Weighted F1: 0.8952
30
+
31
+ ### Test Set
32
+ - Macro F1: 0.8754
33
+ - Weighted F1: 0.9029
34
+
35
+ ## Per-Class Performance (Test Set)
36
+
37
+ | Class | Precision | Recall | F1 | Support |
38
+ |-------|-----------|--------|-----|---------|
39
+ | weak | 0.9667 | 0.9717 | 0.9692 | 2120 |
40
+ | developing | 0.9155 | 0.9089 | 0.9122 | 1394 |
41
+ | proficient | 0.7814 | 0.7555 | 0.7683 | 814 |
42
+ | mastered | 0.8395 | 0.865 | 0.8521 | 859 |
43
+
44
+ ## Known Limitations
45
+
46
+ - Trained on synthetic data only — performance on real student data is unknown.
47
+ - 4-class mastery labels derived from synthetic mastery_score thresholds.
48
+ - All features are numeric; no text or contextual features used.
49
+ - Class distribution may not reflect real-world mastery patterns.
50
+ - No encoding needed since all features are already numeric.
51
+
52
+ ## Fallback Behavior
53
+
54
+ When the model is not loaded or confidence is below the threshold (0.55),
55
+ the system falls back to rule-based mastery estimation using mastery_score
56
+ thresholds: <0.4 weak, <0.6 developing, <0.8 proficient, else mastered.
data/artifacts/models/mastery_model/training_config.json ADDED
@@ -0,0 +1,35 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "model_name": "mastery_model",
3
+ "model_version": "mastery_model_v2_baseline_001",
4
+ "dataset_version": "2.0.0",
5
+ "seed": 42,
6
+ "split_counts": {
7
+ "train": 24515,
8
+ "validation": 5218,
9
+ "test": 5187
10
+ },
11
+ "hyperparameters": {
12
+ "n_estimators": 100,
13
+ "random_state": 42,
14
+ "algorithm": "RandomForestClassifier"
15
+ },
16
+ "feature_columns": [
17
+ "attempt_count",
18
+ "accuracy",
19
+ "average_marks_ratio",
20
+ "average_time_seconds",
21
+ "hint_usage_rate",
22
+ "attendance_percentage",
23
+ "assignment_completion_rate",
24
+ "average_login_per_week",
25
+ "inactive_days_last_14"
26
+ ],
27
+ "target_column": "mastery_label",
28
+ "label_map": {
29
+ "0": "weak",
30
+ "1": "developing",
31
+ "2": "proficient",
32
+ "3": "mastered"
33
+ },
34
+ "algorithm": "RandomForestClassifier"
35
+ }
data/artifacts/models/recommender/encoder.joblib ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:417a9e093b8090b171f8383bc3772bfca90127b02d17a5b543ec3f5517200d2d
3
+ size 699
data/artifacts/models/recommender/feature_columns.json ADDED
@@ -0,0 +1,7 @@
 
 
 
 
 
 
 
 
1
+ [
2
+ "priority",
3
+ "ai_confidence",
4
+ "recommendation_type",
5
+ "grade",
6
+ "subject"
7
+ ]
data/artifacts/models/recommender/metrics.json ADDED
@@ -0,0 +1,33 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "model_name": "recommender",
3
+ "model_version": "recommender_v2_baseline_001",
4
+ "dataset_version": "2.0.0",
5
+ "trained_at": "2026-05-21T05:59:13.148420+00:00",
6
+ "seed": 42,
7
+ "split_counts": {
8
+ "train": 5671,
9
+ "validation": 1214,
10
+ "test": 1215
11
+ },
12
+ "metrics": {
13
+ "validation": {
14
+ "roc_auc_clicked": 0.5439,
15
+ "lift_at_10_clicked": 1.1392,
16
+ "roc_auc_is_completed": 0.5303,
17
+ "lift_at_10_is_completed": 1.1056
18
+ },
19
+ "test": {
20
+ "roc_auc_clicked": 0.5486,
21
+ "lift_at_10_clicked": 1.0471,
22
+ "roc_auc_is_completed": 0.5424,
23
+ "lift_at_10_is_completed": 1.0366
24
+ }
25
+ },
26
+ "limitations": [
27
+ "Trained on synthetic data only.",
28
+ "Two separate GBC models \u2014 no joint optimization of clicked + is_completed.",
29
+ "OrdinalEncoder assumes an ordering for priority/recommendation_type/subject.",
30
+ "Lift@10 depends on the distribution of positive labels in the dataset.",
31
+ "No user-level features (e.g., engagement history) included in baseline."
32
+ ]
33
+ }
data/artifacts/models/recommender/model.joblib ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:1917a4e2cb2387cfd5df82b416ea2794d1943bea3d82e47a8e38a5a6bbbe7db4
3
+ size 201534
data/artifacts/models/recommender/model_card.md ADDED
@@ -0,0 +1,53 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Model Card: Recommender
2
+
3
+ ## Model Details
4
+
5
+ - **Model Name:** recommender
6
+ - **Model Version:** recommender_v2_baseline_001
7
+ - **Algorithm:** GradientBoostingClassifier (two models: clicked, is_completed)
8
+ - **Framework:** scikit-learn
9
+ - **Trained At:** 2026-05-21T05:59:13.148420+00:00
10
+ - **Seed:** 42
11
+
12
+ ## Intended Use
13
+
14
+ Predict whether a student will click on a recommendation and whether they will
15
+ complete the recommended content. Used in the recommendation engine to rank
16
+ content by predicted engagement. Two separate binary classifiers are trained:
17
+ one for `clicked` and one for `is_completed`.
18
+
19
+ ## Training Data
20
+
21
+ - **Source:** training_recommendation_outcomes.csv (synthetic dataset v2)
22
+ - **Split Counts:** train=5671, validation=1214, test=1215
23
+ - **Features:** priority (OrdinalEncoded), ai_confidence (numeric), recommendation_type (OrdinalEncoded), grade (numeric), subject (OrdinalEncoded)
24
+ - **Targets:** clicked (binary), is_completed (binary)
25
+
26
+ ## Metrics
27
+
28
+ ### Validation Set
29
+ - ROC-AUC (clicked): 0.5439
30
+ - ROC-AUC (is_completed): 0.5303
31
+ - Lift@10 (clicked): 1.1392
32
+ - Lift@10 (is_completed): 1.1056
33
+
34
+ ### Test Set
35
+ - ROC-AUC (clicked): 0.5486
36
+ - ROC-AUC (is_completed): 0.5424
37
+ - Lift@10 (clicked): 1.0471
38
+ - Lift@10 (is_completed): 1.0366
39
+
40
+ ## Known Limitations
41
+
42
+ - Trained on synthetic data only — performance on real recommendation data is unknown.
43
+ - Two separate GBC models — no joint optimization of clicked + is_completed.
44
+ - OrdinalEncoder assumes an ordering for priority/recommendation_type/subject.
45
+ - Lift@10 depends on the distribution of positive labels in the dataset.
46
+ - No user-level features (e.g., engagement history) included in baseline.
47
+ - Limited feature set (5 features); adding student history could improve performance.
48
+
49
+ ## Fallback Behavior
50
+
51
+ When the model is not loaded or confidence is below threshold, the system
52
+ falls back to knowledge-graph weakest-prerequisite + content_catalog filtered
53
+ by LO + difficulty, ranked by estimated_mastery_gain.
data/artifacts/models/recommender/training_config.json ADDED
@@ -0,0 +1,39 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "model_name": "recommender",
3
+ "model_version": "recommender_v2_baseline_001",
4
+ "dataset_version": "2.0.0",
5
+ "seed": 42,
6
+ "split_counts": {
7
+ "train": 5671,
8
+ "validation": 1214,
9
+ "test": 1215
10
+ },
11
+ "hyperparameters": {
12
+ "n_estimators": 100,
13
+ "max_depth": 4,
14
+ "random_state": 42,
15
+ "algorithm": "GradientBoostingClassifier",
16
+ "encoder": "OrdinalEncoder"
17
+ },
18
+ "feature_columns": [
19
+ "priority",
20
+ "ai_confidence",
21
+ "recommendation_type",
22
+ "grade",
23
+ "subject"
24
+ ],
25
+ "categorical_columns": [
26
+ "priority",
27
+ "recommendation_type",
28
+ "subject"
29
+ ],
30
+ "numeric_columns": [
31
+ "ai_confidence",
32
+ "grade"
33
+ ],
34
+ "target_columns": [
35
+ "clicked",
36
+ "is_completed"
37
+ ],
38
+ "algorithm": "GradientBoostingClassifier"
39
+ }
data/artifacts/models/risk_model/feature_columns.json ADDED
@@ -0,0 +1,21 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ [
2
+ "avg_mastery_score",
3
+ "weak_lo_count",
4
+ "developing_lo_count",
5
+ "mastered_lo_count",
6
+ "avg_confidence",
7
+ "avg_accuracy",
8
+ "avg_marks_ratio",
9
+ "avg_time_seconds",
10
+ "hint_usage_rate",
11
+ "total_attempts",
12
+ "attendance_percentage",
13
+ "assignment_completion_rate",
14
+ "average_login_per_week",
15
+ "inactive_days_last_14",
16
+ "avg_active_minutes",
17
+ "total_logins",
18
+ "avg_video_watch_ratio",
19
+ "total_content_completed",
20
+ "total_quiz_attempts"
21
+ ]
data/artifacts/models/risk_model/metrics.json ADDED
@@ -0,0 +1,101 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "model_name": "risk_model",
3
+ "model_version": "risk_model_v2_baseline_001",
4
+ "dataset_version": "2.0.0",
5
+ "trained_at": "2026-05-21T05:59:12.289643+00:00",
6
+ "seed": 42,
7
+ "split_counts": {
8
+ "train": 1133,
9
+ "validation": 243,
10
+ "test": 244
11
+ },
12
+ "metrics": {
13
+ "validation": {
14
+ "recall_positive": 0.8333,
15
+ "precision_positive": 0.9459,
16
+ "f1_positive": 0.8861,
17
+ "roc_auc": 0.99,
18
+ "per_class": {
19
+ "not_at_risk": {
20
+ "precision": 0.966,
21
+ "recall": 0.99,
22
+ "f1": 0.9779,
23
+ "support": 201
24
+ },
25
+ "at_risk": {
26
+ "precision": 0.9459,
27
+ "recall": 0.8333,
28
+ "f1": 0.8861,
29
+ "support": 42
30
+ }
31
+ },
32
+ "confusion_matrix": [
33
+ [
34
+ 199,
35
+ 2
36
+ ],
37
+ [
38
+ 7,
39
+ 35
40
+ ]
41
+ ],
42
+ "risk_level_recall": {
43
+ "high": {
44
+ "recall": 0.8158,
45
+ "support": 38
46
+ },
47
+ "critical": {
48
+ "recall": 1.0,
49
+ "support": 4
50
+ }
51
+ }
52
+ },
53
+ "test": {
54
+ "recall_positive": 0.7812,
55
+ "precision_positive": 0.9259,
56
+ "f1_positive": 0.8475,
57
+ "roc_auc": 0.9899,
58
+ "per_class": {
59
+ "not_at_risk": {
60
+ "precision": 0.9677,
61
+ "recall": 0.9906,
62
+ "f1": 0.979,
63
+ "support": 212
64
+ },
65
+ "at_risk": {
66
+ "precision": 0.9259,
67
+ "recall": 0.7812,
68
+ "f1": 0.8475,
69
+ "support": 32
70
+ }
71
+ },
72
+ "confusion_matrix": [
73
+ [
74
+ 210,
75
+ 2
76
+ ],
77
+ [
78
+ 7,
79
+ 25
80
+ ]
81
+ ],
82
+ "risk_level_recall": {
83
+ "high": {
84
+ "recall": 0.7407,
85
+ "support": 27
86
+ },
87
+ "critical": {
88
+ "recall": 1.0,
89
+ "support": 5
90
+ }
91
+ }
92
+ }
93
+ },
94
+ "limitations": [
95
+ "Trained on synthetic data only.",
96
+ "Binary risk_label derived from synthetic risk_score thresholds.",
97
+ "All features are numeric; no text or contextual features used.",
98
+ "Class imbalance (~16% positive) addressed via class_weight='balanced'.",
99
+ "Critical class (~2%) recall should be monitored separately."
100
+ ]
101
+ }
data/artifacts/models/risk_model/model.joblib ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:6545f4d8f57225e6a77bcc4578307b425be3d3cbdad6ac4c9dd02fa7e636cb28
3
+ size 150104
data/artifacts/models/risk_model/model_card.md ADDED
@@ -0,0 +1,71 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Model Card: Risk Model
2
+
3
+ ## Model Details
4
+
5
+ - **Model Name:** risk_model
6
+ - **Model Version:** risk_model_v2_baseline_001
7
+ - **Algorithm:** RandomForestClassifier (class_weight="balanced")
8
+ - **Framework:** scikit-learn
9
+ - **Trained At:** 2026-05-21T05:59:12.289643+00:00
10
+ - **Seed:** 42
11
+
12
+ ## Intended Use
13
+
14
+ Predict whether a student is at-risk (binary: 0=not at-risk, 1=at-risk) based on
15
+ mastery, performance, and engagement features. Used in the risk prediction endpoint
16
+ to identify students who may need intervention. Primary optimization target is
17
+ recall on the positive class to minimize missed at-risk students.
18
+
19
+ ## Training Data
20
+
21
+ - **Source:** training_risk_prediction.csv (synthetic dataset v2)
22
+ - **Split Counts:** train=1133, validation=243, test=244
23
+ - **Features:** avg_mastery_score, weak_lo_count, developing_lo_count, mastered_lo_count, avg_confidence, avg_accuracy, avg_marks_ratio, avg_time_seconds, hint_usage_rate, total_attempts, attendance_percentage, assignment_completion_rate, average_login_per_week, inactive_days_last_14, avg_active_minutes, total_logins, avg_video_watch_ratio, total_content_completed, total_quiz_attempts (all numeric, 19 features)
24
+ - **Target:** risk_label (binary 0/1)
25
+ - **Class Imbalance:** ~16% positive class, addressed via class_weight="balanced"
26
+
27
+ ## Metrics
28
+
29
+ ### Validation Set
30
+ - Recall (positive): 0.8333
31
+ - Precision (positive): 0.9459
32
+ - F1 (positive): 0.8861
33
+ - ROC-AUC: 0.99
34
+
35
+ ### Test Set
36
+ - Recall (positive): 0.7812
37
+ - Precision (positive): 0.9259
38
+ - F1 (positive): 0.8475
39
+ - ROC-AUC: 0.9899
40
+
41
+ ## Per-Class Performance (Test Set)
42
+
43
+ | Class | Precision | Recall | F1 | Support |
44
+ |-------|-----------|--------|-----|---------|
45
+ | not_at_risk | 0.9677 | 0.9906 | 0.979 | 212 |
46
+ | at_risk | 0.9259 | 0.7812 | 0.8475 | 32 |
47
+
48
+ ## Risk Level Recall (Test Set)
49
+
50
+ | Risk Level | Recall | Support |
51
+ |------------|--------|---------|
52
+ | high | 0.7407 | 27 |
53
+ | critical | 1.0 | 5 |
54
+
55
+ ## Known Limitations
56
+
57
+ - Trained on synthetic data only — performance on real student data is unknown.
58
+ - Binary risk_label derived from synthetic risk_score thresholds.
59
+ - All features are numeric; no text or contextual features used.
60
+ - Class imbalance (~16% positive) addressed via class_weight="balanced".
61
+ - Critical class (~2%) is very rare; recall on critical should be monitored.
62
+ - No temporal features (trend over time) included in this baseline.
63
+
64
+ ## Fallback Behavior
65
+
66
+ When the model is not loaded or confidence is below the threshold (0.55),
67
+ the system falls back to rule-based risk estimation using:
68
+ - inactive_days_last_14 > 7 → high risk
69
+ - attendance_percentage < 60% → high risk
70
+ - avg_mastery_score < 0.4 → medium risk
71
+ - Otherwise → low risk
data/artifacts/models/risk_model/training_config.json ADDED
@@ -0,0 +1,44 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "model_name": "risk_model",
3
+ "model_version": "risk_model_v2_baseline_001",
4
+ "dataset_version": "2.0.0",
5
+ "seed": 42,
6
+ "split_counts": {
7
+ "train": 1133,
8
+ "validation": 243,
9
+ "test": 244
10
+ },
11
+ "hyperparameters": {
12
+ "n_estimators": 100,
13
+ "class_weight": "balanced",
14
+ "random_state": 42,
15
+ "algorithm": "RandomForestClassifier"
16
+ },
17
+ "feature_columns": [
18
+ "avg_mastery_score",
19
+ "weak_lo_count",
20
+ "developing_lo_count",
21
+ "mastered_lo_count",
22
+ "avg_confidence",
23
+ "avg_accuracy",
24
+ "avg_marks_ratio",
25
+ "avg_time_seconds",
26
+ "hint_usage_rate",
27
+ "total_attempts",
28
+ "attendance_percentage",
29
+ "assignment_completion_rate",
30
+ "average_login_per_week",
31
+ "inactive_days_last_14",
32
+ "avg_active_minutes",
33
+ "total_logins",
34
+ "avg_video_watch_ratio",
35
+ "total_content_completed",
36
+ "total_quiz_attempts"
37
+ ],
38
+ "target_column": "risk_label",
39
+ "label_map": {
40
+ "0": "not_at_risk",
41
+ "1": "at_risk"
42
+ },
43
+ "algorithm": "RandomForestClassifier"
44
+ }
data/learning_outcome_os_dataset_v2/README.md ADDED
@@ -0,0 +1,134 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Learning Outcome OS AI-Ready Expanded Dataset v2
2
+
3
+ Expanded synthetic, CBSE/NCERT LO-aligned, model-ready dataset for LO tagging, Bloom classification, mastery prediction, recommendation, risk prediction, subjective answer scoring, teacher feedback, and digital twin modelling.
4
+
5
+ All names are synthetic. No real PII is included. CSVs are pre-cleaned with stable IDs, target labels, and train/validation/test split columns.
6
+
7
+ ## Tables
8
+ ### schools.csv
9
+ - Rows: 3
10
+ - Columns: school_id, school_name, board, state, city, region_type, academic_year, is_synthetic
11
+
12
+ ### classes.csv
13
+ - Rows: 18
14
+ - Columns: class_id, school_id, grade, section, class_name, academic_year, class_teacher_id
15
+
16
+ ### subjects.csv
17
+ - Rows: 9
18
+ - Columns: subject_id, grade, subject, subject_code
19
+
20
+ ### chapters.csv
21
+ - Rows: 52
22
+ - Columns: chapter_id, grade, subject, chapter, chapter_order, lo_count, is_active
23
+
24
+ ### teachers.csv
25
+ - Rows: 54
26
+ - Columns: teacher_id, school_id, class_id, teacher_name, grade, section, subject, email, experience_years, ai_feedback_participation_rate
27
+
28
+ ### student_profiles.csv
29
+ - Rows: 540
30
+ - Columns: student_id, school_id, class_id, grade, section, roll_number, student_name, learning_style, learner_archetype, baseline_level, attendance_percentage, assignment_completion_rate, average_login_per_week, inactive_days_last_14, parent_contact_available, is_synthetic, train_split
31
+
32
+ ### learning_outcomes.csv
33
+ - Rows: 194
34
+ - Columns: lo_id, grade, subject, chapter, competency, title, description, difficulty, bloom_level, source_framework, source_pdf, source_pages, alignment_confidence, difficulty_score, bloom_score, embedding_text, is_active, train_split
35
+
36
+ ### lo_dependencies.csv
37
+ - Rows: 174
38
+ - Columns: lo_id, prerequisite_lo_id, relationship_type, strength
39
+
40
+ ### questions.csv
41
+ - Rows: 5,820
42
+ - Columns: question_id, lo_id, grade, subject, chapter, question_text, question_type, difficulty, difficulty_score, bloom_level, bloom_score, correct_answer, rubric, max_marks, source, source_lo_pdf, alignment_confidence, embedding_text, train_split
43
+
44
+ ### question_options.csv
45
+ - Rows: 7,760
46
+ - Columns: question_id, option_label, option_text, is_correct
47
+
48
+ ### content_catalog.csv
49
+ - Rows: 1,552
50
+ - Columns: content_id, lo_id, grade, subject, chapter, title, content_type, target_use, difficulty, duration_minutes, language, description, estimated_mastery_gain, embedding_text, is_active, train_split
51
+
52
+ ### assessments.csv
53
+ - Rows: 216
54
+ - Columns: assessment_id, school_id, class_id, grade, section, subject, assessment_name, assessment_type, scheduled_date, max_marks, question_count, academic_year, train_split
55
+
56
+ ### assessment_questions.csv
57
+ - Rows: 2,592
58
+ - Columns: assessment_id, question_id, question_order
59
+
60
+ ### student_attempts.csv
61
+ - Rows: 77,760
62
+ - Columns: attempt_id, assessment_id, school_id, class_id, student_id, question_id, lo_id, grade, section, subject, question_type, difficulty_score, bloom_score, is_correct, marks_obtained, max_marks, marks_ratio, time_taken_seconds, hint_used, attempt_number, submitted_at, train_split
63
+
64
+ ### initial_mastery_profiles.csv
65
+ - Rows: 34,920
66
+ - Columns: student_id, lo_id, grade, section, subject, chapter, train_split, attempt_count, accuracy, average_marks_ratio, average_time_seconds, hint_usage_rate, mastery_score, status, confidence, last_updated
67
+
68
+ ### mastery_profiles.csv
69
+ - Rows: 34,920
70
+ - Columns: student_id, lo_id, grade, section, subject, chapter, train_split, attempt_count, accuracy, average_marks_ratio, average_time_seconds, hint_usage_rate, mastery_score, status, confidence, last_updated
71
+
72
+ ### engagement_logs.csv
73
+ - Rows: 108,000
74
+ - Columns: engagement_id, student_id, school_id, class_id, activity_date, login_count, active_minutes, content_completed_count, quiz_attempt_count, recommendation_click_count, video_watch_ratio, discussion_posts, device_type, train_split
75
+
76
+ ### risk_profiles.csv
77
+ - Rows: 1,620
78
+ - Columns: risk_prediction_id, student_id, school_id, class_id, grade, section, subject, risk_score, risk_level, risk_label, primary_reasons, recommended_intervention, model_version, generated_at, confidence, train_split
79
+
80
+ ### recommendations.csv
81
+ - Rows: 8,100
82
+ - Columns: recommendation_id, student_id, school_id, class_id, lo_id, content_id, grade, section, subject, recommendation_type, priority, reason, ai_confidence, generated_at, shown_to_student, clicked, is_completed, observed_mastery_gain, model_version, train_split
83
+
84
+ ### teacher_interventions.csv
85
+ - Rows: 162
86
+ - Columns: intervention_id, school_id, class_id, teacher_id, grade, section, subject, lo_id, intervention_type, affected_students, avg_mastery_before, suggested_action, scheduled_week, status, expected_mastery_gain, generated_by_ai, train_split
87
+
88
+ ### subjective_answers.csv
89
+ - Rows: 12,000
90
+ - Columns: answer_id, attempt_id, student_id, question_id, lo_id, grade, subject, question_type, student_answer, model_answer, rubric, max_marks, teacher_marks, ai_predicted_marks, absolute_error, rubric_match_score, concept_coverage_score, feedback_text, teacher_review_required, train_split
91
+
92
+ ### teacher_feedback.csv
93
+ - Rows: 5,986
94
+ - Columns: feedback_id, teacher_id, student_id, related_entity_type, related_entity_id, feedback_type, teacher_rating, correction_required, correction_text, created_at, train_split
95
+
96
+ ### student_digital_twins.csv
97
+ - Rows: 540
98
+ - Columns: digital_twin_id, student_id, school_id, class_id, grade, section, overall_mastery_score, strongest_subject, weakest_subject, top_weak_lo_ids, learning_speed_score, consistency_score, preferred_content_type, current_risk_level, current_risk_score, recommended_next_action, last_updated, train_split
99
+
100
+ ### ai_prediction_logs.csv
101
+ - Rows: 16,620
102
+ - Columns: prediction_log_id, model_name, model_version, entity_type, entity_id, prediction_output, confidence, latency_ms, created_at, train_split
103
+
104
+ ### ml_features_student_subject.csv
105
+ - Rows: 1,620
106
+ - Columns: feature_row_id, student_id, school_id, class_id, grade, section, subject, avg_mastery_score, weak_lo_count, developing_lo_count, mastered_lo_count, avg_confidence, avg_accuracy, avg_marks_ratio, avg_time_seconds, hint_usage_rate, total_attempts, attendance_percentage, assignment_completion_rate, average_login_per_week, inactive_days_last_14, avg_active_minutes, total_logins, avg_video_watch_ratio, total_content_completed, total_quiz_attempts, risk_score, risk_level, risk_label, train_split
107
+
108
+ ### ml_features_student_lo.csv
109
+ - Rows: 34,920
110
+ - Columns: feature_row_id, student_id, lo_id, grade, section, subject, chapter, train_split, attempt_count, accuracy, average_marks_ratio, average_time_seconds, hint_usage_rate, mastery_score, status, confidence, last_updated, school_id, class_id, attendance_percentage, assignment_completion_rate, average_login_per_week, inactive_days_last_14, mastery_label
111
+
112
+ ### training_lo_tagging.csv
113
+ - Rows: 5,820
114
+ - Columns: question_id, question_text, embedding_text, lo_id, grade, subject, chapter, difficulty, difficulty_score, bloom_level, bloom_score, train_split
115
+
116
+ ### training_bloom_classification.csv
117
+ - Rows: 5,820
118
+ - Columns: question_id, question_text, embedding_text, bloom_level, bloom_score, grade, subject, question_type, train_split
119
+
120
+ ### training_risk_prediction.csv
121
+ - Rows: 1,620
122
+ - Columns: feature_row_id, student_id, school_id, class_id, grade, section, subject, avg_mastery_score, weak_lo_count, developing_lo_count, mastered_lo_count, avg_confidence, avg_accuracy, avg_marks_ratio, avg_time_seconds, hint_usage_rate, total_attempts, attendance_percentage, assignment_completion_rate, average_login_per_week, inactive_days_last_14, avg_active_minutes, total_logins, avg_video_watch_ratio, total_content_completed, total_quiz_attempts, risk_score, risk_level, risk_label, train_split
123
+
124
+ ### training_mastery_prediction.csv
125
+ - Rows: 34,920
126
+ - Columns: feature_row_id, student_id, lo_id, grade, section, subject, chapter, train_split, attempt_count, accuracy, average_marks_ratio, average_time_seconds, hint_usage_rate, mastery_score, status, confidence, last_updated, school_id, class_id, attendance_percentage, assignment_completion_rate, average_login_per_week, inactive_days_last_14, mastery_label
127
+
128
+ ### training_answer_scoring.csv
129
+ - Rows: 12,000
130
+ - Columns: answer_id, question_id, student_id, lo_id, grade, subject, question_type, student_answer, model_answer, rubric, max_marks, teacher_marks, ai_predicted_marks, rubric_match_score, concept_coverage_score, teacher_review_required, train_split
131
+
132
+ ### training_recommendation_outcomes.csv
133
+ - Rows: 8,100
134
+ - Columns: recommendation_id, student_id, lo_id, content_id, grade, subject, recommendation_type, priority, ai_confidence, clicked, is_completed, observed_mastery_gain, train_split
data/learning_outcome_os_dataset_v2/ai_prediction_logs.csv ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:f54c6a4a70c988f7098903b9204509644a410207bd61b3c75b0b08b5cb7c6984
3
+ size 3227380
data/learning_outcome_os_dataset_v2/assessment_questions.csv ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:87f6069f6e47175b4f9a1dfe479026d4482e48906f63c978c72d8bda6fab3330
3
+ size 49937
data/learning_outcome_os_dataset_v2/assessments.csv ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:a3358b494742424fd733167015ae1eada0a6e767d079d0bdad647df3a9383bfb
3
+ size 23609
data/learning_outcome_os_dataset_v2/chapters.csv ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:b8d84f61d2f51c609b22888a25cfb92c1a3becdec9fd50bdd1bd25b0869a5cb0
3
+ size 2838
data/learning_outcome_os_dataset_v2/classes.csv ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:aadcf230ada60fd703e3147447c2acfa1df778223cda012eb8c86cb76d99992c
3
+ size 939
data/learning_outcome_os_dataset_v2/content_catalog.csv ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:0b7c6666bba870a5f7c8562b9bc2b98a1e86b41eae1e0db8b93a2a2b434aea42
3
+ size 847643
data/learning_outcome_os_dataset_v2/dataset_metadata.json ADDED
@@ -0,0 +1,98 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "dataset_name": "Learning Outcome OS AI-Ready Expanded Dataset",
3
+ "version": "2.0.0",
4
+ "generated_at": "2026-05-20T11:24:37Z",
5
+ "seed": 20260520,
6
+ "scope": {
7
+ "schools": 3,
8
+ "classes": 18,
9
+ "grades": [
10
+ 6,
11
+ 7,
12
+ 8
13
+ ],
14
+ "sections": [
15
+ "A",
16
+ "B"
17
+ ],
18
+ "subjects": [
19
+ "Mathematics",
20
+ "Science",
21
+ "Social Science"
22
+ ],
23
+ "students_per_class_section": 30,
24
+ "total_students": 540,
25
+ "source_alignment": [
26
+ "mathematics_LO.pdf",
27
+ "science_LO.pdf",
28
+ "social_science_LO.pdf"
29
+ ],
30
+ "source_base_dataset": "cbse_lo_aligned_synthetic_dataset_classes_6_8.zip"
31
+ },
32
+ "model_ready_design": {
33
+ "no_null_values": true,
34
+ "stable_primary_keys": true,
35
+ "foreign_keys_valid": true,
36
+ "train_validation_test_splits": true,
37
+ "numeric_features_available": true,
38
+ "nlp_embedding_text_fields_available": true,
39
+ "human_feedback_tables_available": true,
40
+ "target_labels_available": [
41
+ "lo_id",
42
+ "bloom_level",
43
+ "difficulty_score",
44
+ "mastery_score",
45
+ "mastery_label",
46
+ "risk_label",
47
+ "teacher_marks",
48
+ "clicked",
49
+ "is_completed"
50
+ ]
51
+ },
52
+ "table_counts": {
53
+ "schools.csv": 3,
54
+ "classes.csv": 18,
55
+ "subjects.csv": 9,
56
+ "chapters.csv": 52,
57
+ "teachers.csv": 54,
58
+ "student_profiles.csv": 540,
59
+ "learning_outcomes.csv": 194,
60
+ "lo_dependencies.csv": 174,
61
+ "questions.csv": 5820,
62
+ "question_options.csv": 7760,
63
+ "content_catalog.csv": 1552,
64
+ "assessments.csv": 216,
65
+ "assessment_questions.csv": 2592,
66
+ "student_attempts.csv": 77760,
67
+ "initial_mastery_profiles.csv": 34920,
68
+ "mastery_profiles.csv": 34920,
69
+ "engagement_logs.csv": 108000,
70
+ "risk_profiles.csv": 1620,
71
+ "recommendations.csv": 8100,
72
+ "teacher_interventions.csv": 162,
73
+ "subjective_answers.csv": 12000,
74
+ "teacher_feedback.csv": 5986,
75
+ "student_digital_twins.csv": 540,
76
+ "ai_prediction_logs.csv": 16620,
77
+ "ml_features_student_subject.csv": 1620,
78
+ "ml_features_student_lo.csv": 34920,
79
+ "training_lo_tagging.csv": 5820,
80
+ "training_bloom_classification.csv": 5820,
81
+ "training_risk_prediction.csv": 1620,
82
+ "training_mastery_prediction.csv": 34920,
83
+ "training_answer_scoring.csv": 12000,
84
+ "training_recommendation_outcomes.csv": 8100
85
+ },
86
+ "status_labels": [
87
+ "weak",
88
+ "developing",
89
+ "proficient",
90
+ "mastered"
91
+ ],
92
+ "risk_labels": [
93
+ "low",
94
+ "medium",
95
+ "high",
96
+ "critical"
97
+ ]
98
+ }
data/learning_outcome_os_dataset_v2/engagement_logs.csv ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:18ba724e544dffd9b0b50cfd4a17d5f0cec1acb5448e8164335d99aa73a3f9db
3
+ size 8509736
data/learning_outcome_os_dataset_v2/generation_script.py ADDED
@@ -0,0 +1,354 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+ import json, random, math, shutil, zipfile
3
+ from pathlib import Path
4
+ from datetime import datetime, timedelta
5
+ from collections import defaultdict
6
+ import numpy as np
7
+ import pandas as pd
8
+
9
+ SEED=20260520
10
+ random.seed(SEED); np.random.seed(SEED)
11
+ BASE=Path('/mnt/data/cbse_ds')
12
+ OUT=Path('/mnt/data/learning_outcome_os_ai_dataset_v2')
13
+ ZIP=Path('/mnt/data/learning_outcome_os_ai_ready_expanded_dataset_v2.zip')
14
+ if OUT.exists(): shutil.rmtree(OUT)
15
+ OUT.mkdir(parents=True, exist_ok=True)
16
+ if ZIP.exists(): ZIP.unlink()
17
+
18
+ def sigmoid(x): return 1/(1+math.exp(-x))
19
+ def clip(x,a,b): return max(a,min(b,x))
20
+ def clean(s): return ' '.join(str(s).replace('\n',' ').replace('\r',' ').split()).strip()
21
+ def split_id(x):
22
+ h=sum((i+1)*ord(c) for i,c in enumerate(str(x)))%100
23
+ return 'train' if h<70 else ('validation' if h<85 else 'test')
24
+ def dstr(dt): return dt.strftime('%Y-%m-%d')
25
+ def tstr(dt): return dt.strftime('%Y-%m-%dT%H:%M:%S')
26
+ from time import time
27
+ _t0=time()
28
+ def mark(x):
29
+ with open('/mnt/data/fast_progress.txt','a') as f: f.write(f'{time()-_t0:.1f}s {x}\n')
30
+ mark('start')
31
+
32
+ diff_map={'Easy':1,'Medium':2,'Hard':3}; diff_rev={1:'Easy',2:'Medium',3:'Hard'}
33
+ bloom_map={'Remember':1,'Understand':2,'Apply':3,'Analyze':4,'Evaluate':5,'Create':6}; bloom_rev={v:k for k,v in bloom_map.items()}
34
+ subjects_list=['Mathematics','Science','Social Science']
35
+
36
+ # Source LO layer
37
+ lo=pd.read_csv(BASE/'learning_outcomes.csv').fillna('')
38
+ lo['title']=lo['title'].map(clean); lo['description']=lo['description'].map(clean); lo['chapter']=lo['chapter'].map(clean); lo['competency']=lo['competency'].map(clean)
39
+ lo['difficulty_score']=lo['difficulty'].map(diff_map).fillna(2).astype(int)
40
+ lo['bloom_score']=lo['bloom_level'].map(bloom_map).fillna(2).astype(int)
41
+ lo['embedding_text']=(lo['subject'].astype(str)+' | Grade '+lo['grade'].astype(str)+' | '+lo['chapter']+' | '+lo['competency']+' | '+lo['title']+' | '+lo['description']).map(clean)
42
+ lo['is_active']=1; lo['train_split']=lo['lo_id'].map(split_id)
43
+ mark('lo')
44
+ learning_outcomes=lo.copy()
45
+ lo_map=learning_outcomes.set_index('lo_id').to_dict('index')
46
+ los_by_grade={g: learning_outcomes[learning_outcomes.grade==g].to_dict('records') for g in [6,7,8]}
47
+ los_by_grade_subject={(g,s): learning_outcomes[(learning_outcomes.grade==g)&(learning_outcomes.subject==s)].to_dict('records') for g in [6,7,8] for s in subjects_list}
48
+ lo_dependencies=pd.read_csv(BASE/'lo_dependencies.csv').fillna('')
49
+ chapters=pd.read_csv(BASE/'chapters.csv').fillna(''); chapters['is_active']=1
50
+
51
+ # Dimensions
52
+ school_defs=[('SCH001','Nirmal Vidya Public School','CBSE','Chhattisgarh','Bhilai','Urban'),('SCH002','Pragati Central School','CBSE','Chhattisgarh','Durg','Semi-Urban'),('SCH003','Saraswati Learning Academy','CBSE','Maharashtra','Nagpur','Urban')]
53
+ schools=pd.DataFrame([{'school_id':a,'school_name':b,'board':c,'state':d,'city':e,'region_type':f,'academic_year':'2026-27','is_synthetic':1} for a,b,c,d,e,f in school_defs])
54
+ classes=[]
55
+ for sid, *_ in school_defs:
56
+ for grade in [6,7,8]:
57
+ for sec in ['A','B']:
58
+ classes.append({'class_id':f'{sid}_G{grade}{sec}','school_id':sid,'grade':grade,'section':sec,'class_name':f'Class {grade}-{sec}','academic_year':'2026-27'})
59
+ classes=pd.DataFrame(classes)
60
+ subjects=pd.DataFrame([{'subject_id':f'{code}{g}','grade':g,'subject':subj,'subject_code':code} for g in [6,7,8] for subj,code in [('Mathematics','MATH'),('Science','SCI'),('Social Science','SOC')]])
61
+
62
+ first=['Aarav','Vivaan','Aditya','Ishaan','Kabir','Arjun','Rohan','Karan','Priya','Ananya','Isha','Riya','Neha','Pooja','Kavita','Meera','Sneha','Aditi','Nisha','Vikas','Manish','Deepak','Suman','Rakesh','Naveen','Sanjay','Anita','Swati']
63
+ last=['Sharma','Verma','Sahu','Patel','Gupta','Singh','Yadav','Mishra','Tiwari','Choudhary','Jain','Rao','Das','Nair','Khan','Joshi','Dubey','Agrawal']
64
+
65
+ teachers=[]; tid=1
66
+ for c in classes.to_dict('records'):
67
+ for subj in subjects_list:
68
+ fn,ln=random.choice(first),random.choice(last)
69
+ teachers.append({'teacher_id':f'TCH{tid:04d}','school_id':c['school_id'],'class_id':c['class_id'],'teacher_name':f'{fn} {ln}','grade':c['grade'],'section':c['section'],'subject':subj,'email':f'{fn.lower()}.{ln.lower()}.{tid}@school.example','experience_years':random.randint(2,18),'ai_feedback_participation_rate':round(random.uniform(.55,.95),2)})
70
+ tid+=1
71
+ mark('teachers')
72
+ teachers=pd.DataFrame(teachers)
73
+ classes['class_teacher_id']=classes['class_id'].map(teachers.groupby('class_id')['teacher_id'].first().to_dict())
74
+ teacher_lookup={(r.class_id,r.subject):r.teacher_id for _,r in teachers.iterrows()}
75
+
76
+ # Students
77
+ archetypes={
78
+ 'high_performer':(1.1,(90,98),(88,99),(5,7),.10),'consistent_average':(.25,(82,94),(72,90),(3,6),.07),'conceptually_weak_active':(-.45,(82,94),(70,88),(4,7),.12),'low_engagement':(-.25,(68,84),(40,68),(1,4),.03),'at_risk':(-1.0,(55,76),(25,55),(0,3),.01),'fast_improver':(-.15,(84,96),(76,94),(4,7),.20)}
79
+ weights=[.14,.32,.18,.14,.10,.12]; styles=['visual','auditory','reading_writing','kinesthetic','mixed']
80
+ students=[]; ability={}; sidn=1; students_by_class=defaultdict(list)
81
+ for c in classes.to_dict('records'):
82
+ for roll in range(1,31):
83
+ arch=random.choices(list(archetypes),weights=weights,k=1)[0]
84
+ mean,att_rng,comp_rng,login_rng,growth=archetypes[arch]
85
+ base=np.random.normal(mean,.35); subj_offsets={s:np.random.normal(0,.35) for s in subjects_list}
86
+ sid=f'STU{sidn:05d}'; sidn+=1
87
+ att=random.randint(*att_rng); comp=random.randint(*comp_rng); login=random.randint(*login_rng)
88
+ inactive=int(clip(round(np.random.normal(3+(100-att)/10+(5-login),2)),0,14))
89
+ ability[sid]={s:base+subj_offsets[s] for s in subjects_list}; ability[sid]['growth']=growth; ability[sid]['arch']=arch
90
+ row={'student_id':sid,'school_id':c['school_id'],'class_id':c['class_id'],'grade':c['grade'],'section':c['section'],'roll_number':roll,'student_name':f'{random.choice(first)} {random.choice(last)}','learning_style':random.choice(styles),'learner_archetype':arch,'baseline_level':'high' if base>.65 else ('medium' if base>-.45 else 'low'),'attendance_percentage':att,'assignment_completion_rate':comp,'average_login_per_week':login,'inactive_days_last_14':inactive,'parent_contact_available':1,'is_synthetic':1,'train_split':split_id(sid)}
91
+ students.append(row); students_by_class[c['class_id']].append(sid)
92
+ mark('students')
93
+ student_profiles=pd.DataFrame(students); student_meta=student_profiles.set_index('student_id').to_dict('index')
94
+
95
+ # Questions
96
+ q_plan=[('MCQ',10),('SHORT_ANSWER',8),('CASE_BASED',6),('LONG_ANSWER',4),('ORAL_PROMPT',2)]
97
+ q_templates={
98
+ 'MCQ':['Which option best demonstrates the learning outcome: {title}?','Which choice correctly shows how to {title_lc}?','Which is the best example of {title_lc}?'],
99
+ 'SHORT_ANSWER':['Answer briefly: how can a learner {title_lc} in {chapter}?','Write two points to show: {title}.','State the key idea behind: {title}.'],
100
+ 'CASE_BASED':['A student faces a real-life situation from {chapter}. Describe how the student can {title_lc}.','In a classroom activity on {chapter}, how would you demonstrate: {title}?'],
101
+ 'LONG_ANSWER':['Explain in detail with an example: {title}.','Discuss the process, example, and application related to: {title}.'],
102
+ 'ORAL_PROMPT':['Oral prompt: explain aloud how you would {title_lc}.','Class discussion prompt: what does this outcome mean - {title}?']}
103
+ questions=[]; options=[]; qid=1; q_by_grade_subj_type=defaultdict(list)
104
+ for l in learning_outcomes.to_dict('records'):
105
+ for qtype,count in q_plan:
106
+ for _ in range(count):
107
+ ds=int(clip(l['difficulty_score']+np.random.choice([-1,0,1],p=[.12,.72,.16]),1,3)); bs=int(clip(l['bloom_score']+np.random.choice([-1,0,1],p=[.12,.68,.20]),1,6))
108
+ title=clean(l['title']); title_lc=title[:1].lower()+title[1:]
109
+ text=random.choice(q_templates[qtype]).format(title=title,title_lc=title_lc,chapter=l['chapter'])
110
+ qid_str=f'Q{qid:06d}'; max_marks={'MCQ':1,'SHORT_ANSWER':2,'CASE_BASED':3,'LONG_ANSWER':5,'ORAL_PROMPT':2}[qtype]
111
+ row={'question_id':qid_str,'lo_id':l['lo_id'],'grade':int(l['grade']),'subject':l['subject'],'chapter':l['chapter'],'question_text':clean(text),'question_type':qtype,'difficulty':diff_rev[ds],'difficulty_score':ds,'bloom_level':bloom_rev[bs],'bloom_score':bs,'correct_answer':'A response that accurately demonstrates the learning outcome.','rubric':f'Award marks for accurate concept use, relevant example, and alignment with LO {l["lo_id"]}.','max_marks':max_marks,'source':'synthetic_cbse_lo_aligned_v2','source_lo_pdf':l.get('source_pdf','unknown'),'alignment_confidence':round(random.uniform(.88,.97),2),'embedding_text':clean(f'{l["subject"]} Grade {l["grade"]} {l["chapter"]} {text} {title}'),'train_split':split_id(qid_str)}
112
+ questions.append(row); q_by_grade_subj_type[(int(l['grade']),l['subject'],qtype)].append(qid_str)
113
+ if qtype=='MCQ':
114
+ correct=random.choice(['A','B','C','D'])
115
+ for lab in ['A','B','C','D']:
116
+ options.append({'question_id':qid_str,'option_label':lab,'option_text':'Correctly demonstrates the stated learning outcome.' if lab==correct else random.choice(['Partially related but misses the key concept.','Uses an incorrect example.','Focuses on an unrelated concept.','Shows a common misconception.']),'is_correct':1 if lab==correct else 0})
117
+ qid+=1
118
+ mark('questions')
119
+ questions=pd.DataFrame(questions); question_options=pd.DataFrame(options); q_map=questions.set_index('question_id').to_dict('index')
120
+
121
+ # Content
122
+ ctypes=[('video',8,.07,'practice'),('worksheet',18,.09,'practice'),('interactive_activity',14,.11,'practice'),('flashcard_set',6,.05,'remediation'),('diagnostic_quiz',12,.08,'practice'),('remedial_notes',10,.10,'remediation'),('practice_quiz',15,.09,'practice'),('advanced_challenge',20,.08,'enrichment')]
123
+ content=[]; cid=1; content_by_lo=defaultdict(list)
124
+ for l in learning_outcomes.to_dict('records'):
125
+ for ctype,dur,gain,target in ctypes:
126
+ row={'content_id':f'CNT{cid:06d}','lo_id':l['lo_id'],'grade':int(l['grade']),'subject':l['subject'],'chapter':l['chapter'],'title':clean(f'{l["title"]} - {ctype.replace("_"," ").title()}'),'content_type':ctype,'target_use':target,'difficulty':'Hard' if target=='enrichment' else ('Easy' if target=='remediation' else l['difficulty']),'duration_minutes':max(4,int(np.random.normal(dur,3))),'language':random.choice(['English','English','English','Hindi Support']),'description':clean(f'{ctype.replace("_"," ").title()} aligned to {l["lo_id"]}: {l["description"]}'),'estimated_mastery_gain':round(clip(np.random.normal(gain,.025),.03,.18),2),'embedding_text':clean(f'{l["subject"]} {l["chapter"]} {l["title"]} {ctype} {l["description"]}'),'is_active':1,'train_split':split_id(f'CNT{cid:06d}')}
127
+ content.append(row); content_by_lo[l['lo_id']].append(row); cid+=1
128
+ mark('content')
129
+ content_catalog=pd.DataFrame(content)
130
+
131
+ # Assessments and attempts
132
+ assessments=[]; assessment_questions=[]; attempts=[]; agg=defaultdict(lambda:{'cnt':0,'correct':0,'marks':0.0,'time':0,'hint':0,'last':'2026-06-01'}); non_mcq_attempts=[]
133
+ start=datetime(2026,6,15); aid=1; attid=1
134
+ atype_seq=['diagnostic','formative','unit_test','summative']
135
+ for c in classes.to_dict('records'):
136
+ for subj in subjects_list:
137
+ for idx,atype in enumerate(atype_seq,1):
138
+ aid_str=f'ASM{aid:05d}'; dt=start+timedelta(days=idx*28+random.randint(-2,2)+(c['grade']-6)*3)
139
+ qids=[]
140
+ for qt,n in [('MCQ',5),('SHORT_ANSWER',3),('CASE_BASED',2),('LONG_ANSWER',2)]:
141
+ pool=q_by_grade_subj_type[(c['grade'],subj,qt)]
142
+ qids.extend(random.sample(pool,n))
143
+ max_marks=sum(q_map[q]['max_marks'] for q in qids)
144
+ assessments.append({'assessment_id':aid_str,'school_id':c['school_id'],'class_id':c['class_id'],'grade':c['grade'],'section':c['section'],'subject':subj,'assessment_name':f'{subj} {atype.title()} {idx}','assessment_type':atype,'scheduled_date':dstr(dt),'max_marks':max_marks,'question_count':len(qids),'academic_year':'2026-27','train_split':split_id(aid_str)})
145
+ for order,q in enumerate(qids,1): assessment_questions.append({'assessment_id':aid_str,'question_id':q,'question_order':order})
146
+ progress=(dt-start).days/140
147
+ for sid in students_by_class[c['class_id']]:
148
+ sp=student_meta[sid]; subj_ability=ability[sid][subj]+ability[sid]['growth']*progress
149
+ eng_bonus=(sp['average_login_per_week']-3)*.08+(sp['assignment_completion_rate']-70)*.01+(sp['attendance_percentage']-80)*.008
150
+ for qid_ in qids:
151
+ q=q_map[qid_]; p=clip(sigmoid(subj_ability+eng_bonus-.55*(q['difficulty_score']-1)-.10*(q['bloom_score']-2)+np.random.normal(0,.22)),.02,.98)
152
+ correct=1 if random.random()<p else 0
153
+ partial=0 if correct or q['question_type']=='MCQ' else clip(np.random.normal(p,.18),0,.85)
154
+ mark_ratio=1.0 if correct else partial; marks=round(mark_ratio*q['max_marks'],2)
155
+ base_time=28+q['difficulty_score']*18+(q['bloom_score']-1)*5+{'MCQ':0,'SHORT_ANSWER':22,'CASE_BASED':38,'LONG_ANSWER':70,'ORAL_PROMPT':20}[q['question_type']]
156
+ time_taken=int(clip(np.random.normal(base_time*(1.15-p*.25),16),12,420)); hint=1 if random.random()<clip(.36+.12*q['difficulty_score']-.32*p,.02,.78) else 0
157
+ submitted=dt+timedelta(hours=random.randint(8,17),minutes=random.randint(0,59),seconds=random.randint(0,59)); att_str=f'ATT{attid:07d}'
158
+ row={'attempt_id':att_str,'assessment_id':aid_str,'school_id':c['school_id'],'class_id':c['class_id'],'student_id':sid,'question_id':qid_,'lo_id':q['lo_id'],'grade':c['grade'],'section':c['section'],'subject':subj,'question_type':q['question_type'],'difficulty_score':q['difficulty_score'],'bloom_score':q['bloom_score'],'is_correct':correct,'marks_obtained':marks,'max_marks':q['max_marks'],'marks_ratio':round(mark_ratio,3),'time_taken_seconds':time_taken,'hint_used':hint,'attempt_number':1,'submitted_at':tstr(submitted),'train_split':split_id(att_str)}
159
+ attempts.append(row)
160
+ if q['question_type']!='MCQ': non_mcq_attempts.append(row)
161
+ a=agg[(sid,q['lo_id'])]; a['cnt']+=1; a['correct']+=correct; a['marks']+=mark_ratio; a['time']+=time_taken; a['hint']+=hint; a['last']=max(a['last'],dstr(submitted))
162
+ attid+=1
163
+ aid+=1
164
+ mark('attempts')
165
+ assessments=pd.DataFrame(assessments); assessment_questions=pd.DataFrame(assessment_questions); student_attempts=pd.DataFrame(attempts)
166
+
167
+ # Mastery and initial mastery
168
+ initial=[]; mastery=[]; mastery_by_ss=defaultdict(list)
169
+ for sp in students:
170
+ for l in los_by_grade[sp['grade']]:
171
+ init=clip(sigmoid(ability[sp['student_id']][l['subject']]-.38*(l['difficulty_score']-1)+np.random.normal(0,.2))*.75,.05,.85)
172
+ init_status='weak' if init<.40 else ('developing' if init<.65 else ('proficient' if init<.85 else 'mastered'))
173
+ base={'student_id':sp['student_id'],'lo_id':l['lo_id'],'grade':sp['grade'],'section':sp['section'],'subject':l['subject'],'chapter':l['chapter'],'train_split':split_id(sp['student_id']+l['lo_id'])}
174
+ initial.append({**base,'attempt_count':0,'accuracy':0.0,'average_marks_ratio':0.0,'average_time_seconds':0.0,'hint_usage_rate':0.0,'mastery_score':round(init,3),'status':init_status,'confidence':.35,'last_updated':'2026-06-01'})
175
+ a=agg.get((sp['student_id'],l['lo_id']))
176
+ if a:
177
+ acc=a['correct']/a['cnt']; mr=a['marks']/a['cnt']; tm=a['time']/a['cnt']; hr=a['hint']/a['cnt']; eff=clip(1-(tm-55)/180,0,1)
178
+ score=clip(.18*init+.42*acc+.27*mr+.08*eff+.05*(1-hr)+np.random.normal(0,.025),.02,.99); conf=clip(.45+.12*a['cnt'],.35,.96); lastd=a['last']; cnt=a['cnt']
179
+ else:
180
+ acc=mr=tm=hr=0.0; score=init*.92; conf=.25; lastd='2026-06-01'; cnt=0
181
+ status='weak' if score<.40 else ('developing' if score<.65 else ('proficient' if score<.85 else 'mastered'))
182
+ row={**base,'attempt_count':cnt,'accuracy':round(acc,3),'average_marks_ratio':round(mr,3),'average_time_seconds':round(tm,1),'hint_usage_rate':round(hr,3),'mastery_score':round(score,3),'status':status,'confidence':round(conf,3),'last_updated':lastd}
183
+ mastery.append(row); mastery_by_ss[(sp['student_id'],l['subject'])].append(row)
184
+ mark('mastery')
185
+ initial_mastery_profiles=pd.DataFrame(initial); mastery_profiles=pd.DataFrame(mastery)
186
+
187
+ # Engagement logs + agg
188
+ eng=[]; eng_agg=defaultdict(lambda:{'active':0,'logins':0,'video':0,'content':0,'quiz':0,'days':0}); eid=1
189
+ for sp in students:
190
+ base_login=sp['average_login_per_week']/7
191
+ for day in range(200):
192
+ dt=datetime(2026,6,1)+timedelta(days=day); weekday=.45 if dt.weekday()>=5 else 1.0
193
+ p=clip(base_login*weekday+(.12 if sp['learner_archetype'] in ['high_performer','fast_improver'] else 0)-(.08 if sp['learner_archetype']=='at_risk' else 0),.02,.98)
194
+ logged=random.random()<p; login=int(np.random.poisson(1.2))+1 if logged else 0; active=int(clip(np.random.normal(35+sp['assignment_completion_rate']*.28,18),5,180)) if logged else 0
195
+ content_done=int(np.random.poisson(max(.1,active/45))) if logged else 0; quiz=int(np.random.binomial(2,.18+.002*sp['assignment_completion_rate'])) if logged else 0; rec_click=int(np.random.binomial(2,.18+.003*sp['assignment_completion_rate'])) if logged else 0; video=round(clip(np.random.normal(.58+sp['assignment_completion_rate']/300,.18),0,1),2) if logged else 0.0
196
+ eng.append({'engagement_id':f'ENG{eid:07d}','student_id':sp['student_id'],'school_id':sp['school_id'],'class_id':sp['class_id'],'activity_date':dstr(dt),'login_count':login,'active_minutes':active,'content_completed_count':content_done,'quiz_attempt_count':quiz,'recommendation_click_count':rec_click,'video_watch_ratio':video,'discussion_posts':int(np.random.binomial(2,.05)) if logged else 0,'device_type':random.choice(['mobile','mobile','tablet','desktop']) if logged else 'none','train_split':split_id(f'ENG{eid:07d}')})
197
+ ea=eng_agg[sp['student_id']]; ea['active']+=active; ea['logins']+=login; ea['video']+=video; ea['content']+=content_done; ea['quiz']+=quiz; ea['days']+=1; eid+=1
198
+ mark('engagement')
199
+ engagement_logs=pd.DataFrame(eng)
200
+
201
+ # Subjective answer scoring sample
202
+ subjective=[]
203
+ sub_sample=random.sample(non_mcq_attempts, min(12000,len(non_mcq_attempts)))
204
+ for i,att in enumerate(sub_sample,1):
205
+ q=q_map[att['question_id']]; quality=clip(att['marks_ratio']+np.random.normal(0,.1),0,1)
206
+ ans=f"The answer {'clearly explains' if quality>.78 else ('partially explains' if quality>.45 else 'shows limited understanding of')} {lo_map[att['lo_id']]['title']} in {q['chapter']}."
207
+ teacher_marks=round(att['marks_obtained'],2); ai_marks=round(clip(teacher_marks+np.random.normal(0,.25+.05*q['difficulty_score']),0,q['max_marks']),2); err=round(abs(ai_marks-teacher_marks),2)
208
+ subjective.append({'answer_id':f'ANS{i:06d}','attempt_id':att['attempt_id'],'student_id':att['student_id'],'question_id':att['question_id'],'lo_id':att['lo_id'],'grade':att['grade'],'subject':att['subject'],'question_type':att['question_type'],'student_answer':ans,'model_answer':q['correct_answer'],'rubric':q['rubric'],'max_marks':q['max_marks'],'teacher_marks':teacher_marks,'ai_predicted_marks':ai_marks,'absolute_error':err,'rubric_match_score':round(clip(quality+np.random.normal(0,.08),0,1),3),'concept_coverage_score':round(clip(quality+np.random.normal(0,.08),0,1),3),'feedback_text':'Good concept coverage.' if quality>.7 else ('Revise the key concept and add a clear example.' if quality>.4 else 'Needs remedial support on the prerequisite concept.'),'teacher_review_required':1 if err>.7 or quality<.35 else 0,'train_split':split_id(f'ANS{i:06d}')})
209
+ mark('subjective')
210
+ subjective_answers=pd.DataFrame(subjective)
211
+
212
+ # Features + Risk
213
+ risk=[]; feat_sub=[]; rid=1
214
+ for sp in students:
215
+ ea=eng_agg[sp['student_id']]
216
+ for subj in subjects_list:
217
+ rows=mastery_by_ss[(sp['student_id'],subj)]
218
+ avg_m=sum(r['mastery_score'] for r in rows)/len(rows); weak=sum(1 for r in rows if r['status']=='weak'); dev=sum(1 for r in rows if r['status']=='developing'); mastered=sum(1 for r in rows if r['status']=='mastered'); conf=sum(r['confidence'] for r in rows)/len(rows)
219
+ # Use aggregate dictionaries from generated attempts; no full attempts scan needed.
220
+ lo_attempts=[agg.get((sp['student_id'],r['lo_id'])) for r in rows if agg.get((sp['student_id'],r['lo_id']))]
221
+ total=sum(a['cnt'] for a in lo_attempts) or 1; acc=sum(a['correct'] for a in lo_attempts)/total; mr=sum(a['marks'] for a in lo_attempts)/total; tm=sum(a['time'] for a in lo_attempts)/total; hr=sum(a['hint'] for a in lo_attempts)/total
222
+ score=clip(.32*(1-avg_m)+.14*(weak/25)+.14*(1-acc)+.10*(1-sp['attendance_percentage']/100)+.10*(1-sp['assignment_completion_rate']/100)+.08*(sp['inactive_days_last_14']/14)+.06*hr+.06*(1-min(sp['average_login_per_week'],7)/7)+np.random.normal(0,.025),0,1)
223
+ lvl='critical' if score>=.75 else ('high' if score>=.58 else ('medium' if score>=.38 else 'low'))
224
+ reasons=[]
225
+ if avg_m<.45: reasons.append('low_mastery')
226
+ if weak>=8: reasons.append('multiple_weak_learning_outcomes')
227
+ if sp['attendance_percentage']<75: reasons.append('low_attendance')
228
+ if sp['assignment_completion_rate']<60: reasons.append('low_assignment_completion')
229
+ if sp['inactive_days_last_14']>=5: reasons.append('recent_inactivity')
230
+ if acc<.45: reasons.append('low_assessment_accuracy')
231
+ if not reasons: reasons=['stable_learning_pattern']
232
+ rrow={'risk_prediction_id':f'RISK{rid:06d}','student_id':sp['student_id'],'school_id':sp['school_id'],'class_id':sp['class_id'],'grade':sp['grade'],'section':sp['section'],'subject':subj,'risk_score':round(score,3),'risk_level':lvl,'risk_label':1 if lvl in ['high','critical'] else 0,'primary_reasons':'|'.join(reasons[:4]),'recommended_intervention':'urgent_teacher_intervention' if lvl=='critical' else ('small_group_remediation' if lvl=='high' else ('targeted_practice' if lvl=='medium' else 'continue_current_path')),'model_version':'synthetic-risk-label-v2.0','generated_at':'2026-09-30T08:00:00','confidence':round(.62+abs(score-.5)*.55,3),'train_split':sp['train_split']}
233
+ risk.append(rrow)
234
+ feat_sub.append({'feature_row_id':f'FSUB{rid:06d}','student_id':sp['student_id'],'school_id':sp['school_id'],'class_id':sp['class_id'],'grade':sp['grade'],'section':sp['section'],'subject':subj,'avg_mastery_score':round(avg_m,3),'weak_lo_count':weak,'developing_lo_count':dev,'mastered_lo_count':mastered,'avg_confidence':round(conf,3),'avg_accuracy':round(acc,3),'avg_marks_ratio':round(mr,3),'avg_time_seconds':round(tm,1),'hint_usage_rate':round(hr,3),'total_attempts':total,'attendance_percentage':sp['attendance_percentage'],'assignment_completion_rate':sp['assignment_completion_rate'],'average_login_per_week':sp['average_login_per_week'],'inactive_days_last_14':sp['inactive_days_last_14'],'avg_active_minutes':round(ea['active']/ea['days'],1),'total_logins':ea['logins'],'avg_video_watch_ratio':round(ea['video']/ea['days'],3),'total_content_completed':ea['content'],'total_quiz_attempts':ea['quiz'],'risk_score':round(score,3),'risk_level':lvl,'risk_label':1 if lvl in ['high','critical'] else 0,'train_split':split_id(f'FSUB{rid:06d}')})
235
+ rid+=1
236
+ mark('risk')
237
+ risk_profiles=pd.DataFrame(risk); ml_features_student_subject=pd.DataFrame(feat_sub)
238
+
239
+ # ML LO features
240
+ ml_features_student_lo=mastery_profiles.merge(student_profiles[['student_id','school_id','class_id','attendance_percentage','assignment_completion_rate','average_login_per_week','inactive_days_last_14']],on='student_id',how='left')
241
+ ml_features_student_lo['mastery_label']=ml_features_student_lo['status'].map({'weak':0,'developing':1,'proficient':2,'mastered':3})
242
+ ml_features_student_lo.insert(0,'feature_row_id',[f'FLO{i:07d}' for i in range(1,len(ml_features_student_lo)+1)])
243
+
244
+ # Recommendations
245
+ recommendations=[]; recid=1
246
+ for sp in students:
247
+ for subj in subjects_list:
248
+ rows=sorted(mastery_by_ss[(sp['student_id'],subj)], key=lambda x:(x['mastery_score'],x['attempt_count']))[:5]
249
+ for m in rows:
250
+ pool=content_by_lo[m['lo_id']]
251
+ if m['status']=='weak': cands=[c for c in pool if c['target_use']=='remediation']; rtype='remedial_content'; pr='high'
252
+ elif m['status']=='developing': cands=[c for c in pool if c['target_use']=='practice']; rtype='practice_content'; pr='medium'
253
+ else: cands=[c for c in pool if c['target_use']=='enrichment']; rtype='enrichment_content'; pr='low'
254
+ c=random.choice(cands or pool); cp=.72 if sp['learner_archetype'] in ['high_performer','fast_improver'] else (.45 if sp['learner_archetype'] in ['low_engagement','at_risk'] else .6); clicked=1 if random.random()<cp+.15 else 0; done=1 if clicked and random.random()<cp else 0
255
+ recommendations.append({'recommendation_id':f'REC{recid:07d}','student_id':sp['student_id'],'school_id':sp['school_id'],'class_id':sp['class_id'],'lo_id':m['lo_id'],'content_id':c['content_id'],'grade':sp['grade'],'section':sp['section'],'subject':subj,'recommendation_type':rtype,'priority':pr,'reason':f"{m['status']} mastery in {m['lo_id']} with score {m['mastery_score']}",'ai_confidence':round(clip(.55+(1-float(m['mastery_score']))*.35+random.uniform(-.04,.04),.5,.95),3),'generated_at':'2026-09-30T09:00:00','shown_to_student':1,'clicked':clicked,'is_completed':done,'observed_mastery_gain':round(random.uniform(.01,.12) if done else random.uniform(0,.03),3),'model_version':'hybrid-recommender-synthetic-label-v2.0','train_split':split_id(f'REC{recid:07d}')}); recid+=1
256
+ mark('recommendations')
257
+ recommendations=pd.DataFrame(recommendations)
258
+
259
+ # Teacher interventions
260
+ interventions=[]; iid=1
261
+ for c in classes.to_dict('records'):
262
+ class_sids=set(students_by_class[c['class_id']])
263
+ for subj in subjects_list:
264
+ d=defaultdict(lambda:{'scores':[],'weak':0})
265
+ for sid in class_sids:
266
+ for m in mastery_by_ss[(sid,subj)]:
267
+ d[m['lo_id']]['scores'].append(m['mastery_score']); d[m['lo_id']]['weak']+=1 if m['status']=='weak' else 0
268
+ weakest=sorted([(lo_id, sum(v['scores'])/len(v['scores']), v['weak']) for lo_id,v in d.items()], key=lambda x:(-x[2],x[1]))[:3]
269
+ for lo_id,avg,weak in weakest:
270
+ if weak==0: continue
271
+ level='whole_class_reteach' if weak>=14 else ('small_group_remediation' if weak>=6 else 'individual_support')
272
+ interventions.append({'intervention_id':f'INT{iid:05d}','school_id':c['school_id'],'class_id':c['class_id'],'teacher_id':teacher_lookup.get((c['class_id'],subj),'TCH0000'),'grade':c['grade'],'section':c['section'],'subject':subj,'lo_id':lo_id,'intervention_type':level,'affected_students':weak,'avg_mastery_before':round(avg,3),'suggested_action':f'Use remedial activity and 10-question practice quiz for {lo_id}.','scheduled_week':'2026-W40','status':random.choice(['planned','in_progress','completed']),'expected_mastery_gain':round(random.uniform(.06,.18),3),'generated_by_ai':1,'train_split':split_id(f'INT{iid:05d}')}); iid+=1
273
+ mark('interventions')
274
+ teacher_interventions=pd.DataFrame(interventions)
275
+
276
+ # Feedback, digital twins, logs
277
+ teacher_feedback=[]; fbid=1
278
+ review=subjective_answers[subjective_answers.teacher_review_required==1].to_dict('records')+subjective_answers.sample(n=min(2500,len(subjective_answers)), random_state=SEED).to_dict('records')
279
+ seen=set()
280
+ for ans in review:
281
+ if ans['answer_id'] in seen or len(teacher_feedback)>=5000: continue
282
+ seen.add(ans['answer_id']); sp=student_meta[ans['student_id']]; tid=teacher_lookup.get((sp['class_id'],ans['subject']),'TCH0000')
283
+ teacher_feedback.append({'feedback_id':f'FB{fbid:06d}','teacher_id':tid,'student_id':ans['student_id'],'related_entity_type':'subjective_answer','related_entity_id':ans['answer_id'],'feedback_type':'score_review','teacher_rating':random.choice([3,4,4,5]) if ans['absolute_error']<=.7 else random.choice([1,2,3]),'correction_required':1 if ans['absolute_error']>.7 else 0,'correction_text':'AI score acceptable.' if ans['absolute_error']<=.7 else 'Teacher adjusted marks due to rubric nuance.','created_at':'2026-09-30T12:00:00','train_split':split_id(f'FB{fbid:06d}')}); fbid+=1
284
+ for r in risk_profiles.sample(n=1000, random_state=SEED).to_dict('records'):
285
+ sp=student_meta[r['student_id']]; teacher_feedback.append({'feedback_id':f'FB{fbid:06d}','teacher_id':teacher_lookup.get((sp['class_id'],r['subject']),'TCH0000'),'student_id':r['student_id'],'related_entity_type':'risk_prediction','related_entity_id':r['risk_prediction_id'],'feedback_type':'risk_review','teacher_rating':random.choice([3,4,4,5]),'correction_required':random.choice([0,0,0,1]),'correction_text':'Risk alert reviewed by teacher.','created_at':'2026-09-30T12:00:00','train_split':split_id(f'FB{fbid:06d}')}); fbid+=1
286
+ mark('feedback')
287
+ teacher_feedback=pd.DataFrame(teacher_feedback)
288
+
289
+ student_digital_twins=[]
290
+ risk_by_student=defaultdict(list)
291
+ for r in risk_profiles.to_dict('records'): risk_by_student[r['student_id']].append(r)
292
+ for i,sp in enumerate(students,1):
293
+ rows=[m for subj in subjects_list for m in mastery_by_ss[(sp['student_id'],subj)]]; bysubj={subj:np.mean([m['mastery_score'] for m in mastery_by_ss[(sp['student_id'],subj)]]) for subj in subjects_list}; weak=sorted(rows,key=lambda x:x['mastery_score'])[:5]; top=max(risk_by_student[sp['student_id']], key=lambda x:x['risk_score'])
294
+ pref='video' if sp['learning_style']=='visual' else ('audio_explanation' if sp['learning_style']=='auditory' else ('notes' if sp['learning_style']=='reading_writing' else 'interactive_activity'))
295
+ student_digital_twins.append({'digital_twin_id':f'DT{i:05d}','student_id':sp['student_id'],'school_id':sp['school_id'],'class_id':sp['class_id'],'grade':sp['grade'],'section':sp['section'],'overall_mastery_score':round(np.mean([m['mastery_score'] for m in rows]),3),'strongest_subject':max(bysubj,key=bysubj.get),'weakest_subject':min(bysubj,key=bysubj.get),'top_weak_lo_ids':'|'.join([m['lo_id'] for m in weak]),'learning_speed_score':round(clip((sp['assignment_completion_rate']/100)*.45+(sp['average_login_per_week']/7)*.35+(np.mean([m['attempt_count'] for m in rows])/4)*.20,0,1),3),'consistency_score':round(clip(sp['attendance_percentage']/100*.55+sp['assignment_completion_rate']/100*.45,0,1),3),'preferred_content_type':pref,'current_risk_level':top['risk_level'],'current_risk_score':top['risk_score'],'recommended_next_action':'teacher_intervention' if top['risk_level'] in ['high','critical'] else ('targeted_practice' if min(bysubj.values())<.65 else 'advanced_enrichment'),'last_updated':'2026-09-30','train_split':split_id(f'DT{i:05d}')})
296
+ mark('digital')
297
+ student_digital_twins=pd.DataFrame(student_digital_twins)
298
+
299
+ pred=[]; pid=1
300
+ for r in risk_profiles.to_dict('records'):
301
+ pred.append({'prediction_log_id':f'PRED{pid:07d}','model_name':'risk_prediction','model_version':r['model_version'],'entity_type':'student_subject','entity_id':f"{r['student_id']}:{r['subject']}",'prediction_output':json.dumps({'risk_level':r['risk_level'],'risk_score':r['risk_score']}),'confidence':r['confidence'],'latency_ms':random.randint(18,75),'created_at':r['generated_at'],'train_split':r['train_split']}); pid+=1
302
+ for r in recommendations.sample(n=min(6000,len(recommendations)), random_state=SEED).to_dict('records'):
303
+ pred.append({'prediction_log_id':f'PRED{pid:07d}','model_name':'recommendation_engine','model_version':r['model_version'],'entity_type':'recommendation','entity_id':r['recommendation_id'],'prediction_output':json.dumps({'content_id':r['content_id'],'priority':r['priority'],'type':r['recommendation_type']}),'confidence':r['ai_confidence'],'latency_ms':random.randint(25,110),'created_at':r['generated_at'],'train_split':r['train_split']}); pid+=1
304
+ for r in subjective_answers.sample(n=min(6000,len(subjective_answers)), random_state=SEED+5).to_dict('records'):
305
+ pred.append({'prediction_log_id':f'PRED{pid:07d}','model_name':'subjective_answer_scoring','model_version':'rubric-semantic-v2.0','entity_type':'subjective_answer','entity_id':r['answer_id'],'prediction_output':json.dumps({'ai_marks':r['ai_predicted_marks'],'review_required':r['teacher_review_required']}),'confidence':round(1/(1+r['absolute_error']),3),'latency_ms':random.randint(90,420),'created_at':'2026-09-30T10:00:00','train_split':r['train_split']}); pid+=1
306
+ for r in questions.sample(n=min(3000,len(questions)), random_state=SEED+2).to_dict('records'):
307
+ pred.append({'prediction_log_id':f'PRED{pid:07d}','model_name':'lo_tagging','model_version':'embedding-classifier-v2.0','entity_type':'question','entity_id':r['question_id'],'prediction_output':json.dumps({'lo_id':r['lo_id'],'bloom_level':r['bloom_level'],'difficulty':r['difficulty']}),'confidence':r['alignment_confidence'],'latency_ms':random.randint(12,65),'created_at':'2026-09-30T10:00:00','train_split':r['train_split']}); pid+=1
308
+ mark('pred')
309
+ ai_prediction_logs=pd.DataFrame(pred)
310
+
311
+ # Training tables
312
+ training_lo_tagging=questions[['question_id','question_text','embedding_text','lo_id','grade','subject','chapter','difficulty','difficulty_score','bloom_level','bloom_score','train_split']]
313
+ training_bloom_classification=questions[['question_id','question_text','embedding_text','bloom_level','bloom_score','grade','subject','question_type','train_split']]
314
+ training_risk_prediction=ml_features_student_subject.copy()
315
+ training_mastery_prediction=ml_features_student_lo.copy()
316
+ training_answer_scoring=subjective_answers[['answer_id','question_id','student_id','lo_id','grade','subject','question_type','student_answer','model_answer','rubric','max_marks','teacher_marks','ai_predicted_marks','rubric_match_score','concept_coverage_score','teacher_review_required','train_split']]
317
+ training_recommendation_outcomes=recommendations[['recommendation_id','student_id','lo_id','content_id','grade','subject','recommendation_type','priority','ai_confidence','clicked','is_completed','observed_mastery_gain','train_split']]
318
+
319
+ tables={'schools.csv':schools,'classes.csv':classes,'subjects.csv':subjects,'chapters.csv':chapters,'teachers.csv':teachers,'student_profiles.csv':student_profiles,'learning_outcomes.csv':learning_outcomes,'lo_dependencies.csv':lo_dependencies,'questions.csv':questions,'question_options.csv':question_options,'content_catalog.csv':content_catalog,'assessments.csv':assessments,'assessment_questions.csv':assessment_questions,'student_attempts.csv':student_attempts,'initial_mastery_profiles.csv':initial_mastery_profiles,'mastery_profiles.csv':mastery_profiles,'engagement_logs.csv':engagement_logs,'risk_profiles.csv':risk_profiles,'recommendations.csv':recommendations,'teacher_interventions.csv':teacher_interventions,'subjective_answers.csv':subjective_answers,'teacher_feedback.csv':teacher_feedback,'student_digital_twins.csv':student_digital_twins,'ai_prediction_logs.csv':ai_prediction_logs,'ml_features_student_subject.csv':ml_features_student_subject,'ml_features_student_lo.csv':ml_features_student_lo,'training_lo_tagging.csv':training_lo_tagging,'training_bloom_classification.csv':training_bloom_classification,'training_risk_prediction.csv':training_risk_prediction,'training_mastery_prediction.csv':training_mastery_prediction,'training_answer_scoring.csv':training_answer_scoring,'training_recommendation_outcomes.csv':training_recommendation_outcomes}
320
+
321
+ # Save + validate
322
+ issues=[]
323
+ for name,df in list(tables.items()):
324
+ df=df.copy()
325
+ for col in df.columns:
326
+ if pd.api.types.is_numeric_dtype(df[col]): df[col]=df[col].fillna(0)
327
+ else: df[col]=df[col].fillna('unknown').astype(str).map(clean)
328
+ df.to_csv(OUT/name,index=False); tables[name]=df
329
+ if df.isnull().any().any(): issues.append(f'Nulls in {name}')
330
+
331
+ def fk(child,col,parent,pcol):
332
+ missing=set(tables[child][col])-set(tables[parent][pcol])
333
+ if missing: issues.append(f'{child}.{col} missing {len(missing)} refs to {parent}.{pcol}')
334
+ for child,col,parent,pcol in [('classes.csv','school_id','schools.csv','school_id'),('teachers.csv','class_id','classes.csv','class_id'),('student_profiles.csv','class_id','classes.csv','class_id'),('questions.csv','lo_id','learning_outcomes.csv','lo_id'),('content_catalog.csv','lo_id','learning_outcomes.csv','lo_id'),('assessments.csv','class_id','classes.csv','class_id'),('assessment_questions.csv','assessment_id','assessments.csv','assessment_id'),('assessment_questions.csv','question_id','questions.csv','question_id'),('student_attempts.csv','student_id','student_profiles.csv','student_id'),('student_attempts.csv','question_id','questions.csv','question_id'),('mastery_profiles.csv','student_id','student_profiles.csv','student_id'),('mastery_profiles.csv','lo_id','learning_outcomes.csv','lo_id'),('recommendations.csv','content_id','content_catalog.csv','content_id')]: fk(child,col,parent,pcol)
335
+ metadata={'dataset_name':'Learning Outcome OS AI-Ready Expanded Dataset','version':'2.0.0','generated_at':datetime.utcnow().strftime('%Y-%m-%dT%H:%M:%SZ'),'seed':SEED,'scope':{'schools':len(schools),'classes':len(classes),'grades':[6,7,8],'sections':['A','B'],'subjects':subjects_list,'students_per_class_section':30,'total_students':len(student_profiles),'source_alignment':['mathematics_LO.pdf','science_LO.pdf','social_science_LO.pdf'],'source_base_dataset':'cbse_lo_aligned_synthetic_dataset_classes_6_8.zip'},'model_ready_design':{'no_null_values':len([i for i in issues if 'Nulls' in i])==0,'stable_primary_keys':True,'foreign_keys_valid':len([i for i in issues if 'refs' in i])==0,'train_validation_test_splits':True,'numeric_features_available':True,'nlp_embedding_text_fields_available':True,'human_feedback_tables_available':True,'target_labels_available':['lo_id','bloom_level','difficulty_score','mastery_score','mastery_label','risk_label','teacher_marks','clicked','is_completed']},'table_counts':{name:len(df) for name,df in tables.items()},'status_labels':['weak','developing','proficient','mastered'],'risk_labels':['low','medium','high','critical']}
336
+ (OUT/'dataset_metadata.json').write_text(json.dumps(metadata,indent=2),encoding='utf-8')
337
+ (OUT/'validation_report.json').write_text(json.dumps({'valid':len(issues)==0,'total_issues':len(issues),'issues':issues,'checks':['null_value_absence','foreign_key_integrity','target_label_presence','split_presence'],'table_counts':metadata['table_counts']},indent=2),encoding='utf-8')
338
+ readme=['# Learning Outcome OS AI-Ready Expanded Dataset v2','','Expanded synthetic, CBSE/NCERT LO-aligned, model-ready dataset for LO tagging, Bloom classification, mastery prediction, recommendation, risk prediction, subjective answer scoring, teacher feedback, and digital twin modelling.','','All names are synthetic. No real PII is included. CSVs are pre-cleaned with stable IDs, target labels, and train/validation/test split columns.','','## Tables']
339
+ for name,df in tables.items(): readme += [f'### {name}',f'- Rows: {len(df):,}',f'- Columns: {", ".join(df.columns)}','']
340
+ (OUT/'README.md').write_text('\n'.join(readme),encoding='utf-8')
341
+ schema=['-- Flexible PostgreSQL import schema for Learning Outcome OS AI-ready dataset v2','']
342
+ for name,df in tables.items():
343
+ table=name.replace('.csv',''); schema.append(f'DROP TABLE IF EXISTS {table};')
344
+ cols=[]
345
+ for col in df.columns:
346
+ typ='DOUBLE PRECISION' if pd.api.types.is_float_dtype(df[col]) else ('INTEGER' if pd.api.types.is_integer_dtype(df[col]) else 'TEXT')
347
+ cols.append(f' {col} {typ}')
348
+ schema.append(f'CREATE TABLE {table} (\n'+',\n'.join(cols)+'\n);')
349
+ schema.append(f"-- COPY {table} FROM '/path/{name}' WITH CSV HEADER;\n")
350
+ (OUT/'postgres_schema.sql').write_text('\n'.join(schema),encoding='utf-8')
351
+ mark('zip')
352
+ with zipfile.ZipFile(ZIP,'w',compression=zipfile.ZIP_DEFLATED,compresslevel=6) as z:
353
+ for p in sorted(OUT.iterdir()): z.write(p,arcname=p.name)
354
+ print(json.dumps({'zip':str(ZIP),'valid':len(issues)==0,'issues':issues,'counts':metadata['table_counts']},indent=2))