1165 lines
50 KiB
Python
1165 lines
50 KiB
Python
"""
|
||
generate_plots_questionnaires.py
|
||
|
||
Analyses all questionnaire data and generates plots:
|
||
- IMI (Intrinsic Motivation Inventory) subscales
|
||
- SUS (System Usability Scale) scores
|
||
- UEQ-S (User Experience Questionnaire – Short) pragmatic/hedonic
|
||
- NASA-TLX workload subscales
|
||
- Godspeed tutor impression
|
||
- Social Presence (Legacy, 5 items)
|
||
- Cybersickness (tutoring only, 5 items)
|
||
- Stress / Readiness / Relaxation (Pre-test items)
|
||
- IOS (Inclusion of Other in Self)
|
||
- Reading vs Tutoring phase comparisons
|
||
- Correlations between questionnaire subscales and learning gains
|
||
|
||
Output: Data/plots_questionnaires/*.png
|
||
"""
|
||
|
||
import pandas as pd
|
||
import numpy as np
|
||
import matplotlib.pyplot as plt
|
||
import matplotlib.ticker as ticker
|
||
import seaborn as sns
|
||
from pathlib import Path
|
||
from scipy import stats
|
||
|
||
# =============================================================================
|
||
# PATHS & CONSTANTS
|
||
# =============================================================================
|
||
|
||
BASE = Path(r"F:\GitHub Projekte\VirTu-Eval\Data")
|
||
PLOT_DIR = BASE / "plots_questionnaires"
|
||
PLOT_DIR.mkdir(exist_ok=True)
|
||
STATS_DIR = BASE / "stats"
|
||
STATS_DIR.mkdir(exist_ok=True)
|
||
|
||
MEDIUMS = ['Chat', 'Video', 'VR']
|
||
MED_COLORS = {'Chat': '#42A5F5', 'Video': '#FFA726', 'VR': '#66BB6A'}
|
||
|
||
sns.set_theme(style="whitegrid", font_scale=1.05)
|
||
plt.rcParams['figure.dpi'] = 150
|
||
plt.rcParams['savefig.bbox'] = 'tight'
|
||
|
||
# =============================================================================
|
||
# IMI SUBSCALE DEFINITIONS (indices into Post-Questionnaire cols 2-27)
|
||
#
|
||
# The 26 IMI items span columns 2-27 of both Reading and Tutoring post-
|
||
# questionnaires. Column 4 ("Ich fühle mich gestresst") is a stress item
|
||
# embedded among the IMI items, treated separately.
|
||
#
|
||
# IMI subscale mapping (1-indexed within IMI block, i.e. col 2 = item 1):
|
||
# Interest/Enjoyment: items 4,6,8,10,12,14,16 (cols 5,7,9,11,13,15,17 - 0-indexed from file but relative to block)
|
||
# Value/Usefulness: items 1,3,5,7,9,11,13 (cols 2,4,6,8,10,12,14 - but col 4 is stress, skip)
|
||
# Perceived Choice: items 2,15,17,19,21,23,25 (cols 3,16,18,20,22,24,26)
|
||
#
|
||
# Actually, let me map by the actual German text and standard IMI subscales:
|
||
# =============================================================================
|
||
|
||
# IMI items are at file columns 2-27 (26 items).
|
||
# Col 4 = second "stressed" item → treat as separate relaxed/stress item, not IMI.
|
||
# So IMI = 25 items at cols [2,3,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27]
|
||
|
||
# Standard IMI subscales by column index in the file (0-based):
|
||
# Interest/Enjoyment: 7(Spaß), 9(genossen), 13(genoss), 17(interessant), 19(angenehm), 25(spaßvoll), 5(dachte wie sehr genoss)
|
||
# → reverse: 14(langweilig)
|
||
IMI_INTEREST = {
|
||
'items': [5, 7, 9, 13, 17, 19, 25],
|
||
'reverse': [14],
|
||
'label': 'Interest/\nEnjoyment'
|
||
}
|
||
|
||
# Value/Usefulness: 2(Wert), 6(Konzentration), 8(Verbesserung), 12(wichtig), 15(Lerngewohnheiten), 18(nützlich), 21(Nutzen), 23(Schule), 27(Wert)
|
||
IMI_VALUE = {
|
||
'items': [2, 6, 8, 12, 15, 18, 21, 23, 27],
|
||
'reverse': [],
|
||
'label': 'Value/\nUsefulness'
|
||
}
|
||
|
||
# Perceived Choice: 3(Wahlmöglichkeit), 11(weil wollte), 24(Wahl hatte)
|
||
# → reverse: 10(keine Wahl), 16(keine andere Wahl), 20(musste), 22(weil musste), 26(nicht eigene Entscheidung)
|
||
IMI_CHOICE = {
|
||
'items': [3, 11, 24],
|
||
'reverse': [10, 16, 20, 22, 26],
|
||
'label': 'Perceived\nChoice'
|
||
}
|
||
|
||
IMI_SUBSCALES = [IMI_INTEREST, IMI_VALUE, IMI_CHOICE]
|
||
|
||
# SUS items: cols 32-41 in Post-Questionnaire-Tutoring (and NOT in Reading)
|
||
SUS_COLS = list(range(32, 42)) # 10 items
|
||
|
||
# UEQ-S items: cols 47-54 in Post-Questionnaire-Tutoring (8 items "Die Interaktion war:")
|
||
# In Post-Questionnaire-Reading: cols 28-35 (8 items "Die Erfahrung war:")
|
||
UEQS_COLS_TUTORING = list(range(47, 55)) # 8 items
|
||
UEQS_COLS_READING = list(range(28, 36)) # 8 items
|
||
# UEQ-S: items 1,2,3,4 = pragmatic quality; items 5,6,7,8 = hedonic quality
|
||
UEQS_PRAGMATIC = [0, 1, 2, 3] # relative indices within the 8 UEQ items
|
||
UEQS_HEDONIC = [4, 5, 6, 7]
|
||
|
||
# NASA-TLX: 6 items
|
||
# In Reading: cols 36-41
|
||
# In Tutoring: cols 86-91
|
||
NASATLX_COLS_READING = list(range(36, 42))
|
||
NASATLX_COLS_TUTORING = list(range(86, 92))
|
||
NASATLX_LABELS = ['Mental\nDemand', 'Physical\nDemand', 'Temporal\nDemand',
|
||
'Performance', 'Effort', 'Frustration']
|
||
|
||
# Godspeed: cols 56-79 in Post-Questionnaire-Tutoring (24 items)
|
||
GODSPEED_COLS = list(range(56, 80))
|
||
# Godspeed subscales (5 subscales, standard order):
|
||
# Anthropomorphism (5): items 1-5 → cols 56-60
|
||
# Animacy (6): items 6-11 → cols 61-66
|
||
# Likeability (5): items 12-16 → cols 67-71 (note: col 67 has trailing space typo in header)
|
||
# Perceived Intelligence (5): items 17-21 → cols 72-76
|
||
# Perceived Safety (3): items 22-24 → cols 77-79
|
||
GODSPEED_SUBSCALES = {
|
||
'Anthropo-\nmorphism': list(range(56, 61)),
|
||
'Animacy': list(range(61, 67)),
|
||
'Like-\nability': list(range(67, 72)),
|
||
'Perceived\nIntelligence': list(range(72, 77)),
|
||
'Perceived\nSafety': list(range(77, 80)),
|
||
}
|
||
|
||
# Social Presence (Legacy, 5 items): cols 80-84
|
||
SOCIAL_PRESENCE_COLS = list(range(80, 85))
|
||
SOCIAL_PRESENCE_LABELS = ['Face-to-face', 'Same room', 'Being watched',
|
||
'Aware of me', 'Tutor present']
|
||
|
||
# Cybersickness: cols 42-46 (5 items, tutoring only)
|
||
CYBERSICKNESS_COLS = list(range(42, 47))
|
||
CYBERSICKNESS_LABELS = ['General\nDiscomfort', 'Fatigue', 'Headache',
|
||
'Eye\nStrain', 'Difficulty\nConcentrating']
|
||
|
||
# IOS (Inclusion of Other in Self): col 28 in tutoring
|
||
IOS_COL = 28
|
||
|
||
# Extra items: cols 29-31
|
||
EXTRA_COLS = {'self_use': 29, 'felt_helpful': 30, 'session_length': 31}
|
||
|
||
# Stress items
|
||
STRESS_COL = 1 # "Ich fühle mich gestresst" (both pre/post)
|
||
READY_COL = 2 # Pre-test: "Ich fühle mich bereit"
|
||
RELAXED_COL = 3 # Pre-test: "Ich fühle mich entspannt"
|
||
|
||
# Recommend tutor: col 85
|
||
RECOMMEND_COL = 85
|
||
|
||
# BFI-15 cols in Final-Questionnaire: cols 1-15
|
||
BFI_COLS = list(range(1, 16))
|
||
BFI_TRAITS = {
|
||
'Neuroticism': {'items': [1, 2], 'reverse': [3]},
|
||
'Extraversion': {'items': [4, 5], 'reverse': [6]},
|
||
'Openness': {'items': [7, 8, 9], 'reverse': []},
|
||
'Agreeableness': {'items': [11, 12], 'reverse': [10]},
|
||
'Conscientiousness': {'items': [13, 15], 'reverse': [14]},
|
||
}
|
||
|
||
|
||
# =============================================================================
|
||
# DATA LOADING
|
||
# =============================================================================
|
||
|
||
def load_csv(filename):
|
||
"""Load a CSV, return DataFrame with all columns by position (iloc)."""
|
||
path = BASE / filename
|
||
df = pd.read_csv(path, encoding='utf-8-sig')
|
||
return df
|
||
|
||
|
||
def safe_numeric(series):
|
||
"""Convert series to numeric, coercing errors to NaN."""
|
||
return pd.to_numeric(series, errors='coerce')
|
||
|
||
|
||
def compute_imi(df, subscale, offset=0):
|
||
"""Compute IMI subscale mean from df using column indices.
|
||
offset: shift all indices if the columns start at a different position."""
|
||
items = [df.iloc[:, i + offset] for i in subscale['items']]
|
||
reverse = [8 - df.iloc[:, i + offset] for i in subscale['reverse']] # 7-point: reverse = 8 - x
|
||
all_items = [safe_numeric(s) for s in items + reverse]
|
||
return pd.concat(all_items, axis=1).mean(axis=1)
|
||
|
||
|
||
def compute_sus(df, cols):
|
||
"""Compute SUS score (0-100) from 10 items at given column indices.
|
||
Odd items (1,3,5,7,9): score = response - 1
|
||
Even items (2,4,6,8,10): score = 5 - response
|
||
SUS = sum * 2.5"""
|
||
scores = []
|
||
for i, col_idx in enumerate(cols):
|
||
val = safe_numeric(df.iloc[:, col_idx])
|
||
if i % 2 == 0: # odd items (0-indexed even)
|
||
scores.append(val - 1)
|
||
else: # even items (0-indexed odd)
|
||
scores.append(5 - val)
|
||
return pd.concat(scores, axis=1).sum(axis=1) * 2.5
|
||
|
||
|
||
def compute_ueqs(df, cols):
|
||
"""Compute UEQ-S pragmatic and hedonic quality.
|
||
Items are on 1-7 scale, centered to -3 to +3."""
|
||
vals = pd.concat([safe_numeric(df.iloc[:, c]) for c in cols], axis=1)
|
||
centered = vals - 4 # center: 1→-3, 4→0, 7→3
|
||
pragmatic = centered.iloc[:, UEQS_PRAGMATIC].mean(axis=1)
|
||
hedonic = centered.iloc[:, UEQS_HEDONIC].mean(axis=1)
|
||
overall = centered.mean(axis=1)
|
||
return pragmatic, hedonic, overall
|
||
|
||
|
||
def compute_nasatlx(df, cols):
|
||
"""Compute NASA-TLX subscales and overall workload.
|
||
All items 1-7 scale. Item 4 (Performance/success) is inverted: high = good."""
|
||
subs = {}
|
||
for i, label in enumerate(NASATLX_LABELS):
|
||
val = safe_numeric(df.iloc[:, cols[i]])
|
||
if i == 3: # Performance — reverse so high = high workload
|
||
subs[label] = 8 - val
|
||
else:
|
||
subs[label] = val
|
||
overall = pd.DataFrame(subs).mean(axis=1)
|
||
return subs, overall
|
||
|
||
|
||
def compute_godspeed(df):
|
||
"""Compute Godspeed subscales from tutoring questionnaire."""
|
||
result = {}
|
||
for name, cols in GODSPEED_SUBSCALES.items():
|
||
vals = pd.concat([safe_numeric(df.iloc[:, c]) for c in cols], axis=1)
|
||
result[name] = vals.mean(axis=1)
|
||
result['Overall'] = pd.concat([safe_numeric(df.iloc[:, c]) for c in GODSPEED_COLS], axis=1).mean(axis=1)
|
||
return result
|
||
|
||
|
||
def compute_social_presence(df):
|
||
"""Compute social presence from 5 items."""
|
||
vals = pd.concat([safe_numeric(df.iloc[:, c]) for c in SOCIAL_PRESENCE_COLS], axis=1)
|
||
return vals, vals.mean(axis=1)
|
||
|
||
|
||
def compute_bfi(df):
|
||
"""Compute BFI-15 trait scores."""
|
||
traits = {}
|
||
for trait, spec in BFI_TRAITS.items():
|
||
items = [safe_numeric(df.iloc[:, i]) for i in spec['items']]
|
||
reverse = [8 - safe_numeric(df.iloc[:, i]) for i in spec['reverse']]
|
||
all_items = items + reverse
|
||
traits[trait] = pd.concat(all_items, axis=1).mean(axis=1)
|
||
return traits
|
||
|
||
|
||
# =============================================================================
|
||
# MAIN
|
||
# =============================================================================
|
||
|
||
def main():
|
||
print("Loading questionnaire data...")
|
||
|
||
# Load all questionnaires
|
||
pre_read = load_csv("Pre-Test-Reading.csv")
|
||
post_read = load_csv("Post-Questionnaire-Reading.csv")
|
||
pre_tutor = load_csv("Pre-Test-Tutoring.csv")
|
||
post_tutor = load_csv("Post-Questionnaire-Tutoring.csv")
|
||
final = load_csv("Final-Questionnaire.csv")
|
||
test_scores = load_csv("test_scores_all.csv")
|
||
|
||
# Normalize Zeitpunkt in test scores
|
||
test_scores['Zeitpunkt'] = test_scores['Zeitpunkt'].str.strip().replace('Pre-Tutor', 'Pre-Tutoring')
|
||
|
||
# Add Medium/Participant columns as named references
|
||
# IMPORTANT: capture values BEFORE adding new columns, because iloc[:, -1]
|
||
# shifts when new columns are appended to the DataFrame.
|
||
for df in [post_read, post_tutor, pre_read, pre_tutor]:
|
||
med_vals = df.iloc[:, -2].values.copy()
|
||
part_vals = df.iloc[:, -1].values.copy()
|
||
df['Medium_col'] = med_vals
|
||
df['Participant_col'] = part_vals
|
||
|
||
print(f" Pre-Reading: {len(pre_read)} rows")
|
||
print(f" Post-Reading: {len(post_read)} rows")
|
||
print(f" Pre-Tutoring: {len(pre_tutor)} rows")
|
||
print(f" Post-Tutoring:{len(post_tutor)} rows")
|
||
print(f" Final: {len(final)} rows")
|
||
|
||
plot_num = 0
|
||
|
||
def save(fig, name, desc):
|
||
nonlocal plot_num
|
||
plot_num += 1
|
||
tag = f"Q{plot_num:02d}"
|
||
fname = f"{tag}_{name}.png"
|
||
fig.savefig(PLOT_DIR / fname)
|
||
plt.close(fig)
|
||
print(f" [{tag}] {desc}")
|
||
|
||
# =========================================================================
|
||
# Q01: IMI Subscales — Reading vs Tutoring by Medium
|
||
# =========================================================================
|
||
print("\nA. Intrinsic Motivation (IMI)")
|
||
|
||
# Compute IMI for reading phase
|
||
imi_read = pd.DataFrame({
|
||
sub['label']: compute_imi(post_read, sub) for sub in IMI_SUBSCALES
|
||
})
|
||
imi_read['Medium'] = post_read['Medium_col']
|
||
imi_read['Phase'] = 'Reading'
|
||
|
||
# Compute IMI for tutoring phase
|
||
imi_tutor = pd.DataFrame({
|
||
sub['label']: compute_imi(post_tutor, sub) for sub in IMI_SUBSCALES
|
||
})
|
||
imi_tutor['Medium'] = post_tutor['Medium_col']
|
||
imi_tutor['Phase'] = 'Tutoring'
|
||
|
||
imi_all = pd.concat([imi_read, imi_tutor], ignore_index=True)
|
||
|
||
fig, axes = plt.subplots(1, 3, figsize=(15, 5), sharey=True)
|
||
for ax, sub in zip(axes, IMI_SUBSCALES):
|
||
label = sub['label']
|
||
data_r = imi_all[imi_all['Phase'] == 'Reading']
|
||
data_t = imi_all[imi_all['Phase'] == 'Tutoring']
|
||
|
||
x = np.arange(3)
|
||
width = 0.35
|
||
means_r = [data_r[data_r['Medium'] == m][label].mean() for m in MEDIUMS]
|
||
means_t = [data_t[data_t['Medium'] == m][label].mean() for m in MEDIUMS]
|
||
sems_r = [data_r[data_r['Medium'] == m][label].sem() for m in MEDIUMS]
|
||
sems_t = [data_t[data_t['Medium'] == m][label].sem() for m in MEDIUMS]
|
||
|
||
bars_r = ax.bar(x - width/2, means_r, width, yerr=sems_r, capsize=3,
|
||
label='Reading', color='#BBDEFB', edgecolor='#1565C0', linewidth=1)
|
||
bars_t = ax.bar(x + width/2, means_t, width, yerr=sems_t, capsize=3,
|
||
label='Tutoring', color='#C8E6C9', edgecolor='#2E7D32', linewidth=1)
|
||
|
||
ax.set_xticks(x)
|
||
ax.set_xticklabels(MEDIUMS)
|
||
ax.set_title(label.replace('\n', ' '), fontsize=12, fontweight='bold')
|
||
ax.set_ylim(1, 7)
|
||
ax.yaxis.set_major_locator(ticker.MultipleLocator(1))
|
||
if ax == axes[0]:
|
||
ax.set_ylabel('Mean Score (1-7)')
|
||
ax.legend(fontsize=9)
|
||
|
||
fig.suptitle('IMI Subscales: Reading vs Tutoring by Medium', fontsize=14, fontweight='bold')
|
||
fig.tight_layout()
|
||
save(fig, 'imi_by_medium', 'IMI subscales by medium (Reading vs Tutoring)')
|
||
|
||
# =========================================================================
|
||
# Q02: SUS by Medium (Tutoring only)
|
||
# =========================================================================
|
||
print("\nB. System Usability (SUS)")
|
||
|
||
sus_scores = compute_sus(post_tutor, SUS_COLS)
|
||
sus_df = pd.DataFrame({'SUS': sus_scores, 'Medium': post_tutor['Medium_col']})
|
||
|
||
fig, ax = plt.subplots(figsize=(8, 5))
|
||
for i, m in enumerate(MEDIUMS):
|
||
vals = sus_df[sus_df['Medium'] == m]['SUS'].dropna()
|
||
bp = ax.boxplot([vals], positions=[i], widths=0.5, patch_artist=True,
|
||
boxprops=dict(facecolor=MED_COLORS[m], alpha=0.6),
|
||
medianprops=dict(color='black', linewidth=2))
|
||
ax.scatter([i]*len(vals), vals, color=MED_COLORS[m], alpha=0.7, zorder=3, s=40)
|
||
ax.text(i, vals.mean() + 2, f'M={vals.mean():.1f}', ha='center', fontsize=10, fontweight='bold')
|
||
|
||
ax.set_xticks(range(3))
|
||
ax.set_xticklabels(MEDIUMS, fontsize=12)
|
||
ax.set_ylabel('SUS Score (0-100)')
|
||
ax.set_ylim(0, 105)
|
||
ax.axhline(68, color='gray', ls='--', alpha=0.5, label='Average threshold (68)')
|
||
ax.axhline(80, color='green', ls='--', alpha=0.3, label='Good threshold (80)')
|
||
ax.legend(fontsize=9)
|
||
ax.set_title('System Usability Scale (SUS) by Medium — Tutoring Phase', fontsize=13, fontweight='bold')
|
||
fig.tight_layout()
|
||
save(fig, 'sus_by_medium', 'SUS scores by medium')
|
||
|
||
# =========================================================================
|
||
# Q03: UEQ-S — Pragmatic vs Hedonic, Reading vs Tutoring
|
||
# =========================================================================
|
||
print("\nC. User Experience (UEQ-S)")
|
||
|
||
# Reading
|
||
prag_r, hed_r, overall_r = compute_ueqs(post_read, UEQS_COLS_READING)
|
||
ueq_read = pd.DataFrame({'Pragmatic': prag_r, 'Hedonic': hed_r, 'Overall': overall_r,
|
||
'Medium': post_read['Medium_col'], 'Phase': 'Reading'})
|
||
# Tutoring
|
||
prag_t, hed_t, overall_t = compute_ueqs(post_tutor, UEQS_COLS_TUTORING)
|
||
ueq_tutor = pd.DataFrame({'Pragmatic': prag_t, 'Hedonic': hed_t, 'Overall': overall_t,
|
||
'Medium': post_tutor['Medium_col'], 'Phase': 'Tutoring'})
|
||
|
||
ueq_all = pd.concat([ueq_read, ueq_tutor], ignore_index=True)
|
||
|
||
fig, axes = plt.subplots(1, 3, figsize=(15, 5), sharey=True)
|
||
for ax, dim in zip(axes, ['Pragmatic', 'Hedonic', 'Overall']):
|
||
x = np.arange(3)
|
||
width = 0.35
|
||
means_r = [ueq_all[(ueq_all['Phase']=='Reading') & (ueq_all['Medium']==m)][dim].mean() for m in MEDIUMS]
|
||
means_t = [ueq_all[(ueq_all['Phase']=='Tutoring') & (ueq_all['Medium']==m)][dim].mean() for m in MEDIUMS]
|
||
sems_r = [ueq_all[(ueq_all['Phase']=='Reading') & (ueq_all['Medium']==m)][dim].sem() for m in MEDIUMS]
|
||
sems_t = [ueq_all[(ueq_all['Phase']=='Tutoring') & (ueq_all['Medium']==m)][dim].sem() for m in MEDIUMS]
|
||
|
||
ax.bar(x - width/2, means_r, width, yerr=sems_r, capsize=3,
|
||
label='Reading', color='#BBDEFB', edgecolor='#1565C0', linewidth=1)
|
||
ax.bar(x + width/2, means_t, width, yerr=sems_t, capsize=3,
|
||
label='Tutoring', color='#C8E6C9', edgecolor='#2E7D32', linewidth=1)
|
||
|
||
ax.set_xticks(x)
|
||
ax.set_xticklabels(MEDIUMS)
|
||
ax.set_title(dim, fontsize=12, fontweight='bold')
|
||
ax.set_ylim(-3, 3)
|
||
ax.axhline(0, color='black', ls='-', lw=0.5)
|
||
ax.axhline(0.8, color='green', ls=':', alpha=0.4, label='Good (>0.8)' if dim == 'Overall' else None)
|
||
ax.axhline(-0.8, color='red', ls=':', alpha=0.4, label='Bad (<-0.8)' if dim == 'Overall' else None)
|
||
if ax == axes[0]:
|
||
ax.set_ylabel('UEQ-S Score (-3 to +3)')
|
||
ax.legend(fontsize=8)
|
||
if ax == axes[2]:
|
||
ax.legend(fontsize=8)
|
||
|
||
fig.suptitle('UEQ-S: Pragmatic & Hedonic Quality — Reading vs Tutoring', fontsize=14, fontweight='bold')
|
||
fig.tight_layout()
|
||
save(fig, 'ueqs_by_medium', 'UEQ-S pragmatic/hedonic by medium')
|
||
|
||
# =========================================================================
|
||
# Q04: NASA-TLX — Reading vs Tutoring
|
||
# =========================================================================
|
||
print("\nD. Workload (NASA-TLX)")
|
||
|
||
tlx_r_subs, tlx_r_overall = compute_nasatlx(post_read, NASATLX_COLS_READING)
|
||
tlx_r = pd.DataFrame(tlx_r_subs)
|
||
tlx_r['Overall'] = tlx_r_overall
|
||
tlx_r['Medium'] = post_read['Medium_col']
|
||
tlx_r['Phase'] = 'Reading'
|
||
|
||
tlx_t_subs, tlx_t_overall = compute_nasatlx(post_tutor, NASATLX_COLS_TUTORING)
|
||
tlx_t = pd.DataFrame(tlx_t_subs)
|
||
tlx_t['Overall'] = tlx_t_overall
|
||
tlx_t['Medium'] = post_tutor['Medium_col']
|
||
tlx_t['Phase'] = 'Tutoring'
|
||
|
||
tlx_all = pd.concat([tlx_r, tlx_t], ignore_index=True)
|
||
|
||
# NASA-TLX: direct medium comparison for tutoring phase (grouped bar)
|
||
fig, ax = plt.subplots(figsize=(12, 5))
|
||
sub_names = NASATLX_LABELS + ['Overall']
|
||
x = np.arange(len(sub_names))
|
||
width = 0.25
|
||
for i, m in enumerate(MEDIUMS):
|
||
data_t = tlx_all[(tlx_all['Phase'] == 'Tutoring') & (tlx_all['Medium'] == m)]
|
||
means = [data_t[s].mean() for s in sub_names]
|
||
sems = [data_t[s].sem() for s in sub_names]
|
||
ax.bar(x + (i - 1) * width, means, width, yerr=sems, capsize=2,
|
||
label=m, color=MED_COLORS[m], edgecolor='gray', linewidth=0.5)
|
||
|
||
ax.set_xticks(x)
|
||
ax.set_xticklabels(sub_names, fontsize=10)
|
||
ax.set_ylim(1, 7)
|
||
ax.set_ylabel('Workload Rating (1-7)')
|
||
ax.legend(fontsize=10)
|
||
ax.set_title('NASA-TLX Workload by Medium — Tutoring Phase', fontsize=13, fontweight='bold')
|
||
fig.tight_layout()
|
||
save(fig, 'nasatlx_by_medium', 'NASA-TLX by medium (Tutoring)')
|
||
|
||
# NASA-TLX: Reading vs Tutoring comparison (all mediums combined + per-medium)
|
||
fig, axes = plt.subplots(1, 2, figsize=(14, 5))
|
||
|
||
# Left: Overall workload Reading vs Tutoring per medium
|
||
ax = axes[0]
|
||
x = np.arange(3)
|
||
width = 0.35
|
||
for phase_idx, (phase, color, edge) in enumerate([
|
||
('Reading', '#BBDEFB', '#1565C0'), ('Tutoring', '#C8E6C9', '#2E7D32')
|
||
]):
|
||
means = [tlx_all[(tlx_all['Phase']==phase) & (tlx_all['Medium']==m)]['Overall'].mean() for m in MEDIUMS]
|
||
sems = [tlx_all[(tlx_all['Phase']==phase) & (tlx_all['Medium']==m)]['Overall'].sem() for m in MEDIUMS]
|
||
offset = -width/2 if phase_idx == 0 else width/2
|
||
ax.bar(x + offset, means, width, yerr=sems, capsize=3,
|
||
label=phase, color=color, edgecolor=edge, linewidth=1)
|
||
|
||
ax.set_xticks(x)
|
||
ax.set_xticklabels(MEDIUMS, fontsize=11)
|
||
ax.set_ylim(1, 7)
|
||
ax.set_ylabel('Overall Workload (1-7)')
|
||
ax.set_title('Overall Workload: Reading vs Tutoring', fontweight='bold')
|
||
ax.legend(fontsize=9)
|
||
|
||
# Right: Per-subscale comparison (Tutoring only, all mediums overlaid)
|
||
ax = axes[1]
|
||
sub_only = NASATLX_LABELS
|
||
x2 = np.arange(len(sub_only))
|
||
width2 = 0.25
|
||
for i, m in enumerate(MEDIUMS):
|
||
data_t = tlx_all[(tlx_all['Phase'] == 'Tutoring') & (tlx_all['Medium'] == m)]
|
||
means = [data_t[s].mean() for s in sub_only]
|
||
ax.plot(x2, means, 'o-', color=MED_COLORS[m], label=m, linewidth=2, markersize=8)
|
||
|
||
ax.set_xticks(x2)
|
||
ax.set_xticklabels(sub_only, fontsize=8, rotation=20, ha='right')
|
||
ax.set_ylim(1, 7)
|
||
ax.set_ylabel('Rating (1-7)')
|
||
ax.set_title('Subscale Profiles by Medium (Tutoring)', fontweight='bold')
|
||
ax.legend(fontsize=9)
|
||
|
||
fig.suptitle('NASA-TLX Workload Comparison', fontsize=14, fontweight='bold')
|
||
fig.tight_layout()
|
||
save(fig, 'nasatlx_comparison', 'NASA-TLX reading vs tutoring comparison')
|
||
|
||
# =========================================================================
|
||
# Q05: Godspeed Subscales by Medium (Tutoring only)
|
||
# =========================================================================
|
||
print("\nE. Tutor Impression (Godspeed)")
|
||
|
||
godspeed = compute_godspeed(post_tutor)
|
||
gs_df = pd.DataFrame(godspeed)
|
||
gs_df['Medium'] = post_tutor['Medium_col']
|
||
|
||
gs_names = list(GODSPEED_SUBSCALES.keys()) + ['Overall']
|
||
fig, ax = plt.subplots(figsize=(12, 5))
|
||
x = np.arange(len(gs_names))
|
||
width = 0.25
|
||
for i, m in enumerate(MEDIUMS):
|
||
data = gs_df[gs_df['Medium'] == m]
|
||
means = [data[s].mean() for s in gs_names]
|
||
sems = [data[s].sem() for s in gs_names]
|
||
ax.bar(x + (i - 1) * width, means, width, yerr=sems, capsize=2,
|
||
label=m, color=MED_COLORS[m], edgecolor='gray', linewidth=0.5)
|
||
|
||
ax.set_xticks(x)
|
||
ax.set_xticklabels(gs_names, fontsize=10)
|
||
ax.set_ylim(1, 5)
|
||
ax.set_ylabel('Mean Rating (1-5)')
|
||
ax.legend()
|
||
ax.set_title('Godspeed Tutor Impression by Medium — Tutoring Phase', fontsize=13, fontweight='bold')
|
||
fig.tight_layout()
|
||
save(fig, 'godspeed_by_medium', 'Godspeed tutor impression by medium')
|
||
|
||
# =========================================================================
|
||
# Q06: Social Presence by Medium (Tutoring only)
|
||
# =========================================================================
|
||
print("\nF. Social Presence")
|
||
|
||
sp_items, sp_overall = compute_social_presence(post_tutor)
|
||
sp_items.columns = SOCIAL_PRESENCE_LABELS
|
||
sp_items['Overall'] = sp_overall
|
||
sp_items['Medium'] = post_tutor['Medium_col'].values
|
||
|
||
fig, ax = plt.subplots(figsize=(12, 5))
|
||
sp_names = SOCIAL_PRESENCE_LABELS + ['Overall']
|
||
x = np.arange(len(sp_names))
|
||
width = 0.25
|
||
for i, m in enumerate(MEDIUMS):
|
||
data = sp_items[sp_items['Medium'] == m]
|
||
means = [data[s].mean() for s in sp_names]
|
||
sems = [data[s].sem() for s in sp_names]
|
||
ax.bar(x + (i-1)*width, means, width, yerr=sems, capsize=2,
|
||
label=m, color=MED_COLORS[m], edgecolor='gray', linewidth=0.5)
|
||
|
||
ax.set_xticks(x)
|
||
ax.set_xticklabels(sp_names, fontsize=9)
|
||
ax.set_ylim(1, 5)
|
||
ax.set_ylabel('Rating (1-5)')
|
||
ax.legend()
|
||
ax.set_title('Social Presence (Legacy) by Medium — Tutoring Phase\n(Only filled by participants who wore Meta Quest Pro; N varies by medium)',
|
||
fontsize=11, fontweight='bold')
|
||
fig.tight_layout()
|
||
save(fig, 'social_presence_by_medium', 'Social presence by medium')
|
||
|
||
# =========================================================================
|
||
# Q07: Cybersickness by Medium (VR only, but show all for comparison)
|
||
# =========================================================================
|
||
print("\nG. Cybersickness")
|
||
|
||
cyber = pd.DataFrame({
|
||
label: safe_numeric(post_tutor.iloc[:, col])
|
||
for label, col in zip(CYBERSICKNESS_LABELS, CYBERSICKNESS_COLS)
|
||
})
|
||
# Recode: "Nein" → 0 (no symptom), "Ja" → 1 (has symptom)... wait, these might be Yes/No
|
||
# Check the actual values — they seem to be "Ja"/"Nein" strings
|
||
# Let me handle both cases
|
||
for label in CYBERSICKNESS_LABELS:
|
||
col_data = post_tutor.iloc[:, CYBERSICKNESS_COLS[CYBERSICKNESS_LABELS.index(label)]]
|
||
if col_data.dtype == object:
|
||
# String data: map Ja=1, Nein=0
|
||
cyber[label] = col_data.map({'Ja': 1, 'Nein': 0, 'ja': 1, 'nein': 0})
|
||
else:
|
||
cyber[label] = safe_numeric(col_data)
|
||
|
||
cyber['Medium'] = post_tutor['Medium_col'].values
|
||
|
||
# Check if binary or scale
|
||
is_binary = cyber[CYBERSICKNESS_LABELS[0]].dropna().isin([0, 1]).all()
|
||
|
||
fig, ax = plt.subplots(figsize=(10, 5))
|
||
x = np.arange(len(CYBERSICKNESS_LABELS))
|
||
width = 0.25
|
||
|
||
if is_binary:
|
||
# Show percentage reporting symptoms
|
||
for i, m in enumerate(MEDIUMS):
|
||
data = cyber[cyber['Medium'] == m]
|
||
pcts = [(data[s] == 1).sum() / len(data) * 100 for s in CYBERSICKNESS_LABELS]
|
||
ax.bar(x + (i-1)*width, pcts, width, label=m, color=MED_COLORS[m],
|
||
edgecolor='gray', linewidth=0.5)
|
||
ax.set_ylabel('% Reporting Symptom')
|
||
ax.set_ylim(0, 100)
|
||
else:
|
||
for i, m in enumerate(MEDIUMS):
|
||
data = cyber[cyber['Medium'] == m]
|
||
means = [data[s].mean() for s in CYBERSICKNESS_LABELS]
|
||
sems = [data[s].sem() for s in CYBERSICKNESS_LABELS]
|
||
ax.bar(x + (i-1)*width, means, width, yerr=sems, capsize=2,
|
||
label=m, color=MED_COLORS[m], edgecolor='gray', linewidth=0.5)
|
||
ax.set_ylabel('Severity Rating')
|
||
|
||
ax.set_xticks(x)
|
||
ax.set_xticklabels(CYBERSICKNESS_LABELS, fontsize=9)
|
||
ax.legend()
|
||
ax.set_title('Cybersickness Symptoms by Medium — Tutoring Phase', fontsize=13, fontweight='bold')
|
||
fig.tight_layout()
|
||
save(fig, 'cybersickness_by_medium', 'Cybersickness by medium')
|
||
|
||
# =========================================================================
|
||
# Q08: Stress / Readiness / Relaxation — Pre-Reading vs Pre-Tutoring
|
||
# =========================================================================
|
||
print("\nH. Pre-Session States")
|
||
|
||
pre_r_df = pd.DataFrame({
|
||
'Stressed': safe_numeric(pre_read.iloc[:, STRESS_COL]),
|
||
'Ready': safe_numeric(pre_read.iloc[:, READY_COL]),
|
||
'Relaxed': safe_numeric(pre_read.iloc[:, RELAXED_COL]),
|
||
'Medium': pre_read['Medium_col'],
|
||
'Phase': 'Pre-Reading'
|
||
})
|
||
pre_t_df = pd.DataFrame({
|
||
'Stressed': safe_numeric(pre_tutor.iloc[:, STRESS_COL]),
|
||
'Ready': safe_numeric(pre_tutor.iloc[:, READY_COL]),
|
||
'Relaxed': safe_numeric(pre_tutor.iloc[:, RELAXED_COL]),
|
||
'Medium': pre_tutor['Medium_col'],
|
||
'Phase': 'Pre-Tutoring'
|
||
})
|
||
pre_all = pd.concat([pre_r_df, pre_t_df], ignore_index=True)
|
||
|
||
fig, axes = plt.subplots(1, 3, figsize=(15, 5), sharey=True)
|
||
for ax, item in zip(axes, ['Stressed', 'Ready', 'Relaxed']):
|
||
x = np.arange(3)
|
||
width = 0.35
|
||
means_r = [pre_all[(pre_all['Phase']=='Pre-Reading') & (pre_all['Medium']==m)][item].mean() for m in MEDIUMS]
|
||
means_t = [pre_all[(pre_all['Phase']=='Pre-Tutoring') & (pre_all['Medium']==m)][item].mean() for m in MEDIUMS]
|
||
sems_r = [pre_all[(pre_all['Phase']=='Pre-Reading') & (pre_all['Medium']==m)][item].sem() for m in MEDIUMS]
|
||
sems_t = [pre_all[(pre_all['Phase']=='Pre-Tutoring') & (pre_all['Medium']==m)][item].sem() for m in MEDIUMS]
|
||
|
||
ax.bar(x - width/2, means_r, width, yerr=sems_r, capsize=3,
|
||
label='Pre-Reading', color='#BBDEFB', edgecolor='#1565C0')
|
||
ax.bar(x + width/2, means_t, width, yerr=sems_t, capsize=3,
|
||
label='Pre-Tutoring', color='#C8E6C9', edgecolor='#2E7D32')
|
||
|
||
ax.set_xticks(x)
|
||
ax.set_xticklabels(MEDIUMS)
|
||
ax.set_title(item, fontsize=12, fontweight='bold')
|
||
ax.set_ylim(1, 7)
|
||
if ax == axes[0]:
|
||
ax.set_ylabel('Rating (1-7)')
|
||
ax.legend(fontsize=9)
|
||
|
||
fig.suptitle('Pre-Session State: Reading vs Tutoring by Medium', fontsize=14, fontweight='bold')
|
||
fig.tight_layout()
|
||
save(fig, 'pre_session_states', 'Pre-session stress/readiness/relaxation')
|
||
|
||
# =========================================================================
|
||
# Q09: IOS (Inclusion of Other in Self) by Medium
|
||
# =========================================================================
|
||
print("\nI. Additional Measures")
|
||
|
||
ios_data = pd.DataFrame({
|
||
'IOS': safe_numeric(post_tutor.iloc[:, IOS_COL]),
|
||
'Medium': post_tutor['Medium_col']
|
||
})
|
||
|
||
fig, axes = plt.subplots(1, 3, figsize=(15, 4))
|
||
|
||
# IOS
|
||
ax = axes[0]
|
||
for i, m in enumerate(MEDIUMS):
|
||
vals = ios_data[ios_data['Medium'] == m]['IOS'].dropna()
|
||
bp = ax.boxplot([vals], positions=[i], widths=0.5, patch_artist=True,
|
||
boxprops=dict(facecolor=MED_COLORS[m], alpha=0.6),
|
||
medianprops=dict(color='black', linewidth=2))
|
||
ax.scatter([i]*len(vals), vals, color=MED_COLORS[m], alpha=0.7, zorder=3, s=30)
|
||
ax.text(i, vals.mean() + 0.3, f'M={vals.mean():.2f}', ha='center', fontsize=9, fontweight='bold')
|
||
ax.set_xticks(range(3))
|
||
ax.set_xticklabels(MEDIUMS)
|
||
ax.set_ylim(0.5, 7.5)
|
||
ax.set_ylabel('Closeness (1-7)')
|
||
ax.set_title('IOS: Closeness to Tutor', fontsize=11, fontweight='bold')
|
||
|
||
# Self-use willingness
|
||
ax = axes[1]
|
||
self_use = pd.DataFrame({
|
||
'Value': safe_numeric(post_tutor.iloc[:, EXTRA_COLS['self_use']]),
|
||
'Medium': post_tutor['Medium_col']
|
||
})
|
||
for i, m in enumerate(MEDIUMS):
|
||
vals = self_use[self_use['Medium'] == m]['Value'].dropna()
|
||
bp = ax.boxplot([vals], positions=[i], widths=0.5, patch_artist=True,
|
||
boxprops=dict(facecolor=MED_COLORS[m], alpha=0.6),
|
||
medianprops=dict(color='black', linewidth=2))
|
||
ax.scatter([i]*len(vals), vals, color=MED_COLORS[m], alpha=0.7, zorder=3, s=30)
|
||
ax.text(i, vals.mean() + 0.3, f'M={vals.mean():.2f}', ha='center', fontsize=9, fontweight='bold')
|
||
ax.set_xticks(range(3))
|
||
ax.set_xticklabels(MEDIUMS)
|
||
ax.set_ylim(0.5, 7.5)
|
||
ax.set_ylabel('Agreement (1-7)')
|
||
ax.set_title('Would Use This Method', fontsize=11, fontweight='bold')
|
||
|
||
# Felt helpful
|
||
ax = axes[2]
|
||
helpful = pd.DataFrame({
|
||
'Value': safe_numeric(post_tutor.iloc[:, EXTRA_COLS['felt_helpful']]),
|
||
'Medium': post_tutor['Medium_col']
|
||
})
|
||
for i, m in enumerate(MEDIUMS):
|
||
vals = helpful[helpful['Medium'] == m]['Value'].dropna()
|
||
bp = ax.boxplot([vals], positions=[i], widths=0.5, patch_artist=True,
|
||
boxprops=dict(facecolor=MED_COLORS[m], alpha=0.6),
|
||
medianprops=dict(color='black', linewidth=2))
|
||
ax.scatter([i]*len(vals), vals, color=MED_COLORS[m], alpha=0.7, zorder=3, s=30)
|
||
ax.text(i, vals.mean() + 0.3, f'M={vals.mean():.2f}', ha='center', fontsize=9, fontweight='bold')
|
||
ax.set_xticks(range(3))
|
||
ax.set_xticklabels(MEDIUMS)
|
||
ax.set_ylim(0.5, 7.5)
|
||
ax.set_ylabel('Agreement (1-7)')
|
||
ax.set_title('Felt Helpful for Review', fontsize=11, fontweight='bold')
|
||
|
||
fig.suptitle('Additional Tutoring Measures by Medium', fontsize=14, fontweight='bold')
|
||
fig.tight_layout()
|
||
save(fig, 'additional_measures', 'IOS, self-use, helpfulness by medium')
|
||
|
||
# =========================================================================
|
||
# Q10: Correlation Heatmap — Questionnaire Subscales vs Learning Gains
|
||
# =========================================================================
|
||
print("\nJ. Correlations")
|
||
|
||
# Build per-participant-topic tutoring gain
|
||
pre_scores = test_scores[test_scores['Zeitpunkt'] == 'Pre-Tutoring'].set_index(['Participant', 'Topic'])
|
||
post_scores = test_scores[test_scores['Zeitpunkt'] == 'Post-Tutoring'].set_index(['Participant', 'Topic'])
|
||
common_idx = pre_scores.index.intersection(post_scores.index)
|
||
gains = pd.DataFrame({
|
||
'Participant': [idx[0] for idx in common_idx],
|
||
'Topic': [idx[1] for idx in common_idx],
|
||
'Score_Gain': post_scores.loc[common_idx, 'Score_Pct'].astype(float).values - pre_scores.loc[common_idx, 'Score_Pct'].astype(float).values,
|
||
'Medium': post_scores.loc[common_idx, 'Medium'].values,
|
||
})
|
||
|
||
# Compute questionnaire scores per row of post_tutor and join with gains
|
||
q_scores = pd.DataFrame({
|
||
'Participant': post_tutor['Participant_col'].values,
|
||
'Medium': post_tutor['Medium_col'].values,
|
||
'IMI_Interest': compute_imi(post_tutor, IMI_INTEREST),
|
||
'IMI_Value': compute_imi(post_tutor, IMI_VALUE),
|
||
'IMI_Choice': compute_imi(post_tutor, IMI_CHOICE),
|
||
'SUS': compute_sus(post_tutor, SUS_COLS),
|
||
'NASA_TLX': compute_nasatlx(post_tutor, NASATLX_COLS_TUTORING)[1],
|
||
'Social_Presence': compute_social_presence(post_tutor)[1],
|
||
'IOS': safe_numeric(post_tutor.iloc[:, IOS_COL]),
|
||
})
|
||
|
||
# Add Godspeed overall
|
||
gs = compute_godspeed(post_tutor)
|
||
q_scores['Godspeed'] = gs['Overall']
|
||
|
||
# UEQ-S
|
||
_, _, ueq_overall_t = compute_ueqs(post_tutor, UEQS_COLS_TUTORING)
|
||
q_scores['UEQ_S'] = ueq_overall_t
|
||
|
||
# Match on participant + medium (since topic info isn't in questionnaire directly,
|
||
# we can aggregate by participant to get mean questionnaire scores)
|
||
q_agg = q_scores.groupby('Participant').mean(numeric_only=True)
|
||
gains_agg = gains.groupby('Participant')['Score_Gain'].mean()
|
||
|
||
merged = q_agg.join(gains_agg, how='inner')
|
||
|
||
# Correlation matrix (exclude Social Presence — VR-only, too many NaN)
|
||
corr_cols = ['IMI_Interest', 'IMI_Value', 'IMI_Choice', 'SUS', 'UEQ_S',
|
||
'NASA_TLX', 'IOS', 'Godspeed', 'Score_Gain']
|
||
corr_labels = ['IMI\nInterest', 'IMI\nValue', 'IMI\nChoice', 'SUS', 'UEQ-S',
|
||
'NASA-TLX', 'IOS', 'Godspeed', 'Tutoring\nScore Gain']
|
||
|
||
corr_data = merged[corr_cols]
|
||
corr_matrix = corr_data.corr()
|
||
|
||
# Also compute p-values
|
||
n = len(corr_data.dropna())
|
||
p_matrix = pd.DataFrame(np.ones((len(corr_cols), len(corr_cols))),
|
||
index=corr_cols, columns=corr_cols)
|
||
for i_c in range(len(corr_cols)):
|
||
for j_c in range(i_c + 1, len(corr_cols)):
|
||
valid = corr_data[[corr_cols[i_c], corr_cols[j_c]]].dropna()
|
||
if len(valid) >= 3:
|
||
r, p = stats.pearsonr(valid.iloc[:, 0], valid.iloc[:, 1])
|
||
p_matrix.iloc[i_c, j_c] = p
|
||
p_matrix.iloc[j_c, i_c] = p
|
||
|
||
fig, ax = plt.subplots(figsize=(10, 8))
|
||
n_vars = len(corr_cols)
|
||
|
||
# Relabel the correlation matrix
|
||
corr_plot = corr_matrix.copy()
|
||
corr_plot.index = corr_labels
|
||
corr_plot.columns = corr_labels
|
||
|
||
# Build annotation with significance stars
|
||
annot_strs = []
|
||
for i_c in range(n_vars):
|
||
row_strs = []
|
||
for j_c in range(n_vars):
|
||
r = corr_matrix.iloc[i_c, j_c]
|
||
if np.isnan(r):
|
||
row_strs.append('')
|
||
else:
|
||
p = p_matrix.iloc[i_c, j_c]
|
||
star = ''
|
||
if i_c != j_c:
|
||
if p < 0.01: star = '**'
|
||
elif p < 0.05: star = '*'
|
||
row_strs.append(f'{r:.2f}{star}')
|
||
annot_strs.append(row_strs)
|
||
annot_arr = np.array(annot_strs)
|
||
|
||
# Lower triangle mask
|
||
mask = np.triu(np.ones((n_vars, n_vars), dtype=bool), k=1)
|
||
|
||
sns.heatmap(corr_plot, mask=mask, annot=annot_arr, fmt='',
|
||
cmap='RdBu_r', center=0, vmin=-1, vmax=1,
|
||
linewidths=1, linecolor='white',
|
||
ax=ax, annot_kws={'fontsize': 10, 'fontweight': 'bold'},
|
||
cbar_kws={'shrink': 0.8})
|
||
|
||
ax.set_title('Questionnaire Subscale Correlations & Learning Gain\n(* p<.05, ** p<.01)',
|
||
fontsize=13, fontweight='bold')
|
||
fig.tight_layout()
|
||
save(fig, 'correlation_heatmap', 'Subscale correlations with learning gain')
|
||
|
||
# =========================================================================
|
||
# Q11: Reading vs Tutoring Phase Comparison Dashboard
|
||
# =========================================================================
|
||
print("\nK. Phase Comparisons")
|
||
|
||
fig, axes = plt.subplots(2, 2, figsize=(14, 10))
|
||
|
||
# IMI Interest: Reading vs Tutoring (overall means)
|
||
ax = axes[0, 0]
|
||
for sub in IMI_SUBSCALES:
|
||
label = sub['label'].replace('\n', ' ')
|
||
r_mean = compute_imi(post_read, sub).mean()
|
||
t_mean = compute_imi(post_tutor, sub).mean()
|
||
r_sem = compute_imi(post_read, sub).sem()
|
||
t_sem = compute_imi(post_tutor, sub).sem()
|
||
idx = IMI_SUBSCALES.index(sub)
|
||
ax.errorbar([0, 1], [r_mean, t_mean], yerr=[r_sem, t_sem],
|
||
marker='o', capsize=4, label=label, linewidth=2)
|
||
ax.set_xticks([0, 1])
|
||
ax.set_xticklabels(['Reading', 'Tutoring'])
|
||
ax.set_ylabel('Mean Score (1-7)')
|
||
ax.set_ylim(2, 6)
|
||
ax.set_title('IMI Subscales', fontweight='bold')
|
||
ax.legend(fontsize=8)
|
||
|
||
# NASA-TLX Overall: Reading vs Tutoring by Medium
|
||
ax = axes[0, 1]
|
||
for m in MEDIUMS:
|
||
r_vals = tlx_all[(tlx_all['Phase']=='Reading') & (tlx_all['Medium']==m)]['Overall']
|
||
t_vals = tlx_all[(tlx_all['Phase']=='Tutoring') & (tlx_all['Medium']==m)]['Overall']
|
||
ax.errorbar([0, 1], [r_vals.mean(), t_vals.mean()],
|
||
yerr=[r_vals.sem(), t_vals.sem()],
|
||
marker='o', capsize=4, label=m, color=MED_COLORS[m], linewidth=2)
|
||
ax.set_xticks([0, 1])
|
||
ax.set_xticklabels(['Reading', 'Tutoring'])
|
||
ax.set_ylabel('Workload (1-7)')
|
||
ax.set_title('NASA-TLX Overall', fontweight='bold')
|
||
ax.legend(fontsize=9)
|
||
|
||
# UEQ-S Overall: Reading vs Tutoring by Medium
|
||
ax = axes[1, 0]
|
||
for m in MEDIUMS:
|
||
r_vals = ueq_all[(ueq_all['Phase']=='Reading') & (ueq_all['Medium']==m)]['Overall']
|
||
t_vals = ueq_all[(ueq_all['Phase']=='Tutoring') & (ueq_all['Medium']==m)]['Overall']
|
||
ax.errorbar([0, 1], [r_vals.mean(), t_vals.mean()],
|
||
yerr=[r_vals.sem(), t_vals.sem()],
|
||
marker='o', capsize=4, label=m, color=MED_COLORS[m], linewidth=2)
|
||
ax.set_xticks([0, 1])
|
||
ax.set_xticklabels(['Reading', 'Tutoring'])
|
||
ax.set_ylabel('UEQ-S Score (-3 to +3)')
|
||
ax.set_title('UEQ-S Overall', fontweight='bold')
|
||
ax.axhline(0, color='black', ls='-', lw=0.5)
|
||
ax.legend(fontsize=9)
|
||
|
||
# Stress: Pre-Reading vs Pre-Tutoring by Medium
|
||
ax = axes[1, 1]
|
||
for m in MEDIUMS:
|
||
r_vals = pre_all[(pre_all['Phase']=='Pre-Reading') & (pre_all['Medium']==m)]['Stressed']
|
||
t_vals = pre_all[(pre_all['Phase']=='Pre-Tutoring') & (pre_all['Medium']==m)]['Stressed']
|
||
ax.errorbar([0, 1], [r_vals.mean(), t_vals.mean()],
|
||
yerr=[r_vals.sem(), t_vals.sem()],
|
||
marker='o', capsize=4, label=m, color=MED_COLORS[m], linewidth=2)
|
||
ax.set_xticks([0, 1])
|
||
ax.set_xticklabels(['Pre-Reading', 'Pre-Tutoring'])
|
||
ax.set_ylabel('Stress Rating (1-7)')
|
||
ax.set_title('Pre-Session Stress', fontweight='bold')
|
||
ax.legend(fontsize=9)
|
||
|
||
fig.suptitle('Reading vs Tutoring Phase Comparison', fontsize=15, fontweight='bold')
|
||
fig.tight_layout()
|
||
save(fig, 'phase_comparison_dashboard', 'Reading vs Tutoring phase comparison dashboard')
|
||
|
||
# =========================================================================
|
||
# Q12: VR-Specific Analysis (Social Presence + Cybersickness + Godspeed)
|
||
# =========================================================================
|
||
print("\nL. VR-Specific Analysis")
|
||
|
||
vr_data = post_tutor[post_tutor['Medium_col'] == 'VR']
|
||
|
||
fig, axes = plt.subplots(1, 3, figsize=(16, 5))
|
||
|
||
# Social Presence: VR vs others
|
||
ax = axes[0]
|
||
for i, m in enumerate(MEDIUMS):
|
||
data = sp_items[sp_items['Medium'] == m]
|
||
means = data['Overall'].mean()
|
||
sems = data['Overall'].sem()
|
||
color = MED_COLORS[m]
|
||
ax.bar(i, means, yerr=sems, capsize=4, color=color, edgecolor='gray',
|
||
width=0.6, label=m)
|
||
ax.text(i, means + sems + 0.15, f'{means:.2f}', ha='center', fontsize=10, fontweight='bold')
|
||
ax.set_xticks(range(3))
|
||
ax.set_xticklabels(MEDIUMS)
|
||
ax.set_ylabel('Mean Social Presence (1-5)')
|
||
ax.set_ylim(1, 5)
|
||
ax.set_title('Social Presence', fontweight='bold')
|
||
|
||
# Cybersickness: VR-specific item detail
|
||
ax = axes[1]
|
||
if is_binary:
|
||
vr_cyber = cyber[cyber['Medium'] == 'VR']
|
||
pcts = [(vr_cyber[s] == 1).sum() / len(vr_cyber) * 100 for s in CYBERSICKNESS_LABELS]
|
||
colors = ['#EF5350' if p > 30 else '#FFA726' if p > 10 else '#66BB6A' for p in pcts]
|
||
bars = ax.bar(range(len(CYBERSICKNESS_LABELS)), pcts, color=colors, edgecolor='gray')
|
||
for bar, p in zip(bars, pcts):
|
||
ax.text(bar.get_x() + bar.get_width()/2, bar.get_height() + 1.5,
|
||
f'{p:.0f}%', ha='center', fontsize=10, fontweight='bold')
|
||
ax.set_ylabel('% VR Participants Reporting')
|
||
ax.set_ylim(0, 100)
|
||
else:
|
||
vr_cyber = cyber[cyber['Medium'] == 'VR']
|
||
means = [vr_cyber[s].mean() for s in CYBERSICKNESS_LABELS]
|
||
sems = [vr_cyber[s].sem() for s in CYBERSICKNESS_LABELS]
|
||
ax.bar(range(len(CYBERSICKNESS_LABELS)), means, yerr=sems, capsize=3,
|
||
color='#66BB6A', edgecolor='gray')
|
||
ax.set_ylabel('Severity')
|
||
ax.set_xticks(range(len(CYBERSICKNESS_LABELS)))
|
||
ax.set_xticklabels(CYBERSICKNESS_LABELS, fontsize=8)
|
||
ax.set_title('VR Cybersickness', fontweight='bold')
|
||
|
||
# Godspeed: VR vs others
|
||
ax = axes[2]
|
||
gs_overall_by_m = gs_df.groupby('Medium')['Overall'].agg(['mean', 'sem'])
|
||
for i, m in enumerate(MEDIUMS):
|
||
mean_v = gs_overall_by_m.loc[m, 'mean']
|
||
sem_v = gs_overall_by_m.loc[m, 'sem']
|
||
ax.bar(i, mean_v, yerr=sem_v, capsize=4, color=MED_COLORS[m],
|
||
edgecolor='gray', width=0.6)
|
||
ax.text(i, mean_v + sem_v + 0.05, f'{mean_v:.2f}', ha='center', fontsize=10, fontweight='bold')
|
||
ax.set_xticks(range(3))
|
||
ax.set_xticklabels(MEDIUMS)
|
||
ax.set_ylabel('Mean Godspeed (1-5)')
|
||
ax.set_ylim(1, 5)
|
||
ax.set_title('Godspeed: Tutor Impression', fontweight='bold')
|
||
|
||
fig.suptitle('VR-Specific Comparisons', fontsize=14, fontweight='bold')
|
||
fig.tight_layout()
|
||
save(fig, 'vr_specific', 'VR-specific: social presence, cybersickness, Godspeed')
|
||
|
||
# =========================================================================
|
||
# Export statistics to CSV
|
||
# =========================================================================
|
||
|
||
def stats_by_medium(data_df, value_col, mediums=MEDIUMS):
|
||
rows = []
|
||
for m in mediums:
|
||
vals = data_df[data_df['Medium'] == m][value_col].dropna()
|
||
rows.append({
|
||
'Medium': m,
|
||
'N': len(vals),
|
||
'Mean': vals.mean(),
|
||
'SD': vals.std(),
|
||
'SEM': vals.sem(),
|
||
'Median': vals.median(),
|
||
'Min': vals.min(),
|
||
'Max': vals.max(),
|
||
})
|
||
return pd.DataFrame(rows)
|
||
|
||
# SUS
|
||
stats_by_medium(sus_df, 'SUS').to_csv(
|
||
STATS_DIR / 'questionnaire_sus_by_medium.csv', index=False, float_format='%.3f')
|
||
|
||
# IMI subscales (tutoring phase)
|
||
imi_rows = []
|
||
for sub in IMI_SUBSCALES:
|
||
label = sub['label'].replace('\n', ' ')
|
||
for m in MEDIUMS:
|
||
vals = imi_tutor[imi_tutor['Medium'] == m][sub['label']].dropna()
|
||
imi_rows.append({'Subscale': label, 'Phase': 'Tutoring', 'Medium': m,
|
||
'N': len(vals), 'Mean': vals.mean(), 'SD': vals.std(), 'SEM': vals.sem()})
|
||
for m in MEDIUMS:
|
||
vals = imi_read[imi_read['Medium'] == m][sub['label']].dropna()
|
||
imi_rows.append({'Subscale': label, 'Phase': 'Reading', 'Medium': m,
|
||
'N': len(vals), 'Mean': vals.mean(), 'SD': vals.std(), 'SEM': vals.sem()})
|
||
pd.DataFrame(imi_rows).to_csv(
|
||
STATS_DIR / 'questionnaire_imi_by_medium.csv', index=False, float_format='%.3f')
|
||
|
||
# UEQ-S
|
||
ueq_rows = []
|
||
for dim in ['Pragmatic', 'Hedonic', 'Overall']:
|
||
for phase in ['Reading', 'Tutoring']:
|
||
for m in MEDIUMS:
|
||
vals = ueq_all[(ueq_all['Phase'] == phase) & (ueq_all['Medium'] == m)][dim].dropna()
|
||
ueq_rows.append({'Dimension': dim, 'Phase': phase, 'Medium': m,
|
||
'N': len(vals), 'Mean': vals.mean(), 'SD': vals.std(), 'SEM': vals.sem()})
|
||
pd.DataFrame(ueq_rows).to_csv(
|
||
STATS_DIR / 'questionnaire_ueqs_by_medium.csv', index=False, float_format='%.3f')
|
||
|
||
# NASA-TLX
|
||
tlx_rows = []
|
||
sub_names_all = NASATLX_LABELS + ['Overall']
|
||
for phase in ['Reading', 'Tutoring']:
|
||
for m in MEDIUMS:
|
||
data = tlx_all[(tlx_all['Phase'] == phase) & (tlx_all['Medium'] == m)]
|
||
for s in sub_names_all:
|
||
vals = data[s].dropna()
|
||
tlx_rows.append({'Subscale': s.replace('\n', ' '), 'Phase': phase, 'Medium': m,
|
||
'N': len(vals), 'Mean': vals.mean(), 'SD': vals.std(), 'SEM': vals.sem()})
|
||
pd.DataFrame(tlx_rows).to_csv(
|
||
STATS_DIR / 'questionnaire_nasatlx_by_medium.csv', index=False, float_format='%.3f')
|
||
|
||
# Godspeed
|
||
gs_rows = []
|
||
for subscale in list(GODSPEED_SUBSCALES.keys()) + ['Overall']:
|
||
for m in MEDIUMS:
|
||
vals = gs_df[gs_df['Medium'] == m][subscale].dropna()
|
||
gs_rows.append({'Subscale': subscale.replace('\n', ' '), 'Medium': m,
|
||
'N': len(vals), 'Mean': vals.mean(), 'SD': vals.std(), 'SEM': vals.sem()})
|
||
pd.DataFrame(gs_rows).to_csv(
|
||
STATS_DIR / 'questionnaire_godspeed_by_medium.csv', index=False, float_format='%.3f')
|
||
|
||
# Social Presence
|
||
sp_rows = []
|
||
for item_name in SOCIAL_PRESENCE_LABELS + ['Overall']:
|
||
for m in MEDIUMS:
|
||
vals = sp_items[sp_items['Medium'] == m][item_name].dropna()
|
||
sp_rows.append({'Item': item_name, 'Medium': m,
|
||
'N': len(vals), 'Mean': vals.mean(), 'SD': vals.std(), 'SEM': vals.sem()})
|
||
pd.DataFrame(sp_rows).to_csv(
|
||
STATS_DIR / 'questionnaire_social_presence_by_medium.csv', index=False, float_format='%.3f')
|
||
|
||
# IOS + Extra measures
|
||
ios_rows = []
|
||
for col_name, col_key in [('IOS', 'IOS'), ('Self_Use', 'self_use'), ('Felt_Helpful', 'felt_helpful')]:
|
||
if col_name == 'IOS':
|
||
source_df = ios_data.rename(columns={'IOS': col_name})
|
||
else:
|
||
source_df = pd.DataFrame({
|
||
col_name: pd.to_numeric(post_tutor.iloc[:, EXTRA_COLS[col_key]], errors='coerce'),
|
||
'Medium': post_tutor['Medium_col']
|
||
})
|
||
for m in MEDIUMS:
|
||
vals = source_df[source_df['Medium'] == m][col_name].dropna()
|
||
ios_rows.append({'Measure': col_name, 'Medium': m,
|
||
'N': len(vals), 'Mean': vals.mean(), 'SD': vals.std(), 'SEM': vals.sem()})
|
||
pd.DataFrame(ios_rows).to_csv(
|
||
STATS_DIR / 'questionnaire_additional_by_medium.csv', index=False, float_format='%.3f')
|
||
|
||
# Correlation matrix with p-values
|
||
corr_export_rows = []
|
||
for i_c in range(len(corr_cols)):
|
||
for j_c in range(len(corr_cols)):
|
||
corr_export_rows.append({
|
||
'Var1': corr_cols[i_c],
|
||
'Var2': corr_cols[j_c],
|
||
'r': corr_matrix.iloc[i_c, j_c],
|
||
'p': p_matrix.iloc[i_c, j_c],
|
||
'sig': ('**' if p_matrix.iloc[i_c, j_c] < 0.01
|
||
else '*' if p_matrix.iloc[i_c, j_c] < 0.05 else ''),
|
||
})
|
||
pd.DataFrame(corr_export_rows).to_csv(
|
||
STATS_DIR / 'questionnaire_correlations.csv', index=False, float_format='%.4f')
|
||
|
||
# Pre-session states
|
||
pre_state_rows = []
|
||
for item in ['Stressed', 'Ready', 'Relaxed']:
|
||
for phase in ['Pre-Reading', 'Pre-Tutoring']:
|
||
for m in MEDIUMS:
|
||
vals = pre_all[(pre_all['Phase'] == phase) & (pre_all['Medium'] == m)][item].dropna()
|
||
pre_state_rows.append({'Item': item, 'Phase': phase, 'Medium': m,
|
||
'N': len(vals), 'Mean': vals.mean(), 'SD': vals.std(), 'SEM': vals.sem()})
|
||
pd.DataFrame(pre_state_rows).to_csv(
|
||
STATS_DIR / 'questionnaire_pre_session_states.csv', index=False, float_format='%.3f')
|
||
|
||
print(f"\n Stats exported to: {STATS_DIR}")
|
||
|
||
# =========================================================================
|
||
# Print summary statistics
|
||
# =========================================================================
|
||
print("\n" + "=" * 70)
|
||
print("QUESTIONNAIRE SUMMARY STATISTICS")
|
||
print("=" * 70)
|
||
|
||
# SUS
|
||
print("\nSUS Scores by Medium:")
|
||
for m in MEDIUMS:
|
||
vals = sus_df[sus_df['Medium'] == m]['SUS'].dropna()
|
||
print(f" {m}: M={vals.mean():.1f}, SD={vals.std():.1f}, Median={vals.median():.1f}")
|
||
|
||
# IMI
|
||
print("\nIMI by Medium (Tutoring):")
|
||
for sub in IMI_SUBSCALES:
|
||
label = sub['label'].replace('\n', ' ')
|
||
for m in MEDIUMS:
|
||
vals = imi_tutor[imi_tutor['Medium'] == m][sub['label']].dropna()
|
||
print(f" {label}/{m}: M={vals.mean():.2f}, SD={vals.std():.2f}")
|
||
|
||
# UEQ-S
|
||
print("\nUEQ-S Overall by Medium (Tutoring):")
|
||
for m in MEDIUMS:
|
||
vals = ueq_tutor[ueq_tutor['Medium'] == m]['Overall'].dropna()
|
||
print(f" {m}: M={vals.mean():.2f}, SD={vals.std():.2f}")
|
||
|
||
# NASA-TLX
|
||
print("\nNASA-TLX Overall by Medium (Tutoring):")
|
||
for m in MEDIUMS:
|
||
vals = tlx_all[(tlx_all['Phase'] == 'Tutoring') & (tlx_all['Medium'] == m)]['Overall'].dropna()
|
||
print(f" {m}: M={vals.mean():.2f}, SD={vals.std():.2f}")
|
||
|
||
# Social Presence
|
||
print("\nSocial Presence Overall by Medium:")
|
||
for m in MEDIUMS:
|
||
vals = sp_items[sp_items['Medium'] == m]['Overall'].dropna()
|
||
print(f" {m}: M={vals.mean():.2f}, SD={vals.std():.2f}")
|
||
|
||
# Godspeed
|
||
print("\nGodspeed Overall by Medium:")
|
||
for m in MEDIUMS:
|
||
vals = gs_df[gs_df['Medium'] == m]['Overall'].dropna()
|
||
print(f" {m}: M={vals.mean():.2f}, SD={vals.std():.2f}")
|
||
|
||
# IOS
|
||
print("\nIOS by Medium:")
|
||
for m in MEDIUMS:
|
||
vals = ios_data[ios_data['Medium'] == m]['IOS'].dropna()
|
||
print(f" {m}: M={vals.mean():.2f}, SD={vals.std():.2f}")
|
||
|
||
print(f"\n{plot_num} plots saved to: {PLOT_DIR}")
|
||
|
||
|
||
if __name__ == "__main__":
|
||
main()
|