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." )
@@ -244,11 +255,11 @@ class Cholesky(Correlator):
244255
245256 """
246257
247- def set_target (self , correlation_matrix ) :
258+ def set_target (self , correlation_matrix : Array2D ) -> Self :
248259 super ().set_target (correlation_matrix )
249260 return self
250261
251- def __call__ (self , X ) :
262+ def __call__ (self , X : Array2D ) -> Array2D :
252263 """Transform an input matrix X.
253264
254265 Parameters
@@ -368,11 +379,11 @@ class ImanConover(Correlator):
368379 np.float64(0.592541)
369380 """
370381
371- def set_target (self , correlation_matrix ) :
382+ def set_target (self , correlation_matrix : Array2D ) -> Self :
372383 super ().set_target (correlation_matrix )
373384 return self
374385
375- def __call__ (self , X ) :
386+ def __call__ (self , X : Array2D ) -> Array2D :
376387 """Transform an input matrix X.
377388
378389 The output will have the same marginal distributions, but with
@@ -453,13 +464,13 @@ class SwapIndexGenerator:
453464 (array([7, 0, 4, 2]), array([3, 5, 1, 8]))
454465 """
455466
456- def __init__ (self , rng , n : int ):
467+ def __init__ (self , rng : np . random . Generator , n : int ):
457468 assert n >= 2
458469 self .rng = rng
459470 self .indices = np .arange (n )
460471 self .permutation = self .rng .permutation (self .indices )
461472
462- def __call__ (self , size : int ):
473+ def __call__ (self , size : int ) -> tuple [ Array1D , Array1D ] :
463474 assert size >= 1
464475
465476 # Get 2 * size elements
@@ -482,12 +493,12 @@ class Permutation(Correlator):
482493 def __init__ (
483494 self ,
484495 * ,
485- weights = None ,
486- iterations = 1000 ,
487- tol = 0.01 ,
488- correlation_type = "pearson" ,
489- random_state = None ,
490- verbose = False ,
496+ weights : Array2D | None = None ,
497+ iterations : int = 1000 ,
498+ tol : float = 0.01 ,
499+ correlation_type : CorrelationType = "pearson" ,
500+ random_state : np . random . Generator | int | None = None ,
501+ verbose : bool = False ,
491502 ):
492503 """Create a Permutation instance, which induces correlations
493504 between variables in X by randomly shuffling rows within each column.
@@ -584,7 +595,12 @@ def __init__(
584595 self .verbose = verbose
585596 self .correlation_type = correlation_type
586597
587- def set_target (self , correlation_matrix , * , weights = None ):
598+ def set_target (
599+ self ,
600+ correlation_matrix : Array2D ,
601+ * ,
602+ weights : Array2D | None = None ,
603+ ) -> Self :
588604 """Set the target correlation matrix.
589605
590606 Parameters
@@ -601,14 +617,14 @@ def set_target(self, correlation_matrix, *, weights=None):
601617 self .triu_indices = np .triu_indices (self .C .shape [0 ], k = 1 )
602618 return self
603619
604- def _error (self , observed , target ) :
620+ def _error (self , observed : Array2D , target : Array2D ) -> float :
605621 """Compute RMSE over upper triangular part of corr(X) - target."""
606622 idx = self .triu_indices # Get upper triangular indices (ignore diag)
607623 weighted_residuals_sq = self .weights [idx ] * (observed [idx ] - target [idx ]) ** 2.0
608624 return float (np .sqrt (np .sum (weighted_residuals_sq )))
609625
610626 @staticmethod
611- def subiters (n , i ) :
627+ def subiters (n : int , i : int ) -> int :
612628 """Number of sub-iterations (swaps) per iteration."""
613629 # Use longer swap lengths in early iterations. The last half
614630 # of the iterations will use 1 sub-iteration. The second half of the
@@ -622,7 +638,7 @@ def subiters(n, i):
622638 C = np .log2 (n ) + 1
623639 return int (np .ceil (C ** (1 - (2 * i / n ))))
624640
625- def __call__ (self , X ) :
641+ def __call__ (self , X : Array2D ) -> Array2D :
626642 """Cycle through through columns (variables), and for each
627643 column it swaps random rows (observations). If the result
628644 leads to a smaller error (correlation closer to target), then it is
@@ -652,7 +668,9 @@ def __call__(self, X):
652668 f"Running permutation correlator for { self .iters if self .iters else 'inf' } iterations."
653669 )
654670
655- def product (iterations_gen , variables_gen ):
671+ def product (
672+ iterations_gen : Iterable [int ], variables_gen : Iterable [int ]
673+ ) -> Iterator [tuple [int , int ]]:
656674 # itertools.product only works for finite inputs, so we need this
657675 for i in iterations_gen :
658676 for j in variables_gen :
@@ -709,7 +727,7 @@ def product(iterations_gen, variables_gen):
709727 return corr_mat .X # The permuted data stored in CorrelationMatrix
710728
711729
712- def decorrelate (X , remove_variance = True ):
730+ def decorrelate (X : Array2D , remove_variance : bool = True ) -> Array2D :
713731 """Removes correlations or covariance from data X.
714732
715733 Examples
@@ -823,7 +841,12 @@ class CorrelationMatrix:
823841 [ 0.325, -0.6 , -0.151, 1. ]])
824842 """
825843
826- def __init__ (self , X , correlation_type = "pearson" , check = True ):
844+ def __init__ (
845+ self ,
846+ X : Array2D ,
847+ correlation_type : CorrelationType = "pearson" ,
848+ check : bool = True ,
849+ ):
827850 valid_corrs = ("pearson" , "spearman" )
828851 assert correlation_type in valid_corrs
829852 assert X .ndim == 2
@@ -858,13 +881,18 @@ def __init__(self, X, correlation_type="pearson", check=True):
858881 :, None
859882 ]
860883
861- def __repr__ (self ):
884+ def __repr__ (self ) -> str :
862885 return repr (self .corr_mat )
863886
864- def __getitem__ (self , * args , ** kwargs ) :
887+ def __getitem__ (self , * args : Any , ** kwargs : Any ) -> float :
865888 return self .corr_mat .__getitem__ (* args , ** kwargs )
866889
867- def commit (self , col , i , j ):
890+ def commit (
891+ self ,
892+ col : int ,
893+ i : int | list [int ],
894+ j : int | list [int ],
895+ ) -> Self :
868896 """Commit a swap, storing new data and new correlation matrix."""
869897
870898 # Compute everything we need to update internal state
@@ -885,7 +913,12 @@ def commit(self, col, i, j):
885913 self .X [i , col ], self .X [j , col ] = self .X [j , col ], self .X [i , col ]
886914 return self
887915
888- def _delta_numerator (self , col , i , j ):
916+ def _delta_numerator (
917+ self ,
918+ col : int ,
919+ i : int | list [int ],
920+ j : int | list [int ],
921+ ) -> Array1D :
889922 """Compute the delta in the numerator when swapping."""
890923 if self .check :
891924 assert isinstance (col , int )
@@ -912,14 +945,24 @@ def _delta_numerator(self, col, i, j):
912945 delta_numerator [col ] = 0.0
913946 return delta_numerator
914947
915- def delta_column (self , col , i , j ):
948+ def delta_column (
949+ self ,
950+ col : int ,
951+ i : int | list [int ],
952+ j : int | list [int ],
953+ ) -> Array1D :
916954 """Returns the change in the column `col` in the correlation matrix
917955 when rows i and j are swapped. To save a change, use `.commit()`."""
918956
919957 diff = self ._delta_numerator (col , i , j )
920958 return diff / (self .m * self .denominator * self .denominator [col ])
921959
922- def update_column (self , col , i , j ):
960+ def update_column (
961+ self ,
962+ col : int ,
963+ i : int | list [int ],
964+ j : int | list [int ],
965+ ) -> Array1D :
923966 """Returns the new value of column `col` in the correlation matrix
924967 when rows i and j are swapped. To save a change, use `.commit()`."""
925968
@@ -953,16 +996,21 @@ class Composite(Correlator):
953996 array([-0.23, 1. , -0.25, -0.27, -0.88, -0.24])
954997 """
955998
956- def __init__ (self , * args , ** kwargs ):
999+ def __init__ (self , * args : Any , ** kwargs : Any ):
9571000 self .iman_conover_correlator = ImanConover ()
9581001 self .permutation_correlator = Permutation (* args , ** kwargs )
9591002
960- def set_target (self , correlation_matrix , * , weights = None ):
1003+ def set_target (
1004+ self ,
1005+ correlation_matrix : Array2D ,
1006+ * ,
1007+ weights : Array2D | None = None ,
1008+ ) -> Self :
9611009 self .iman_conover_correlator .set_target (correlation_matrix )
9621010 self .permutation_correlator .set_target (correlation_matrix , weights = weights )
9631011 return self
9641012
965- def __call__ (self , X ) :
1013+ def __call__ (self , X : Array2D ) -> Array2D :
9661014 try :
9671015 # First run ImanConover to get a good starting point
9681016 X_ic = self .iman_conover_correlator (X )
@@ -971,14 +1019,13 @@ def __call__(self, X):
9711019 X_ic = X
9721020 else :
9731021 raise # re-raise if it's a different ValueError
974-
9751022 # Then run Permutation
9761023 return self .permutation_correlator (X_ic )
9771024
9781025
9791026if __name__ == "__main__" :
980- import pytest
9811027 import matplotlib .pyplot as plt
1028+ import pytest
9821029
9831030 pytest .main (args = [__file__ , "--doctest-modules" , "-v" , "--capture=sys" ])
9841031
0 commit comments