-
Notifications
You must be signed in to change notification settings - Fork 252
[Stack 17/27] Fix D6: match Clojure two-proportion test formula (+1 pseudocount) #2449
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -95,33 +95,51 @@ def prop_test(succ: int, n: int) -> float: | |||||||||||||||||||||||||||||||||||||||||||||||||||||
| return 2 * math.sqrt(n_pc) * (succ_pc / n_pc - 0.5) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
| def two_prop_test(p1: float, n1: int, p2: float, n2: int) -> float: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| def two_prop_test(succ_in: int, succ_out: int, pop_in: int, pop_out: int) -> float: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| """ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Two-proportion z-test. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Two-proportion z-test with +1 pseudocount on all inputs. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Matches Clojure's stats/two-prop-test (stats.clj:18-33): | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| (let [[succ-in succ-out pop-in pop-out] (map inc [succ-in succ-out pop-in pop-out]) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| pi1 (/ succ-in pop-in) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| pi2 (/ succ-out pop-out) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| pi-hat (/ (+ succ-in succ-out) (+ pop-in pop-out))] | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ...) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
| The +1 pseudocount (Laplace smoothing) regularizes the z-score for small | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| samples, preventing extreme values when group sizes are tiny. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Args: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| p1: First proportion | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| n1: Number of observations for first proportion | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| p2: Second proportion | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| n2: Number of observations for second proportion | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| succ_in: Number of successes in the group (e.g., agrees) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| succ_out: Number of successes outside the group | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| pop_in: Total votes in the group | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| pop_out: Total votes outside the group | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Returns: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Z-score | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Z-score (positive means group proportion > other proportion) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| """ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if n1 == 0 or n2 == 0: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if pop_in == 0 or pop_out == 0: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return 0.0 | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
| # Pooled probability | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| p = (p1 * n1 + p2 * n2) / (n1 + n2) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
| # Standard error | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| se = math.sqrt(p * (1 - p) * (1/n1 + 1/n2)) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
| # Z-score calculation | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
| # Add +1 pseudocount to all four inputs (Clojure: map inc) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| s1 = succ_in + 1 | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| s2 = succ_out + 1 | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| p1 = pop_in + 1 | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| p2 = pop_out + 1 | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
| pi1 = s1 / p1 | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| pi2 = s2 / p2 | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| pi_hat = (s1 + s2) / (p1 + p2) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if pi_hat == 1.0: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| # Clojure note (stats.clj:26-27): "this isn't quite right... could | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| # actually solve this using limits" — returning 0 for now, matching Clojure. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return 0.0 | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
| se = math.sqrt(pi_hat * (1 - pi_hat) * (1/p1 + 1/p2)) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+127
to
+139
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
| p1 = pop_in + 1 | |
| p2 = pop_out + 1 | |
| pi1 = s1 / p1 | |
| pi2 = s2 / p2 | |
| pi_hat = (s1 + s2) / (p1 + p2) | |
| if pi_hat == 1.0: | |
| # Clojure note (stats.clj:26-27): "this isn't quite right... could | |
| # actually solve this using limits" — returning 0 for now, matching Clojure. | |
| return 0.0 | |
| se = math.sqrt(pi_hat * (1 - pi_hat) * (1/p1 + 1/p2)) | |
| pop_in_pc = pop_in + 1 | |
| pop_out_pc = pop_out + 1 | |
| pi1 = s1 / pop_in_pc | |
| pi2 = s2 / pop_out_pc | |
| pi_hat = (s1 + s2) / (pop_in_pc + pop_out_pc) | |
| if pi_hat == 1.0: | |
| # Clojure note (stats.clj:26-27): "this isn't quite right... could | |
| # actually solve this using limits" — returning 0 for now, matching Clojure. | |
| return 0.0 | |
| se = math.sqrt(pi_hat * (1 - pi_hat) * (1 / pop_in_pc + 1 / pop_out_pc)) |
Copilot
AI
Mar 23, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The vectorized implementation can still emit runtime warnings (divide-by-zero/invalid) when se == 0 (e.g., pi_hat == 1), even though you later replace inf/NaN with 0. Consider masking the computation before the division (e.g., only compute z where (pop_in>0)&(pop_out>0)&(se>0)&(pi_hat<1) and set 0 elsewhere) to avoid noisy warnings and improve numerical robustness.
Copilot
AI
Mar 30, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
two_prop_test_vectorized() zeroes results when pop_in==0 or pop_out==0, which diverges from the referenced Clojure implementation (it applies +1 pseudocounts and still computes the statistic). If the goal is Clojure parity, drop this mask and rely on the +1 adjustment plus inf/NaN handling; if the goal is to treat no-data rows as 0, please document that deviation clearly (and align scalar/vectorized behavior + tests accordingly).
| # Handle edge cases: pop_in=0 or pop_out=0 → 0, pi_hat=1 → 0 | |
| z = z.where((pop_in > 0) & (pop_out > 0), 0.0) | |
| z = z.fillna(0.0) | |
| z = z.replace([np.inf, -np.inf], 0.0) | |
| # Handle edge cases: pi_hat=0 or 1 → se=0 → inf/NaN; map these to 0 | |
| z = z.replace([np.inf, -np.inf], 0.0) | |
| z = z.fillna(0.0) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
two_prop_test() claims to match Clojure's stats/two-prop-test, but it returns 0.0 when pop_in==0 or pop_out==0. Clojure does not special-case zero populations; it increments pop-in/pop-out (so division-by-zero is avoided) and can yield a non-zero z-score when one side has no votes. Either remove this early return for true parity, or update the docstring/expected behavior to explicitly document this intentional deviation (and consider validating succ_* <= pop_* instead).