Confidence Intervals
====================

Two-Sample Effect Sizes 
-----------------------
Researchers can use hierarch to compute confidence intervals for effect sizes. 
These intervals are computed via test inversion and, as a result, have the advantage
of essentially always achieving the nominal coverage. 

To put it another way, hierarch computes a 95% confidence interval by performing a 
permutation test against the null hypothesis that true effect size is exactly equal 
to the observed effect size. Then, the bounds of the acceptance region at alpha = 0.05
are the bounds of the confidence interval. Let's consider the dataset from earlier. ::

    import pandas as pd
    import numpy as np
    import hierarch as ha

    data = pd.read_clipboard()

    print(data)

+------------+------+-------------+----------+
|  Condition | Well | Measurement |  Values  |
+============+======+=============+==========+
|    None    |   1  |      1      | 5.202258 |
+------------+------+-------------+----------+
|    None    |   1  |      2      | 5.136128 |
+------------+------+-------------+----------+
|    None    |   1  |      3      | 5.231401 |
+------------+------+-------------+----------+
|    None    |   2  |      1      | 5.336643 |
+------------+------+-------------+----------+
|    None    |   2  |      2      | 5.287973 |
+------------+------+-------------+----------+
|    None    |   2  |      3      | 5.375359 |
+------------+------+-------------+----------+
|    None    |   3  |      1      | 5.350692 |
+------------+------+-------------+----------+
|    None    |   3  |      2      | 5.465206 |
+------------+------+-------------+----------+
|    None    |   3  |      3      | 5.422602 |
+------------+------+-------------+----------+
| +Treatment |   4  |      1      | 5.695427 |
+------------+------+-------------+----------+
| +Treatment |   4  |      2      | 5.668457 |
+------------+------+-------------+----------+
| +Treatment |   4  |      3      | 5.752592 |
+------------+------+-------------+----------+
| +Treatment |   5  |      1      | 5.583562 |
+------------+------+-------------+----------+
| +Treatment |   5  |      2      | 5.647895 |
+------------+------+-------------+----------+
| +Treatment |   5  |      3      | 5.618315 |
+------------+------+-------------+----------+
| +Treatment |   6  |      1      | 5.642983 |
+------------+------+-------------+----------+
| +Treatment |   6  |      2      |  5.47072 |
+------------+------+-------------+----------+
| +Treatment |   6  |      3      | 5.686654 |
+------------+------+-------------+----------+

You can use the confidence_interval function in hierarch.stats to compute the 
confidence interval. ::

    from hierarch.stats import confidence_interval

    confidence_interval(
    data,
    treatment_col=0,
    compare='means',
    interval=95,
    bootstraps=500,
    permutations="all",
    random_state=1,
    )

    (-0.5373088054909549, -0.12010079984237881)

This interval does not cross 0, so it is consistent with significance at the alpha = 0.05
level.

Because ha.stats.confidence_interval is based on a hypothesis test, it requires
the same input parameters as hypothesis_test. However, 
the new **interval** parameter determines the width of the interval. ::

    confidence_interval(
    data,
    treatment_col=0,
    compare='means',
    interval=99,
    bootstraps=500,
    permutations="all",
    random_state=1,
    )

    (-0.9086402840632387, 0.25123067872990457)

    confidence_interval(
    data,
    treatment_col=0,
    compare='means',
    interval=68,
    bootstraps=500,
    permutations="all",
    random_state=1,
    )

    (-0.40676489798778065, -0.25064470734555316)

The 99% confidence interval does indeed cross 0, so we could not reject the null hypothesis
at the alpha = 0.01 level.

To build your confidence, you can perform a simulation analysis to ensure 
the confidence interval achieves the nominal coverage. You can set up a 
DataSimulator using the functions in hierarch.power as follows. ::

    from hierarch.power import DataSimulator

    parameters = [[0, 1.525], #difference in means due to treatment
                [stats.norm, 0, 1], #column 1 distribution - stats.norm(loc=0, scale=1)
                [stats.lognorm, 0.75]] #column 2 distribution - stats.lognorm(s = 0.75)

    sim = DataSimulator(parameters, random_state=1)

    import scipy.stats as stats

    hierarchy = [2, #treatments
                3, #samples
                3] #within-sample measurements

    sim.fit(hierarchy)

The "true" difference between the two samples is 1.525 according to the simulation
parameters, so 95% of 95% confidence intervals that hierarch calculates should contain
this value. You can test this with the following code. ::

    true_difference = 1.525
    coverage = 0
    loops = 1000

    for i in range(loops):
        data = sim.generate()
        lower, upper = confidence_interval(data, 0, interval=95, 
                                           bootstraps=100, permutations='all')
        if lower <= true_difference <= upper:
            coverage += 1

    print("Coverage:", coverage/loops)
    
    Coverage: 0.946

This is within the Monte Carlo error of the simulation (+/- 0.7%) of 95%, so we can feel
confident in this method of interval computation.

Regression Coefficient Confidence Intervals
-------------------------------------------
The confidence_interval function can also be used on many-sample datasets that represent
a hypothesized linear relationship. Let's generate a dataset with a "true" slope of 
2/3. ::

    paramlist = [[0, 2/3, 4/3, 2], [stats.norm], [stats.norm]]
    hierarchy = [4, 2, 3]
    datagen = DataSimulator(paramlist, random_state=2)
    datagen.fit(hierarchy)
    data = datagen.generate()
    data

+---+---+---+----------+
| 0 | 1 | 2 | 3        |
+===+===+===+==========+
| 1 | 1 | 1 | 0.470264 |
+---+---+---+----------+
| 1 | 1 | 2 | -0.36477 |
+---+---+---+----------+
| 1 | 1 | 3 | 1.166621 |
+---+---+---+----------+
| 1 | 2 | 1 | -0.8333  |
+---+---+---+----------+
| 1 | 2 | 2 | -0.85157 |
+---+---+---+----------+
| 1 | 2 | 3 | -1.3149  |
+---+---+---+----------+
| 2 | 1 | 1 | 0.708561 |
+---+---+---+----------+
| 2 | 1 | 2 | 0.154405 |
+---+---+---+----------+
| 2 | 1 | 3 | 0.798892 |
+---+---+---+----------+
| 2 | 2 | 1 | -2.38199 |
+---+---+---+----------+
| 2 | 2 | 2 | -1.64797 |
+---+---+---+----------+
| 2 | 2 | 3 | -2.66707 |
+---+---+---+----------+
| 3 | 1 | 1 | 3.974506 |
+---+---+---+----------+
| 3 | 1 | 2 | 3.321076 |
+---+---+---+----------+
| 3 | 1 | 3 | 3.463612 |
+---+---+---+----------+
| 3 | 2 | 1 | 2.888003 |
+---+---+---+----------+
| 3 | 2 | 2 | 1.466742 |
+---+---+---+----------+
| 3 | 2 | 3 | 3.26068  |
+---+---+---+----------+
| 4 | 1 | 1 | 3.73128  |
+---+---+---+----------+
| 4 | 1 | 2 | 0.036135 |
+---+---+---+----------+
| 4 | 1 | 3 | -0.05483 |
+---+---+---+----------+
| 4 | 2 | 1 | 1.268975 |
+---+---+---+----------+
| 4 | 2 | 2 | 3.615265 |
+---+---+---+----------+
| 4 | 2 | 3 | 2.902522 |
+---+---+---+----------+

You can compute a confidence interval in the same manner as above. This time, you should set the
**compare** keyword argument to "corr" for clarity, but "corr" is also the default setting
for **compare** when computing a confidence interval. ::

    from hierarch.stats import confidence_interval

    confidence_interval(
    data,
    treatment_col=0,
    compare='corr',
    interval=95,
    bootstraps=500,
    permutations="all",
    random_state=1,
    )

    (0.3410887712843298, 1.7540918236455125)

This confidence interval corresponds to the slope in a linear model. You can check this by
computing the slope coefficient via Ordinary Least Squares. ::

    import scipy.stats
    from hierarch.internal_functions import GroupbyMean

    grouper = GroupbyMean()
    test = grouper.fit_transform(data)
    stats.linregress(test[:,0], test[:,-1])

    LinregressResult(slope=1.0515132531203024, intercept=-1.6658194480556106, 
    rvalue=0.6444075548383587, pvalue=0.08456152533094284, 
    stderr=0.5094006523081002, intercept_stderr=1.3950511403849626)

The slope, 1.0515, is indeed in the center of our computed interval (within Monte Carlo error).

Again, it is worthwhile to check that confidence_interval is performing adequately. You can
set up a simulation as above to check the coverage of the 95% confidence interval. ::

    true_difference = 2/3
    coverage = 0
    loops = 1000

    for i in range(loops):
        data = datagen.generate()
        lower, upper = confidence_interval(data, 0, interval=95, 
                                           bootstraps=100, permutations='all')
        if lower <= true_difference <= upper:
            coverage += 1

    print(coverage/loops)

    0.956

This is within the Monte Carlo error of the simulation (+/- 0.7%) of 95% and therefore
acceptable. You can check the coverage of other intervals by changing the **interval** keyword
argument, though be aware that Monte Carlo error depends on the probability of the event of
interest. ::

    true_difference = 2/3
    coverage = 0
    loops = 1000

    for i in range(loops):
        data = datagen.generate()
        lower, upper = confidence_interval(data, 0, interval=99, 
                                           bootstraps=100, permutations='all')
        if lower <= true_difference <= upper:
            coverage += 1

    print(coverage/loops)

    0.99

Using the confidence_interval function, researchers can rapidly calculate confidence intervals for
effect sizes that maintain nominal coverage without worrying about distributional assumptions. 