added ranking
This commit is contained in:
19
Data/medium_order.csv
Normal file
19
Data/medium_order.csv
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
Participant,Rank1,Rank2,Rank3
|
||||||
|
P1,VR,Chat,Video
|
||||||
|
P2,Chat,Video,VR
|
||||||
|
P3,VR,Chat,Video
|
||||||
|
P4,VR,Video,Chat
|
||||||
|
P5,VR,Chat,Video
|
||||||
|
P6,Chat,Video,VR
|
||||||
|
P7,Chat,Video,VR
|
||||||
|
P8,Chat,Video,VR
|
||||||
|
P9,Chat,VR,Video
|
||||||
|
P10,Chat,VR,Video
|
||||||
|
P11,Chat,VR,Video
|
||||||
|
P12,Chat,VR,Video
|
||||||
|
P13,Video,VR,Chat
|
||||||
|
P14,Chat,Video,VR
|
||||||
|
P15,VR,Video,Chat
|
||||||
|
P16,Chat,Video,VR
|
||||||
|
P17,VR,Video,Chat
|
||||||
|
P18,Chat,Video,VR
|
||||||
|
BIN
Data/plots_questionnaires/Q14_medium_ranking.png
Normal file
BIN
Data/plots_questionnaires/Q14_medium_ranking.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 118 KiB |
@ -395,6 +395,12 @@ Four-panel dashboard comparing reading and tutoring phases across IMI subscales,
|
|||||||
|
|
||||||
VR-specific panel comparing social presence, cybersickness, and Godspeed across mediums. VR achieves the highest social presence (M=3.01 vs Chat M=2.10), moderate cybersickness symptoms, and Godspeed impressions comparable to the other mediums. The elevated social presence in VR without a corresponding increase in Godspeed tutor impression suggests that VR enhances the sense of "being there" without necessarily changing how the tutor is perceived.
|
VR-specific panel comparing social presence, cybersickness, and Godspeed across mediums. VR achieves the highest social presence (M=3.01 vs Chat M=2.10), moderate cybersickness symptoms, and Godspeed impressions comparable to the other mediums. The elevated social presence in VR without a corresponding increase in Godspeed tutor impression suggests that VR enhances the sense of "being there" without necessarily changing how the tutor is perceived.
|
||||||
|
|
||||||
|
### F14 – Medium Preference Rankings (Friedman Test)
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
Within-subject ranking analysis: each of the 18 participants ranked the three tutoring mediums from 1 (most preferred) to 3 (least preferred). **Left panel** shows mean rank per condition with individual data points (jitter) and SEM error bars. **Right panel** shows the percentage of participants assigning each rank position to each condition. Chat received the lowest mean rank (M=1.61), indicating it was most often preferred first (50% of participants ranked it 1st). Video received the highest mean rank (M=2.33), most often placed last. A Friedman test showed no statistically significant difference across mediums (χ²(2) = 4.78, p = .092, Kendall's W = 0.13), likely due to limited power with N=18. Post-hoc Wilcoxon tests (Bonferroni-corrected α = .017) revealed a trend for Chat > Video (p = .027 raw, p = .080 adjusted) that did not survive correction.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Summary
|
## Summary
|
||||||
|
|||||||
197
analysis_medium_ranking.py
Normal file
197
analysis_medium_ranking.py
Normal file
@ -0,0 +1,197 @@
|
|||||||
|
"""
|
||||||
|
Friedman Test on medium preference rankings.
|
||||||
|
|
||||||
|
Data format: each row = one participant; Rank1/Rank2/Rank3 hold the
|
||||||
|
condition name (VR, Chat, Video) assigned to that rank position.
|
||||||
|
|
||||||
|
We reshape so every participant has a numeric rank (1–3) for every
|
||||||
|
condition, then run a Friedman test and save a summary plot.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import numpy as np
|
||||||
|
import pandas as pd
|
||||||
|
import matplotlib.pyplot as plt
|
||||||
|
import matplotlib.patches as mpatches
|
||||||
|
import seaborn as sns
|
||||||
|
from pathlib import Path
|
||||||
|
from scipy import stats
|
||||||
|
|
||||||
|
# ── 1. Load & reshape ─────────────────────────────────────────────────────────
|
||||||
|
df_raw = pd.read_csv("Data/medium_order.csv")
|
||||||
|
|
||||||
|
# Build a long table: (Participant, Condition, Rank)
|
||||||
|
records = []
|
||||||
|
for _, row in df_raw.iterrows():
|
||||||
|
for rank_pos, col in enumerate(["Rank1", "Rank2", "Rank3"], start=1):
|
||||||
|
records.append({
|
||||||
|
"Participant": row["Participant"],
|
||||||
|
"Condition": row[col],
|
||||||
|
"Rank": rank_pos,
|
||||||
|
})
|
||||||
|
|
||||||
|
df_long = pd.DataFrame(records)
|
||||||
|
|
||||||
|
# Pivot to wide: rows = participants, columns = conditions
|
||||||
|
df_wide = df_long.pivot(index="Participant", columns="Condition", values="Rank")
|
||||||
|
df_wide = df_wide[["VR", "Chat", "Video"]] # fix column order
|
||||||
|
|
||||||
|
print("=" * 55)
|
||||||
|
print("Reshaped ranking matrix (numeric ranks per condition)")
|
||||||
|
print("=" * 55)
|
||||||
|
print(df_wide.to_string())
|
||||||
|
print()
|
||||||
|
|
||||||
|
# ── 2. Descriptive statistics ─────────────────────────────────────────────────
|
||||||
|
desc = df_wide.agg(["mean", "median", "std"]).T
|
||||||
|
desc.index.name = "Condition"
|
||||||
|
desc.columns = ["Mean rank", "Median rank", "SD"]
|
||||||
|
|
||||||
|
print("=" * 55)
|
||||||
|
print("Descriptive statistics (lower rank = more preferred)")
|
||||||
|
print("=" * 55)
|
||||||
|
print(desc.round(3).to_string())
|
||||||
|
print()
|
||||||
|
|
||||||
|
# ── 3. Friedman test ──────────────────────────────────────────────────────────
|
||||||
|
# scipy expects one array per group (condition), each of length n_participants
|
||||||
|
statistic, p_value = stats.friedmanchisquare(
|
||||||
|
df_wide["VR"],
|
||||||
|
df_wide["Chat"],
|
||||||
|
df_wide["Video"],
|
||||||
|
)
|
||||||
|
|
||||||
|
n = len(df_wide)
|
||||||
|
k = 3 # number of conditions
|
||||||
|
df_friedman = k - 1 # degrees of freedom
|
||||||
|
# Kendall's W (effect size)
|
||||||
|
W = statistic / (n * (k - 1))
|
||||||
|
|
||||||
|
print("=" * 55)
|
||||||
|
print("Friedman test")
|
||||||
|
print("=" * 55)
|
||||||
|
print(f" N (participants) : {n}")
|
||||||
|
print(f" k (conditions) : {k}")
|
||||||
|
print(f" chi2({df_friedman}) : {statistic:.4f}")
|
||||||
|
print(f" p-value : {p_value:.4f}")
|
||||||
|
print(f" Kendall's W : {W:.4f}")
|
||||||
|
print()
|
||||||
|
if p_value < 0.05:
|
||||||
|
print(" --> Significant difference in rankings (p < .05).")
|
||||||
|
else:
|
||||||
|
print(" --> No significant difference in rankings (p >= .05).")
|
||||||
|
print()
|
||||||
|
|
||||||
|
# ── 4. Post-hoc: Wilcoxon signed-rank tests (Bonferroni-corrected) ────────────
|
||||||
|
from itertools import combinations
|
||||||
|
|
||||||
|
pairs = list(combinations(["VR", "Chat", "Video"], 2))
|
||||||
|
n_pairs = len(pairs)
|
||||||
|
alpha_corr = 0.05 / n_pairs # Bonferroni threshold
|
||||||
|
|
||||||
|
print("=" * 55)
|
||||||
|
print(f"Post-hoc: Wilcoxon signed-rank tests")
|
||||||
|
print(f"(Bonferroni-corrected alpha = {alpha_corr:.4f})")
|
||||||
|
print("=" * 55)
|
||||||
|
|
||||||
|
posthoc_rows = []
|
||||||
|
for a, b in pairs:
|
||||||
|
stat_w, p_w = stats.wilcoxon(df_wide[a], df_wide[b])
|
||||||
|
sig = "*" if p_w < alpha_corr else ""
|
||||||
|
posthoc_rows.append({
|
||||||
|
"Pair": f"{a} vs {b}",
|
||||||
|
"W statistic": round(stat_w, 4),
|
||||||
|
"p (raw)": round(p_w, 4),
|
||||||
|
"p (x3)": round(min(p_w * n_pairs, 1.0), 4),
|
||||||
|
"Sig.": sig,
|
||||||
|
})
|
||||||
|
|
||||||
|
df_posthoc = pd.DataFrame(posthoc_rows).set_index("Pair")
|
||||||
|
print(df_posthoc.to_string())
|
||||||
|
print(f"\n * Significant after Bonferroni correction (alpha = {alpha_corr:.4f})")
|
||||||
|
|
||||||
|
# ── 5. Plot ───────────────────────────────────────────────────────────────────
|
||||||
|
CONDITIONS = ["VR", "Chat", "Video"]
|
||||||
|
MED_COLORS = {"Chat": "#42A5F5", "Video": "#FFA726", "VR": "#66BB6A"}
|
||||||
|
RANK_COLORS = ["#4CAF50", "#FFC107", "#F44336"] # 1st, 2nd, 3rd
|
||||||
|
|
||||||
|
sns.set_theme(style="whitegrid", font_scale=1.05)
|
||||||
|
plt.rcParams["figure.dpi"] = 150
|
||||||
|
plt.rcParams["savefig.bbox"] = "tight"
|
||||||
|
|
||||||
|
fig, axes = plt.subplots(1, 2, figsize=(13, 5))
|
||||||
|
|
||||||
|
# ── Left panel: mean rank + individual jitter ─────────────────────────────────
|
||||||
|
ax = axes[0]
|
||||||
|
x_pos = np.arange(len(CONDITIONS))
|
||||||
|
|
||||||
|
for i, cond in enumerate(CONDITIONS):
|
||||||
|
vals = df_wide[cond].values
|
||||||
|
jitter = np.random.default_rng(42).uniform(-0.12, 0.12, size=len(vals))
|
||||||
|
ax.scatter(np.full(len(vals), i) + jitter, vals,
|
||||||
|
color=MED_COLORS[cond], alpha=0.55, s=40, zorder=3)
|
||||||
|
|
||||||
|
means = [df_wide[c].mean() for c in CONDITIONS]
|
||||||
|
sems = [df_wide[c].sem() for c in CONDITIONS]
|
||||||
|
|
||||||
|
bars = ax.bar(x_pos, means, yerr=sems, capsize=5,
|
||||||
|
color=[MED_COLORS[c] for c in CONDITIONS],
|
||||||
|
edgecolor="gray", linewidth=0.6, alpha=0.75, width=0.5, zorder=2)
|
||||||
|
|
||||||
|
for i, (m, s) in enumerate(zip(means, sems)):
|
||||||
|
ax.text(i, m + s + 0.08, f"M={m:.2f}", ha="center", fontsize=10, fontweight="bold")
|
||||||
|
|
||||||
|
ax.set_xticks(x_pos)
|
||||||
|
ax.set_xticklabels(CONDITIONS, fontsize=12)
|
||||||
|
ax.set_ylim(0.5, 3.8)
|
||||||
|
ax.set_yticks([1, 2, 3])
|
||||||
|
ax.set_yticklabels(["1st\n(most preferred)", "2nd", "3rd\n(least preferred)"], fontsize=9)
|
||||||
|
ax.set_ylabel("Rank (lower = more preferred)", fontsize=11)
|
||||||
|
ax.set_title("Mean Rank per Condition\n(individual observations + SEM)", fontsize=11, fontweight="bold")
|
||||||
|
|
||||||
|
stat_label = (
|
||||||
|
f"Friedman: chi2({df_friedman}) = {statistic:.2f}, "
|
||||||
|
f"p = {p_value:.3f}, W = {W:.2f}"
|
||||||
|
)
|
||||||
|
ax.text(0.5, 0.03, stat_label, transform=ax.transAxes,
|
||||||
|
ha="center", va="bottom", fontsize=8.5, color="dimgray",
|
||||||
|
bbox=dict(boxstyle="round,pad=0.3", fc="white", ec="lightgray", alpha=0.8))
|
||||||
|
|
||||||
|
# ── Right panel: stacked rank-distribution bars ───────────────────────────────
|
||||||
|
ax = axes[1]
|
||||||
|
n_participants = len(df_wide)
|
||||||
|
rank_counts = {
|
||||||
|
cond: [(df_wide[cond] == r).sum() / n_participants * 100 for r in [1, 2, 3]]
|
||||||
|
for cond in CONDITIONS
|
||||||
|
}
|
||||||
|
|
||||||
|
bottoms = np.zeros(len(CONDITIONS))
|
||||||
|
for rank_idx, (rank_label, color) in enumerate(
|
||||||
|
zip(["1st choice", "2nd choice", "3rd choice"], RANK_COLORS)
|
||||||
|
):
|
||||||
|
heights = [rank_counts[c][rank_idx] for c in CONDITIONS]
|
||||||
|
bars = ax.bar(x_pos, heights, bottom=bottoms,
|
||||||
|
color=color, edgecolor="white", linewidth=0.8,
|
||||||
|
width=0.55, label=rank_label)
|
||||||
|
for xi, (h, b) in enumerate(zip(heights, bottoms)):
|
||||||
|
if h >= 8:
|
||||||
|
ax.text(xi, b + h / 2, f"{h:.0f}%",
|
||||||
|
ha="center", va="center", fontsize=10,
|
||||||
|
fontweight="bold", color="white")
|
||||||
|
bottoms += np.array(heights)
|
||||||
|
|
||||||
|
ax.set_xticks(x_pos)
|
||||||
|
ax.set_xticklabels(CONDITIONS, fontsize=12)
|
||||||
|
ax.set_ylim(0, 105)
|
||||||
|
ax.set_ylabel("Percentage of participants (%)", fontsize=11)
|
||||||
|
ax.set_title("Rank Distribution per Condition\n(% participants assigning each rank)", fontsize=11, fontweight="bold")
|
||||||
|
ax.legend(loc="upper right", fontsize=9,
|
||||||
|
handles=[mpatches.Patch(color=c, label=l)
|
||||||
|
for c, l in zip(RANK_COLORS, ["1st choice", "2nd choice", "3rd choice"])])
|
||||||
|
|
||||||
|
fig.suptitle("Medium Preference Rankings (N=18, within-subjects)", fontsize=13, fontweight="bold")
|
||||||
|
fig.tight_layout()
|
||||||
|
|
||||||
|
out_path = Path("Data/plots_questionnaires/Q14_medium_ranking.png")
|
||||||
|
fig.savefig(out_path)
|
||||||
|
plt.close(fig)
|
||||||
|
print(f"\nPlot saved: {out_path}")
|
||||||
Reference in New Issue
Block a user