Nafs Results Analytics — Aseer Region¶

Adel Asiri · CDMP · PMP

Official Nafs results for Aseer schools, 1444–1447 AH.

Step Output
1. Load & profile school-level report 1444–1446 + consolidated 1447 results
2. Data quality (DQ-01…DQ-15) rule scorecard + reconciliation to official averages
3. Model conformed rows; 1447 tested counts derived from the proficiency shares
4. Back-test exact-count recovery on 1445 and 1446
5. Analysis trends, gaps, segments, statistics
6. Export dashboard.json, charts, DQ report

Privacy: this notebook prints aggregates only. School names and ministerial numbers stay in data/private/.

In [1]:
import pandas as pd, numpy as np
import nafs_pipeline as P
pd.set_option('display.width', 160)
hist, cur, govs, exports = P.load()
print(f'1444–1446 report : {len(hist):,} rows × {hist.shape[1]} columns')
print(f'1447 results     : {len(cur):,} rows (consolidated file); earlier exports: {len(exports):,} rows')
1444–1446 report : 77,076 rows × 21 columns
1447 results     : 30,649 rows (consolidated file); earlier exports: 14,671 rows

1 · Profiling¶

Identifier and name columns are profiled but never printed.

In [2]:
SAFE = ['year','grade','domain','gender','authority','enrolled','tested','proficient','prof_rate','score_500','ref_dept','ref_kingdom']
cols = [c for c in SAFE if c in hist.columns]
pd.DataFrame({'dtype': hist[cols].dtypes.astype(str), 'nulls': hist[cols].isna().sum(), 'distinct': hist[cols].nunique()})
Out[2]:
dtype nulls distinct
year int64 0 3
grade int64 0 3
domain str 0 3
gender str 0 2
authority str 0 3
enrolled int64 0 220
tested int64 0 204
proficient int64 0 149
prof_rate float64 0 889
score_500 float64 0 70874
ref_dept float64 0 140
ref_kingdom float64 0 150
In [3]:
hist[hist['domain'] == hist['subdomain']].groupby(['year','grade','domain']).agg(rows=('tested','size'), tested=('tested','sum'), proficient=('proficient','sum')).unstack('domain').fillna(0).astype(int)
Out[3]:
rows tested proficient
domain الرياضيات العلوم القراءة الرياضيات العلوم القراءة الرياضيات العلوم القراءة
year grade
1444 3 389 0 389 12565 0 12565 3731 0 4073
6 1137 1137 1137 34186 34186 34186 9536 13036 11512
9 751 751 751 33053 33053 33053 5207 9638 14750
1445 3 319 0 319 12313 0 12313 2945 0 4172
6 1176 1176 1176 35504 35504 35504 9866 12772 16408
9 776 776 776 34077 34077 34077 5830 8902 18091
1446 3 751 0 751 32218 0 32218 7588 0 8499
6 1255 1255 1255 39120 39120 39120 14937 22428 17485
9 824 824 824 38748 38748 38748 9335 16647 14883

2 · Data-quality rules¶

Each rule maps to a DAMA dimension and has a status: pass, fixed (remediated in the pipeline) or open (raised with the data owner).

In [4]:
hist, cur, gmap, dq, recon, summary, rec_n = P.data_quality(hist, cur, govs, exports)
print(summary)
dq[['id','dim_en','rule_en','checked','failed','pass_rate','status','action_en']]
{'rows': 107725, 'rows_hist': 77076, 'rows_cur': 30649, 'n_rules': 15, 'passed': 8, 'fixed': 5, 'open': 2, 'recon_checked': 48, 'recon_matched': 48, 'schools': 2308, 'unmapped': 145, 'rec_school_grades': 2962, 'rec_students': 109586, 'rec_bad': 0}
Out[4]:
id dim_en rule_en checked failed pass_rate status action_en
0 DQ-01 Completeness Ministerial school number present 107725 0 100.00 pass Quarantine & return to source
1 DQ-02 Uniqueness One row per year/school/grade/sub-domain 107725 0 100.00 pass Remove duplicates after review
2 DQ-03 Validity Tested ≤ enrolled and proficient ≤ tested 77076 0 100.00 pass Verify against source file
3 DQ-04 Accuracy Published rate equals proficient ÷ tested 77076 0 100.00 pass Recompute from counts
4 DQ-05 Consistency Four performance levels sum to 100% 77076 0 100.00 pass Review level distribution
5 DQ-06 Reconciliation Computed proficiency matches the official depa... 48 0 100.00 pass Investigate with data owner
6 DQ-07 Conformity Proficiency on a common 0–100 scale 30649 30649 0.00 fixed Convert 1447 export from 0–1 fraction to %
7 DQ-08 Comparability Average-score scale stable across years 4 1 75.00 fixed Do not compare scores across years (≈500 in 14...
8 DQ-09 Completeness Education office present on every row 77076 30205 60.81 open Use governorate reference data; raise with dat...
9 DQ-10 Consistency Office label matches reference list 46871 1328 97.17 fixed Standardise 7 short labels (e.g. 'Bariq' → 'Ba...
10 DQ-11 Master data One name per ministerial number across years 2308 313 86.44 fixed Adopt latest-year name as golden record
11 DQ-12 Reference data Governorate known for every school 2308 145 93.72 open Join on ministerial number; unmatched shown as...
12 DQ-13 Completeness Tested / proficient counts available 30649 30649 0.00 fixed Tested count rebuilt per school-grade from the...
13 DQ-14 Reconciliation Consolidated 1447 file matches the earlier exp... 14671 0 100.00 pass Adopt the consolidated file as the primary source
14 DQ-15 Consistency One tested count per school-grade across all s... 7378 0 100.00 pass Basis for the 1447 count reconstruction

Reconciliation¶

Proficiency computed from student counts vs the official department average, per year × grade × subject × gender.

In [5]:
r = recon.copy()
print(f"matched {int((r['diff'].abs() <= 0.05).sum())} / {len(r)} · max |diff| = {r['diff'].abs().max():.2f}")
r.pivot_table(index=['year','grade','subject'], columns='gender', values=['calc','official']).round(1)
matched 48 / 48 · max |diff| = 0.05
Out[5]:
calc official
gender B G B G
year grade subject
1444 3 MATH 24.4 30.9 24.4 30.9
READ 19.6 35.4 19.5 35.4
6 MATH 27.1 28.7 27.1 28.7
READ 26.3 41.1 26.3 41.1
SCI 35.1 41.2 35.1 41.2
9 MATH 16.5 15.0 16.5 15.0
READ 36.2 53.3 36.2 53.3
SCI 26.8 31.6 26.8 31.6
1445 3 MATH 24.5 23.7 24.5 23.7
READ 26.6 36.2 26.6 36.2
6 MATH 28.0 27.6 28.0 27.6
READ 39.5 53.0 39.5 53.0
SCI 33.6 38.4 33.6 38.4
9 MATH 17.9 16.3 17.9 16.3
READ 44.4 61.9 44.4 61.9
SCI 24.2 28.1 24.2 28.1
1446 3 MATH 50.7 23.5 50.7 23.5
READ 41.3 26.3 41.3 26.3
6 MATH 38.1 38.3 38.1 38.3
READ 39.4 50.1 39.4 50.1
SCI 53.2 61.6 53.2 61.6
9 MATH 23.7 24.5 23.7 24.5
READ 31.7 45.5 31.7 45.5
SCI 39.2 47.0 39.1 47.0

3 · Deriving the 1447 tested counts¶

Each published proficiency is an exact fraction proficient ÷ tested. In 1444–1446 every result of a school-grade (all subjects and sub-domains) shares one tested count (DQ-15), so the count is the least common multiple of the reduced denominators of its 7–12 shares.

In [6]:
print(rec_n['n'].describe().round(1))
print('school-grades without an informative share:', int((rec_n['informative'] == 0).sum()))
rec_n.groupby(level='grade')['n'].agg(['count', 'sum', 'median'])
count    2962.0
mean       37.0
std        34.2
min         1.0
25%        14.0
50%        26.0
75%        48.0
max       283.0
Name: n, dtype: float64
school-grades without an informative share: 1
Out[6]:
count sum median
grade
3 979 39733 30.0
6 1168 32025 19.0
9 815 37828 33.0
In [7]:
main, subs, UNM = P.build_model(hist, cur, gmap, govs, rec_n)
main.groupby('year').agg(rows=('tested','size'), results=('tested','sum'))
Out[7]:
rows results
year
1444 6442 226847
1445 6494 233369
1446 7739 298040
1447 7907 289025

4 · Back-test of the reconstruction¶

Apply the same method to 1445 and 1446, where the true counts are known.

In [8]:
back = P.backtest(hist)
back
Out[8]:
{'exact_1445': 99.91,
 'exact_1446': 99.79,
 'exact': 99.8,
 'max_err': 0.012,
 'region_err': 0.006,
 'students_1446': [110086, 110042]}

5 · Analysis¶

In [9]:
res = P.analyse(main, subs, govs, UNM)
pd.DataFrame(res['trend'])
Out[9]:
year region national participation estimated derived schools students results
0 1444 31.51 34.59 88.55 False False 1989 79804 226847
1 1445 33.85 37.51 91.07 False False 2012 81894 233369
2 1446 37.51 40.57 94.07 False False 2208 110086 298040
3 1447 32.54 NaN NaN False True 2163 109586 289025
In [10]:
res['grid']
Out[10]:
subject MATH READ SCI
year grade
1444 3 29.69 32.42 NaN
6 27.89 33.67 38.13
9 15.75 44.63 29.16
1445 3 23.92 33.88 NaN
6 27.79 46.21 35.97
9 17.11 53.09 26.12
1446 3 23.55 26.38 NaN
6 38.18 44.70 57.33
9 24.09 38.41 42.96
1447 3 25.35 20.87 NaN
6 46.43 36.04 56.10
9 17.52 23.56 41.67
In [11]:
for i in res['insights']:
    print('•', i['en'])
• Aseer proficiency rose from 31.5% (1444 AH) to 37.5% (1446 AH), +6.0 pts — reconciled to the official department averages.
• 1447 AH proficiency is 32.54% (-4.97 pts vs 1446). The largest drop is Grade 9 Reading (-14.9 pts) and the largest gain Grade 6 Mathematics (+8.2).
• Of the -4.97 pt change, -3.57 is real performance change within grade and subject and -1.40 is the shift in who was tested (grade 3 testers -3.8%, grade 6 -16.6% in the same schools). At the 1446 mix, 1447 would be 33.94%.
• The gap to the national average is -3.1 pts in 1446 AH vs -3.1 in 1444 AH: the region improved at the national pace without closing the gap.
• In 1447 AH the highest proficiency is Grade 6 Science (56.1%) and the lowest Grade 9 Mathematics (17.5%); the weakest sub-domain is Reading comprehension in Grade 3 Reading (21.0%).
• In 1447 AH the top governorate is Al-Namas (43.6%) and the lowest Al-Birk (22.2%); most improved since 1446 is Tarib (+5.5).
• Boys and girls converge at student level in 1447 AH (32.0% vs 32.9%) after girls led in 1444; at school level the difference is +0.3 pts (not statistically significant).
• 224 schools flagged for priority intervention: ≥5 pts below the regional average and declining ≥5 pts more than the region (of 1,831 classified).
In [12]:
pd.DataFrame({k: v for k, v in res['statistics'].items() if isinstance(v, dict) and k not in ('gender_students', 'decomp')}).T
Out[12]:
girls boys diff t p n_girls n_boys private public n_private n_public rho from_year to_year value base since_first
gender 33.69 33.4 0.29 0.38 0.704674 1044.0 787.0 NaN NaN NaN NaN NaN NaN NaN NaN NaN NaN
authority NaN NaN 4.11 2.57 0.011569 NaN NaN 37.48 33.36 88.0 1743.0 NaN NaN NaN NaN NaN NaN
size_corr NaN NaN NaN NaN 0.070564 NaN NaN NaN NaN NaN NaN -0.042 NaN NaN NaN NaN NaN
change NaN NaN NaN NaN NaN NaN NaN NaN NaN NaN NaN NaN 1446.0 1447.0 -4.97 37.51 6.0

Decomposing the 1446 → 1447 change¶

Rate effect = change within grade × subject at the 1446 mix; mix effect = shift in the share of results by grade × subject.

In [13]:
res['statistics']['decomp']
Out[13]:
{'rate_effect': -3.57,
 'mix_effect': -1.4,
 'standardised': 33.94,
 'cohort_change': {3: -3.8, 6: -16.6, 9: -2.4}}

Priority schools (masked)¶

The public list uses P-codes; the named list is written to data/private/ only.

In [14]:
pd.DataFrame(res['priority'])[['id','gov_en','stage','gender','prev','rate','change','rel','tested']]
Out[14]:
id gov_en stage gender prev rate change rel tested
0 P-01 Rijal Alma ابتدائية بنات 88.7 15.0 -73.7 -68.7 27
1 P-02 Bisha ابتدائية بنين 84.4 23.9 -60.5 -55.6 30
2 P-03 Muhayil ابتدائية بنات 45.7 6.8 -38.8 -33.8 42
3 P-04 Ahad Rufaidah ابتدائية بنات 65.0 17.5 -47.5 -42.5 42
4 P-05 Rijal Alma ابتدائية بنات 65.0 19.8 -45.2 -40.3 45
5 P-06 Rijal Alma ابتدائية بنات 27.7 1.5 -26.2 -21.2 56
6 P-07 Rijal Alma ابتدائية بنات 36.3 6.7 -29.6 -24.6 43
7 P-08 Tathlith متوسطة بنات 52.0 14.9 -37.1 -32.1 25
8 P-09 Rijal Alma ابتدائية بنات 62.5 20.6 -41.9 -36.9 31
9 P-10 Abha ابتدائية بنات 47.1 13.5 -33.5 -28.6 42
10 P-11 Abha ابتدائية بنات 44.7 12.5 -32.2 -27.3 35
11 P-12 Bisha ابتدائية بنات 73.3 26.9 -46.4 -41.4 25

6 · Charts & export¶

In [15]:
P.charts(res, dq, govs)
feed = P.export(res, dq, recon, summary, govs, back)
from IPython.display import Image, display
for f in sorted(P.CHARTS.glob('*.png')):
    display(Image(filename=str(f), width=680))
No description has been provided for this image
No description has been provided for this image
No description has been provided for this image
No description has been provided for this image
No description has been provided for this image