@@ -95,33 +95,51 @@ def prop_test(succ: int, n: int) -> float:
9595 return 2 * math .sqrt (n_pc ) * (succ_pc / n_pc - 0.5 )
9696
9797
98- def two_prop_test (p1 : float , n1 : int , p2 : float , n2 : int ) -> float :
98+ def two_prop_test (succ_in : int , succ_out : int , pop_in : int , pop_out : int ) -> float :
9999 """
100- Two-proportion z-test.
101-
100+ Two-proportion z-test with +1 pseudocount on all inputs.
101+
102+ Matches Clojure's stats/two-prop-test (stats.clj:18-33):
103+ (let [[succ-in succ-out pop-in pop-out] (map inc [succ-in succ-out pop-in pop-out])
104+ pi1 (/ succ-in pop-in)
105+ pi2 (/ succ-out pop-out)
106+ pi-hat (/ (+ succ-in succ-out) (+ pop-in pop-out))]
107+ ...)
108+
109+ The +1 pseudocount (Laplace smoothing) regularizes the z-score for small
110+ samples, preventing extreme values when group sizes are tiny.
111+
102112 Args:
103- p1: First proportion
104- n1 : Number of observations for first proportion
105- p2: Second proportion
106- n2: Number of observations for second proportion
107-
113+ succ_in: Number of successes in the group (e.g., agrees)
114+ succ_out : Number of successes outside the group
115+ pop_in: Total votes in the group
116+ pop_out: Total votes outside the group
117+
108118 Returns:
109- Z-score
119+ Z-score (positive means group proportion > other proportion)
110120 """
111- if n1 == 0 or n2 == 0 :
121+ if pop_in == 0 or pop_out == 0 :
112122 return 0.0
113-
114- # Pooled probability
115- p = (p1 * n1 + p2 * n2 ) / (n1 + n2 )
116-
117- # Standard error
118- se = math .sqrt (p * (1 - p ) * (1 / n1 + 1 / n2 ))
119-
120- # Z-score calculation
123+
124+ # Add +1 pseudocount to all four inputs (Clojure: map inc)
125+ s1 = succ_in + 1
126+ s2 = succ_out + 1
127+ p1 = pop_in + 1
128+ p2 = pop_out + 1
129+
130+ pi1 = s1 / p1
131+ pi2 = s2 / p2
132+ pi_hat = (s1 + s2 ) / (p1 + p2 )
133+
134+ if pi_hat == 1.0 :
135+ # Clojure note (stats.clj:26-27): "this isn't quite right... could
136+ # actually solve this using limits" — returning 0 for now, matching Clojure.
137+ return 0.0
138+
139+ se = math .sqrt (pi_hat * (1 - pi_hat ) * (1 / p1 + 1 / p2 ))
121140 if se == 0 :
122141 return 0.0
123- else :
124- return (p1 - p2 ) / se
142+ return (pi1 - pi2 ) / se
125143
126144
127145def comment_stats (votes : np .ndarray , group_members : List [int ]) -> Dict [str , Any ]:
@@ -182,15 +200,17 @@ def add_comparative_stats(comment_stats: Dict[str, Any],
182200 result ['ra' ] = result ['pa' ] / other_stats ['pa' ] if other_stats ['pa' ] > 0 else 1.0
183201 result ['rd' ] = result ['pd' ] / other_stats ['pd' ] if other_stats ['pd' ] > 0 else 1.0
184202
185- # Calculate representativeness tests
203+ # Calculate representativeness tests — pass raw counts, matching Clojure's
204+ # (stats/two-prop-test (:na in-stats) (sum :na rest-stats)
205+ # (:ns in-stats) (sum :ns rest-stats)) (repness.clj:97-100)
186206 result ['rat' ] = two_prop_test (
187- result ['pa ' ], result [ 'ns ' ],
188- other_stats [ 'pa ' ], other_stats ['ns' ]
207+ result ['na ' ], other_stats [ 'na ' ],
208+ result [ 'ns ' ], other_stats ['ns' ]
189209 )
190-
210+
191211 result ['rdt' ] = two_prop_test (
192- result ['pd ' ], result [ 'ns ' ],
193- other_stats [ 'pd ' ], other_stats ['ns' ]
212+ result ['nd ' ], other_stats [ 'nd ' ],
213+ result [ 'ns ' ], other_stats ['ns' ]
194214 )
195215
196216 return result
@@ -493,30 +513,38 @@ def prop_test_vectorized(succ: pd.Series, n: pd.Series) -> pd.Series:
493513 return z
494514
495515
496- def two_prop_test_vectorized (p1 : pd .Series , n1 : pd .Series ,
497- p2 : pd .Series , n2 : pd .Series ) -> pd .Series :
516+ def two_prop_test_vectorized (succ_in : pd .Series , succ_out : pd .Series ,
517+ pop_in : pd .Series , pop_out : pd .Series ) -> pd .Series :
498518 """
499- Vectorized two-proportion z-test.
519+ Vectorized two-proportion z-test with +1 pseudocount on all inputs.
520+
521+ Matches Clojure's stats/two-prop-test (stats.clj:18-33).
522+ See two_prop_test() scalar version for formula details.
500523
501524 Args:
502- p1 : Series of first proportions
503- n1 : Series of number of observations for first proportion
504- p2 : Series of second proportions
505- n2 : Series of number of observations for second proportion
525+ succ_in : Series of success counts in the group
526+ succ_out : Series of success counts outside the group
527+ pop_in : Series of total vote counts in the group
528+ pop_out : Series of total vote counts outside the group
506529
507530 Returns:
508531 Series of z-scores
509532 """
510- # Pooled probability
511- p_pooled = (p1 * n1 + p2 * n2 ) / (n1 + n2 )
533+ # Add +1 pseudocount to all four inputs (Clojure: map inc)
534+ s1 = succ_in + 1
535+ s2 = succ_out + 1
536+ p1 = pop_in + 1
537+ p2 = pop_out + 1
512538
513- # Standard error
514- se = np .sqrt (p_pooled * (1 - p_pooled ) * (1 / n1 + 1 / n2 ))
539+ pi1 = s1 / p1
540+ pi2 = s2 / p2
541+ pi_hat = (s1 + s2 ) / (p1 + p2 )
515542
516- # Z-score calculation
517- z = (p1 - p2 ) / se
543+ se = np . sqrt ( pi_hat * ( 1 - pi_hat ) * ( 1 / p1 + 1 / p2 ))
544+ z = (pi1 - pi2 ) / se
518545
519- # Handle edge cases
546+ # Handle edge cases: pop_in=0 or pop_out=0 → 0, pi_hat=1 → 0
547+ z = z .where ((pop_in > 0 ) & (pop_out > 0 ), 0.0 )
520548 z = z .fillna (0.0 )
521549 z = z .replace ([np .inf , - np .inf ], 0.0 )
522550 return z
@@ -649,14 +677,16 @@ def compute_group_comment_stats_df(votes_long: pd.DataFrame,
649677 stats_df ['ra' ] = stats_df ['ra' ].replace ([np .inf , - np .inf ], 1.0 ).fillna (1.0 )
650678 stats_df ['rd' ] = stats_df ['rd' ].replace ([np .inf , - np .inf ], 1.0 ).fillna (1.0 )
651679
652- # Compute representativeness tests (two-proportion z-test: group vs other)
680+ # Compute representativeness tests — pass raw counts, matching Clojure's
681+ # (stats/two-prop-test (:na in-stats) (sum :na rest-stats)
682+ # (:ns in-stats) (sum :ns rest-stats)) (repness.clj:97-100)
653683 stats_df ['rat' ] = two_prop_test_vectorized (
654- stats_df ['pa ' ], stats_df ['ns ' ],
655- stats_df ['other_pa ' ], stats_df ['other_votes' ]
684+ stats_df ['na ' ], stats_df ['other_agree ' ],
685+ stats_df ['ns ' ], stats_df ['other_votes' ]
656686 )
657687 stats_df ['rdt' ] = two_prop_test_vectorized (
658- stats_df ['pd ' ], stats_df ['ns ' ],
659- stats_df ['other_pd ' ], stats_df ['other_votes' ]
688+ stats_df ['nd ' ], stats_df ['other_disagree ' ],
689+ stats_df ['ns ' ], stats_df ['other_votes' ]
660690 )
661691
662692 # Compute metrics
0 commit comments