Analyze survey responses after collection: clean the data, apply post-stratification weights, build cross-tabs, and run basic significance tests with honest interpretation of non-significant results. Use when you have survey responses in a file and need trustworthy numbers from them.
./install.sh survey-data-analysisname: "survey_data_analysis" description: "Analyze survey responses after collection: clean the data, apply post-stratification weights, build cross-tabs, and run basic significance tests with honest interpretation of non-significant results. Use when you have survey responses in a file and need trustworthy numbers from them." metadata: { "includeInPrompt": true }
Turn raw survey responses into numbers you can defend. Cleaning, weighting, cross-tabs, and basic significance testing, with plain rules for saying "this is not significant" instead of dressing noise up as a finding.
Not this skill: survey-design-basics is "Design quantitative surveys that give usable answers: write unbiased questions, pick the right question type, size the sample, compute response rates, and check for bias. Use when building a poll, questionnaire, or feedback survey whose results will be counted." That skill owns everything up to the moment responses arrive. This one starts there.
responses.csv with one row per response and at least these columns: respondent_id, age_group, q_sat, minutes. Adapt column names in the script if yours differ. Keep the raw file untouched; the script never edits it.analyze.py, put it next to responses.csv, then run python3 analyze.py. It uses only the standard library (csv, math), so it runs anywhere.python
import csv, math
# ---------- config ----------
POP_SHARES = {"18-34": 0.42, "35-54": 0.38, "55+": 0.20} # known population age mix
SATISFIED = {"Very satisfied", "Satisfied"}
SPEEDER_CUTOFF_MIN = 1.0
Z95 = 1.96
# ---------- load ----------
with open("responses.csv") as f:
raw = list(csv.DictReader(f))
# ---------- 1. cleaning ----------
seen, deduped = set(), []
dups = 0
for r in raw:
if r["respondent_id"] in seen:
dups += 1
continue
seen.add(r["respondent_id"])
deduped.append(r)
clean, speeders, missing = [], 0, 0
for r in deduped:
if float(r["minutes"]) < SPEEDER_CUTOFF_MIN:
speeders += 1
continue
if not r["q_sat"].strip() or not r["age_group"].strip():
missing += 1
continue
clean.append(r)
n_clean = len(clean)
# ---------- 2. post-stratification weights ----------
stratum_n = {}
for r in clean:
stratum_n[r["age_group"]] = stratum_n.get(r["age_group"], 0) + 1
weights = {s: POP_SHARES[s] / (stratum_n[s] / n_clean) for s in POP_SHARES}
def kish_n_eff(ws):
return sum(ws) ** 2 / sum(w * w for w in ws)
for r in clean:
r["w"] = weights[r["age_group"]]
r["sat"] = 1 if r["q_sat"] in SATISFIED else 0
# ---------- 3. overall weighted estimate + margin of error ----------
sw = sum(r["w"] for r in clean)
p_overall = sum(r["w"] * r["sat"] for r in clean) / sw
n_eff_all = kish_n_eff([r["w"] for r in clean])
moe = Z95 * math.sqrt(p_overall * (1 - p_overall) / n_eff_all)
# ---------- 4. cross-tab: weighted satisfied % by age group ----------
crosstab = {}
for s in POP_SHARES:
g = [r for r in clean if r["age_group"] == s]
swg = sum(r["w"] for r in g)
p = sum(r["w"] * r["sat"] for r in g) / swg
crosstab[s] = (len(g), p, kish_n_eff([r["w"] for r in g]))
# ---------- 5. two-proportion z-test: 18-34 vs 55+ ----------
def norm_cdf(x):
return 0.5 * (1 + math.erf(x / math.sqrt(2)))
n1, p1, ne1 = crosstab["18-34"]
n2, p2, ne2 = crosstab["55+"]
x1, x2 = p1 * ne1, p2 * ne2
p_pool = (x1 + x2) / (ne1 + ne2)
se = math.sqrt(p_pool * (1 - p_pool) * (1 / ne1 + 1 / ne2))
z = (p1 - p2) / se
p_value = 2 * (1 - norm_cdf(abs(z)))
# ---------- report ----------
print(f"raw rows: {len(raw)}")
print(f"duplicates removed: {dups}")
print(f"speeders dropped (<{SPEEDER_CUTOFF_MIN} min): {speeders}")
print(f"rows dropped for missing answers: {missing}")
print(f"clean n: {n_clean}")
print()
print("post-stratification weights (stratum: weight):")
for s in POP_SHARES:
print(f" {s}: n={stratum_n[s]} sample_share={stratum_n[s]/n_clean:.3f} "
f"pop_share={POP_SHARES[s]:.2f} weight={weights[s]:.3f}")
print()
print(f"overall satisfied: {p_overall*100:.1f}% +/- {moe*100:.1f}pp (95% MoE, effective n={n_eff_all:.0f})")
print()
print("cross-tab: satisfied % by age group (weighted):")
for s in POP_SHARES:
n, p, ne = crosstab[s]
print(f" {s}: n={n} -> {p*100:.1f}% (effective n={ne:.0f})")
print()
print(f"two-proportion z-test, 18-34 vs 55+: z={z:.3f}, p={p_value:.3f}")
print("conclusion: " + ("difference IS statistically significant at 95%."
if p_value < 0.05 else
"this difference is NOT statistically significant at 95%. "
"Write that plainly: the data does not support a real gap between these groups."))
Every analysis you hand over includes: the cleaning counts (raw, dropped, clean n), the weighting scheme (strata, population shares used, resulting weights), each reported estimate with its margin of error and effective n, the cross-tab behind any group comparison, and the test statistic plus p-value for every claimed difference. Non-significant results are stated plainly, in the same prominence as significant ones. Nothing is reported from the raw unweighted counts alone when weights were applied.
publisher key 2e43c913833850e6d80f939677e3de76e806d47785c0f1caaca90f9d18c98e1e
No ratings yet — be the first to rate it.