""" 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}")