1313import re
1414import json
1515
16+ # Small epsilon for floating-point tolerance when comparing p-values in Monte Carlo test
17+ _PVAL_TOL = 1e-10
18+
1619#'Ranking the most interactive gene (ligand or receptor)
1720#'
1821#'@param data lrobject
@@ -404,6 +407,67 @@ def add_node_type(df):
404407 return df
405408
406409
410+ def _pairwise_stats (joined , B = 1000 , rng = None ):
411+ """Compute Fisher's exact test and Monte Carlo permutation p-values for each cell pair.
412+
413+ Parameters
414+ ----------
415+ joined : pandas.DataFrame
416+ DataFrame with columns 'cellpair' (str), 'measure_ctr' (numeric count for the
417+ control condition), and 'measure_exp' (numeric count for the experimental condition).
418+ B : int
419+ Number of Monte Carlo permutations.
420+ rng : numpy.random.Generator, optional
421+ Random number generator for reproducibility; created with seed 42 if None.
422+
423+ Returns
424+ -------
425+ pandas.DataFrame
426+ DataFrame with columns 'cellpair', 'p_value', 'perm_p_value', 'lodds'
427+ """
428+ if rng is None :
429+ rng = np .random .default_rng (42 )
430+
431+ measure_ctr_sum = joined ['measure_ctr' ].sum ()
432+ measure_exp_sum = joined ['measure_exp' ].sum ()
433+ pvals = []
434+
435+ for _ , row in joined .iterrows ():
436+ ctotal = measure_ctr_sum - row ['measure_ctr' ]
437+ etotal = measure_exp_sum - row ['measure_exp' ]
438+ matrix = np .array ([[row ['measure_exp' ], etotal ], [row ['measure_ctr' ], ctotal ]])
439+
440+ odds_ratio , p_value = fisher_exact (matrix , alternative = "two-sided" )
441+
442+ # Monte Carlo permutation test:
443+ # np.tile creates B copies of the 4-element flat matrix; rng.permuted then
444+ # independently shuffles each row (permutation), giving a (B, 4) array that
445+ # is reshaped into B individual 2×2 matrices.
446+ flat = matrix .flatten ()
447+ permuted = rng .permuted (np .tile (flat , (B , 1 )), axis = 1 ).reshape (B , 2 , 2 )
448+ # Deduplicate: with only 4 values there are at most 4!=24 unique arrangements,
449+ # so we call fisher_exact only for each unique matrix instead of all B copies.
450+ unique_perms , inverse = np .unique (permuted .reshape (B , 4 ), axis = 0 , return_inverse = True )
451+ unique_pvals = np .array ([
452+ fisher_exact (p .reshape (2 , 2 ), alternative = "two-sided" )[1 ]
453+ for p in unique_perms
454+ ])
455+ perm_pvals = unique_pvals [inverse ]
456+ count = (perm_pvals <= p_value + _PVAL_TOL ).sum ()
457+ perm_p_value = (count + 1 ) / (B + 1 )
458+
459+ lodds = np .log2 (odds_ratio ) if odds_ratio > 0 else None
460+
461+ pvals .append ({
462+ 'cellpair' : row ['cellpair' ],
463+ 'p_value' : p_value ,
464+ 'perm_p_value' : perm_p_value ,
465+ 'lodds' : lodds ,
466+ })
467+
468+ return pd .DataFrame (pvals )
469+
470+
407471def fisher_test_cci (annData , measure , out_path , comparison = None ):
408472 """
409473 Evaluate Differences in the edge proportion
@@ -423,6 +487,7 @@ def fisher_test_cci(annData, measure, out_path, comparison=None):
423487
424488 """
425489 data = annData .uns ['pycrosstalker' ]['results' ]
490+ rng = np .random .default_rng (42 )
426491
427492 if comparison is not None :
428493 for pair in comparison :
@@ -434,38 +499,8 @@ def fisher_test_cci(annData, measure, out_path, comparison=None):
434499
435500 joined = pd .merge (c , e , on = 'cellpair' , how = 'outer' , suffixes = ('_ctr' , '_exp' ))
436501 joined .fillna (0 , inplace = True )
437- pvals = []
438- measure_ctr_sum = joined ['measure_ctr' ].sum ()
439- measure_exp_sum = joined ['measure_exp' ].sum ()
440- for idx , row in joined .iterrows ():
441- ctotal = joined ['measure_ctr' ].sum () - row ['measure_ctr' ]
442- etotal = joined ['measure_exp' ].sum () - row ['measure_exp' ]
443- matrix = np .array ([[row ['measure_exp' ], etotal ], [row ['measure_ctr' ], ctotal ]])
444-
445- odds_ratio , p_value = fisher_exact (matrix , alternative = "two-sided" )
446-
447- # Monte Carlo resampling (if B > 0)
448- B = 1000
449- np .random .seed (42 ) # For reproducibility
450- if B > 0 :
451- count = 0
452- for _ in range (B ):
453- shuffled = np .random .permutation (matrix .flatten ()).reshape (matrix .shape )
454- if fisher_exact (shuffled , alternative = "two-sided" )[1 ] <= p_value :
455- count += 1
456- perm_p_value = (count + 1 ) / (B + 1 ) # Avoid zero probability
457- else :
458- perm_p_value = None
459-
460- lodds = np .log2 (odds_ratio ) if odds_ratio > 0 else None # Compute log odds ratio
461502
462- pvals .append ({
463- 'cellpair' : row ['cellpair' ],
464- 'p_value' : p_value ,
465- 'lodds' : lodds
466- })
467-
468- pval_df = pd .DataFrame (pvals )
503+ pval_df = _pairwise_stats (joined , rng = rng )
469504 data ['stats' ][f'{ exp_name } _x_{ ctr_name } ' ] = pval_df
470505
471506 # with open(os.path.join(out_path, "LR_data_final.pkl"), "wb") as f:
@@ -486,37 +521,8 @@ def fisher_test_cci(annData, measure, out_path, comparison=None):
486521 # Merge control and experimental data on 'cellpair'
487522 joined = pd .merge (c , e , on = 'cellpair' , how = 'inner' , suffixes = ('_ctr' , '_exp' ))
488523 joined .fillna (0 , inplace = True )
489- pvals = []
490- measure_ctr_sum = joined ['measure_ctr' ].sum ()
491- measure_exp_sum = joined ['measure_exp' ].sum ()
492- for idx , row in joined .iterrows ():
493- ctotal = measure_ctr_sum - row ['measure_ctr' ]
494- etotal = measure_exp_sum - row ['measure_exp' ]
495- matrix = np .array ([[row ['measure_exp' ], etotal ], [row ['measure_ctr' ], ctotal ]])
496- odds_ratio , p_value = fisher_exact (matrix , alternative = "two-sided" )
497-
498- # Monte Carlo resampling (if B > 0)
499- B = 1000
500- np .random .seed (42 ) # For reproducibility
501- if B > 0 :
502- count = 0
503- for _ in range (B ):
504- shuffled = np .random .permutation (matrix .flatten ()).reshape (matrix .shape )
505- if fisher_exact (shuffled , alternative = "two-sided" )[1 ] <= p_value :
506- count += 1
507- perm_p_value = (count + 1 ) / (B + 1 ) # Avoid zero probability
508- else :
509- perm_p_value = None
510-
511- lodds = np .log2 (odds_ratio ) if odds_ratio > 0 else None # Compute log odds ratio
512-
513- pvals .append ({
514- 'cellpair' : row ['cellpair' ],
515- 'p_value' : p_value ,
516- 'lodds' : lodds
517- })
518524
519- pval_df = pd . DataFrame ( pvals )
525+ pval_df = _pairwise_stats ( joined , rng = rng )
520526 data ['stats' ][f'{ list (data ["tables" ].keys ())[i ]} _x_{ list (data ["tables" ].keys ())[0 ]} ' ] = pval_df
521527
522528 # with open(os.path.join(out_path, "LR_data_final.pkl"), "wb") as f:
0 commit comments