added ranking
This commit is contained in:
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