Skip to main content Skills Marketplace Descubra e explore skills de IA criadas pela comunidade.
Instalar com Codex ou Claude Copie este prompt, cole no Codex, Claude ou outro assistente e deixe que ele revise a página da skill e instale para você.
Copiar promptMostrar detalhes do prompt Um comando direto ignora o prompt de revisão. Verifique a origem antes de executá-lo.
npx skills add https://github.com/xjtulyc/awesome-rosetta-skills --skill experimental-designO comando permanece em uma só linha. Role horizontalmente para revisá-lo antes de copiar.
Prefere uma cópia local? Baixe os arquivos disponíveis atualmente no SkillsMP.
Baixar Zip Baixando... Ocupações relacionadas SOC
Baseado na classificação ocupacional SOC
name experimental-design description Design rigorous experiments: sample size calculation, randomization strategies, pre-registration, and AB test duration estimation.
tags ["experimental-design","sample-size","randomization","pre-registration","ab-testing","power-analysis"] version 1.0.0 authors [{"name":"awesome-rosetta-skills contributors","github":"@awesome-rosetta-skills"}] license MIT platforms ["claude-code","codex","gemini-cli","cursor"] dependencies {"python":["statsmodels>=0.14.0","numpy>=1.24.0","pandas>=2.0.0","scipy>=1.10.0","osfclient>=0.0.5"]} last_updated 2026-03-17
Experimental Design
Plan experiments with correct statistical power, principled randomization, and
transparent pre-registration before data collection begins.
Key Concepts
Term Definition α (alpha) Type I error rate — false positive probability (typically 0.05) β (beta) Type II error rate — false negative probability (typically 0.20) Power (1−β) Probability of detecting a true effect (typically 0.80 or 0.90) MDES Minimum Detectable Effect Size — smallest effect your design can detect ICC Intra-cluster correlation — needed for cluster-randomised trials
Setup
pip install statsmodels numpy pandas scipy osfclient
Core Implementation
"""
experimental_design.py
Sample size, randomization, and pre-registration utilities.
"""
import hashlib
import math
import os
import warnings
from typing import Dict , List , Optional , Tuple , Union
import numpy as np
import pandas as pd
from scipy import stats
from statsmodels.stats.power import (
TTestIndPower,
TTestPower,
FTestAnovaPower,
NormalIndPower,
)
from statsmodels.stats.proportion import proportion_effectsize
def calculate_sample_size ( ) -> [ , ]:
alternative = two_tailed
p1 p2 :
effect_size = (proportion_effectsize(p1, p2))
test_type =
effect_size :
ValueError( )
test_type == :
analysis = TTestIndPower()
n = analysis.solve_power(effect_size=effect_size, alpha=alpha, power=power,
ratio=ratio, alternative=alternative)
test_type == :
analysis = TTestPower()
n = analysis.solve_power(effect_size=effect_size, alpha=alpha, power=power,
alternative=alternative)
ratio =
test_type == :
analysis = TTestPower()
n = analysis.solve_power(effect_size=effect_size, alpha=alpha, power=power,
alternative=alternative)
ratio =
test_type == :
analysis = FTestAnovaPower()
n = analysis.solve_power(effect_size=effect_size, alpha=alpha, power=power,
k_groups=k_groups)
test_type ( , ):
analysis = NormalIndPower()
n = analysis.solve_power(effect_size=effect_size, alpha=alpha, power=power,
ratio=ratio, alternative=alternative)
:
ValueError( )
n_ceil = math.ceil(n)
total_n = math.ceil(n_ceil * ( + ratio)) test_type ( , ) n_ceil
result = {
: n_ceil,
: total_n,
: (effect_size, ),
: alpha,
: power,
: test_type,
: ratio,
}
verbose:
( )
( )
( )
( )
( )
( )
p1 :
( )
()
result
( ) -> :
analysis = TTestIndPower() test_type == TTestPower()
es = analysis.solve_power(nobs1=n_per_group, alpha=alpha, power=power,
ratio= , alternative= )
(es, )
( ) -> :
<= expected_attrition_rate < :
ValueError( )
math.ceil(n / ( - expected_attrition_rate))
( ) -> pd.DataFrame:
rng = np.random.default_rng(seed)
group_labels :
group_labels = [ i (n_groups)]
assignments = rng.choice(group_labels, size=n)
df = pd.DataFrame({ : ( , n + ), : assignments})
df
( ) -> pd.DataFrame:
block_size % n_groups != :
ValueError( )
rng = np.random.default_rng(seed)
group_labels :
group_labels = [ i (n_groups)]
per_arm = block_size // n_groups
block_template = group_labels * per_arm
assignments = []
(assignments) < n:
block = block_template.copy()
rng.shuffle(block)
assignments.extend(block)
assignments = assignments[:n]
df = pd.DataFrame({ : ( , n + ), : assignments})
df
( ) -> pd.DataFrame:
group_labels :
group_labels = [ i (n_groups)]
result_frames = []
df = participants.copy().reset_index(drop= )
df[ ] = df[stratify_cols].astype( ).agg( .join, axis= )
stratum, group_df df.groupby( ):
n_stratum = (group_df)
assigned = block_randomize(
n=n_stratum,
block_size=block_size,
n_groups=n_groups,
group_labels=group_labels,
seed= ( (stratum)) % ( ** ) seed seed + ( (stratum)) % ,
)
group_df = group_df.reset_index(drop= )
group_df[ ] = assigned[ ].values
result_frames.append(group_df)
result = pd.concat(result_frames).drop(columns=[ ]).sort_index()
result
( ) -> pd.DataFrame:
rng = np.random.default_rng(seed)
group_labels :
group_labels = [ i (n_groups)]
shuffled_clusters = (clusters)
rng.shuffle(shuffled_clusters)
assignments = []
i, cluster (shuffled_clusters):
assignments.append({ : cluster, : group_labels[i % n_groups]})
pd.DataFrame(assignments)
( ) -> [ , ]:
p1 = baseline_rate
p2 = baseline_rate * ( + minimum_detectable_effect)
p2 = (p2, )
size_result = calculate_sample_size(
p1=p1, p2=p2, alpha=alpha / (n_variants - ),
power=power, verbose=
)
required_n = size_result[ ] * n_variants
daily_n = (daily_traffic * allocation_fraction / n_variants) * n_variants
days_needed = math.ceil(required_n / daily_n)
{
: required_n,
: daily_n,
: days_needed,
: p1,
: (p2, ),
: (alpha / (n_variants - ), ),
}
( ) -> :
lines = [
,
,
,
,
,
,
]
i, h (hypotheses, ):
lines.append( )
lines += [
,
,
,
primary_outcome,
,
,
,
sample_size_justification,
,
,
,
randomization_method,
,
,
,
blinding,
,
,
,
analysis_plan,
,
,
,
]
content = .join(lines)
(output_path, , encoding= ) fh:
fh.write(content)
( )
content
effect_size: Optional [float ] = None ,
alpha: float = 0.05 ,
power: float = 0.80 ,
test_type: str = "two-sample-t" ,
ratio: float = 1.0 ,
k_groups: int = 2 ,
p1: Optional [float ] = None ,
p2: Optional [float ] = None ,
two_tailed: bool = True ,
verbose: bool = True ,
Dict
str
float
"""
Calculate required sample size for common experimental designs.
Parameters
----------
effect_size : float, optional
Standardised effect size (Cohen's d, f, h, w).
Required unless p1 and p2 are provided.
alpha : float
Type I error rate.
power : float
Desired statistical power (1 - beta).
test_type : str
One of: 'two-sample-t', 'paired-t', 'one-sample-t',
'anova', 'proportion-z', 'proportion-chi2'
ratio : float
n_group2 / n_group1 (for unequal allocation).
k_groups : int
Number of groups (for ANOVA only).
p1, p2 : float, optional
Event rates for proportion tests (auto-computes Cohen's h).
two_tailed : bool
Whether to use two-tailed test.
Returns
-------
dict with n_per_group, total_n, effect_size, alpha, power, test_type
"""
"two-sided"
if
else
"larger"
if
is
not
None
and
is
not
None
abs
"proportion-z"
if
is
None
raise
"Provide effect_size or both p1 and p2."
if
"two-sample-t"
elif
"paired-t"
1.0
elif
"one-sample-t"
1.0
elif
"anova"
elif
in
"proportion-z"
"proportion-chi2"
else
raise
f"Unknown test_type: {test_type} "
1
if
not
in
"paired-t"
"one-sample-t"
else
"n_per_group"
"total_n"
"effect_size"
round
4
"alpha"
"power"
"test_type"
"ratio"
if
print
f"Sample size calculation ({test_type} )"
print
f" Effect size : {effect_size:.3 f} "
print
f" Alpha : {alpha} "
print
f" Power : {power} "
print
f" N per group : {n_ceil} "
print
f" Total N : {total_n} "
if
is
not
None
print
f" Rates : p1={p1} , p2={p2} "
print
return
def
mdes
n_per_group: int ,
alpha: float = 0.05 ,
power: float = 0.80 ,
test_type: str = "two-sample-t" ,
float
"""
Minimum Detectable Effect Size given a fixed sample size.
"""
if
"two-sample-t"
else
1.0
"two-sided"
return
round
4
def
inflate_for_attrition
n: int , expected_attrition_rate: float = 0.15
int
"""
Inflate sample size to account for expected dropout / missing data.
Parameters
----------
n : int
Required complete-case sample size.
expected_attrition_rate : float
Fraction of participants expected to drop out (0–1).
"""
if
not
0
1
raise
"attrition_rate must be in [0, 1)."
return
1
def
simple_randomize
n: int ,
n_groups: int = 2 ,
group_labels: Optional [List [str ]] = None ,
seed: Optional [int ] = None ,
"""Simple (unrestricted) randomization."""
if
is
None
f"Group_{i} "
for
in
range
"participant_id"
range
1
1
"assignment"
return
def
block_randomize
n: int ,
block_size: int = 4 ,
n_groups: int = 2 ,
group_labels: Optional [List [str ]] = None ,
seed: Optional [int ] = None ,
"""
Permuted block randomization.
Ensures balance within each block. Block size must be a multiple of n_groups.
Parameters
----------
n : int
Total number of participants to randomize.
block_size : int
Size of each block (must be divisible by n_groups).
n_groups : int
Number of treatment arms.
group_labels : list, optional
seed : int, optional
Returns
-------
DataFrame with participant_id and assignment columns.
"""
if
0
raise
f"block_size ({block_size} ) must be divisible by n_groups ({n_groups} )."
if
is
None
f"Group_{i} "
for
in
range
while
len
"participant_id"
range
1
1
"assignment"
return
def
stratified_randomize
participants: pd.DataFrame,
stratify_cols: List [str ],
block_size: int = 4 ,
n_groups: int = 2 ,
group_labels: Optional [List [str ]] = None ,
seed: Optional [int ] = None ,
"""
Stratified block randomization.
Randomizes participants within strata defined by stratify_cols, using
permuted blocks within each stratum to maintain balance.
Parameters
----------
participants : DataFrame
Must contain an 'id' column and the stratify_cols.
stratify_cols : list of str
Columns to stratify on (e.g. ['site', 'sex', 'age_group']).
block_size : int
n_groups : int
group_labels : list, optional
seed : int, optional
Returns
-------
DataFrame with original columns plus 'assignment'.
"""
if
is
None
f"Group_{i} "
for
in
range
True
"_stratum"
str
"|"
1
for
in
"_stratum"
len
abs
hash
2
31
if
is
None
else
abs
hash
1000
True
"assignment"
"assignment"
"_stratum"
return
def
cluster_randomize
clusters: List [str ],
n_groups: int = 2 ,
group_labels: Optional [List [str ]] = None ,
seed: Optional [int ] = None ,
"""
Cluster randomization — randomize entire clusters to arms.
Parameters
----------
clusters : list of str
Cluster identifiers (e.g. school names, clinic IDs).
n_groups : int
group_labels : list, optional
seed : int, optional
"""
if
is
None
f"Group_{i} "
for
in
range
list
for
in
enumerate
"cluster"
"assignment"
return
def
ab_test_duration
baseline_rate: float ,
minimum_detectable_effect: float ,
daily_traffic: int ,
allocation_fraction: float = 1.0 ,
alpha: float = 0.05 ,
power: float = 0.80 ,
n_variants: int = 2 ,
Dict
str
float
"""
Estimate how many days an A/B test should run.
Parameters
----------
baseline_rate : float
Baseline conversion / success rate (0–1).
minimum_detectable_effect : float
Relative change to detect (e.g. 0.05 = 5% lift).
daily_traffic : int
Total eligible users per day.
allocation_fraction : float
Fraction of traffic included in the test.
alpha : float
power : float
n_variants : int
Number of arms including control.
Returns
-------
dict with required_n, daily_n_in_test, days_needed
"""
1
min
0.9999
1
False
"n_per_group"
int
return
"required_n_total"
"daily_n_in_test"
"days_needed"
"baseline_rate"
"target_rate"
round
4
"alpha_adjusted"
round
1
4
def
generate_preregistration_document
study_title: str ,
hypotheses: List [str ],
primary_outcome: str ,
sample_size_justification: str ,
analysis_plan: str ,
randomization_method: str ,
blinding: str ,
output_path: str = "preregistration.md" ,
str
"""
Generate a structured pre-registration markdown document.
Upload manually to OSF (https://osf.io) or via the OSF Python client.
"""
f"# Pre-registration: {study_title} "
""
f"**Date:** {pd.Timestamp.now().strftime('%Y-%m-%d' )} "
""
"## Hypotheses"
""
for
in
enumerate
1
f"{i} . {h} "
""
"## Primary Outcome"
""
""
"## Sample Size Justification"
""
""
"## Randomization"
""
""
"## Blinding"
""
""
"## Analysis Plan"
""
""
"---"
"_This document was auto-generated and should be reviewed before submission._"
"\n"
with
open
"w"
"utf-8"
as
print
f"Pre-registration document written to: {output_path} "
return
Example 1 — Two-Arm RCT (Clinical Trial) import pandas as pd
from experimental_design import (
calculate_sample_size,
inflate_for_attrition,
block_randomize,
generate_preregistration_document,
)
result = calculate_sample_size(p1=0.40 , p2=0.55 , alpha=0.05 , power=0.80 )
n_inflated = inflate_for_attrition(result["n_per_group" ], expected_attrition_rate=0.15 )
print (f"Enrol {n_inflated} per arm ({n_inflated * 2 } total) to account for 15% attrition." )
allocation = block_randomize(
n=n_inflated * 2 ,
block_size=4 ,
n_groups=2 ,
group_labels=["Control" , "Intervention" ],
seed=2024 ,
)
print (allocation.head(8 ))
print (allocation["assignment" ].value_counts())
generate_preregistration_document(
study_title="Effect of Intervention X on Recovery Rate" ,
hypotheses=[
"Intervention X will increase 30-day recovery rate compared to control." ,
"No difference in serious adverse events between arms." ,
],
primary_outcome="30-day binary recovery status (recovered / not recovered)." ,
sample_size_justification=(
f"N={n_inflated} per arm (inflated from {result['n_per_group' ]} "
f"for 15% attrition). Based on 40% vs 55% recovery rates, "
f"alpha=0.05, power=0.80."
),
analysis_plan=(
"Primary analysis: chi-square test. Secondary: logistic regression "
"adjusting for age and baseline severity."
),
randomization_method="Permuted block randomization, block size 4, centralised allocation." ,
blinding="Outcome assessors blinded; participants and clinicians not blinded." ,
output_path="rct_preregistration.md" ,
)
Example 2 — Multi-Arm Web Experiment with Stratification import pandas as pd
from experimental_design import (
calculate_sample_size,
stratified_randomize,
ab_test_duration,
)
result = calculate_sample_size(
effect_size=0.25 ,
alpha=0.05 ,
power=0.80 ,
test_type="anova" ,
k_groups=3 ,
)
print (result)
rng_sim = pd.np.random.default_rng(99 ) if hasattr (pd, 'np' ) else __import__ ('numpy' ).random.default_rng(99 )
import numpy as np
rng_sim = np.random.default_rng(99 )
n_users = result["total_n" ]
participants = pd.DataFrame({
"id" : range (1 , n_users + 1 ),
"country" : np.random.choice(["US" , "UK" , "DE" ], size=n_users, p=[0.5 , 0.3 , 0.2 ]),
"user_type" : np.random.choice(["free" , "paid" ], size=n_users, p=[0.7 , 0.3 ]),
})
allocation = stratified_randomize(
participants=participants,
stratify_cols=["country" , "user_type" ],
block_size=6 ,
n_groups=3 ,
group_labels=["Control" , "Variant_A" , "Variant_B" ],
seed=2024 ,
)
print (allocation.groupby(["country" , "user_type" , "assignment" ]).size().unstack())
duration = ab_test_duration(
baseline_rate=0.03 ,
minimum_detectable_effect=0.20 ,
daily_traffic=5000 ,
allocation_fraction=0.80 ,
alpha=0.05 ,
power=0.80 ,
n_variants=3 ,
)
print (f"Run for at least {duration['days_needed' ]} days." )
Blinding Protocols Trial Type Who Is Blinded Open-label Nobody (appropriate when blinding is impractical) Single-blind Participants only Double-blind Participants + investigators Triple-blind Participants + investigators + outcome assessors Cluster-blind Outcome assessors blinded to cluster allocation
Pre-registration on OSF
pip install osfclient
export OSF_TOKEN=<paste-your-osf-token>
osf -p <your-project-id> upload rct_preregistration.md osfstorage/prereg/rct_preregistration.md
osf -p <your-project-id> ls
Checklist Before Data Collection