Mixed methods research design with qualitative-quantitative integration, triangulation, content analysis, and systematic comparison using NVivo-style coding.
Mixed methods research design with qualitative-quantitative integration, triangulation, content analysis, and systematic comparison using NVivo-style coding.
import pandas as pd
import numpy as np
from scipy.stats import chi2_contingency
import matplotlib.pyplot as plt
print("Mixed methods environment ready")
Core Workflow
Step 1: Systematic Content Analysis with Coding Scheme
import numpy as np
import pandas as pd
from scipy.stats import chi2_contingency
import matplotlib.pyplot as plt
import re
# -----------------------------------------------------------------# Simulate interview data: 50 interview segments# Two coders independently assign codes# -----------------------------------------------------------------
np.random.seed(42)
CODE_SCHEME = {
"BARRIER": ["Barrier/Obstacle to implementation"],
"ENABLER": ["Enabler/Facilitator of implementation"],
"OUTCOME": ["Outcome/Result described"],
"RECOMMENDATION": ["Policy/Practice recommendation"],
"CONTEXT": ["Context/Background information"],
}
CODES = list(CODE_SCHEME.keys())
# Interview segments
themes_pool = [
"lack of funding prevented the initiative from scaling",
"management support was crucial for adoption",
"the program led to improved outcomes for participants",
"we recommend investing in training for frontline staff",
"this occurred in a resource-constrained setting",
"regulatory barriers slowed down implementation",
"strong leadership facilitated rapid adoption",
"participants reported significant benefits",
"future policy should prioritize equity concerns",
"the rural context posed unique challenges",
]
n_segments = 50
segments = np.random.choice(themes_pool, n_segments)
# True codes (ground truth based on segment content)defassign_true_code(segment):
"""Assign code based on keyword matching."""ifany(w in segment for w in ["barrier", "lack", "prevented", "slowed"]):
return"BARRIER"elifany(w in segment for w in ["support", "leadership", "facilitated"]):
return"ENABLER"elifany(w in segment for w in ["outcomes", "benefits", "improved", "reported"]):
return"OUTCOME"elifany(w in segment for w in ["recommend", "policy", "should"]):
return"RECOMMENDATION"else:
return"CONTEXT"
true_codes = [assign_true_code(s) for s in segments]
# Coder 1: 90% agreement with truth# Coder 2: 85% agreement with truthdefapply_coder_noise(true_codes, accuracy):
noisy = []
for code in true_codes:
if np.random.random() < accuracy:
noisy.append(code)
else:
noisy.append(np.random.choice([c for c in CODES if c != code]))
return noisy
coder1 = apply_coder_noise(true_codes, 0.90)
coder2 = apply_coder_noise(true_codes, 0.85)
df_coding = pd.DataFrame({
"segment_id": range(n_segments),
"text": segments,
"true_code": true_codes,
"coder1": coder1,
"coder2": coder2,
})
# -----------------------------------------------------------------# Inter-rater reliability: Cohen's Kappa# -----------------------------------------------------------------defcohen_kappa(a, b):
"""Compute Cohen's kappa between two coders."""
categories = sorted(set(a) | set(b))
n = len(a)
# Observed agreement
P_o = sum(x == y for x, y inzip(a, b)) / n
# Expected agreement
freq_a = {c: a.count(c) / n for c in categories}
freq_b = {c: b.count(c) / n for c in categories}
P_e = sum(freq_a.get(c, 0) * freq_b.get(c, 0) for c in categories)
if P_e == 1.0:
return1.0return (P_o - P_e) / (1 - P_e)
kappa = cohen_kappa(coder1, coder2)
print(f"Cohen's Kappa (Coder1 vs Coder2): κ = {kappa:.3f}")
if kappa >= 0.80: print(" → Excellent agreement")
elif kappa >= 0.61: print(" → Substantial agreement")
elif kappa >= 0.41: print(" → Moderate agreement")
else: print(" → Fair/poor agreement — recalibrate coders")
# -----------------------------------------------------------------# Krippendorff's Alpha (nominal scale, 2 coders)# -----------------------------------------------------------------defkrippendorff_alpha_nominal(codings_matrix):
"""Compute Krippendorff's alpha for nominal scale.
Args:
codings_matrix: (n_coders, n_items) array of codes (integers)
Returns:
alpha value
"""
k, n = codings_matrix.shape
# Convert to integer labels
categories = sorted(set(codings_matrix.flatten()))
cat_map = {c: i for i, c inenumerate(categories)}
C = np.array([[cat_map[v] for v in row] for row in codings_matrix])
# Observed disagreement (all pairs within same item)
D_o_count = 0
D_e_count = 0
n_total_pairs = 0for unit inrange(n):
unit_codes = C[:, unit]
for i inrange(k):
for j inrange(i + 1, k):
D_o_count += int(unit_codes[i] != unit_codes[j])
n_total_pairs += 1# Expected disagreement (random pair from all code values)
all_codes = C.flatten()
n_vals = len(all_codes)
for i inrange(n_vals):
for j inrange(i + 1, n_vals):
D_e_count += int(all_codes[i] != all_codes[j])
D_o = D_o_count / max(n_total_pairs, 1)
D_e = D_e_count / max(n_vals * (n_vals - 1) / 2, 1)
if D_e == 0:
return1.0return1 - D_o / D_e
cat_map = {c: i for i, c inenumerate(CODES)}
coding_matrix = np.array([
[cat_map[c] for c in coder1],
[cat_map[c] for c in coder2],
])
alpha = krippendorff_alpha_nominal(coding_matrix)
print(f"Krippendorff's Alpha: α = {alpha:.3f}")
# -----------------------------------------------------------------# Code frequency analysis# -----------------------------------------------------------------# Use consensus code (coder1 where agree, majority where disagree)defconsensus_code(c1, c2):
return c1 if c1 == c2 else np.random.choice([c1, c2])
df_coding["consensus"] = [consensus_code(c1, c2)
for c1, c2 inzip(coder1, coder2)]
code_freq = df_coding["consensus"].value_counts()
print("\n=== Code Frequency Distribution ===")
print(code_freq)
# -----------------------------------------------------------------# Visualization# -----------------------------------------------------------------
fig, axes = plt.subplots(1, 2, figsize=(12, 5))
# Code frequency bar chart
code_freq.plot(kind="bar", ax=axes[0], color="steelblue", edgecolor="black")
axes[0].set_title("Code Frequency Distribution")
axes[0].set_xlabel("Code"); axes[0].set_ylabel("Count")
axes[0].set_xticklabels(code_freq.index, rotation=30, ha="right")
# Confusion matrix (coder1 vs. coder2)from sklearn.metrics import confusion_matrix, ConfusionMatrixDisplay
cm = confusion_matrix(coder1, coder2, labels=CODES)
disp = ConfusionMatrixDisplay(cm, display_labels=CODES)
disp.plot(ax=axes[1], colorbar=False)
axes[1].set_title(f"Coder Agreement Matrix\n(κ={kappa:.2f})")
plt.setp(axes[1].get_xticklabels(), rotation=30, ha="right")
plt.tight_layout()
plt.savefig("content_analysis.png", dpi=150, bbox_inches="tight")
plt.close()
print("\nFigure saved: content_analysis.png")
Step 2: Qualitative Comparative Analysis (QCA)
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
from itertools import product
# -----------------------------------------------------------------# Simulate a crisp-set QCA dataset: 20 cases, 4 conditions, 1 outcome# Research question: What combinations of conditions lead to successful reform?# -----------------------------------------------------------------
np.random.seed(42)
n_cases = 20
conditions = {
"leadership": [1,1,1,1,0,0,0,0,1,1,0,0,1,1,0,1,0,0,1,0], # strong leadership"resources": [1,1,0,0,1,1,0,0,1,0,1,0,1,0,1,0,1,0,0,1], # adequate resources"stakeholders": [1,0,1,0,1,0,1,0,0,1,1,0,0,1,1,0,0,1,1,0], # stakeholder buy-in"policy_window": [1,0,0,1,0,1,1,0,1,1,0,0,1,0,0,1,0,1,0,1], # policy window open
}
# Outcome: reform succeeded
outcome = [1,1,0,1,0,0,0,0,1,1,0,0,1,1,0,1,0,0,1,0]
df_qca = pd.DataFrame(conditions)
df_qca["outcome"] = outcome
df_qca.index = [f"Case_{i+1:02d}"for i inrange(n_cases)]
print("=== QCA Truth Table (first 10 cases) ===")
print(df_qca.head(10))
# -----------------------------------------------------------------# Build truth table# -----------------------------------------------------------------defbuild_truth_table(df_qca, conditions, outcome_col="outcome"):
"""Aggregate cases into truth table rows.
Returns truth table with consistency and coverage for each configuration.
"""
cond_cols = list(conditions.keys())
groups = df_qca.groupby(cond_cols)
rows = []
for config, group in groups:
n = len(group)
n_outcome = group[outcome_col].sum()
consistency = n_outcome / n if n > 0else0
rows.append({
**dict(zip(cond_cols, config)),
"n_cases": n,
"n_outcome": n_outcome,
"consistency": consistency,
"outcome": int(consistency >= 0.75), # threshold at 0.75
})
return pd.DataFrame(rows).sort_values("n_cases", ascending=False)
truth_table = build_truth_table(df_qca, conditions)
print("\n=== Truth Table ===")
print(truth_table.round(2).to_string(index=False))
# -----------------------------------------------------------------# Necessity analysis: which single conditions are necessary?# -----------------------------------------------------------------defnecessity_analysis(df_qca, conditions, outcome_col="outcome"):
"""Test each condition and its negation for necessity."""
results = []
for cond in conditions.keys():
# Consistency of necessity: sum(min(C, O)) / sum(O)
c = df_qca[cond].values
o = df_qca[outcome_col].values
consistency = np.minimum(c, o).sum() / max(o.sum(), 1)
coverage = np.minimum(c, o).sum() / max(c.sum(), 1)
results.append({"condition": cond, "type": "presence",
"necessity_consistency": consistency,
"necessity_coverage": coverage})
# Negation
c_neg = 1 - c
cons_neg = np.minimum(c_neg, o).sum() / max(o.sum(), 1)
cov_neg = np.minimum(c_neg, o).sum() / max(c_neg.sum(), 1)
results.append({"condition": f"~{cond}", "type": "absence",
"necessity_consistency": cons_neg,
"necessity_coverage": cov_neg})
return pd.DataFrame(results).sort_values("necessity_consistency", ascending=False)
nec_df = necessity_analysis(df_qca, conditions)
print("\n=== Necessity Analysis ===")
print(nec_df.round(3).to_string(index=False))
print("\nNecessary conditions (consistency > 0.9):")
necessary = nec_df[nec_df["necessity_consistency"] > 0.90]
print(necessary[["condition", "necessity_consistency", "necessity_coverage"]].to_string(index=False))
# -----------------------------------------------------------------# Sufficiency: check two-condition combinations# -----------------------------------------------------------------defsufficiency_analysis(df_qca, conditions, outcome_col="outcome", min_cases=2):
"""Test all 2-condition combinations for sufficiency."""from itertools import combinations
cond_names = list(conditions.keys())
results = []
for (c1, c2) in combinations(cond_names, 2):
for val1, val2 in product([0, 1], repeat=2):
mask = (df_qca[c1] == val1) & (df_qca[c2] == val2)
n = mask.sum()
if n < min_cases:
continue
n_out = df_qca.loc[mask, outcome_col].sum()
consistency = n_out / n
label = f"{'~'if val1==0else''}{c1} * {'~'if val2==0else''}{c2}"
results.append({
"configuration": label,
"n_cases": n,
"consistency": consistency,
"coverage": n_out / max(df_qca[outcome_col].sum(), 1),
})
return pd.DataFrame(results).sort_values("consistency", ascending=False)
suf_df = sufficiency_analysis(df_qca, conditions)
print("\n=== Top Sufficient Configurations ===")
print(suf_df[suf_df["consistency"] >= 0.80].head(8).round(3).to_string(index=False))
# -----------------------------------------------------------------# Visualization: necessity-sufficiency plot# -----------------------------------------------------------------
fig, axes = plt.subplots(1, 2, figsize=(12, 5))
# Necessity plot
axes[0].scatter(nec_df["necessity_coverage"], nec_df["necessity_consistency"],
s=80, c="steelblue", edgecolors="k")
for _, row in nec_df.iterrows():
axes[0].annotate(row["condition"],
(row["necessity_coverage"], row["necessity_consistency"]),
fontsize=7, xytext=(3, 3), textcoords="offset points")
axes[0].axhline(0.90, color="red", ls="--", label="Threshold (0.90)")
axes[0].axvline(0.75, color="blue", ls="--", alpha=0.5)
axes[0].set_xlabel("Coverage"); axes[0].set_ylabel("Consistency")
axes[0].set_title("Necessity Analysis")
axes[0].legend()
# Truth table grid
cond_cols = list(conditions.keys())
pivot = truth_table.set_index(cond_cols + ["n_cases"])[["consistency", "outcome"]]
im = axes[1].imshow(truth_table[cond_cols].values.T, cmap="RdYlGn",
aspect="auto", vmin=0, vmax=1)
axes[1].set_yticks(range(len(cond_cols)))
axes[1].set_yticklabels(cond_cols)
axes[1].set_xlabel("Truth Table Row")
axes[1].set_title("Truth Table (Green=Present)")
plt.colorbar(im, ax=axes[1])
plt.tight_layout()
plt.savefig("qca_analysis.png", dpi=150, bbox_inches="tight")
plt.close()
print("\nFigure saved: qca_analysis.png")
Step 3: Triangulation and Integration of Qual+Quant Findings