30300.279652...
3131"""
3232
33- import numpy as np
34- import scipy as sp
3533import abc
36- import dataclasses
37-
3834import contextlib
39- import os
35+ import dataclasses
4036import itertools
37+ import os
38+ from collections .abc import Iterable , Iterator
39+ from typing import Any
40+
41+ import numpy as np
42+ import scipy as sp
43+ from typing_extensions import Self
44+
45+ from probabilit .types import Array1D , Array2D , CorrelationType
4146
4247# CVXPY prints error messages about incompatible ortools version during import.
4348# Since we use the SCS solver and not GLOP/PDLP (which need ortools), these errors
@@ -56,7 +61,13 @@ class CorrelatorError(Exception):
5661 pass
5762
5863
59- def nearest_correlation_matrix (matrix , * , weights = None , eps = 1e-6 , verbose = False ):
64+ def nearest_correlation_matrix (
65+ matrix : Array2D ,
66+ * ,
67+ weights : Array2D | None = None ,
68+ eps : float = 1e-6 ,
69+ verbose : bool = False ,
70+ ) -> Array2D :
6071 """Returns the correlation matrix nearest to `matrix`, weighted elementwise
6172 by `weights`.
6273
@@ -156,7 +167,7 @@ def nearest_correlation_matrix(matrix, *, weights=None, eps=1e-6, verbose=False)
156167 return X
157168
158169
159- def _is_positive_definite (X ) :
170+ def _is_positive_definite (X : Array2D ) -> bool :
160171 try :
161172 np .linalg .cholesky (X )
162173 return True
@@ -165,7 +176,7 @@ def _is_positive_definite(X):
165176
166177
167178class Correlator (abc .ABC ):
168- def set_target (self , correlation_matrix , cholesky = True ):
179+ def set_target (self , correlation_matrix : Array2D , cholesky : bool = True ) -> Self :
169180 """Set target correlation matrix."""
170181 if not isinstance (correlation_matrix , np .ndarray ):
171182 raise TypeError ("Input argument `correlation_matrix` must be NumPy array." )
@@ -185,7 +196,7 @@ def set_target(self, correlation_matrix, cholesky=True):
185196 self .P = np .linalg .cholesky (self .C )
186197 return self
187198
188- def _validate_X (self , X , check_rows_cols = True ):
199+ def _validate_X (self , X : Array2D , check_rows_cols : bool = True ) -> tuple [ int , int ] :
189200 """Validate array X of shape (observations, variables)."""
190201 if not (hasattr (self , "C" )):
191202 raise CorrelatorError ("User must call `set_target` first." )
@@ -246,11 +257,11 @@ class Cholesky(Correlator):
246257
247258 """
248259
249- def set_target (self , correlation_matrix ) :
260+ def set_target (self , correlation_matrix : Array2D ) -> Self :
250261 super ().set_target (correlation_matrix )
251262 return self
252263
253- def __call__ (self , X ) :
264+ def __call__ (self , X : Array2D ) -> Array2D :
254265 """Transform an input matrix X.
255266
256267 Parameters
@@ -370,11 +381,11 @@ class ImanConover(Correlator):
370381 np.float64(0.592541)
371382 """
372383
373- def set_target (self , correlation_matrix ) :
384+ def set_target (self , correlation_matrix : Array2D ) -> Self :
374385 super ().set_target (correlation_matrix )
375386 return self
376387
377- def __call__ (self , X ) :
388+ def __call__ (self , X : Array2D ) -> Array2D :
378389 """Transform an input matrix X.
379390
380391 The output will have the same marginal distributions, but with
@@ -455,13 +466,13 @@ class SwapIndexGenerator:
455466 (array([7, 0, 4, 2]), array([3, 5, 1, 8]))
456467 """
457468
458- def __init__ (self , rng , n : int ):
469+ def __init__ (self , rng : np . random . Generator , n : int ):
459470 assert n >= 2
460471 self .rng = rng
461472 self .indices = np .arange (n )
462473 self .permutation = self .rng .permutation (self .indices )
463474
464- def __call__ (self , size : int ):
475+ def __call__ (self , size : int ) -> tuple [ Array1D , Array1D ] :
465476 assert size >= 1
466477
467478 # Get 2 * size elements
@@ -484,12 +495,12 @@ class Permutation(Correlator):
484495 def __init__ (
485496 self ,
486497 * ,
487- weights = None ,
488- iterations = 1000 ,
489- tol = 0.01 ,
490- correlation_type = "pearson" ,
491- random_state = None ,
492- verbose = False ,
498+ weights : Array2D | None = None ,
499+ iterations : int = 1000 ,
500+ tol : float = 0.01 ,
501+ correlation_type : CorrelationType = "pearson" ,
502+ random_state : np . random . Generator | int | None = None ,
503+ verbose : bool = False ,
493504 ):
494505 """Create a Permutation instance, which induces correlations
495506 between variables in X by randomly shuffling rows within each column.
@@ -586,7 +597,12 @@ def __init__(
586597 self .verbose = verbose
587598 self .correlation_type = correlation_type
588599
589- def set_target (self , correlation_matrix , * , weights = None ):
600+ def set_target (
601+ self ,
602+ correlation_matrix : Array2D ,
603+ * ,
604+ weights : Array2D | None = None ,
605+ ) -> Self :
590606 """Set the target correlation matrix.
591607
592608 Parameters
@@ -603,14 +619,14 @@ def set_target(self, correlation_matrix, *, weights=None):
603619 self .triu_indices = np .triu_indices (self .C .shape [0 ], k = 1 )
604620 return self
605621
606- def _error (self , observed , target ) :
622+ def _error (self , observed : Array2D , target : Array2D ) -> float :
607623 """Compute RMSE over upper triangular part of corr(X) - target."""
608624 idx = self .triu_indices # Get upper triangular indices (ignore diag)
609625 weighted_residuals_sq = self .weights [idx ] * (observed [idx ] - target [idx ]) ** 2.0
610626 return float (np .sqrt (np .sum (weighted_residuals_sq )))
611627
612628 @staticmethod
613- def subiters (n , i ) :
629+ def subiters (n : int , i : int ) -> int :
614630 """Number of sub-iterations (swaps) per iteration."""
615631 # Use longer swap lengths in early iterations. The last half
616632 # of the iterations will use 1 sub-iteration. The second half of the
@@ -624,7 +640,7 @@ def subiters(n, i):
624640 C = np .log2 (n ) + 1
625641 return int (np .ceil (C ** (1 - (2 * i / n ))))
626642
627- def __call__ (self , X ) :
643+ def __call__ (self , X : Array2D ) -> Array2D :
628644 """Cycle through through columns (variables), and for each
629645 column it swaps random rows (observations). If the result
630646 leads to a smaller error (correlation closer to target), then it is
@@ -654,7 +670,9 @@ def __call__(self, X):
654670 f"Running permutation correlator for { self .iters if self .iters else 'inf' } iterations."
655671 )
656672
657- def product (iterations_gen , variables_gen ):
673+ def product (
674+ iterations_gen : Iterable [int ], variables_gen : Iterable [int ]
675+ ) -> Iterator [tuple [int , int ]]:
658676 # itertools.product only works for finite inputs, so we need this
659677 for i in iterations_gen :
660678 for j in variables_gen :
@@ -711,7 +729,7 @@ def product(iterations_gen, variables_gen):
711729 return corr_mat .X # The permuted data stored in CorrelationMatrix
712730
713731
714- def decorrelate (X , remove_variance = True ):
732+ def decorrelate (X : Array2D , remove_variance : bool = True ) -> Array2D :
715733 """Removes correlations or covariance from data X.
716734
717735 Examples
@@ -825,7 +843,12 @@ class CorrelationMatrix:
825843 [ 0.325, -0.6 , -0.151, 1. ]])
826844 """
827845
828- def __init__ (self , X , correlation_type = "pearson" , check = True ):
846+ def __init__ (
847+ self ,
848+ X : Array2D ,
849+ correlation_type : CorrelationType = "pearson" ,
850+ check : bool = True ,
851+ ):
829852 valid_corrs = ("pearson" , "spearman" )
830853 assert correlation_type in valid_corrs
831854 assert X .ndim == 2
@@ -860,13 +883,18 @@ def __init__(self, X, correlation_type="pearson", check=True):
860883 :, None
861884 ]
862885
863- def __repr__ (self ):
886+ def __repr__ (self ) -> str :
864887 return repr (self .corr_mat )
865888
866- def __getitem__ (self , * args , ** kwargs ) :
889+ def __getitem__ (self , * args : Any , ** kwargs : Any ) -> float :
867890 return self .corr_mat .__getitem__ (* args , ** kwargs )
868891
869- def commit (self , col , i , j ):
892+ def commit (
893+ self ,
894+ col : int ,
895+ i : int | list [int ],
896+ j : int | list [int ],
897+ ) -> Self :
870898 """Commit a swap, storing new data and new correlation matrix."""
871899
872900 # Compute everything we need to update internal state
@@ -887,7 +915,12 @@ def commit(self, col, i, j):
887915 self .X [i , col ], self .X [j , col ] = self .X [j , col ], self .X [i , col ]
888916 return self
889917
890- def _delta_numerator (self , col , i , j ):
918+ def _delta_numerator (
919+ self ,
920+ col : int ,
921+ i : int | list [int ],
922+ j : int | list [int ],
923+ ) -> Array1D :
891924 """Compute the delta in the numerator when swapping."""
892925 if self .check :
893926 assert isinstance (col , int )
@@ -914,14 +947,24 @@ def _delta_numerator(self, col, i, j):
914947 delta_numerator [col ] = 0.0
915948 return delta_numerator
916949
917- def delta_column (self , col , i , j ):
950+ def delta_column (
951+ self ,
952+ col : int ,
953+ i : int | list [int ],
954+ j : int | list [int ],
955+ ) -> Array1D :
918956 """Returns the change in the column `col` in the correlation matrix
919957 when rows i and j are swapped. To save a change, use `.commit()`."""
920958
921959 diff = self ._delta_numerator (col , i , j )
922960 return diff / (self .m * self .denominator * self .denominator [col ])
923961
924- def update_column (self , col , i , j ):
962+ def update_column (
963+ self ,
964+ col : int ,
965+ i : int | list [int ],
966+ j : int | list [int ],
967+ ) -> Array1D :
925968 """Returns the new value of column `col` in the correlation matrix
926969 when rows i and j are swapped. To save a change, use `.commit()`."""
927970
@@ -933,25 +976,30 @@ def update_column(self, col, i, j):
933976class Composite (Correlator ):
934977 """A composition where we first run ImanConover, then Permutation."""
935978
936- def __init__ (self , * args , ** kwargs ):
979+ def __init__ (self , * args : Any , ** kwargs : Any ):
937980 self .iman_conover_correlator = ImanConover ()
938981 self .permutation_correlator = Permutation (* args , ** kwargs )
939982
940- def set_target (self , correlation_matrix , * , weights = None ):
983+ def set_target (
984+ self ,
985+ correlation_matrix : Array2D ,
986+ * ,
987+ weights : Array2D | None = None ,
988+ ) -> Self :
941989 self .iman_conover_correlator .set_target (correlation_matrix )
942990 self .permutation_correlator .set_target (correlation_matrix , weights = weights )
943991 return self
944992
945- def __call__ (self , X ) :
993+ def __call__ (self , X : Array2D ) -> Array2D :
946994 # First run ImanConover to get a good starting point
947995 X_ic = self .iman_conover_correlator (X )
948996 # Then run Permutation
949997 return self .permutation_correlator (X_ic )
950998
951999
9521000if __name__ == "__main__" :
953- import pytest
9541001 import matplotlib .pyplot as plt
1002+ import pytest
9551003
9561004 pytest .main (args = [__file__ , "--doctest-modules" , "-v" , "--capture=sys" ])
9571005
0 commit comments