Repository: polsys/ennemi

Issue:
Add `estimate_corr` and `pairwise_corr`
Add shorter aliases for the estimation methods with `normalize=True`.

Discussion:

Patch:
diff --git a/docs/api-reference.md b/docs/api-reference.md
index 769186d..72daffd 100644
--- a/docs/api-reference.md
+++ b/docs/api-reference.md
@@ -98,6 +98,16 @@ with column names matching `x`.
 
 <hr style="margin: 4rem 0;">
 
+# `estimate_corr`
+Equivalent to `estimate_mi` with `normalize=True`;
+please refer to the documentation below.
+
+The correlation coefficient is calculated using the formula
+$$
+\rho = \sqrt{1 - \exp(-2\, \mathrm{MI}(X;Y))}.
+$$
+
+
 # `estimate_mi`
 Estimate the mutual information between `y` and each `x` variable.
 
@@ -105,7 +115,8 @@ Estimate the mutual information between `y` and each `x` variable.
 - Conditional MI: pass a `cond` array.
 - Discrete-continuous MI: set `discrete_y` to True.
 
-Returns the estimated mutual information (in nats or in correlation scale).
+Returns the estimated mutual information in nats, or if `normalize` is set,
+as a correlation coefficient.
 The result is a 2D `ndarray` where the first index represents `lag` values
 and the second index represents `x` columns.
 If `x` is a pandas `DataFrame` or `Series`, the result is a `DataFrame`
@@ -240,6 +251,11 @@ an infinite amount of entropy.
 
 <hr style="margin: 4rem 0;">
 
+# `pairwise_corr`
+Equivalent to `pairwise_mi` with `normalize=True`;
+please refer to the documentation below.
+
+
 # `pairwise_mi`
 Estimate the pairwise MI between each variable.
 
diff --git a/docs/kaisaniemi.md b/docs/kaisaniemi.md
index 786c931..fca2a93 100644
--- a/docs/kaisaniemi.md
+++ b/docs/kaisaniemi.md
@@ -18,7 +18,7 @@ Let's start by importing the data; nothing special here.
 We use the `pandas` library for data manipulation.
 
 ```python
-from ennemi import estimate_mi, pairwise_mi
+from ennemi import estimate_corr, pairwise_corr
 import matplotlib.pyplot as plt
 import numpy as np
 import pandas as pd
@@ -61,12 +61,12 @@ afternoon_mask = (data.index.hour == 13)
 
 To get an overview of the dependencies between variables, we create a pairwise MI plot.
 This is a matrix where the rows and columns correspond to variables.
-`ennemi` includes a `pairwise_mi()` method for this purpose.
-We pass the `normalize` parameter to transform the MI values to a correlation
+`ennemi` includes a `pairwise_corr()` method for this purpose.
+This method returns values on a correlation
 coefficient scale (0 to 1, sign of correlation not determined).
 
 ```python
-pairwise = pairwise_mi(data, mask=afternoon_mask, normalize=True)
+pairwise = pairwise_corr(data, mask=afternoon_mask)
 
 # Plot a matrix where the color represents the correlation coefficient.
 # We clip the color values at 0.2 because of significant random noise,
@@ -95,8 +95,8 @@ in our case this variable is DayOfYear.
 
 ```python
 # Now we pass the 'cond' parameter
-pairwise_doy = pairwise_mi(data, mask=afternoon_mask,
-    cond=data["DayOfYear"], normalize=True)
+pairwise_doy = pairwise_corr(data, mask=afternoon_mask,
+    cond=data["DayOfYear"])
 
 # The same plotting code as above
 fig, ax = plt.subplots(figsize=(8,6))
@@ -134,7 +134,7 @@ This is demonstrated more in the next steps.
 ## Time dependency
 
 How does temperature depend on past observations of the variables?
-We can answer this question with the time lag feature of `estimate_mi()`.
+We can answer this question with the time lag feature of `estimate_corr()`.
 We calculate the correlation between "temperature now" and
 "[other variable] [0, 2, ..., 48] hours ago".
 Again, the temperature observations are fixed at 15:00.
@@ -145,8 +145,8 @@ covariates = ["Temperature", "DewPoint", "WindDir", "AirPressure", "WindSpeed"]
 
 # Lag up to two days with 2-hour spacing
 lags = np.arange(0, 2*24 + 1, 2)
-temp = estimate_mi(data["Temperature"], data[covariates], lags,
-    cond=data["DayOfYear"], mask=afternoon_mask, normalize=True)
+temp = estimate_corr(data["Temperature"], data[covariates], lags,
+    cond=data["DayOfYear"], mask=afternoon_mask)
 
 # Plot the MI correlation coefficients as a line plot
 fig, ax = plt.subplots(figsize=(8,6))
@@ -197,12 +197,12 @@ We can therefore repeat the estimation with two more masks.
 
 ```python
 # Use two more masks, higher k, and average of the three runs
-temp_12 = estimate_mi(data["Temperature"], data[covariates], lags, k=8,
-    cond=data["DayOfYear"], mask=(data.index.hour == 12), normalize=True)
-temp_13 = estimate_mi(data["Temperature"], data[covariates], lags, k=8,
-    cond=data["DayOfYear"], mask=afternoon_mask, normalize=True)
-temp_14 = estimate_mi(data["Temperature"], data[covariates], lags, k=8,
-    cond=data["DayOfYear"], mask=(data.index.hour == 14), normalize=True)
+temp_12 = estimate_corr(data["Temperature"], data[covariates], lags, k=8,
+    cond=data["DayOfYear"], mask=(data.index.hour == 12))
+temp_13 = estimate_corr(data["Temperature"], data[covariates], lags, k=8,
+    cond=data["DayOfYear"], mask=afternoon_mask)
+temp_14 = estimate_corr(data["Temperature"], data[covariates], lags, k=8,
+    cond=data["DayOfYear"], mask=(data.index.hour == 14))
 
 temp_avg = (temp_12 + temp_13 + temp_14) / 3
 
diff --git a/docs/kaisaniemi_casestudy.py b/docs/kaisaniemi_casestudy.py
index 0eb4a9d..cb3b62e 100644
--- a/docs/kaisaniemi_casestudy.py
+++ b/docs/kaisaniemi_casestudy.py
@@ -4,7 +4,7 @@
 #
 # STEP 1: Import the data
 #
-from ennemi import estimate_mi, pairwise_mi
+from ennemi import estimate_corr, pairwise_corr
 import matplotlib.pyplot as plt
 import numpy as np
 import pandas as pd
@@ -29,7 +29,7 @@ afternoon_mask = (data.index.hour == 13)
 #
 # STEP 4: Plot pairwise MI
 #
-pairwise = pairwise_mi(data, mask=afternoon_mask, normalize=True)
+pairwise = pairwise_corr(data, mask=afternoon_mask)
 
 # Plot a matrix where the color represents the correlation coefficient.
 # We clip the color values at 0.2 because of significant random noise,
@@ -53,8 +53,8 @@ plt.savefig("casestudy_pairwise.png", transparent=True)
 #
 
 # Now we pass the 'cond' parameter
-pairwise_doy = pairwise_mi(data, mask=afternoon_mask,
-    cond=data["DayOfYear"], normalize=True)
+pairwise_doy = pairwise_corr(data, mask=afternoon_mask,
+    cond=data["DayOfYear"])
 
 # The same plotting code as above
 fig, ax = plt.subplots(figsize=(8,6))
@@ -79,8 +79,8 @@ covariates = ["Temperature", "DewPoint", "WindDir", "AirPressure", "WindSpeed"]
 
 # Lag up to two days with 2-hour spacing
 lags = np.arange(0, 2*24 + 1, 2)
-temp = estimate_mi(data["Temperature"], data[covariates], lags,
-    cond=data["DayOfYear"], mask=afternoon_mask, normalize=True)
+temp = estimate_corr(data["Temperature"], data[covariates], lags,
+    cond=data["DayOfYear"], mask=afternoon_mask)
 
 # Plot the MI correlation coefficients as a line plot
 fig, ax = plt.subplots(figsize=(8,6))
@@ -106,12 +106,12 @@ plt.savefig("casestudy_lags.png", transparent=True)
 #
 
 # Use two more masks, higher k, and average of the three runs
-temp_12 = estimate_mi(data["Temperature"], data[covariates], lags, k=8,
-    cond=data["DayOfYear"], mask=(data.index.hour == 12), normalize=True)
-temp_13 = estimate_mi(data["Temperature"], data[covariates], lags, k=8,
-    cond=data["DayOfYear"], mask=afternoon_mask, normalize=True)
-temp_14 = estimate_mi(data["Temperature"], data[covariates], lags, k=8,
-    cond=data["DayOfYear"], mask=(data.index.hour == 14), normalize=True)
+temp_12 = estimate_corr(data["Temperature"], data[covariates], lags, k=8,
+    cond=data["DayOfYear"], mask=(data.index.hour == 12))
+temp_13 = estimate_corr(data["Temperature"], data[covariates], lags, k=8,
+    cond=data["DayOfYear"], mask=afternoon_mask)
+temp_14 = estimate_corr(data["Temperature"], data[covariates], lags, k=8,
+    cond=data["DayOfYear"], mask=(data.index.hour == 14))
 
 temp_avg = (temp_12 + temp_13 + temp_14) / 3
 
diff --git a/docs/snippets.md b/docs/snippets.md
index 6fd95f2..4c730b1 100644
--- a/docs/snippets.md
+++ b/docs/snippets.md
@@ -51,6 +51,7 @@ def get_pairwise_mi_callback(x, factor=1):
 ```
 
 To use the callback, pass the returned value to `estimate_mi`/`pairwise_mi`
+(or their `..._corr` counterparts)
 as the optional `callback` keyword parameter:
 ```python
 y, covariates = ...
diff --git a/docs/tutorial.md b/docs/tutorial.md
index c21416d..44665ec 100644
--- a/docs/tutorial.md
+++ b/docs/tutorial.md
@@ -72,6 +72,11 @@ To continue the above example, we could execute
 print(estimate_mi(y, x, normalize=True))
 ```
 to get the estimated correlation coefficient (`0.79980729`).
+Because the correlation coefficient is more useful in applied analysis,
+there is a shorter alias for it:
+```python
+print(estimate_corr(y, x))
+```
 
 The returned coefficient approximately matches the **absolute value**
 of the linear correlation coefficient after suitable transformations.
@@ -86,14 +91,14 @@ transformed the X axis to represent $\sin(x)$, and kept the Y axis as is.
 
 We calculate both the linear correlation and correlation from MI:
 ```python
-from ennemi import estimate_mi
+from ennemi import estimate_corr
 import numpy as np
 
 rng = np.random.default_rng(1234)
 x = rng.normal(0.0, 3.0, size=800)
 y = np.sin(x) + rng.normal(0.0, 0.5, size=800)
 
-print(f"From MI: {estimate_mi(y, x, normalize=True)[0,0]:.3}")
+print(f"From MI: {estimate_corr(y, x)[0,0]:.3}")
 print(f"Pearson, transformed: {np.corrcoef(y, np.sin(x))[0,1]:.3}")
 print(f"Pearson, straight line: {np.corrcoef(y, x)[0,1]:.3}")
 ```
@@ -121,7 +126,7 @@ Therefore, the returned coefficient **should be considered only approximate**.
 Let's extend the above example by adding another, unrelated variable.
 The mutual information between independent variables is 0.
 
-The `estimate_mi()` method accepts a 2D array for the `x` parameter.
+The `estimate_mi()` and `estimate_corr()` methods accept a 2D array for the `x` parameter.
 In that case, it splits the array into columns $X_1$ to $X_m$,
 and calculates $\mathrm{MI}(X_i, Y)$ for each $i = 1, \ldots, m$.
 
@@ -138,7 +143,7 @@ as the results would be difficult to interpret.
 
 Here's the above example updated with a new, independent variable $Z$:
 ```python
-from ennemi import estimate_mi
+from ennemi import estimate_corr
 import numpy as np
 
 rng = np.random.default_rng(1234)
@@ -152,7 +157,7 @@ z = rng.normal(0, 1, size=800)
 
 covariates = np.column_stack((x, z))
 
-print(estimate_mi(y, covariates, normalize=True))
+print(estimate_corr(y, covariates))
 ```
 
 The code prints
@@ -179,14 +184,14 @@ There may be a clear dependence between $Y(t)$ and $X(t-\Delta)$,
 whereas the apparent link between $Y(t)$ and $X(t)$ may be weaker or nonexistent.
 
 The time lag is specified by passing an integer or a list/array of integers as
-the `lag` parameter to `estimate_mi()`.
+the `lag` parameter to `estimate_mi()`/`estimate_corr()`.
 The lags are applied to the $X_i$ variables and may be positive or negative
 (in which case information flows from $Y$ to $X$ instead).
 
 Let's consider a model where $Y(t) = X(t-1) + \varepsilon$,
 and estimate the MI for various time lags:
 ```python
-from ennemi import estimate_mi
+from ennemi import estimate_corr
 import numpy as np
 
 rng = np.random.default_rng(1234)
@@ -195,7 +200,7 @@ y = np.zeros(400)
 y[1:] = x[0:-1]
 y += rng.normal(0, 0.01, size=400)
 
-print(estimate_mi(y, x, lag=[1, 0, -1], normalize=True))
+print(estimate_corr(y, x, lag=[1, 0, -1]))
 ```
 
 The code prints:
@@ -229,13 +234,13 @@ each in turn.
 ## Combining the above with Pandas
 
 In this example, we import the data set from a file using the Pandas package
-and pass the imported data straight to `estimate_mi()`.
+and pass the imported data straight to `estimate_corr()`.
 We calculate the MI for several time lags and plot the results with Matplotlib.
 
 To try this example, download the [mi_example.csv](mi_example.csv) file.
 
 ```python
-from ennemi import estimate_mi
+from ennemi import estimate_corr
 import matplotlib.pyplot as plt
 import numpy as np
 import pandas as pd
@@ -243,8 +248,8 @@ import pandas as pd
 data = pd.read_csv("mi_example.csv")
 time_lags = np.arange(-3, 6)
 
-mi = estimate_mi(data["y"], data[["x1", "x2", "x3"]],
-                 lag=time_lags, normalize=True)
+mi = estimate_corr(data["y"], data[["x1", "x2", "x3"]],
+                   lag=time_lags)
 
 plt.plot(time_lags, mi["x1"], label="$x_1$", marker="o")
 plt.plot(time_lags, mi["x2"], label="$x_2$", marker="^")
@@ -295,7 +300,7 @@ We pass the $X_2$ column as the `cond` parameter,
 and specify that the condition should be lagged by additional two steps:
 
 ```python
-from ennemi import estimate_mi
+from ennemi import estimate_corr
 import matplotlib.pyplot as plt
 import numpy as np
 import pandas as pd
@@ -303,8 +308,8 @@ import pandas as pd
 data = pd.read_csv("mi_example.csv")
 time_lags = np.arange(-3, 6)
 
-mi = estimate_mi(data["y"], data[["x1", "x3"]], lag=time_lags,
-                 cond=data["x2"], cond_lag=time_lags+2, normalize=True)
+mi = estimate_corr(data["y"], data[["x1", "x3"]], lag=time_lags,
+                   cond=data["x2"], cond_lag=time_lags+2)
 print(mi)
 
 plt.plot(time_lags, mi["x1"], label="$x_1$", marker="o")
@@ -363,7 +368,8 @@ This is easy to do unless some variables are time lagged.
 The time lags are easy to get the wrong way around, and
 the calculation must be repeated for each lag value.
 
-To make subsetting with time lags easier, `estimate_mi()` accepts a `mask` parameter.
+To make subsetting with time lags easier, `estimate_mi()` and `estimate_corr()`
+accept a `mask` parameter.
 The mask is an array (or list) of boolean values.
 An `y` observation,
 and the relevant `x` and `cond` observations after lagging,
@@ -378,7 +384,7 @@ values only in the daytime.
 
 To analyze the full data set, you would execute:
 ```python
-from ennemi import estimate_mi
+from ennemi import estimate_corr
 import matplotlib.pyplot as plt
 import numpy as np
 
@@ -389,7 +395,7 @@ x = -0.3 * np.cos(2 * np.pi * t / 24) + rng.normal(0, 0.01, len(t))
 y = np.sqrt(np.maximum(0, -np.cos(2 * np.pi * (t-3) / 24)))\
                           + rng.normal(0, 0.01, len(t))
 
-print(estimate_mi(y, x, lag=[0, 1, 2, 3], normalize=True))
+print(estimate_corr(y, x, lag=[0, 1, 2, 3]))
 ```
 The result is:
 ```
@@ -402,7 +408,7 @@ The result is:
 To constrain to daytime observations of $Y$ only, replace the last line with
 ```python
 mask = (t % 24 > 6) & (t % 24 < 18)
-print(estimate_mi(y, x, lag=[0, 1, 2, 3], mask=mask, normalize=True))
+print(estimate_corr(y, x, lag=[0, 1, 2, 3], mask=mask))
 ```
 This produces slightly different MI values:
 ```
@@ -416,4 +422,4 @@ if $X &lt; 0$, then $Y$ is probably zero.
 The MI at the correct lag term increased a bit.
 
 If some observations were missing altogether, indicated by a `NaN` value,
-we could pass the `drop_nan` parameter to `estimate_mi()` and get the same results.
+we could pass the `drop_nan` parameter to `estimate_corr()` and get the same results.
diff --git a/ennemi/__init__.py b/ennemi/__init__.py
index 36b4799..86353e1 100644
--- a/ennemi/__init__.py
+++ b/ennemi/__init__.py
@@ -3,6 +3,8 @@
 
 """Non-linear correlation detection with mutual information."""
 
-from ._driver import estimate_entropy, estimate_mi, normalize_mi, pairwise_mi
+from ._driver import estimate_entropy, estimate_corr, estimate_mi,\
+    normalize_mi, pairwise_corr, pairwise_mi
 
-__all__ = [ "estimate_entropy", "estimate_mi", "normalize_mi", "pairwise_mi" ]
+__all__ = [ "estimate_entropy", "estimate_corr", "estimate_mi",
+    "normalize_mi", "pairwise_corr", "pairwise_mi" ]
diff --git a/ennemi/_driver.py b/ennemi/_driver.py
index 5f16ca7..c97b391 100644
--- a/ennemi/_driver.py
+++ b/ennemi/_driver.py
@@ -246,8 +246,9 @@ def estimate_mi(y: ArrayLike, x: ArrayLike,
     - Conditional MI: pass a `cond` array.
     - Discrete-continuous MI: set `discrete_y` to True.
  
-    Returns the estimated mutual information (in nats) for continuous
-    variables. The result is a 2D `ndarray` where the first index represents `lag` values
+    Returns the estimated mutual information in nats, or if `normalize` is set,
+    as a correlation coefficient.
+    The result is a 2D `ndarray` where the first index represents `lag` values
     and the second index represents `x` columns. If `x` is a pandas
     `DataFrame` or `Series`, the result is a `DataFrame`.
 
@@ -360,6 +361,95 @@ def estimate_mi(y: ArrayLike, x: ArrayLike,
             return pandas.DataFrame(result, index=lag_arr, columns=[x.name])
     return result
 
+def estimate_corr(y: ArrayLike, x: ArrayLike,
+                  lag: Union[Sequence[int], ArrayLike, int] = 0,
+                  *, k: int = 3,
+                  cond: Optional[ArrayLike] = None,
+                  cond_lag: Union[Sequence[int], Sequence[Sequence[int]], ArrayLike, int] = 0,
+                  mask: Optional[ArrayLike] = None,
+                  preprocess: bool = True,
+                  drop_nan: bool = False,
+                  max_threads: Optional[int] = None,
+                  callback: Optional[Callable[[int, int], None]] = None) -> FloatArray:
+    """Estimate MI correlation between continuous variables.
+    
+    This method is equivalent to `estimate_mi` with `normalize=True`.
+
+    - Unconditional MI: the default.
+    - Conditional MI: pass a `cond` array.
+ 
+    Returns the estimated mutual information correlation for continuous
+    variables. The result is a 2D `ndarray` where the first index represents `lag` values
+    and the second index represents `x` columns. If `x` is a pandas
+    `DataFrame` or `Series`, the result is a `DataFrame`.
+
+    The time lag is interpreted as `y(t) ~ x(t - lag) | z(t - cond_lag)`.
+    The time lags are applied to the `x` and `cond` arrays such that the `y`
+    array stays the same every time.
+    This means that `y` is cropped to `y[max(max_lag,0) : N+min(min_lag,0)]`.
+    The `cond_lag` parameter specifies the lag for the `cond` array separately
+    from the `x` lag.
+
+    If the `mask` parameter is set, only those `y` observations with the
+    matching mask element set to `True` are used for estimation.
+    
+    For accurate results, the variables should be transformed to roughly
+    symmetrical distributions. If `preprocess==True`, the variables are
+    automatically scaled to unit variance.
+
+    The calculation is based on "Kraskov et al. (2004): Estimating mutual
+    information. Physical Review E 69. doi:10.1103/PhysRevE.69.066138" and
+    derivatives by (Frenzel and Pompe 2007) and (Ross 2014).
+
+    Positional or keyword parameters:
+    ---
+    y : array_like
+        A 1D array of observations. If `discrete_y` is True, the values may be
+        of any type. Otherwise the values must be numeric.
+    x : array_like
+        A 1D or 2D array where the columns are one or more variables and the
+        rows are observations. The number of rows must be the same as in y.
+    lag : int or array_like, default 0
+        A time lag or 1D array of time lags to apply. A positive lag means that
+        `y` depends on earlier `x` values and vice versa.
+
+    Optional keyword parameters:
+    ---
+    k : int, default 3
+        The number of neighbors to consider.
+        Ignored if both variables are discrete.
+    cond : array_like or None
+        Optional 1D or 2D array of observations used for conditioning.
+        Must have as many observations as y.
+        All variables in a 2D array are used together.
+    cond_lag : int or array_like, default 0
+        Lag applied to the cond array. Must be broadcastable to the size of `lag`.
+        Can be two-dimensional to lag each conditioning variable separately.
+    mask : array_like or None
+        If specified, an array of booleans that gives the `y` elements to use for
+        estimation. Use this to exclude some observations from consideration
+        while preserving the time series structure of the data. Elements of
+        `x` and `cond` are masked with the lags applied.
+    preprocess : bool, default True
+        By default, continuous variables are scaled to unit variance and
+        added with low-amplitude noise. The noise uses a fixed random seed.
+    drop_nan : bool, default False
+        If True, all NaN (not a number) values are masked out.
+    max_threads : int or None
+        The maximum number of threads to use for estimation.
+        By default, the number of CPU cores is used.
+    callback : method or None
+        A method to call when each estimation task is completed. The method
+        must take two integer parameters: `x` variable index and lag value.
+        This method should be very short. Because Python code is not executed
+        concurrently, the callback may slow down other estimation tasks.
+    """
+
+    return estimate_mi(y, x, lag, k=k, cond=cond, cond_lag=cond_lag, mask=mask,
+        preprocess=preprocess, drop_nan=drop_nan, normalize=True,
+        max_threads=max_threads, callback=callback)
+
+
 def _estimate_mi(y: FloatArray, x: FloatArray, lag: FloatArray, k: int,
         cond: Optional[FloatArray], cond_lag: FloatArray,
         mask: Optional[FloatArray], discrete_x: bool, discrete_y: bool,
@@ -535,6 +625,59 @@ def pairwise_mi(data: ArrayLike,
     return result
 
 
+def pairwise_corr(data: ArrayLike,
+    *, k: int = 3,
+    cond: Optional[ArrayLike] = None,
+    mask: Optional[ArrayLike] = None,
+    preprocess: bool = True,
+    drop_nan: bool = False,
+    max_threads: Optional[int] = None,
+    callback: Optional[Callable[[int, int], None]] = None) -> FloatArray:
+    """Estimate the pairwise MI correlation between each variable.
+
+    This method is equivalent to `pairwise_mi` with `normalize=True`.
+
+    Returns a matrix where the (i,j)'th element is the mutual information
+    between the i'th and j'th columns in the data. The values are
+    correlation coefficients between 0 and 1. The diagonal
+    contains NaNs (for better visualization, as the autocorrelation is 1).
+
+    Positional or keyword parameters:
+    ---
+    data : array_like
+        A 2D array where the columns represent variables.
+
+    Optional keyword parameters:
+    ---
+    k : int, default 3
+        The number of neighbors to consider.
+        Ignored if both variables are discrete.
+    cond : array_like or None
+        Optional 1D or 2D array of observations used for conditioning.
+        Must have as many observations as the data.
+        All variables in a 2D array are used together.
+    mask : array_like or None
+        If specified, an array of booleans that gives the data elements to use for
+        estimation. Use this to exclude some observations from consideration.
+    preprocess : bool, default True
+        By default, continuous variables are scaled to unit variance and
+        added with low-amplitude noise. The noise uses a fixed random seed.
+    drop_nan : bool, default False
+        If True, all NaN (not a number) values in `x` and `cond` are masked out.
+    max_threads : int or None
+        The maximum number of threads to use for estimation.
+        By default, the number of CPU cores is used.
+    callback : method or None
+        A method to call when each estimation task is completed. The method
+        must take two integer parameters, representing the variable indices.
+        This method should be very short. Because Python code is not executed
+        concurrently, the callback may slow down other estimation tasks.
+    """
+
+    return pairwise_mi(data, k=k, cond=cond, mask=mask, preprocess=preprocess,
+        drop_nan=drop_nan, normalize=True, max_threads=max_threads, callback=callback)
+
+
 def _pairwise_mi(data: FloatArray, k: int, cond: Optional[FloatArray], preprocess: bool,
     drop_nan: bool, mask: Optional[FloatArray], discrete: FloatArray, max_threads: Optional[int],
     callback: Optional[Callable[[int, int], None]]) -> FloatArray:


Test patch:
diff --git a/tests/pandas/test_pandas_discrete.py b/tests/pandas/test_pandas_discrete.py
index 3356e99..910fa9c 100644
--- a/tests/pandas/test_pandas_discrete.py
+++ b/tests/pandas/test_pandas_discrete.py
@@ -29,7 +29,7 @@ class TestPandasDiscrete(unittest.TestCase):
         weather[(weather==1) & (wind_dir==3)] = 0
         weather[(weather==1) & (wind_dir==2) & rng.choice([0,1], N)] = 0
 
-        self.data = data = pd.DataFrame({
+        self.data = pd.DataFrame({
             "Temp": np.round(actual_temp + rng.normal(0, 1, N)),
             "Press": np.round(actual_press + rng.normal(0, 1, N)),
             "Wind": wind_dir,
diff --git a/tests/pandas/test_pandas_workflow.py b/tests/pandas/test_pandas_workflow.py
index a1493d6..9166649 100644
--- a/tests/pandas/test_pandas_workflow.py
+++ b/tests/pandas/test_pandas_workflow.py
@@ -4,8 +4,7 @@
 """A simplified version of the case study in the documentation."""
 
 from __future__ import annotations
-from ennemi import estimate_mi, pairwise_mi
-import numpy as np
+from ennemi import estimate_corr, pairwise_corr
 import pandas as pd
 import unittest
 import os
@@ -20,13 +19,13 @@ class TestPandasWorkflow(unittest.TestCase):
         self.data = pd.read_csv(data_path, index_col=0, parse_dates=True)
 
 
-    def test_pairwise_mi(self) -> None:
-        # Determine the pairwise MI between three variables
+    def test_pairwise_corr(self) -> None:
+        # Determine the pairwise correlation between three variables
         columns = ["Temperature", "WindDir", "DayOfYear"]
         afternoon_mask = (self.data.index.hour == 13)
 
-        uncond = pairwise_mi(self.data[columns], mask=afternoon_mask, normalize=True) # type: pd.DataFrame
-        cond_doy = pairwise_mi(self.data[columns], mask=afternoon_mask, normalize=True,
+        uncond = pairwise_corr(self.data[columns], mask=afternoon_mask) # type: pd.DataFrame
+        cond_doy = pairwise_corr(self.data[columns], mask=afternoon_mask,
             cond=self.data["DayOfYear"]) # type: pd.DataFrame
 
         # The result is a 3x3 data frame
@@ -54,9 +53,9 @@ class TestPandasWorkflow(unittest.TestCase):
     def test_autocorrelation(self) -> None:
         # Determine the autocorrelation of temperature, conditional on DOY
         afternoon_mask = (self.data.index.hour == 13)
-        result = estimate_mi(self.data["Temperature"], self.data["Temperature"],
-            lag=[0, -24, -10*24], cond=self.data["DayOfYear"], mask=afternoon_mask,
-            normalize=True) # type: pd.DataFrame
+        result = estimate_corr(self.data["Temperature"], self.data["Temperature"],
+            lag=[0, -24, -10*24], cond=self.data["DayOfYear"],
+            mask=afternoon_mask) # type: pd.DataFrame
 
         # The result is a 3x1 data frame
         self.assertEqual(result.shape, (3,1))
diff --git a/tests/unit/test_driver.py b/tests/unit/test_driver.py
index 0222c74..75a6338 100644
--- a/tests/unit/test_driver.py
+++ b/tests/unit/test_driver.py
@@ -13,7 +13,8 @@ import pandas as pd
 import random
 from typing import List, Optional, Tuple
 import unittest
-from ennemi import estimate_entropy, estimate_mi, normalize_mi, pairwise_mi
+from ennemi import estimate_entropy, estimate_mi, normalize_mi, pairwise_mi,\
+    estimate_corr, pairwise_corr
 
 try:
     import numpy.typing as npt
@@ -902,6 +903,25 @@ class TestEstimateMi(unittest.TestCase):
         mi = estimate_mi(data[:,1], data[:,0], normalize=True, drop_nan=True)
         self.assertAlmostEqual(mi, 0.8, delta=0.02)
 
+    def test_alias(self) -> None:
+        rng = np.random.default_rng(2022_04_06)
+        cov = np.array([[1, 0.8], [0.8, 1]])
+        data = rng.multivariate_normal([0, 0], cov, size=1000)
+        data[:50,0] = np.nan
+        data[950:,1] = np.nan
+        cond = np.roll(data[:,0] + 0.3*data[:,1], shift=-2)
+        mask = rng.choice([True, False], size=1000, replace=True, p=[0.9, 0.1])
+
+        # estimate_mi with normalize=True
+        expected = estimate_mi(data[:,1], data[:,0], k=7, cond=cond, cond_lag=2,
+            mask=mask, preprocess=False, drop_nan=True, normalize=True)
+        self.assertAlmostEqual(expected, 0.83, delta=0.03)
+
+        # estimate_corr should produce EXACTLY same results
+        actual = estimate_corr(data[:,1], data[:,0], k=7, cond=cond, cond_lag=2,
+            mask=mask, preprocess=False, drop_nan=True)
+        self.assertEqual(actual, expected)
+
 
     def test_normalize_with_discrete_warns(self) -> None:
         cont = np.zeros(10)
@@ -1504,4 +1524,20 @@ class TestPairwiseMi(unittest.TestCase):
                     if i < j:
                         self.assertIn((i, j), callback_results)
                     else:
-                        self.assertNotIn((i, j), callback_results)
\ No newline at end of file
+                        self.assertNotIn((i, j), callback_results)
+
+    def test_alias(self) -> None:
+        data = self.generate_normal(2022_04_06)
+        data[7,0] = np.nan
+        data[8,1] = np.nan
+        mask = [True, False] * 500
+
+        # All parameters of pairwise_mi tweaked
+        expected = pairwise_mi(data, cond=np.roll(data[:,0], shift=1), k=7, mask=mask,
+            preprocess=False, drop_nan=True, normalize=True)
+        self.assertAlmostEqual(expected[0,1], 0.62, delta=0.03)
+
+        # ...and pairwise_corr should produce EXACTLY same results
+        actual = pairwise_corr(data, cond=np.roll(data[:,0], shift=1), k=7, mask=mask,
+            preprocess=False, drop_nan=True)
+        self.assertEqual(actual[0,1], expected[0,1])