---
title: Open University Data Case Study 
description: 我的第一篇筆記
---
# Open University Data Case Study 
[**本篇文章**](https://hackmd.io/@jwWbsnjzRaKD0UNbiB5jfQ/ylkk)將分析案例來自[Open University](https://www.nature.com/articles/sdata2017171)的數據集進行深入的數據分析,數據集涵蓋了學生的人口統計資料及其在虛擬學習環境中的互動記錄,目標是從中挖掘洞見,並設計一個切實可行且可衡量的計畫,以提升學生的學習成果,過程中將配合我的程式碼[^code]使用,但礙於方便閱讀,程式碼將在筆記中只呈現部份,完整的部分可再自行查看,方能有一個更清晰的理解,同時這也是我第一篇<i class="fa fa-file-text"></i> **HackMD**的筆記,希望在呈現上可以給讀者一個直觀的體驗。
---
[TOC]
1.資料探索性分析(EDA)
===

這裡我們先檢視各檔案的資訊,以下是各 CSV 檔案的介紹:
- courses.csv:包含模組代碼、呈現時間(年份及季節)及模組持續時間(天數)。
- assessments.csv:記錄模組評估的類型、提交日期、權重及評估的識別碼。
- vle.csv:提供模組中的教學材料、其使用時間範圍以及材料的活動類型。
- studentInfo.csv:包含學生的基本信息(性別、地區、教育背景、年齡)、學習成績、以及是否有殘疾等。
- studentRegistration.csv:記錄學生的註冊和退註冊日期,及模組的相關信息。
- studentAssessment.csv:包含學生的評估提交日期、評估分數,以及是否從之前呈現轉移的標記。
- studentVle.csv:記錄學生與教學材料的互動次數和互動日期。
這些檔案共同提供了關於學生學習過程的詳細資料,從學術表現到學習行為,為分析學生學習成果提供了豐富的數據支持,為了方便表示後續將其依序簡稱為course、ass、materials、info、reg、results、vle。
## 觀察基本資料資訊及修正
### Course、Ass、Results
我們先查看Course、Ass的基本欄位與其告訴我們的相關訊息,且比較其真實被Results記錄的部分
:point_down:發現冬季J課程通常較長(260-270天)、夏季B課程較短(235-240天)
:::success
:::spoiler 執行結果

:::
:point_down:歷年課程評估指標數,大致維持相同,只有B、D有減少過(考試或作業的次數)
:::success
:::spoiler 執行結果

:::
:point_down:這裡則是檢查是否權重(weight)合為200%
:::success
:::spoiler 執行結果

:::
:point_down: 手冊說明了考試權重應為100%、剩餘評估權重合也該100%,但C課程超過、G課程低於預期

仔細檢查過後發現,C課程的不吻合來自於,出現兩次考試的評估指標,這邊是第一個<font color="#f00">錯誤</font>,而G課程的權重不吻合原因來自於非Exam的總合為0導致剩下總合只剩100,是第二個<font color="#f00">錯誤</font>。
:::success
:::spoiler 執行結果


:::
後續我們做資料修正的動作

:point_up: 比對Results紀錄,有許多權重為0的G課程評估仍然被記錄,其中包含15219筆多達2107名學生(總數2504),代表其仍有參考價值,後續應修正其權重使其與其他課程都保持200%的標準。
經過檢查將G課程的其他Ass紀錄(TMA、CMA)列出,同時比對其他課程的標準,觀察到大部分CMA的權重低於5%,甚至出現許多0%的紀錄,相比之下TMA則是幾乎沒有0%的權重(所有課程中只有一筆),意即對於任何課程TMA的重視程度應大於CMA,所以我們將G課程遺失的100%權重平均分散到有出現的TMA紀錄上。
:::success
:::spoiler 修正程式碼
```python=
# 修正 GGG這堂課 TMA、CMA的權重
ass.loc[(ass.code_module=='GGG') & (ass.assessment_type=='TMA'),'weight'] = (100/3)
ass.loc[(ass.code_module=='GGG') & (ass.assessment_type=='CMA'),'weight'] = (0)
```
:::
後續檢查所有課程評估的指標,是否都有出現在真實最後紀錄中?
:point_down: 發現有18個評估指標完全沒有被記錄(且都是Exam)
:::success
:::spoiler 執行結果

:::
這裡有發現一個現象,C課程的考試評估指標,雖然Ass中說有兩種,但是實際Results中只出現其中一種的紀錄,所以剛好若我們將其刪除(因未實際出現沒參考價值)即可解決總合不對的問題,這邊我們就將上面Index為63、73的評估指標給刪除,剩餘的評估指標不刪除是因為手冊本身已經說明Exam通常不被明確記錄,可以當作合理現象。
:::success
:::spoiler 修正程式碼
```python=
# print('最後這邊,將CCC多餘的評估指標刪除')
ass = ass[~ass['id_assessment'].isin(miss_exam[miss_exam['code_module']=='CCC']['id_assessment'])]
# print('檢查所有課程總權重無誤皆為200%,即修正完成')
display(ass.groupby(['code_module', 'code_presentation'])\
.agg(total_weight = ('weight',sum),total_count = ('weight',len),count_by_type=('assessment_type', lambda x: dict(x.value_counts()))))
```
:::
最後的成果如下,使各課程的評估指標為200%,吻合手冊中的說明,以及確認所有課堂的Exam的評估權重皆為一筆100%。
:::success
:::spoiler 各課程評估指標修正結果

:::
---
### Reg、Info、Results
再來我們先查看Reg、Info的基本欄位與其告訴我們的相關訊息,且比較其真實被Results的記錄
Reg、Info的筆數都是32593,但依據Unique Values的數量可以看出id_student只有28785,所以代表有同一個學生,重複出現修習不同課程的或重修的意思。
| Attribute_Reg | Unique Values |
|:---------------------|:----------------|
| code_module | 7 |
| code_presentation | 4 |
| id_student | 28785 |
| date_registration | 333 |
| date_unregistration | 417 |
| Attribute_Info | Unique Values |
|:---------------------|:----------------|
| code_module | 7 |
| code_presentation | 4 |
| id_student | 28785 |
| gender | 2 |
| region | 13 |
| highest_education | 5 |
| imd_band | 11 |
| age_band | 3 |
| num_of_prev_attempts | 7 |
| studied_credits | 61 |
| disability | 2 |
| final_result | 4 |
:point_down: 查看Withdrawn是否都有註銷紀錄與未Withdrawn是否都為沒註銷紀錄
```python=
withdrawn_info = info_reg[info_reg['final_result']=='Withdrawn']
continue_info = info_reg[info_reg['final_result']!='Withdrawn']
display(continue_info[continue_info['date_unregistration'] != '?'])
display(withdrawn_info[withdrawn_info['date_unregistration'] == '?'])
```
發現有些非停休學生有退出課程紀錄(9/22437)、
有停休卻沒停休紀錄的(93/10156),前後不吻合的資料共 (102/32593) 筆。
---
### Materials、Vle
最後,我們查看Materials、Vle的基本欄位與其告訴我們的相關訊息。
:::success
:::spoiler 執行結果

:::
經過檢查後發現各教材(Materials)在實際使用紀錄(Vle)上的使用率非常高,超過98%都有實際被使用的紀錄,未被使用的教材只有(96/6364)。
:::success
:::spoiler 執行程式碼
```python=
vle_materials = pd.merge(vle, materials, on=['code_module', 'code_presentation', 'id_site'], how='inner')
display(vle_materials.head())
display(vle_materials.applymap(lambda x: x == '?').sum()/len(vle_materials))
# Drop columns
vle_materials.drop(columns=['week_from', 'week_to'], inplace=True) # ? 缺失比例太高 刪除
# 按照指定的列進行分組,並對 sum_click 進行加總
vlematerials_sum = vle_materials.groupby(['code_module', 'code_presentation', 'id_student'], as_index=False)['sum_click'].sum()
# 顯示結果
vlematerials_sum
```

:::
我們將Vle、Materials資料合併並刪除遺失值比例過多的欄位,並且根據每個學生在各堂不一樣的課程中( code_module )、( code_presentation )、( id_student )使用的所有教材點擊次數做分組加總,統計結果如上,總共有29228筆學生使用的紀錄。
----
## 將Vle紀錄與學生資訊合併
### 合併Info+Reg+Course+Vle+Materials
```python=
# info+reg+course+vle+materials
info_vle = pd.merge(regCoursesInfo,vlematerials_sum, on=['code_module', 'code_presentation', 'id_student'], how='left')
info_vle['sum_click'] = info_vle['sum_click'].fillna(0)
info_vle[info_vle['sum_click']==0]['final_result'].value_counts()
```
| code_module | code_presentation | id_student | gender | region | highest_education | imd_band | age_band | num_of_prev_attempts | studied_credits | disability | final_result | date_registration | date_unregistration | module_presentation_length | sum_click |
|:--------------|:--------------------|-------------:|:---------|:---------------------|:----------------------|:-----------|:-----------|-----------------------:|------------------:|:-------------|:---------------|--------------------:|:----------------------|-----------------------------:|------------:|
| AAA | 2013J | 11391 | M | East Anglian Region | HE Qualification | 90-100% | 55<= | 0 | 240 | N | Pass | -159 | ? | 268 | 934 |
| AAA | 2013J | 28400 | F | Scotland | HE Qualification | 20-30% | 35-55 | 0 | 60 | N | Pass | -53 | ? | 268 | 1435 |
| AAA | 2013J | 30268 | F | North Western Region | A Level or Equivalent | 30-40% | 35-55 | 0 | 60 | Y | Withdrawn | -92 | 12 | 268 | 281 |
| AAA | 2013J | 31604 | F | South East Region | A Level or Equivalent | 50-60% | 35-55 | 0 | 60 | N | Pass | -52 | ? | 268 | 2158 |
| AAA | 2013J | 32885 | F | West Midlands Region | Lower Than A Level | 50-60% | 0-35 | 0 | 60 | N | Pass | -176 | ? | 268 | 1034 |
:point_up: 整理完後的資料格式如上,同時也將完全未有使用Vle教材的欄位(sum_click)補上0

:point_up: 我們可以觀察到,完全未使用Vle紀錄的學生,與其最後成績表現極差呈高度相關,符合我們的預期,顯示了未有Vle互動紀錄的學生,幾乎不可能通過課程(只有3人)。

這張圖展示了「點擊總和」(Sum Clicks)的分佈情況,橫軸表示點擊總和的範圍(Sum of Clicks),縱軸表示相應點擊次數範圍內的資料條目數量(Number of Entries)。從圖中可以觀察到以下特點:
- 分佈偏態:大部分的學生點擊總和集中在較小的範圍內,尤其是接近 0 的範圍,有超過 14,000 條記錄(數量最多的部分),表明有很多學生的互動次數較少。
- 長尾效應:隨著點擊總和的增加,出現長尾現象,資料量顯著減少。這表明少數學生與學習平台的互動非常頻繁,達到了幾千次以上的點擊數。
- 極少的高點擊值:有極少數的學生點擊總和超過 10,000 次,這部分顯示在圖的右側。
總體來說,這個分佈圖反映了大部分學生在學習平台上的互動點擊數相對較少,少數學生的互動次數明顯更多,可能是由於他們參與了更多的學習活動或使用了更多的學習資源。
### 新增點擊率分組欄位(click_group)
:::success
:::spoiler 程式碼
```python=
# 自定義分組邊界,調整成大致上分組比例不要太低
bins = [-1, 0, 100 , 250 , 500, 750 , 1000, 1500, 2500 , 4000, info_vle['sum_click'].max()]
# 使用 pd.cut 進行分組
info_vle['click_group'] = pd.cut(info_vle['sum_click'], bins=bins)
grouped_table = get_value_counts_table(info_vle,'click_group')
plot_group_distribution(grouped_table,'click_group','percentage')
```
:::
對於極度右偏的數據,這種分組調整可以幫助平衡不同區間的資料量,讓模型能夠更好地學習數據中的模式,同時減少極端值對結果的影響,從而提升模型的穩定性和解釋性。這對於分類或回歸任務中的數據處理都是有幫助的。

:point_up: 自定義分組邊界,調整成大致上分組比例不要太低即可。
根據:point_down:這兩張圖表,我們可以觀察點擊次數分組在不同課程上對應最終成績的關係。在這裡,我們選擇了兩個課程進行比較:課程 A(AAA) 和課程 F(FFF),這兩個課程來自不同領域


- 課程 A(AAA):
在點擊次數最少的一組(<10.0 次)中,100%的學生最終成績都是「Withdrawn」(退選),這表明與學習平台的互動極少的學生最終幾乎無法完成課程。隨著點擊次數的增加,我們可以看到成績逐漸好轉,點擊次數在「(100.0, 250.0]」這組中,Pass 和 Distinction 的比例顯著提高,分別達到了 29.6% 和 29.7%。而在「(1000.0, 1500.0]」這一組中,Pass 和 Distinction 的總比例甚至達到了 78.6%,表明與學習平台的高互動頻率對成績有積極影響。
- 課程 F(FFF):
與課程 A 相似,點擊次數最少的學生群體也表現出較高的退選率(94.8%),這反映了學生互動不足與課程未完成的直接關聯。然而,在課程 F 中,點擊次數較高的學生群體中,成績分佈相對課程 A 更加分散。例如在「(1000.0, 1500.0]」這組中,Pass 的比例達到 60.6%,但 Fail 的比例仍然存在,表明該課程可能存在更多影響成績的因素,除了與學習平台的互動次數外,可能還與其他學習習慣或學術能力相關。
通過這兩張圖表,我們可以得出以下結論:
1. 在互動次數較少的學生中,退選比例普遍較高,這可能是由於學習平台參與度不足所致。
2. 點擊次數較高的學生往往能獲得較好的成績,但在不同課程中,點擊次數對成績的影響程度可能不同。
3. 課程 A 的成績分佈相對更規律,互動次數越多,成績越好;而課程 F 的成績分佈則更複雜,表明其他變數可能也在影響學習成效。
這些結果提示我們,在設計學習平台和課程結構時,可以根據學生的互動次數來進行針對性干預,特別是對於互動次數較少的學生群體,及早介入可能會改善學習成績。
---
## 將Ass+Results紀錄與學生資訊合併
```python=
assResults = pd.merge(ass, results, on=['id_assessment'], how='inner')
```
查看合併後資料格式如下
| code_module | code_presentation | id_assessment | assessment_type | date | weight | id_student | date_submitted | is_banked | score |
|:--------------|:--------------------|----------------:|:------------------|-------:|---------:|-------------:|-----------------:|------------:|--------:|
| AAA | 2013J | 1752 | TMA | 19 | 10 | 11391 | 18 | 0 | 78 |
| AAA | 2013J | 1752 | TMA | 19 | 10 | 28400 | 22 | 0 | 70 |
| AAA | 2013J | 1752 | TMA | 19 | 10 | 31604 | 17 | 0 | 72 |
| AAA | 2013J | 1752 | TMA | 19 | 10 | 32885 | 26 | 0 | 69 |
| AAA | 2013J | 1752 | TMA | 19 | 10 | 38053 | 19 | 0 | 79 |
### 新增成績相關欄位
原本的成績高低彼此之間並不對等,因其還需考慮權重的影響,所以後續將依據'weight'、'score'、'assessment_type'新增四個欄位來更完整闡述彼此攜帶的訊息。
1. non_exam_score:
含意:非「Exam」類型的評估分數。
2. exam_score:
含意:「Exam」類型的評估分數。
3. weighted_0_score:
含意:非「Exam」類型且權重為0評估的分數,雖然不算分但仍有參考價值。
4. weight(重新分配的欄位):
含意:因資料不完整,需紀錄權重對比non_exam_score的影響力。
```flow
st=>start: 開始
e=>end: 結束
op1=>operation: 創建非考試評分(non_exam_score)
op2=>operation: 創建考試評分(exam_score)
op3=>operation: 創建權重為0的評分(weighted_0_score)
op4=>operation: 創建Exam類型的評估權重(weight)
cond1=>condition: assessment_type 是 Exam?
cond2=>condition: weight == 0?
cond1(yes)->op2
cond1(no)->cond2
cond2(yes)->op3
cond2(no)->op1
op1->op4
```
:::success
:::spoiler 執行程式碼
```python=
assResults['non_exam_score'] = assResults.apply(lambda x: x['score']*x['weight']/100 if x['assessment_type'] != 'Exam' else None,axis=1)
assResults['exam_score'] = assResults.apply(lambda x: x['score'] if x['assessment_type'] == 'Exam' else None,axis=1)
assResults['weighted_0_score'] = assResults.apply(lambda x: x['score'] if x['weight'] == 0 else None,axis=1)
assResults['weight'] = assResults.apply(lambda x: 0 if x['assessment_type'] == 'Exam' else x['weight'],axis=1)
assResults_sum = assResults.groupby(['code_module','code_presentation','id_student'], as_index=False)\
.agg(ass_counts = ('assessment_type', lambda x:len(x)) , weight_sum = ('weight','sum'),non_exam_score=('non_exam_score','sum')
,weight_0_score = ('weighted_0_score','mean'),exam_score = ('exam_score','mean'))
assResults_sum['weight_0_score'] = assResults_sum['weight_0_score'].fillna(0)
assResults_sum['exam_score'] = assResults_sum['exam_score'].fillna(0)
assResults_sum
```
:::

從最後的資料格式可以看出,每個學生在該堂課中,獲得的考試成績(exam_score)、作業成績(non_exam_score)及其對應的權重(weight_sum),其中權重合並非完全都為100,最後是作業但權重為0的參考評估分數(weighted_0_score)。
---
## 完整合併資料
最後,我們將上述學生與Vle互動的紀錄與實際評估分數(Results)合併,並同時處理資料遺失值與'?'的問題,且將不合適的欄位刪除同時轉換資料的格式,確保其完整性,完整的處理可見程式碼內容。我們最後還新增了( module_domain )新變數,當作課程領域的分類(Social Science、STEM),依據是由手冊中的介紹決定。
:::success
:::spoiler 執行程式碼
```python=
finalResults = pd.merge(info_vle,assResults_sum, on=['code_module', 'code_presentation', 'id_student'], how='left')
# 處理缺值或?
finalResults['imd_band'] = finalResults['imd_band'].replace('?',None).fillna(method='bfill') # 3.4087 %
finalResults['date_registration'] = finalResults['date_registration'].replace('?',None).fillna(method='bfill').astype(int) # 0.1381 %
finalResults.drop(columns=['date_unregistration'], inplace=True) # ? 69.0977% 比例太高 刪除
finalResults[['ass_counts','weight_sum','weight_0_score','non_exam_score','exam_score']] = finalResults[['ass_counts','weight_sum','weight_0_score','non_exam_score','exam_score']].fillna(0)
# 新增module_domain
finalResults['module_domain'] = finalResults['code_module'].apply(lambda x: 'Social Sciences' if x in ["AAA","BBB","GGG"] else 'STEM')
```
:::

:point_up_2: 上面是依據OU介紹手冊中,給的課程模組領域,分成Social_Sciences、STEM兩大類。
---
2.特徵工程與變數檢定
===
將資料完整合併後,查看各自變數與應變數間的分布關係,並且優化類別變數分類與篩選自變數。
- 數值變數
- 檢定方法:
- 是常態: ANOVA
- 非常態: Kruskal-Wallis、Mann-Whitney U
- 量化方法:Cliff's Delta
- 額外檢定:共線性
- 類別變數
- 檢定方法:Chi-square
- 量化方法:Cramers_v
- 其他處理:
- 1. 優化分組,合併過少的類別
> [!NOTE]為什麼需要量化指標? 直接看檢定P-value不行嗎?
> :::info p-value 受到樣本量影響
> 樣本量大小的影響:當樣本量很大時,哪怕某變數對於目標變數的影響微不足道,也可能會得到非常顯著的 p-value。這會導致 p-value 在大樣本情況下幾乎總是顯著,使得我們難以分辨變數的重要性。
> 量化指標不受樣本量影響:量化指標,如 Cliff's Delta 或 Cramér's V,不會因為樣本量的增大而導致影響大小被高估。這些指標是直接基於數據間的相對差異來衡量的,因此提供了對變數影響力度的客觀衡量。
> :::
## 變數檢定與結果 :bar_chart:
:::success
:::spoiler 程式碼
```python=
def check_relation_with_factor(df, target_column):
""" 對DataFrame中的所有變數進行與target_column的檢定 """
results = []
for col in df.columns:
if col == target_column:
continue
if pd.api.types.is_numeric_dtype(df[col]):
# 進行正態性檢驗
stat, p_normal = normaltest(df[col].dropna())
if p_normal > 0.05:
# 數值變數符合正態分佈,進行ANOVA檢定
formula = f'{col} ~ C({target_column})'
model = ols(formula, data=df).fit()
eta_sq = eta_squared_anova(model)
p_value = sm.stats.anova_lm(model)['PR(>F)'][0]
results.append({
'variable': col,
'test': 'ANOVA',
'p-value': p_value,
'effect_size': eta_sq,
'others' : None
})
else:
# 數值變數不符合正態分佈,使用Kruskal-Wallis檢定
groups = [df[df[target_column] == category][col].dropna() for category in df[target_column].unique()]
if len(groups) > 1:
stat, p_value = kruskal(*groups)
# 如果Kruskal-Wallis檢定顯著,進行Mann-Whitney U檢定
mannwhitney_results = []
for group1, group2 in combinations(groups, 2):
u_stat, mw_p_value = mannwhitneyu(group1, group2, alternative='two-sided')
cliff_effect_size = cliff_delta(group1, group2)
mannwhitney_results.append({
'group1_size': len(group1),
'group2_size': len(group2),
'mannwhitney_p_value': mw_p_value,
'effect_size': cliff_effect_size
})
# 計算 Cliff's Delta 的平均值作為總體的效應大小
effect_sizes = [result['effect_size'] for result in mannwhitney_results]
avg_effect_size = np.mean(np.abs(effect_sizes))
# 添加結果
results.append({
'variable': col,
'test': 'Kruskal-Wallis',
'p-value': p_value,
'effect_size': avg_effect_size, # 使用 Mann-Whitney U 檢定中 Cliff's Delta 的平均值
'others':mannwhitney_results
})
else:
# 類別變數進行Cramér's V檢定
cramer_v_value = cramers_v(df[col], df[target_column])
chi2, p_value, _, _ = chi2_contingency(pd.crosstab(df[col], df[target_column]))
results.append({
'variable': col,
'test': 'chi-square',
'p-value': p_value,
'effect_size': cramer_v_value,
'others' : None
})
results_df = pd.DataFrame(results).sort_values(by='effect_size')
return results_df
display(check_relation_with_factor(finalResults, 'final_result'))
```
:::
:::success
:::spoiler 執行結果

:::
| variable | test | p-value | effect_size |
|:--------------------------|:----------------|:------------------|-------------:|
| gender | chi-square | 8.827204e-04 | 0.0225 |
| age_band | chi-square | 2.831091e-45 | 0.0584 |
| code_presentation | chi-square | 4.736105e-81 | 0.0641 |
| disability | chi-square | 8.143196e-30 | 0.0652 |
| region | chi-square | 6.470404e-73 | 0.0678 |
| module_presentation_length| Kruskal-Wallis | 1.399743e-39 | 0.0712 |
| num_of_prev_attempts | Kruskal-Wallis | 7.971631e-108 | 0.0745 |
| imd_band | chi-square | 3.875400e-117 | 0.0808 |
| id_student | Kruskal-Wallis | 4.247670e-36 | 0.0816 |
| date_registration | Kruskal-Wallis | 1.098084e-100 | 0.0906 |
| module_domain | chi-square | 3.445930e-64 | 0.0955 |
| highest_education | chi-square | 9.182113e-212 | 0.1024 |
| code_module | chi-square | 7.589955e-296 | 0.1215 |
| studied_credits | Kruskal-Wallis | 1.286554e-256 | 0.1292 |
| exam_score | Kruskal-Wallis | 0.000000e+00 | 0.1969 |
| weight_0_score | Kruskal-Wallis | 0.000000e+00 | 0.2186 |
| click_group | chi-square | 0.000000e+00 | 0.4190 |
| sum_click | Kruskal-Wallis | 0.000000e+00 | 0.6008 |
| ass_counts | Kruskal-Wallis | 0.000000e+00 | 0.6218 |
| weight_sum | Kruskal-Wallis | 0.000000e+00 | 0.6179 |
| non_exam_score | Kruskal-Wallis | 0.000000e+00 | 0.8125 |
結果如我們所預期的,任何變數的顯著性都非常高,不過我們有量化指標當作後續的挑選準則。
---
依據量化指標( effect_size ),我選擇了幾個較低的欄位,再將其打印出與最終成績 ( final_results )的關係,觀察到比較特別的現象列出:
#### id_student


看的出來學生編號與最後結果的關係並無明顯關聯,分組後的差異也不大,符合學生編號代表的隨機性,並無法攜帶太多相關資訊。
#### mudule_presentation_length

:point_up: 課程長度也無明顯差關聯。
#### num_of_prev_attemps

:point_up: 修習的次數並不像我們常理認為的呈現一個正相關(即越有好表現)。
#### imd_band

:point_up: 雖說(Index of Multiple Deprivation, IMD)的影響力不高,但該指標與學習表顯高度相關,通常越富裕的家庭背景在學習的表現上有明顯的幫助,特別是達到最優異的成績。
#### date_registration

:point_up: 最後是依據學生註冊課堂的時間點來看,反而在退出課程中有越早註冊的傾向,並不像常理認為的越早註冊顯示了學生的積極度,在表現上會較好的想法。
綜合考量: 將移除學生編號(id_student)、模組長度(module_presentation_length)、修課次數(num_of_prev_attempts)、註冊日期(date_registration)、性別(gender)、學分數(studied_credits)等變數。
## 共線性與高相關係數
| Variable | VIF |
|:---------------------------|------------:|
| id_student | 2.658614 |
| num_of_prev_attempts | 1.166469 |
| studied_credits | 5.148766 |
| date_registration | 3.042466 |
| module_presentation_length | 11.210414 |
| sum_click | 2.481305 |
| ass_counts | 10.793394 |
| weight_sum | 39.709853 |
| non_exam_score | 31.981209 |
| weight_0_score | 2.053865 |
| exam_score | 1.531870 |
| Variable 1 | Variable 2 | Correlation |
|:------------|:--------------|-------------:|
| sum_click | ass_counts | 0.547565 |
| sum_click | non_exam_score| 0.550439 |
| ass_counts | weight_sum | 0.850915 |
| ass_counts | non_exam_score| 0.818467 |
| weight_sum | non_exam_score| 0.953408 |
:::info
> [!IMPORTANT] 解釋含意
> - VIF(方差膨脹因子):表示變數之間的共線性程度,VIF 值超過 10 表示該變數與其他變數有高度共線性,應該考慮移除。
> - 相關係數:相關係數範圍是 -1 到 1,接近 1 表示正相關,接近 -1 表示負相關,0 表示無關聯。通常相關係數超過 0.8 會被視為高度相關。
:::
綜合考量:
1. 移除 weight_sum:它不僅在 VIF 中顯示出很高的共線性,而且與其他多個變數有較高的相關性,容易造成冗餘。
2. 移除 module_presentation_length 和 ass_counts:它們的 VIF 值也超過了 10,建議移除以減少共線性。
2. 保留 non_exam_score:儘管與 weight_sum 有高度相關,但 weight_sum 已被刪除,因此可以保留 non_exam_score。
## 處理與合併類別變數
合併太少比例的類別變數是一種在數據預處理中特徵工程的重要步驟。它的目的是為了解決那些出現頻次過低的類別可能帶來的數據分析和建模問題,以下是處理這些小比例類別的重要原因及其可能的影響:
:::info
> [!IMPORTANT] 為什麼要處理類別變數合併:
> 1. 避免數據稀疏性,低頻類別缺乏足夠樣本學習特徵
> 2. 減少噪音,過少類別可能是異常值,影響模型穩定性
> 3. 防止過擬合,模型過度擬合稀少類別可能導致泛化能力差
> 4. 提高模型計算效率,減少 one-hot 編碼後的特徵數
> 5. 提升統計檢定的有效性,增加樣本量讓檢定結果更穩定
:::
以下是自變數年齡分組( Age Band ) 處理前分布與處理後分布 :point_down:


自變數最高學歷( Highest Education )處理前分布與處理後分布 :point_down:


---
## 完整建模的資料欄位
經過一系列的處理後,完整的資料欄位如下,在先前的處理後,共線性與自變數間高度相關的問題已經解決,且留下影響力至少大於0.05的欄位。
:::success
:::spoiler 執行結果

:::
3.模型建構
===
## 分類問題預測
接下來我們初步對三個分類模型(邏輯迴歸(**Logistic Regression**)、隨機森林(**Random Forest**) 和 **XGBoost**)進行多次訓練的過程,並對結果進行評估。重複訓練 20 次,旨在比較各模型的表現,並減少模型表現因隨機性影響而產生的偏差。
在每次迭代中,首先將數據集進行處理,對類別特徵進行 One-hot 編碼,然後將數據分成訓練集和測試集,並對數值型特徵進行標準化。然後使用三種不同模型分別進行訓練和預測。
對於每個模型,會計算準確率並保存,同時也會生成詳細的分類報告來評估各類別的表現。最終,通過多次訓練後,程式碼計算各模型準確率的平均值和標準差,並對分類報告的平均值進行統計,從而對各模型的穩定性和分類能力進行比較。
:::success
:::spoiler 執行結果



:::
根據三個模型的比較結果,包括邏輯迴歸、隨機森林和 XGBoost,在精確度、召回率、F1 分數等多項評估指標上,XGBoost 模型表現出最優異的性能。具體來說,XGBoost 模型在整體準確率(Mean Accuracy)上達到了 0.7528,相較於邏輯回歸的 0.7339 和隨機森林的 0.7419,表現更好。此外,XGBoost 在各類別(如 Distinction、Fail、Pass 和 Withdrawn)上的 F1 分數整體也較高,特別是在 "Pass" 和 "Withdrawn" 類別中,顯示出更強的分類能力。
基於這些結果,我在後續的研究中使用 XGBoost 模型,因為它能更準確地預測不同類別的學生成績,並且具有更強的泛化能力。這將有助於提升模型在實際應用場景中的效果,特別是在預測學生表現和識別可能失敗或需要更多支持的學生方面。
:::success
:::spoiler XGboost Model調參
```python=
def train_xgboost_pipeline(data, target_column, param_grid=None, test_size=0.2, random_state=None, cv=5, scoring='f1_weighted'):
# 分割特徵和目標變數
X = data.drop(target_column, axis=1)
y = data[target_column]
# 使用 LabelEncoder 對目標變數進行編碼
label_encoder = LabelEncoder()
y = label_encoder.fit_transform(y)
# 對類別特徵進行 One-Hot 編碼
X = pd.get_dummies(X, drop_first=True)
# 確保所有特徵名稱為字符串,並且移除不允許的字符,避免 XGBoost 錯誤
X.columns = X.columns.astype(str).str.replace('[\[\],<>]', '', regex=True)
# 分割數據集
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=test_size, random_state=random_state)
# 建立 XGBoost 管道
pipeline = Pipeline([
('scaler', StandardScaler()),
('model', XGBClassifier(use_label_encoder=False, eval_metric='mlogloss',
random_state=random_state))
])
# 如果沒有提供 param_grid,使用默認的參數網格
if param_grid is None:
param_grid = {
'model__n_estimators': [100, 200],
'model__max_depth': [3, 5],
'model__learning_rate': [0.1, 0.2],
'model__subsample': [0.8, 1.0],
'model__colsample_bytree': [0.8, 1.0]
}
# 初始化 GridSearchCV
grid_search = GridSearchCV(estimator=pipeline, param_grid=param_grid, cv=cv, n_jobs=-1, verbose=2, scoring=scoring)
# 訓練模型
grid_search.fit(X_train, y_train)
# 打印最佳參數和交叉驗證分數
print("\nBest Parameters for XGBoost:", grid_search.best_params_)
print("Best Cross-Validation F1 Score for XGBoost:", grid_search.best_score_)
# 使用最佳參數進行測試
best_pipeline = grid_search.best_estimator_
y_preds = best_pipeline.predict(X_test)
print("\nTest F1 Score:", f1_score(y_test, y_preds, average='weighted'))
print("Classification Report:\n", classification_report(y_test, y_preds))
print(dict(enumerate(label_encoder.classes_)))
# 顯示特徵重要性
xgb_model = best_pipeline.named_steps['model']
feature_importances = xgb_model.feature_importances_
feature_names = X.columns
# 排序特徵重要性
sorted_indices = np.argsort(feature_importances)[::-1] # 從大到小排序
sorted_feature_names = feature_names[sorted_indices]
sorted_feature_importances = feature_importances[sorted_indices]
# 打印排序後的特徵重要性
# print("\nFeature Importance (sorted):")
# for name, importance in zip(sorted_feature_names, sorted_feature_importances):
# print(f"{name}: {importance}")
# 繪製排序後的特徵重要性圖表
plt.figure(figsize=(10, 8))
plt.barh(sorted_feature_names, sorted_feature_importances, color='skyblue')
plt.xlabel('Feature Importance')
plt.title('Feature Importance of the Best XGBoost Model')
plt.gca().invert_yaxis() # 反轉 y 軸以使最重要的特徵在最上面
plt.show()
```
:::
:point_up:訓練一個 XGBoost 模型並對其進行參數調優。首先,從資料集中分離特徵 (X) 和目標變數 (y),並對目標變數進行編碼。接著,透過 One-Hot 編碼處理類別特徵,並將資料分為訓練集和測試集。
接著建立一個管道,包括標準化特徵和 XGBoost 分類器,用 GridSearchCV 在不同參數組合下進行交叉驗證(CV=5),從而找到最佳的模型參數。最後,使用最佳模型對測試集進行預測,並計算加權 F1 分數和打印分類報告,以評估模型的性能,同時對特徵預測結果的重要性排序。:point_down:

重要性圖表 (Feature importance) 顯示了模型中的特徵對預測結果的重要性排序,在圖中每個橫條代表一個特徵,橫條的長度表示該特徵對模型決策的重要性。
以下是一些觀察到的重點:
- 1. 非考試成績 (non_exam_score) 和考試成績 (exam_score):這兩個特徵顯示出非常高的影響力,這表示在這些資料中,學生的分數對最終的預測結果有關鍵影響,模型非常依賴這些特徵來預測結果。
- 2. 特定模組 (code_module) 和 模組領域 (module_domain):不同的模組與模組領域也是影響結果的重要因素,這意味著某些特定科目對學生最終的學習結果有較大的影響。
- 3. 點擊群組 (click_group) 和 總點擊次數 (sum_click):這些特徵表示學生在學習平台上活動的頻率和程度也對預測結果有影響,可能反映出參與程度和學習效果之間的關聯。
總的來說,模型顯示分數、模組特徵、以及學習參與度(例如點擊次數和行為)是最主要的預測因素,而其他社會經濟指標(如居住地區的貧困指數)相對影響較小。
這些特徵重要性幫助我們了解哪些因素在預測學生學習結果中是最重要的,進而提供方向來優化教學策略或針對某些特定學生群體進行支持。
---
## 改變預測結果的格式
因為分類的結果表現大約為F1 Score = 0.75,後續嘗試了將應變數(Y)做轉換,以下嘗試不同類型的資料格式,最終在最好的表現上可達**0.95**的F1 Score。
### 調整預測結果格式
這裡我們依據2024年[英國公開大學的《模組結果確定政策》](https://help.open.ac.uk/documents/policies/module-results-determination-policy/files/239/Module%20Results%20Determination%20Policy%20(April24).pdf)。它詳細說明了如何確定學生的模組結果,包括成績評定標準、結果的決定流程、申訴和重考的程序。

:point_up: 當中對於各種成績等級的解釋,將原本的成績做數值上的轉換,如下
- Distinction:
將這個結果對應的目標變數轉換為隨機範圍內的分數,範圍在 80 到 100 之間。這意味著如果結果是 Distinction,模型會生成一個位於 80 到 100 的分數來表示高成績。
- Pass:
將這個結果對應的目標變數轉換為隨機範圍內的分數,範圍在 40 到 80 之間。這表示如果結果是 Pass,模型會生成一個位於 40 到 80 的分數來表示通過。
- Fail:
將這個結果對應的目標變數轉換為隨機範圍內的分數,範圍在 0 到 40 之間。如果結果是 Fail,模型會生成一個位於 0 到 40 的分數,代表較低的成績或不及格。
- Withdrawn:
直接將這個結果轉換為 0,表示這個分類結果對應於完全沒有參加或者退出的情況。
:::success
:::spoiler 轉換程式碼
```python=
def convert_to_score(result):
if result == 'Distinction':
return random.uniform(80, 100)
elif result == 'Pass':
return random.uniform(40, 80)
elif result == 'Fail':
return random.uniform(0, 40)
elif result == 'Withdrawn':
return 0
def convert_to_score(result):
if result == 'Distinction':
return random.gauss(90, 5) # 平均值 90,標準差 5
elif result == 'Pass':
return random.gauss(60, 10) # 平均值 60,標準差 10
elif result == 'Fail':
return random.gauss(20, 10) # 平均值 20,標準差 10
elif result == 'Withdrawn':
return 0
```
:::
在進行回歸預測後,我根據不同的誤差容忍度計算了準確率:
| Accuracy Range | Accuracy Score |
|:---------------------|---------------:|
| within 10 points | 0.7559 |
| within 5 points | 0.4988 |
| within 1 point | 0.1002 |
- 在 10 分誤差範圍內的準確度:有 75.59% 的預測值與真實值的誤差在 10 分以內。
- 在 5 分誤差範圍內的準確度:有 49.88% 的預測值與真實值的誤差在 5 分以內。
- 在 1 分誤差範圍內的準確度:只有 10.02% 的預測值與真實值的誤差在 1 分以內。
這表示隨著誤差範圍的縮小,模型的準確度逐漸下降。

模型訓練完成後,根據 XGBoost 提供的特徵重要性,圖中展示了各個特徵對於模型預測結果的重要性。其中:
1. 非考試成績 (non_exam_score) 是最具影響力的特徵,對模型結果的貢獻最大。
2. 課程領域 (module_domain) 和點擊總數 (sum_click) 也有顯著影響,位居第二和第三。
3. 其他特徵如考試成績 (exam_score) 和多個 課程模組(code_module) 等也對模型預測有不錯程度的貢獻。
---
### 將應變數分組合併
應變數中的Distinction、Pass、Fail、Withdrawn其實是有順序性的關係的,即Distinction>Pass>Fail>Withdrawn,所以這裡嘗試將相近的分類合併建模。
#### 將Withdrawn合併進Fail


#### 將Distinction合併進Pass


#### 同時合併只剩,Pass、Fail


#### 結論
1. 分類為四類別時,模型表現相對一般,特別是對於 Fail 和 Distinction 類別的預測效果較差。
2. 分類為三類別(去除 Withdrawn)後,模型在 Distinction, Fail, Pass 的預測效果有所改善,整體 F1-score 提升。
3. 分類為三類別(去除 Distinction)後, Fail, Pass, Withdrawn 模型的準確度保持在 0.82,但 Fail 類別的預測效果仍需進一步優化。
4. 最後,當僅分類為兩個類別(Fail 和 Pass)時,模型表現最佳,準確率高達 0.95,F1-score 也顯著提升,表明模型在這種簡化情境下達到了最優的預測性能。
4.結論與建議
===
依據資料以及特徵重要性分析結果,我們可以看出最難以預測的會是Fail成績的類,在這個類中的人,沒有 Withdrawn 的極端特徵(類似完全無Vle互動紀錄、Results紀錄等),卻同時擁有Pass類別的特徵,但樣本數又不及前兩者,導致判別其結果相對較不準,我們可以針對三個問題進行具體建議改善成績問題:
## 1. 如何提升成績表現 :thinking_face: (Fail :point_right: Pass)
主要影響因素: 根據特徵重要性分析,non_exam_score 和 exam_score 是對成績預測最重要的兩個因素。這表明學生的課堂參與度(如課後作業、測驗成績)和正式考試表現都直接影響學生成績。此外,學生的互動頻率(sum_click,即學生在學習平台上的點擊次數)和所選模組(如 module_domain)也是影響成績的重要因素。
建議:
- 提升課堂參與度:
根據 non_exam_score 的重要性,學生應積極參與作業、測驗、課堂活動等非考試評估,以提升表現。
- 準備正式考試:
由於 exam_score 對最終成績有顯著影響,學生應提早準備考試,確保自己對核心知識點有充分的掌握。由於 exam_score 對最終成績有顯著影響,應提醒學生為考試做準備,並確保自己對核心知識點有充分的掌握。
- 利用在線學習資源:
互動點擊次數(sum_click) 反映了學生與學習平台的互動頻率,學生應更多地利用在線資源、視頻和討論區來鞏固學習成果。
- 選擇適合的學科模組:
學生在選擇課程模組(module_domain 、 code_module)時應根據自己的興趣與能力來選擇適合的學科領域,這有助於更好地掌握課程內容並提升成績。
## 2. 如何有效避免退選 ( Withdrawn :point_right: Fail )
主要影響因素: 在所有模型中,Withdrawn 類別與其他類別的區分主要取決於學生的學習參與度(如 sum_click)和課堂表現(如 non_exam_score 和 exam_score),參與度低或成績不佳的學生更容易選擇退選。
建議:
- 早期識別高風險學生:
通過追蹤學生的課堂參與情況和在線行為(點擊率、參與度),可以及早識別出有可能退選的學生,並對這些學生進行干預,提供額外支持。
- 提高課堂參與度:
增加課堂參與和課後互動,尤其是對成績處於邊緣的學生,積極參與學習活動可以減少退選可能性。
- 提供學術和心理支持:
對於有可能退選的學生,學校應提供學術輔導和心理支持,幫助他們應對學習壓力,從而降低退選率。
## 3. 如何提高獲得特優成績的機率(Pass :point_right: Distinction)
主要影響因素: non_exam_score 是決定學生是否能獲得 Distinction 的關鍵因素之一。也就是說,除了正式考試之外,學生的課堂作業和其他評估活動同樣重要。此外,學生所在的學科模組(如 module_domain_Social Sciences)和在線學習參與度(sum_click)也起到了重要作用。
建議:
- 全方位提升表現:
鼓勵學生不僅要在正式考試中表現優異,還需要在日常課堂作業、討論、測驗等活動中取得高分。這樣可以顯著提高獲得特優成績的機會。
- 充分利用學習平台:
由於在線參與度對成績的影響顯著,學生應多參加課程活動、閱讀補充資料、參與討論區互動等,從而加強對課程內容的理解。
- 選擇具挑戰性的模組:
選擇適合自己興趣和能力的模組,並挑戰自己掌握更深層次的知識,有助於獲得 Distinction。
## 4. 總結與未來工作 :mag_right:
根據以上三個面向的問題解答,提供更多在線資源和強化日常評估、鼓勵學生參與課堂互動,將是提高學生成績的關鍵。除此之外,及早識別參與度低的學生並設立預警系統,及時干預以降低退選率,能進一步提升學生的完成率。最後,高優秀率學生的進階學習機會和挑戰,對於學生和學校整體表現而言,是很好的激勵政策,打造雙方雙贏的局面 :handshake:。
以上建議都是基於數據分析結果,且相信對 OU 的業務發展以及技術實現都能提供實質的幫助。
[^code]: 附檔中應包含主程式code、輔助code文件。