Repository: statsmodels/statsmodels

Issue:
Issue with lilliefors after upgrading to 0.9.0 from 0.8.0 (regression?)
I had some code running with v0.8.0, but it won't work anymore with version 0.9.0. The issue can be reproduced simply with the following data and code:

```
from statsmodels.stats.diagnostic import lilliefors
lilliefors([18.50117949, 18.24272814, 18.50674601])
```

With 0.8.0 I just got a pandas warning when importing
```
/usr/lib64/python3.6/site-packages/statsmodels/compat/pandas.py:56: FutureWarning: The pandas.core.datetools module is deprecated and will be removed in a future version. Please use the pandas.tseries module instead.                     
  from pandas.core import datetools
```

But now, I get an error when actually running lilliefors on those data:

```
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "/home/testone/lib64/python3.6/site-packages/statsmodels/stats/_lilliefors.py", line 344, in kstest_fit
    pval = lilliefors_table.prob(d_ks, nobs)
  File "/home/testone/lib64/python3.6/site-packages/statsmodels/stats/tabledist.py", line 120, in prob
    critv = self._critvals(n)
  File "/home/testone/lib64/python3.6/site-packages/statsmodels/stats/tabledist.py", line 99, in _critvals
    return np.array([p(n) for p in self.polyn])
  File "/home/testone/lib64/python3.6/site-packages/statsmodels/stats/tabledist.py", line 99, in <listcomp>
    return np.array([p(n) for p in self.polyn])
  File "/usr/lib64/python3.6/site-packages/scipy/interpolate/polyint.py", line 79, in __call__
    y = self._evaluate(x)
  File "/usr/lib64/python3.6/site-packages/scipy/interpolate/interpolate.py", line 634, in _evaluate
    below_bounds, above_bounds = self._check_bounds(x_new)
  File "/usr/lib64/python3.6/site-packages/scipy/interpolate/interpolate.py", line 663, in _check_bounds
    raise ValueError("A value in x_new is below the interpolation "
ValueError: A value in x_new is below the interpolation range.
```

I checked the doc, but I did not see any specific instruction on how data should be: is this a regression or am I actually providing wrong data? In the latter case, can you refer me to the relative doc?

Thanks


Discussion:
My guess is that this is a bug/regression. AFAIR the pvalue handling has been refactored in 0.9.
(It could be a floating point issue at the boundary.)
I will try to investigate later today.


(sorry for the delay, slow in switching computers again)
It is a regression if it worked before 0.9.
AFAICS, using the debugger, the minimum sample size for the p-value computation with `lilliefors_table.prob(d_ks, nobs)` is 4.
The relevant `scipy.interpolate.interpolate.interp1d` has `x` values from 4 to 900 (inclusive)

There is also a problem with nobs > 900 but not in all cases
```
>>> lilliefors(np.random.rand(1500))
(0.064788061921975615, 3.5366480034031793e-16)

>>> lilliefors(np.random.randn(1500))
...
    raise ValueError("A value in x_new is above the interpolation "
ValueError: A value in x_new is above the interpolation range.
```

```
>>> np.random.seed(1)
>>> xx = np.random.randn(1500)
>>> lilliefors(xx[:900])
(0.014717319572921217, 0.20000000000000001)
>>> lilliefors(xx[:901])
...
raise ValueError("A value in x_new is above the interpolation "
ValueError: A value in x_new is above the interpolation range.
```

also `lilliefors(xx[:901], pvalmethod='approx')` and `lilliefors(xx[:901], pvalmethod='table')` produce the same exception

definitely a bug introduced during refactoring in 0.9 (assuming it worked before which I haven't checked)

PR that made the change is  #3936
I guess the relevant lines are https://github.com/statsmodels/statsmodels/pull/3936/files#diff-6ad3bd17ebe1ff645184564be437e2e1R341

i.e. we switch to table even if pvalmethod='approx' if approx `pval > 0.1`

another issue is that tabledist should handle "out of support" points
maybe relevant #4355

general:
- some hypothesis test require a minimum number of observations, This is usually not checked and enforce and possibly raises an exception at some "arbitrary" code.
- if an approximation or table method has only limited support, e.g. for pvalues between 0.01 and 0.1, the we return the boundary with a warning, if outside (recent example in scipy was that extrapolation beyond the supported range causes non-sensical pvalues)
- in this case, table has extended values for p-value > 0.1 compared to the 'approx' method. We still want to switch when possible.

maybe switch default to `pvalmethod=None` which is essentially an 'auto' option to choose the best that we have for a given case.
then 'table' and 'approx' don't have to switch methods.

Note: table and approx are just different ways of computing the same distribution values. It is not like a substantive method like choosing between asymptotic and bootstrap pvalues.



the tables (norm and expon) go only up to pvalue=0.2, 
approx formula says intended for p-value<=0.1 but bounds are not checked in pval_lf

get_lilliefors_table has an extension to larger samples, but it is not used in `norm`. I guess the reason was that the larger sample extension is missing one value, i.e. shape mismatch.
I think we could take just the last value of the smaller sample table, i.e for nobs=900

from `f(n)`
```
>>> np.array([0.736, 0.768, 0.805, 0.886, 1.031]) / np.sqrt(900) * 1000
array([ 24.53333333,  25.6       ,  26.83333333,  29.53333333,  34.36666667])
```
which is close to the smaller nobs table, for nobs=900
`25,  26,  28,  30,  35,  42`

Instead of extending the table, we could also just use 1-D interpolation on `f(n)` for n is sample nobs.
This would avoid the need for any upper limit.

I'm not sure what to do for nobs=3.
any p-values are likely large, and the only information we might have is pvalue > 0.1.

I don't see any checking for nobs=2.
Based on recent scipy discussion, I guess the pvalue should be 1. (we estimate mean and var and then there is no degrees of freedom left in normal case.)
https://github.com/scipy/scipy/issues/7730#issuecomment-433062911


Resolving patch:
diff --git a/statsmodels/stats/_lilliefors.py b/statsmodels/stats/_lilliefors.py
--- a/statsmodels/stats/_lilliefors.py
+++ b/statsmodels/stats/_lilliefors.py
@@ -1,4 +1,3 @@
-# -*- coding: utf-8 -*-
 """
 Implements Lilliefors corrected Kolmogorov-Smirnov tests for normal and
 exponential distributions.
@@ -15,14 +14,17 @@
 
 pvalues for Lilliefors test are based on formula and table in
 
-An Analytic Approximation to the Distribution of Lilliefors's Test Statistic for Normality
-Author(s): Gerard E. Dallal and Leland WilkinsonSource: The American Statistician, Vol. 40, No. 4 (Nov., 1986), pp. 294-296Published by: American Statistical AssociationStable URL: http://www.jstor.org/stable/2684607 .
+An Analytic Approximation to the Distribution of Lilliefors's Test Statistic
+for Normality
+Author(s): Gerard E. Dallal and Leland WilkinsonSource: The American
+Statistician, Vol. 40, No. 4 (Nov., 1986), pp. 294-296
+Published by: American Statistical Association
+Stable URL: http://www.jstor.org/stable/2684607 .
 
-On the Kolmogorov-Smirnov Test for Normality with Mean and Variance
-Unknown
+On the Kolmogorov-Smirnov Test for Normality with Mean and Variance Unknown
 Hubert W. Lilliefors
-Journal of the American Statistical Association, Vol. 62, No. 318. (Jun., 1967), pp. 399-402.
-
+Journal of the American Statistical Association, Vol. 62, No. 318.
+(Jun., 1967), pp. 399-402.
 
 ---
 
@@ -31,23 +33,47 @@
 
 Ref:
 Lilliefors, H.W.
-On the Kolmogorov-Smirnov test for the exponential distribution with mean unknown.
-Journal of the American Statistical Association, Vol 64, No. 325. (1969), pp. 387–389.
+On the Kolmogorov-Smirnov test for the exponential distribution with mean
+unknown. Journal of the American Statistical Association, Vol 64, No. 325.
+(1969), pp. 387–389.
 """
 from functools import partial
 
-from statsmodels.compat.python import string_types
 import numpy as np
 from scipy import stats
+
+from ._lilliefors_critical_values import (critical_values,
+                                          asymp_critical_values,
+                                          PERCENTILES)
 from .tabledist import TableDist
 
 
+def _make_asymptotic_function(params):
+    """
+    Generates an asymptotic distribution callable from a param matrix
+
+    Polynomial is a[0] * x**(-1/2) + a[1] * x**(-1) + a[2] * x**(-3/2)
+
+    Parameters
+    ----------
+    params : ndarray
+        Array with shape (nalpha, 3) where nalpha is the number of
+        significance levels
+    """
+
+    def f(n):
+        poly = np.array([1, np.log(n), np.log(n) ** 2])
+        return np.exp(poly.dot(params.T))
+
+    return f
+
+
 def ksstat(x, cdf, alternative='two_sided', args=()):
     """
     Calculate statistic for the Kolmogorov-Smirnov test for goodness of fit
 
-    This calculates the test statistic for a test of the distribution G(x) of an observed
-    variable against a given distribution F(x). Under the null
+    This calculates the test statistic for a test of the distribution G(x) of
+    an observed variable against a given distribution F(x). Under the null
     hypothesis the two distributions are identical, G(x)=F(x). The
     alternative hypothesis can be either 'two_sided' (default), 'less'
     or 'greater'. The KS test is only valid for continuous distributions.
@@ -89,7 +115,7 @@ def ksstat(x, cdf, alternative='two_sided', args=()):
     """
     nobs = float(len(x))
 
-    if isinstance(cdf, string_types):
+    if isinstance(cdf, str):
         cdf = getattr(stats.distributions, cdf).cdf
     elif hasattr(cdf, 'cdf'):
         cdf = getattr(cdf, 'cdf')
@@ -97,26 +123,18 @@ def ksstat(x, cdf, alternative='two_sided', args=()):
     x = np.sort(x)
     cdfvals = cdf(x, *args)
 
-    if alternative in ['two_sided', 'greater']:
-        Dplus = (np.arange(1.0, nobs+1)/nobs - cdfvals).max()
-        if alternative == 'greater':
-            return Dplus
-
-    if alternative in ['two_sided', 'less']:
-        Dmin = (cdfvals - np.arange(0.0, nobs)/nobs).max()
-        if alternative == 'less':
-            return Dmin
-
-    D = np.max([Dplus,Dmin])
+    d_plus = (np.arange(1.0, nobs + 1) / nobs - cdfvals).max()
+    d_min = (cdfvals - np.arange(0.0, nobs) / nobs).max()
+    if alternative == 'greater':
+        return d_plus
+    elif alternative == 'less':
+        return d_min
 
-    return D
+    return np.max([d_plus, d_min])
 
 
-# new version with tabledist
-# --------------------------
-
 def get_lilliefors_table(dist='norm'):
-    '''
+    """
     Generates tables for significance levels of Lilliefors test statistics
 
     Tables for available normal and exponential distribution testing,
@@ -131,113 +149,43 @@ def get_lilliefors_table(dist='norm'):
     -------
     lf : TableDist object.
         table of critical values
-    '''
+    """
     # function just to keep things together
     # for this test alpha is sf probability, i.e. right tail probability
 
-    if dist == 'norm':
-        alpha = np.array([ 0.2  ,  0.15 ,  0.1  ,  0.05 ,  0.01 ,  0.001])[::-1]
-        size = np.array([ 4,   5,   6,   7,   8,   9,  10,  11,  12,  13,  14,  15,
-                     16,  17,  18,  19,  20,  25,  30,  40, 100, 400, 900], float)
-
-        # critical values, rows are by sample size, columns are by alpha
-        crit_lf = np.array(   [[303, 321, 346, 376, 413, 433],
-                               [289, 303, 319, 343, 397, 439],
-                               [269, 281, 297, 323, 371, 424],
-                               [252, 264, 280, 304, 351, 402],
-                               [239, 250, 265, 288, 333, 384],
-                               [227, 238, 252, 274, 317, 365],
-                               [217, 228, 241, 262, 304, 352],
-                               [208, 218, 231, 251, 291, 338],
-                               [200, 210, 222, 242, 281, 325],
-                               [193, 202, 215, 234, 271, 314],
-                               [187, 196, 208, 226, 262, 305],
-                               [181, 190, 201, 219, 254, 296],
-                               [176, 184, 195, 213, 247, 287],
-                               [171, 179, 190, 207, 240, 279],
-                               [167, 175, 185, 202, 234, 273],
-                               [163, 170, 181, 197, 228, 266],
-                               [159, 166, 176, 192, 223, 260],
-                               [143, 150, 159, 173, 201, 236],
-                               [131, 138, 146, 159, 185, 217],
-                               [115, 120, 128, 139, 162, 189],
-                               [ 74,  77,  82,  89, 104, 122],
-                               [ 37,  39,  41,  45,  52,  61],
-                               [ 25,  26,  28,  30,  35,  42]])[:,::-1] / 1000.
-
-        # also build a table for larger sample sizes
-        def f(n):
-            return np.array([0.736, 0.768, 0.805, 0.886, 1.031]) / np.sqrt(n)
-
-        higher_sizes = np.array([35, 40, 45, 50, 60, 70,
-                                 80, 100, 200, 500, 1000,
-                                 2000, 3000, 5000, 10000, 100000], float)
-        higher_crit_lf = np.zeros([higher_sizes.shape[0], crit_lf.shape[1]-1])
-        for i in range(len(higher_sizes)):
-            higher_crit_lf[i, :] = f(higher_sizes[i])
-
-        alpha_large = alpha[:-1]
-        size_large = np.concatenate([size, higher_sizes])
-        crit_lf_large = np.vstack([crit_lf[:-4,:-1], higher_crit_lf])
-        lf = TableDist(alpha, size, crit_lf)
+    alpha = 1 - np.array(PERCENTILES) / 100.0
+    alpha = alpha[::-1]
+    dist = 'normal' if dist == 'norm' else dist
+    if dist not in critical_values:
+        raise ValueError("Invalid dist parameter. Must be 'norm' or 'exp'")
+    cv_data = critical_values[dist]
+    acv_data = asymp_critical_values[dist]
 
-    elif dist == 'exp':
-        alpha = np.array([0.2,  0.15,  0.1,  0.05, 0.01])[::-1]
-        size = np.array([3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,25,30],
-                        float)
-
-        crit_lf = np.array([   [451, 479, 511, 551, 600],
-                                [396, 422, 499, 487, 548],
-                                [359, 382, 406, 442, 504],
-                                [331, 351, 375, 408, 470],
-                                [309, 327, 350, 382, 442],
-                                [291, 308, 329, 360, 419],
-                                [277, 291, 311, 341, 399],
-                                [263, 277, 295, 325, 380],
-                                [251, 264, 283, 311, 365],
-                                [241, 254, 271, 298, 351],
-                                [232, 245, 261, 287, 338],
-                                [224, 237, 252, 277, 326],
-                                [217, 229, 244, 269, 315],
-                                [211, 222, 236, 261, 306],
-                                [204, 215, 229, 253, 297],
-                                [199, 210, 223, 246, 289],
-                                [193, 204, 218, 239, 283],
-                                [188, 199, 212, 234, 278],
-                                [170, 180, 191, 210, 247],
-                                [155, 164, 174, 192, 226]])[:,::-1] / 1000.
-
-        def f(n):
-            return np.array([.86, .91, .96, 1.06, 1.25]) / np.sqrt(n)
-
-        higher_sizes = np.array([35, 40, 45, 50, 60, 70,
-                                80, 100, 200, 500, 1000,
-                                2000, 3000, 5000, 10000, 100000], float)
-        higher_crit_lf = np.zeros([higher_sizes.shape[0], crit_lf.shape[1]])
-        for i in range(len(higher_sizes)):
-            higher_crit_lf[i,:] = f(higher_sizes[i])
-
-        size = np.concatenate([size, higher_sizes])
-        crit_lf = np.vstack([crit_lf, higher_crit_lf])
-        lf = TableDist(alpha, size, crit_lf)
-    else:
-        raise ValueError("Invalid dist parameter. dist must be 'norm' or 'exp'")
+    size = np.array(sorted(cv_data), dtype=np.float)
+    crit_lf = np.array([cv_data[key] for key in sorted(cv_data)])
+    crit_lf = crit_lf[:, ::-1]
+
+    asym_params = np.array([acv_data[key] for key in sorted(acv_data)])
+    asymp_fn = _make_asymptotic_function((asym_params[::-1]))
 
+    lf = TableDist(alpha, size, crit_lf, asymptotic=asymp_fn)
     return lf
 
+
 lilliefors_table_norm = get_lilliefors_table(dist='norm')
 lilliefors_table_expon = get_lilliefors_table(dist='exp')
 
 
-def pval_lf(Dmax, n):
-    '''approximate pvalues for Lilliefors test
+def pval_lf(d_max, n):
+    """
+    Approximate pvalues for Lilliefors test
 
     This is only valid for pvalues smaller than 0.1 which is not checked in
     this function.
 
     Parameters
     ----------
-    Dmax : array_like
+    d_max : array_like
         two-sided Kolmogorov-Smirnov test statistic
     n : int or float
         sample size
@@ -260,54 +208,41 @@ def pval_lf(Dmax, n):
     References
     ----------
     DallalWilkinson1986
-
-    '''
-
-    #todo: check boundaries, valid range for n and Dmax
+    """
+    # todo: check boundaries, valid range for n and Dmax
     if n > 100:
-        Dmax *= (n / 100.)**0.49
+        d_max *= (n / 100.) ** 0.49
         n = 100
-    pval = np.exp(-7.01256 * Dmax**2 * (n + 2.78019)
-                  + 2.99587 * Dmax * np.sqrt(n + 2.78019) - 0.122119
-                  + 0.974598/np.sqrt(n) + 1.67997/n)
+    pval = np.exp(-7.01256 * d_max ** 2 * (n + 2.78019)
+                  + 2.99587 * d_max * np.sqrt(n + 2.78019) - 0.122119
+                  + 0.974598 / np.sqrt(n) + 1.67997 / n)
     return pval
 
 
-def kstest_fit(x, dist='norm', pvalmethod='approx'):
+def kstest_fit(x, dist='norm', pvalmethod=None):
     """
-    Lilliefors test for normality or an exponential distribution.
+    Lilliefors test for normality or an exponential distribution
 
-    Kolmogorov Smirnov test with estimated mean and variance
+    Lilliefors test is a Kolmogorov-Smirnov test with estimated parameters
 
     Parameters
     ----------
     x : array_like, 1d
-        data series, sample
+        Data to test
     dist : {'norm', 'exp'}, optional
-        Distribution to test in set.
+        Assumed distribution
     pvalmethod : {'approx', 'table'}, optional
-        'approx' is only valid for normality. if `dist = 'exp'`,
-        `table` is returned.
-        'approx' uses the approximation formula of Dalal and Wilkinson,
-        valid for pvalues < 0.1. If the pvalue is larger than 0.1, then the
-        result of `table` is returned
-
-        For normality:
-        'table' uses the table from Dalal and Wilkinson, which is available
-        for pvalues between 0.001 and 0.2, and the formula of Lilliefors for
-        large n (n>900). Values in the table are linearly interpolated.
-        Values outside the range will be returned as bounds, 0.2 for large and
-        0.001 for small pvalues.
-        For exponential:
-        'table' uses the table from Lilliefors 1967, available for pvalues
-        between 0.01 and 0.2.
-        Values outside the range will be returned as bounds, 0.2 for large and
-        0.01 for small pvalues.
+        The method used to compute the p-value of the test statistic. In
+        general, 'table' is preferred and makes use of a very large simulation.
+        'approx' is only valid for normality. if `dist = 'exp'` `table` is
+        always used. 'approx' uses the approximation formula of Dalal and
+        Wilkinson, valid for pvalues < 0.1. If the pvalue is larger than 0.1,
+        then the result of `table` is returned
 
     Returns
     -------
     ksstat : float
-        Kolmogorov-Smirnov test statistic with estimated mean and variance.
+        Kolmogorov-Smirnov test statistic with estimated mean and variance
     pvalue : float
         If the pvalue is lower than some threshold, e.g. 0.05, then we can
         reject the Null hypothesis that the sample comes from a normal
@@ -315,11 +250,28 @@ def kstest_fit(x, dist='norm', pvalmethod='approx'):
 
     Notes
     -----
-    Reported power to distinguish normal from some other distributions is lower
-    than with the Anderson-Darling test.
-
-    could be vectorized
+    'table' uses an improved table based on 10,000,000 simulations. The
+    critical values are approximated using
+    log(cv_alpha) = b_alpha + c[0] log(n) + c[1] log(n)**2
+    where cv_alpha is the critical value for a test with size alpha,
+    b_alpha is an alpha-specific intercept term and c[1] and c[2] are
+    coefficients that are shared all alphas.
+    Values in the table are linearly interpolated. Values outside the
+    range are be returned as bounds, 0.990 for large and 0.001 for small
+    pvalues.
+
+    For implementation details, see  lilliefors_critical_value_simulation.py in
+    the test directory.
     """
+    if pvalmethod is None:
+        pvalmethod = 'approx'
+        import warnings
+        msg = 'The default pvalmethod will change from "approx" to "table" ' \
+              'after 0.11. The "table" method uses values from a very large ' \
+              'simulation and is more accurate.  Explicitly set this ' \
+              'parameter to "approx" or "table" to silence this warning'
+        warnings.warn(msg, FutureWarning)
+
     x = np.asarray(x)
     nobs = len(x)
 
@@ -333,7 +285,12 @@ def kstest_fit(x, dist='norm', pvalmethod='approx'):
         lilliefors_table = lilliefors_table_expon
         pvalmethod = 'table'
     else:
-        raise ValueError("Invalid dist parameter. dist must be 'norm' or 'exp'")
+        raise ValueError("Invalid dist parameter, must be 'norm' or 'exp'")
+
+    min_nobs = 4 if dist == 'norm' else 3
+    if nobs < min_nobs:
+        raise ValueError('Test for distribution {0} requires at least {1} '
+                         'observations'.format(dist, min_nobs))
 
     d_ks = ksstat(z, test_d, alternative='two_sided')
 
@@ -342,7 +299,7 @@ def kstest_fit(x, dist='norm', pvalmethod='approx'):
         # check pval is in desired range
         if pval > 0.1:
             pval = lilliefors_table.prob(d_ks, nobs)
-    elif pvalmethod == 'table':
+    else:  # pvalmethod == 'table'
         pval = lilliefors_table.prob(d_ks, nobs)
 
     return d_ks, pval
@@ -350,133 +307,5 @@ def kstest_fit(x, dist='norm', pvalmethod='approx'):
 
 lilliefors = kstest_fit
 
-# namespace aliases
 kstest_normal = kstest_fit
 kstest_exponential = partial(kstest_fit, dist='exp')
-
-# old version:
-# ------------
-'''
-tble = \
-00 20 15 10 05 01 .1
-4 .303 .321 .346 .376 .413 .433
-5 .289 .303 .319 .343 .397 .439
-6 .269 .281 .297 .323 .371 .424
-7 .252 .264 .280 .304 .351 .402
-8 .239 .250 .265 .288 .333 .384
-9 .227 .238 .252 .274 .317 .365
-10 .217 .228 .241 .262 .304 .352
-11 .208 .218 .231 .251 .291 .338
-12 .200 .210 .222 .242 .281 .325
-13 .193 .202 .215 .234 .271 .314
-14 .187 .196 .208 .226 .262 .305
-15 .181 .190 .201 .219 .254 .296
-16 .176 .184 .195 .213 .247 .287
-17 .171 .179 .190 .207 .240 .279
-18 .167 .175 .185 .202 .234 .273
-19 .163 .170 .181 .197 .228 .266
-20 .159 .166 .176 .192 .223 .260
-25 .143 .150 .159 .173 .201 .236
-30 .131 .138 .146 .159 .185 .217
-40 .115 .120 .128 .139 .162 .189
-100 .074 .077 .082 .089 .104 .122
-400 .037 .039 .041 .045 .052 .061
-900 .025 .026 .028 .030 .035 .042'''
-
-'''
-parr = np.array([line.split() for line in tble.split('\n')],float)
-size = parr[1:,0]
-alpha = parr[0,1:] / 100.
-crit = parr[1:, 1:]
-
-alpha = np.array([ 0.2  ,  0.15 ,  0.1  ,  0.05 ,  0.01 ,  0.001])
-size = np.array([ 4,   5,   6,   7,   8,   9,  10,  11,  12,  13,  14,  15,
-                 16,  17,  18,  19,  20,  25,  30,  40, 100, 400, 900], float)
-
-#critical values, rows are by sample size, columns are by alpha
-crit_lf = np.array(   [[303, 321, 346, 376, 413, 433],
-                       [289, 303, 319, 343, 397, 439],
-                       [269, 281, 297, 323, 371, 424],
-                       [252, 264, 280, 304, 351, 402],
-                       [239, 250, 265, 288, 333, 384],
-                       [227, 238, 252, 274, 317, 365],
-                       [217, 228, 241, 262, 304, 352],
-                       [208, 218, 231, 251, 291, 338],
-                       [200, 210, 222, 242, 281, 325],
-                       [193, 202, 215, 234, 271, 314],
-                       [187, 196, 208, 226, 262, 305],
-                       [181, 190, 201, 219, 254, 296],
-                       [176, 184, 195, 213, 247, 287],
-                       [171, 179, 190, 207, 240, 279],
-                       [167, 175, 185, 202, 234, 273],
-                       [163, 170, 181, 197, 228, 266],
-                       [159, 166, 176, 192, 223, 260],
-                       [143, 150, 159, 173, 201, 236],
-                       [131, 138, 146, 159, 185, 217],
-                       [115, 120, 128, 139, 162, 189],
-                       [ 74,  77,  82,  89, 104, 122],
-                       [ 37,  39,  41,  45,  52,  61],
-                       [ 25,  26,  28,  30,  35,  42]]) / 1000.
-
-#original Lilliefors paper
-crit_greater30 = lambda n: np.array([0.736, 0.768, 0.805, 0.886, 1.031])/np.sqrt(n)
-alpha_greater30 = np.array([ 0.2  ,  0.15 ,  0.1  ,  0.05 ,  0.01 ,  0.001])
-
-
-from scipy.interpolate import interp1d
-n_alpha = 6
-polyn = [interp1d(size, crit[:,i]) for i in range(n_alpha)]
-
-def critpolys(n):
-    return np.array([p(n) for p in polyn])
-
-def pval_lftable(x, n):
-    #returns extrem probabilities, 0.001 and 0.2, for out of range
-    critvals = critpolys(n)
-    if x < critvals[0]:
-        return alpha[0]
-    elif x > critvals[-1]:
-        return alpha[-1]
-    else:
-        return interp1d(critvals, alpha)(x)
-
-for n in [19, 19.5, 20, 21, 25]:
-    print critpolys(n)
-
-print pval_lftable(0.166, 20)
-print pval_lftable(0.166, 21)
-
-
-
-print 'n=25:', '.103 .052 .010'
-print [pval_lf(x, 25) for x in [.159, .173, .201, .236]]
-
-print 'n=10', '.103 .050 .009'
-print [pval_lf(x, 10) for x in [.241, .262, .304, .352]]
-
-
-print 'n=400', '.104 .050 .011'
-print [pval_lf(x, 400) for x in crit[-2,2:-1]]
-print 'n=900', '.093 .054 .011'
-print [pval_lf(x, 900) for x in crit[-1,2:-1]]
-print [pval_lftable(x, 400) for x in crit[-2,:]]
-print [pval_lftable(x, 300) for x in crit[-3,:]]
-
-xx = np.random.randn(40)
-print kstest_normal(xx)
-
-xx2 = np.array([ 1.138, -0.325, -1.461, -0.441, -0.005, -0.957, -1.52 ,  0.481,
-        0.713,  0.175, -1.764, -0.209, -0.681,  0.671,  0.204,  0.403,
-       -0.165,  1.765,  0.127, -1.261, -0.101,  0.527,  1.114, -0.57 ,
-       -1.172,  0.697,  0.146,  0.704,  0.422,  0.63 ,  0.661,  0.025,
-        0.177,  0.578,  0.945,  0.211,  0.153,  0.279,  0.35 ,  0.396])
-
-( 1.138, -0.325, -1.461, -0.441, -0.005, -0.957, -1.52 ,  0.481,
-        0.713,  0.175, -1.764, -0.209, -0.681,  0.671,  0.204,  0.403,
-       -0.165,  1.765,  0.127, -1.261, -0.101,  0.527,  1.114, -0.57 ,
-       -1.172,  0.697,  0.146,  0.704,  0.422,  0.63 ,  0.661,  0.025,
-        0.177,  0.578,  0.945,  0.211,  0.153,  0.279,  0.35 ,  0.396)
-r_lillieTest = [0.15096827429598147, 0.02225473302348436]
-print kstest_normal(xx2), np.array(kstest_normal(xx2)) - r_lillieTest
-print kstest_normal(xx2, 'table')
-'''
diff --git a/statsmodels/stats/_lilliefors_critical_values.py b/statsmodels/stats/_lilliefors_critical_values.py
new file mode 100644
--- /dev/null
+++ b/statsmodels/stats/_lilliefors_critical_values.py
@@ -0,0 +1,382 @@
+"""
+This file is automatically generated by littlefors_critical_values.py.
+Do not directly modifythis file.
+
+Value based on 10,000,000 simulations."""
+
+from numpy import array
+
+PERCENTILES = [1, 5, 10, 25, 50, 75, 90, 92.5, 95, 97.5, 99, 99.5, 99.7, 99.9]
+
+SAMPLE_SIZES = [
+    3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 25, 30,
+    40, 50, 100, 200, 400, 800, 1600
+]
+normal_crit_vals = {
+    4:
+    array([
+        0.14467854, 0.16876575, 0.18664724, 0.22120362, 0.25828924, 0.29341032,
+        0.34532673, 0.35917374, 0.37521968, 0.39563998, 0.41307904, 0.42157653,
+        0.4261507, 0.43265213
+    ]),
+    5:
+    array([
+        0.13587046, 0.16098893, 0.17638354, 0.20235666, 0.2333944, 0.27766941,
+        0.31900772, 0.32936832, 0.34309223, 0.36727643, 0.39671728, 0.41322814,
+        0.42293504, 0.4386304
+    ]),
+    6:
+    array([
+        0.12919635, 0.15139467, 0.16384021, 0.18597849, 0.2186713, 0.2585473,
+        0.29713753, 0.30829444, 0.32338252, 0.3456671, 0.37038945, 0.38760943,
+        0.40001813, 0.42304439
+    ]),
+    7:
+    array([
+        0.12263812, 0.14163065, 0.15238656, 0.17435948, 0.20617949, 0.24243592,
+        0.28031415, 0.29068512, 0.3042307, 0.32532967, 0.35070348, 0.3678189,
+        0.37908881, 0.40078798
+    ]),
+    8:
+    array([
+        0.11633728, 0.13297288, 0.14353311, 0.16537078, 0.19477376, 0.22936164,
+        0.2652001, 0.27501697, 0.28804474, 0.30862157, 0.33279908, 0.34911188,
+        0.3603517, 0.38252055
+    ]),
+    9:
+    array([
+        0.11029593, 0.126086, 0.1365291, 0.15748048, 0.18510669, 0.21822055,
+        0.25223085, 0.26161129, 0.27415243, 0.29383667, 0.31708299, 0.33312043,
+        0.34406772, 0.36524182
+    ]),
+    10:
+    array([
+        0.10487398, 0.12044377, 0.13065174, 0.15042835, 0.17679904, 0.20848607,
+        0.24098605, 0.25001629, 0.26202351, 0.2809226, 0.30341763, 0.31888089,
+        0.32960742, 0.35061378
+    ]),
+    11:
+    array([
+        0.10036835, 0.11563925, 0.12540948, 0.14421024, 0.1695621, 0.19993088,
+        0.23119563, 0.23987246, 0.25142048, 0.26961902, 0.29148756, 0.30646087,
+        0.31678589, 0.33743951
+    ]),
+    12:
+    array([
+        0.09649147, 0.11137479, 0.12069511, 0.13877133, 0.16313672, 0.19233706,
+        0.22244871, 0.23082937, 0.24194831, 0.25959488, 0.28077229, 0.29526801,
+        0.30538827, 0.32558601
+    ]),
+    13:
+    array([
+        0.09318309, 0.10753337, 0.11647389, 0.13389325, 0.15739248, 0.18555597,
+        0.21463087, 0.22267463, 0.23346007, 0.25054975, 0.27100552, 0.28519234,
+        0.29504428, 0.31445229
+    ]),
+    14:
+    array([
+        0.09024138, 0.1040431, 0.11266368, 0.12948657, 0.15221479, 0.17944478,
+        0.2075745, 0.21536969, 0.22585144, 0.2423819, 0.26231339, 0.2760657,
+        0.28555592, 0.30466148
+    ]),
+    15:
+    array([
+        0.08750578, 0.10085666, 0.1092263, 0.12552356, 0.14751778, 0.17390304,
+        0.2011939, 0.20879449, 0.21891697, 0.23499116, 0.25427802, 0.26772298,
+        0.27706522, 0.29551017
+    ]),
+    16:
+    array([
+        0.08501529, 0.09795236, 0.10607182, 0.1219047, 0.14327014, 0.16885124,
+        0.19537444, 0.20274039, 0.21257643, 0.22822187, 0.24703504, 0.26003621,
+        0.26913735, 0.28745211
+    ]),
+    17:
+    array([
+        0.0827393, 0.09529635, 0.10320903, 0.11859867, 0.13936135, 0.16422592,
+        0.1900207, 0.19722541, 0.20683438, 0.22204864, 0.2402864, 0.25306469,
+        0.26200104, 0.2798316
+    ]),
+    18:
+    array([
+        0.08063198, 0.09285546, 0.10055901, 0.11554003, 0.13575222, 0.15996707,
+        0.18509495, 0.1921063, 0.20145911, 0.2163409, 0.23420221, 0.24665892,
+        0.25527246, 0.27285099
+    ]),
+    19:
+    array([
+        0.0786934, 0.09059543, 0.09810264, 0.11271608, 0.1324079, 0.15602767,
+        0.18052002, 0.18736281, 0.19652196, 0.21105313, 0.22854301, 0.24068403,
+        0.24912401, 0.26624557
+    ]),
+    20:
+    array([
+        0.07686372, 0.08852309, 0.09582876, 0.11008398, 0.12930804, 0.15236687,
+        0.1762829, 0.18297149, 0.19190794, 0.20610189, 0.22327259, 0.23513834,
+        0.24349604, 0.2602868
+    ]),
+    25:
+    array([
+        0.06933943, 0.07985746, 0.08645091, 0.09927555, 0.11656692, 0.1373056,
+        0.15889286, 0.16493761, 0.17300926, 0.18582499, 0.2013265, 0.21221932,
+        0.21979198, 0.2350391
+    ]),
+    30:
+    array([
+        0.06380332, 0.07339662, 0.07942847, 0.09118039, 0.10701841, 0.12603791,
+        0.14586121, 0.15138566, 0.15878307, 0.17058849, 0.18492078, 0.19492393,
+        0.20198353, 0.21612086
+    ]),
+    40:
+    array([
+        0.055784, 0.06414642, 0.06940064, 0.07961554, 0.09339439, 0.10994266,
+        0.12719016, 0.13202416, 0.13850018, 0.14877502, 0.16131916, 0.17009968,
+        0.17633078, 0.18870328
+    ]),
+    50:
+    array([
+        0.05022994, 0.05773701, 0.06243507, 0.07160481, 0.08395875, 0.09882109,
+        0.11431303, 0.11863768, 0.12444044, 0.1337458, 0.14504504, 0.15299605,
+        0.15854061, 0.16985345
+    ]),
+    100:
+    array([
+        0.0361047, 0.04146075, 0.0448043, 0.05131964, 0.06011456, 0.07068396,
+        0.08173283, 0.08483991, 0.08900091, 0.09564506, 0.10373761, 0.10937278,
+        0.11338394, 0.12161442
+    ]),
+    200:
+    array([
+        0.02584151, 0.02963511, 0.03200533, 0.0366259, 0.04286406, 0.05036816,
+        0.05820564, 0.06041126, 0.06336215, 0.06809025, 0.0738379, 0.07788879,
+        0.08074047, 0.08663425
+    ]),
+    400:
+    array([
+        0.01844162, 0.02112065, 0.02280475, 0.02607295, 0.03049198, 0.03580284,
+        0.04135097, 0.04290933, 0.04500619, 0.04834792, 0.0524239, 0.05530289,
+        0.05733374, 0.06145404
+    ]),
+    800:
+    array([
+        0.0131231, 0.01501723, 0.01620598, 0.01852257, 0.02164634, 0.02540595,
+        0.02933599, 0.03043873, 0.03192094, 0.03429131, 0.03718247, 0.03922491,
+        0.0406419, 0.04361741
+    ]),
+    1600:
+    array([
+        0.00932049, 0.01066126, 0.01150366, 0.01314135, 0.0153528, 0.0180115,
+        0.02079512, 0.02157469, 0.02262168, 0.02429563, 0.02634302, 0.02777611,
+        0.02879721, 0.03088286
+    ])
+}
+
+# Coefficients are model log(cv) = b[0] + b[1] log(n) + b[2] log(n)**2
+normal_asymp_crit_vals = {
+    1.0: array([-1.17114969, -0.45068579, -0.00356741]),
+    5.0: array([-1.03298277, -0.45068579, -0.00356741]),
+    10.0: array([-0.95518114, -0.45068579, -0.00356741]),
+    25.0: array([-0.81912169, -0.45068579, -0.00356741]),
+    50.0: array([-0.6607348, -0.45068579, -0.00356741]),
+    75.0: array([-0.49861004, -0.45068579, -0.00356741]),
+    90.0: array([-0.35446139, -0.45068579, -0.00356741]),
+    92.5: array([-0.31737193, -0.45068579, -0.00356741]),
+    95.0: array([-0.26969888, -0.45068579, -0.00356741]),
+    97.5: array([-0.1979977, -0.45068579, -0.00356741]),
+    99.0: array([-0.11709649, -0.45068579, -0.00356741]),
+    99.5: array([-0.06398777, -0.45068579, -0.00356741]),
+    99.7: array([-0.0281863, -0.45068579, -0.00356741]),
+    99.9: array([0.04129756, -0.45068579, -0.00356741])
+}
+
+
+exp_crit_vals = {
+    3:
+    array([
+        0.2109761, 0.22988438, 0.25444542, 0.30397879, 0.36446945, 0.42814169,
+        0.51106702, 0.52962262, 0.55088872, 0.57754526, 0.60036174, 0.61179874,
+        0.61823126, 0.6294291
+    ]),
+    4:
+    array([
+        0.17384601, 0.20723235, 0.22806298, 0.26607407, 0.31624998, 0.38384024,
+        0.44420279, 0.46088139, 0.4843941, 0.521064, 0.55751191, 0.57850145,
+        0.59207384, 0.62098085
+    ]),
+    5:
+    array([
+        0.15948619, 0.18836122, 0.20519471, 0.2387884, 0.28771415, 0.34487558,
+        0.40440534, 0.42088543, 0.44200346, 0.47455921, 0.51269224, 0.53654197,
+        0.55188279, 0.58123527
+    ]),
+    6:
+    array([
+        0.1480075, 0.17258589, 0.18825927, 0.21978293, 0.2645031, 0.31713484,
+        0.37308973, 0.38833891, 0.4084249, 0.43908986, 0.47469271, 0.49984099,
+        0.5170463, 0.54989819
+    ]),
+    7:
+    array([
+        0.13783797, 0.16039265, 0.17499898, 0.20510973, 0.24578726, 0.29584306,
+        0.34806914, 0.36231862, 0.38111852, 0.41060597, 0.44579783, 0.46945914,
+        0.48562857, 0.5179461
+    ]),
+    8:
+    array([
+        0.12914489, 0.15056654, 0.16462426, 0.19280967, 0.23086956, 0.27829277,
+        0.3273405, 0.34086546, 0.35894819, 0.3874127, 0.42078514, 0.44361638,
+        0.45939843, 0.4910784
+    ]),
+    9:
+    array([
+        0.12209612, 0.1426062, 0.15604163, 0.1823721, 0.21856178, 0.26352862,
+        0.3101114, 0.32311637, 0.3404356, 0.36744906, 0.39952102, 0.42164587,
+        0.43719767, 0.46827333
+    ]),
+    10:
+    array([
+        0.11622346, 0.13594053, 0.14864516, 0.17351161, 0.20820576, 0.25083243,
+        0.29544984, 0.3078714, 0.32438433, 0.35032272, 0.38135762, 0.40282213,
+        0.41758788, 0.44762269
+    ]),
+    11:
+    array([
+        0.11116996, 0.13008811, 0.14214361, 0.16587204, 0.19916, 0.23987837,
+        0.28264075, 0.29456614, 0.31039145, 0.3353923, 0.36531224, 0.38597334,
+        0.40039914, 0.42945227
+    ]),
+    12:
+    array([
+        0.10682852, 0.12494111, 0.13642386, 0.15924117, 0.19119135, 0.23024225,
+        0.27140493, 0.28285565, 0.29813962, 0.32235591, 0.35118835, 0.37131437,
+        0.38541362, 0.41355258
+    ]),
+    13:
+    array([
+        0.10299158, 0.12035679, 0.13139418, 0.15340996, 0.18411715, 0.22174274,
+        0.26139337, 0.27247472, 0.28721874, 0.31061368, 0.3386004, 0.3579752,
+        0.37154656, 0.399054
+    ]),
+    14:
+    array([
+        0.09952544, 0.11620577, 0.12685351, 0.14816724, 0.17777806, 0.21413923,
+        0.25247245, 0.26312392, 0.27746948, 0.30001199, 0.32724359, 0.34615172,
+        0.3591787, 0.38578716
+    ]),
+    15:
+    array([
+        0.09635825, 0.11247223, 0.12279761, 0.1434653, 0.17203838, 0.20728334,
+        0.2443769, 0.25476058, 0.26865606, 0.29062696, 0.31686937, 0.33520734,
+        0.34795825, 0.37374779
+    ]),
+    16:
+    array([
+        0.093446, 0.10911215, 0.11910394, 0.1391716, 0.16686968, 0.20103119,
+        0.23706762, 0.24716908, 0.26064721, 0.28192033, 0.30753741, 0.32530837,
+        0.33776015, 0.36287832
+    ]),
+    17:
+    array([
+        0.09080542, 0.10601186, 0.11575975, 0.13523043, 0.16215573, 0.19534591,
+        0.2303959, 0.24019386, 0.25330648, 0.27400789, 0.29895143, 0.31623639,
+        0.32845602, 0.35308388
+    ]),
+    18:
+    array([
+        0.08844468, 0.10320066, 0.11271261, 0.13163588, 0.15780097, 0.19009349,
+        0.22420159, 0.2337161, 0.24646825, 0.26673429, 0.29105285, 0.30803457,
+        0.31984228, 0.34387696
+    ]),
+    19:
+    array([
+        0.08623885, 0.1006206, 0.10988138, 0.12831526, 0.15380297, 0.18527462,
+        0.21845938, 0.22776537, 0.2402398, 0.2600306, 0.28378696, 0.30035609,
+        0.31189906, 0.33544625
+    ]),
+    20:
+    array([
+        0.08417538, 0.09821659, 0.10727612, 0.12520467, 0.15011026, 0.18077566,
+        0.21317237, 0.2222683, 0.23446664, 0.25382491, 0.27704397, 0.29319327,
+        0.30456436, 0.32759304
+    ]),
+    25:
+    array([
+        0.07575606, 0.08842104, 0.09651937, 0.1126094, 0.13493277, 0.1624784,
+        0.19169294, 0.19987007, 0.2108557, 0.22828626, 0.24930174, 0.26403731,
+        0.2743157, 0.29520476
+    ]),
+    30:
+    array([
+        0.06952264, 0.08110063, 0.08850223, 0.10324683, 0.12365394, 0.14885572,
+        0.1756019, 0.18308805, 0.19314473, 0.20920585, 0.2285569, 0.24218995,
+        0.25167821, 0.270999
+    ]),
+    40:
+    array([
+        0.06066843, 0.07071874, 0.07713823, 0.0899287, 0.10765412, 0.12954118,
+        0.15278494, 0.15934334, 0.16813288, 0.18214059, 0.19908466, 0.21086902,
+        0.2192688, 0.23619981
+    ]),
+    50:
+    array([
+        0.05453287, 0.06354216, 0.06931727, 0.0807706, 0.09664734, 0.11625969,
+        0.13711892, 0.14300016, 0.15087554, 0.16345379, 0.17865445, 0.18939212,
+        0.19681564, 0.21177526
+    ]),
+    100:
+    array([
+        0.03911314, 0.04550435, 0.04958972, 0.05770944, 0.06896805, 0.08288491,
+        0.09771459, 0.10189766, 0.10752233, 0.1164863, 0.12735376, 0.13497319,
+        0.14038704, 0.15150129
+    ]),
+    200:
+    array([
+        0.02793837, 0.03246737, 0.03536757, 0.04111863, 0.04910105, 0.0589757,
+        0.06949443, 0.0724547, 0.07645096, 0.08280575, 0.09054264, 0.09600477,
+        0.09980637, 0.10760793
+    ]),
+    400:
+    array([
+        0.01991454, 0.023116, 0.02516769, 0.0292415, 0.03488679, 0.0418774,
+        0.04931093, 0.05141711, 0.054236, 0.05874581, 0.06421502, 0.06808278,
+        0.07079796, 0.07628466
+    ]),
+    800:
+    array([
+        0.01415514, 0.01642927, 0.0178805, 0.02075938, 0.02475783, 0.02970526,
+        0.03496731, 0.03645351, 0.03845017, 0.04164885, 0.04552405, 0.0482418,
+        0.05017098, 0.05406992
+    ]),
+    1600:
+    array([
+        0.01005851, 0.01165883, 0.0126869, 0.01472426, 0.01754881, 0.02104396,
+        0.02476615, 0.02581898, 0.02723521, 0.02950141, 0.03224859, 0.03418578,
+        0.03553487, 0.03829879
+    ])
+}
+
+# Coefficients are model log(cv) = b[0] + b[1] log(n) + b[2] log(n)**2
+exp_asymp_crit_vals = {
+    1.0: array([-1.04988875, -0.46444021, -0.00252528]),
+    5.0: array([-0.89825066, -0.46444021, -0.00252528]),
+    10.0: array([-0.81197037, -0.46444021, -0.00252528]),
+    25.0: array([-0.66000008, -0.46444021, -0.00252528]),
+    50.0: array([-0.4814209, -0.46444021, -0.00252528]),
+    75.0: array([-0.29737272, -0.46444021, -0.00252528]),
+    90.0: array([-0.13397474, -0.46444021, -0.00252528]),
+    92.5: array([-0.09220104, -0.46444021, -0.00252528]),
+    95.0: array([-0.03868789, -0.46444021, -0.00252528]),
+    97.5: array([0.0411943, -0.46444021, -0.00252528]),
+    99.0: array([0.130122, -0.46444021, -0.00252528]),
+    99.5: array([0.1882404, -0.46444021, -0.00252528]),
+    99.7: array([0.22712671, -0.46444021, -0.00252528]),
+    99.9: array([0.30189677, -0.46444021, -0.00252528])
+}
+
+
+# Critical Value
+critical_values = {'normal': normal_crit_vals, 'exp': exp_crit_vals}
+asymp_critical_values = {
+    'normal': normal_asymp_crit_vals,
+    'exp': exp_asymp_crit_vals
+}
diff --git a/statsmodels/stats/tabledist.py b/statsmodels/stats/tabledist.py
--- a/statsmodels/stats/tabledist.py
+++ b/statsmodels/stats/tabledist.py
@@ -5,24 +5,24 @@
 Author: Josef Perktold
 License: BSD-3
 
-
 TODO:
 check orientation, size and alpha should be increasing for interp1d,
 but what is alpha? can be either sf or cdf probability
 change it to use one consistent notation
 
-check: instead of bound checking I could use the fill-value of the interpolators
-
-
+check: instead of bound checking I could use the fill-value of the
+interpolators
 """
-from statsmodels.compat.python import range
 import numpy as np
 from scipy.interpolate import interp1d, interp2d, Rbf
+
+from statsmodels.compat.python import range
 from statsmodels.tools.decorators import cache_readonly
 
 
 class TableDist(object):
-    '''Distribution, critical values and p-values from tables
+    """
+    Distribution, critical values and p-values from tables
 
     currently only 1 extra parameter, e.g. sample size
 
@@ -32,51 +32,107 @@ class TableDist(object):
         probabiliy in the table, could be either sf (right tail) or cdf (left
         tail)
     size : array_like, 1d
-        second paramater in the table
+        The sample sizes for the table
     crit_table : array_like, 2d
+        The sample sizes in the table
         array with critical values for sample size in rows and probability in
         columns
+    asymptotic : callable, optional
+        Callable function with the form fn(nobs) that returns len(alpha)
+        critical values where the critical value in position i corresponds to
+        alpha[i]
+    min_nobs : int, optional
+        Minimum number of observations to use the asymptotic distribution. If
+        not provided, uses max(size).
+    max_nobs : int, optional
+        Maximum number of observations to use the tabular distribution. If not
+        provided, uses max(size)
 
     Notes
     -----
-    size and alpha should be increasing
-
-
-    '''
-
-    def __init__(self, alpha, size, crit_table):
+    size and alpha must be sorted and increasing.
+
+    If both min_nobs and max_nobs are provided, then
+    the critical values from the tabular distribution and the asymptotic
+    distribution are linearly blended using the formula
+    :math:`w cv_a + (1-w) cv_t` where the weight is
+    :math:`w = (n - a_{min}) / (a_{max} - a_{min})`. This ensures the
+    transition between the tabular and the asymptotic critical values is
+    continuous. If these are not provided, then the asymptotic critical value
+    is used for nobs > max(size).
+    """
+
+    def __init__(self, alpha, size, crit_table, asymptotic=None,
+                 min_nobs=None, max_nobs=None):
         self.alpha = np.asarray(alpha)
+        if self.alpha.ndim != 1:
+            raise ValueError('alpha is not 1d')
+        elif (np.diff(self.alpha) <= 0).any():
+            raise ValueError('alpha is not sorted')
         self.size = np.asarray(size)
+        if self.size.ndim != 1:
+            raise ValueError('size is not 1d')
+        elif (np.diff(self.size) <= 0).any():
+            raise ValueError('size is not sorted')
+        if self.size.ndim == 1:
+            if (np.diff(alpha) <= 0).any():
+                raise ValueError('alpha is not sorted')
         self.crit_table = np.asarray(crit_table)
+        if self.crit_table.shape != (self.size.shape[0], self.alpha.shape[0]):
+            raise ValueError('crit_table must have shape'
+                             '(len(size), len(alpha))')
 
         self.n_alpha = len(alpha)
         self.signcrit = np.sign(np.diff(self.crit_table, 1).mean())
-        if self.signcrit > 0: #increasing
-            self.critv_bounds = self.crit_table[:,[0,1]]
+        if self.signcrit > 0:  # increasing
+            self.critv_bounds = self.crit_table[:, [0, 1]]
         else:
-            self.critv_bounds = self.crit_table[:,[1,0]]
+            self.critv_bounds = self.crit_table[:, [1, 0]]
+        self.asymptotic = None
+        max_size = self.max_size = max(size)
+
+        if asymptotic is not None:
+            try:
+                cv = asymptotic(self.max_size + 1)
+            except Exception as exc:
+                raise type(exc)('Calling asymptotic(self.size+1) failed. The '
+                                'error message was:'
+                                '\n\n{err_msg}'.format(err_msg=exc.args[0]))
+            if len(cv) != len(alpha):
+                raise ValueError('asymptotic does not return len(alpha) '
+                                 'values')
+            self.asymptotic = asymptotic
+
+        self.min_nobs = max_size if min_nobs is None else min_nobs
+        self.max_nobs = max_size if max_nobs is None else max_nobs
+        if self.min_nobs > max_size:
+            raise ValueError('min_nobs > max(size)')
+        if self.max_nobs > max_size:
+            raise ValueError('max_nobs > max(size)')
 
     @cache_readonly
     def polyn(self):
-        polyn = [interp1d(self.size, self.crit_table[:,i])
-                               for i in range(self.n_alpha)]
+        polyn = [interp1d(self.size, self.crit_table[:, i])
+                 for i in range(self.n_alpha)]
         return polyn
 
     @cache_readonly
     def poly2d(self):
-        #check for monotonicity ?
-        #fix this, interp needs increasing
+        # check for monotonicity ?
+        # fix this, interp needs increasing
         poly2d = interp2d(self.size, self.alpha, self.crit_table)
         return poly2d
 
     @cache_readonly
     def polyrbf(self):
         xs, xa = np.meshgrid(self.size.astype(float), self.alpha)
-        polyrbf = Rbf(xs.ravel(), xa.ravel(), self.crit_table.T.ravel(),function='linear')
+        polyrbf = Rbf(xs.ravel(), xa.ravel(), self.crit_table.T.ravel(),
+                      function='linear')
         return polyrbf
 
     def _critvals(self, n):
-        '''rows of the table, linearly interpolated for given sample size
+        """
+        Rows of the table, linearly interpolated for given sample size
 
         Parameters
         ----------
@@ -93,14 +149,28 @@ def _critvals(self, n):
         This is used in two step interpolation, or if we want to know the
         critical values for all alphas for any sample size that we can obtain
         through interpolation
+        """
+        if n > self.max_size:
+            if self.asymptotic is not None:
+                cv = self.asymptotic(n)
+            else:
+                raise ValueError('n is above max(size) and no asymptotic '
+                                 'distribtuion is provided')
+        else:
+            cv = ([p(n) for p in self.polyn])
+            if n > self.min_nobs:
+                w = (n - self.min_nobs) / (self.max_nobs - self.min_nobs)
+                w = min(1.0, w)
+                a_cv = self.asymptotic(n)
+                cv = w * a_cv + (1 - w) * cv
 
-        '''
-        return np.array([p(n) for p in self.polyn])
+        return cv
 
     def prob(self, x, n):
-        '''find pvalues by interpolation, eiter cdf(x) or sf(x)
+        """
+        Find pvalues by interpolation, either cdf(x)
 
-        returns extrem probabilities, 0.001 and 0.2, for out of range
+        Returns extreme probabilities, 0.001 and 0.2, for out of range
 
         Parameters
         ----------
@@ -111,29 +181,18 @@ def prob(self, x, n):
 
         Returns
         -------
-        prob : arraylike
+        prob : array_like
             This is the probability for each value of x, the p-value in
             underlying distribution is for a statistical test.
-
-        '''
+        """
         critv = self._critvals(n)
         alpha = self.alpha
-#        if self.signcrit == 1:
-#            if x < critv[0]:  #generalize: ? np.sign(x - critvals[0]) == self.signcrit:
-#                return alpha[0]
-#            elif x > critv[-1]:
-#                return alpha[-1]
-#        elif self.signcrit == -1:
-#            if x > critv[0]:
-#                return alpha[0]
-#            elif x < critv[-1]:
-#                return alpha[-1]
 
         if self.signcrit < 1:
-            #reverse if critv is decreasing
+            # reverse if critv is decreasing
             critv, alpha = critv[::-1], alpha[::-1]
 
-        #now critv is increasing
+        # now critv is increasing
         if np.size(x) == 1:
             if x < critv[0]:
                 return alpha[0]
@@ -141,32 +200,21 @@ def prob(self, x, n):
                 return alpha[-1]
             return interp1d(critv, alpha)(x)[()]
         else:
-            #vectorized
+            # vectorized
             cond_low = (x < critv[0])
             cond_high = (x > critv[-1])
             cond_interior = ~np.logical_or(cond_low, cond_high)
 
-            probs = np.nan * np.ones(x.shape) #mistake if nan left
+            probs = np.nan * np.ones(x.shape)  # mistake if nan left
             probs[cond_low] = alpha[0]
             probs[cond_low] = alpha[-1]
             probs[cond_interior] = interp1d(critv, alpha)(x[cond_interior])
 
             return probs
 
-
-    def crit2(self, prob, n):
-        '''returns interpolated quantiles, similar to ppf or isf
-
-        this can be either cdf or sf depending on the table, twosided?
-
-        this doesn't work, no more knots warning
-
-        '''
-        return self.poly2d(n, prob)
-
-
     def crit(self, prob, n):
-        '''returns interpolated quantiles, similar to ppf or isf
+        """
+        Returns interpolated quantiles, similar to ppf or isf
 
         use two sequential 1d interpolation, first by n then by prob
 
@@ -181,31 +229,31 @@ def crit(self, prob, n):
         -------
         ppf : array_like
             critical values with same shape as prob
-
-        '''
+        """
         prob = np.asarray(prob)
         alpha = self.alpha
         critv = self._critvals(n)
 
-        #vectorized
+        # vectorized
         cond_ilow = (prob > alpha[0])
         cond_ihigh = (prob < alpha[-1])
         cond_interior = np.logical_or(cond_ilow, cond_ihigh)
 
-        #scalar
+        # scalar
         if prob.size == 1:
             if cond_interior:
                 return interp1d(alpha, critv)(prob)
             else:
                 return np.nan
 
-        #vectorized
-        quantile = np.nan * np.ones(prob.shape) #nans for outside
+        # vectorized
+        quantile = np.nan * np.ones(prob.shape)  # nans for outside
         quantile[cond_interior] = interp1d(alpha, critv)(prob[cond_interior])
         return quantile
 
     def crit3(self, prob, n):
-        '''returns interpolated quantiles, similar to ppf or isf
+        """
+        Returns interpolated quantiles, similar to ppf or isf
 
         uses Rbf to interpolate critical values as function of `prob` and `n`
 
@@ -222,98 +270,24 @@ def crit3(self, prob, n):
             critical values with same shape as prob, returns nan for arguments
             that are outside of the table bounds
 
-        '''
+        """
         prob = np.asarray(prob)
         alpha = self.alpha
 
-        #vectorized
+        # vectorized
         cond_ilow = (prob > alpha[0])
         cond_ihigh = (prob < alpha[-1])
         cond_interior = np.logical_or(cond_ilow, cond_ihigh)
 
-        #scalar
+        # scalar
         if prob.size == 1:
             if cond_interior:
                 return self.polyrbf(n, prob)
             else:
                 return np.nan
 
-        #vectorized
-        quantile = np.nan * np.ones(prob.shape) #nans for outside
+        # vectorized
+        quantile = np.nan * np.ones(prob.shape)  # nans for outside
 
         quantile[cond_interior] = self.polyrbf(n, prob[cond_interior])
         return quantile
-
-
-
-if __name__ == '__main__':
-
-    '''
-    example Lilliefors test for normality
-    An Analytic Approximation to the Distribution of Lilliefors's Test Statistic for Normality
-    Author(s): Gerard E. Dallal and Leland WilkinsonSource: The American Statistician, Vol. 40, No. 4 (Nov., 1986), pp. 294-296Published by: American Statistical AssociationStable URL: http://www.jstor.org/stable/2684607 .
-    '''
-
-    #for this test alpha is sf probability, i.e. right tail probability
-
-    alpha = np.array([ 0.2  ,  0.15 ,  0.1  ,  0.05 ,  0.01 ,  0.001])[::-1]
-    size = np.array([ 4,   5,   6,   7,   8,   9,  10,  11,  12,  13,  14,  15,
-                     16,  17,  18,  19,  20,  25,  30,  40, 100, 400, 900], float)
-
-    #critical values, rows are by sample size, columns are by alpha
-    crit_lf = np.array(   [[303, 321, 346, 376, 413, 433],
-                           [289, 303, 319, 343, 397, 439],
-                           [269, 281, 297, 323, 371, 424],
-                           [252, 264, 280, 304, 351, 402],
-                           [239, 250, 265, 288, 333, 384],
-                           [227, 238, 252, 274, 317, 365],
-                           [217, 228, 241, 262, 304, 352],
-                           [208, 218, 231, 251, 291, 338],
-                           [200, 210, 222, 242, 281, 325],
-                           [193, 202, 215, 234, 271, 314],
-                           [187, 196, 208, 226, 262, 305],
-                           [181, 190, 201, 219, 254, 296],
-                           [176, 184, 195, 213, 247, 287],
-                           [171, 179, 190, 207, 240, 279],
-                           [167, 175, 185, 202, 234, 273],
-                           [163, 170, 181, 197, 228, 266],
-                           [159, 166, 176, 192, 223, 260],
-                           [143, 150, 159, 173, 201, 236],
-                           [131, 138, 146, 159, 185, 217],
-                           [115, 120, 128, 139, 162, 189],
-                           [ 74,  77,  82,  89, 104, 122],
-                           [ 37,  39,  41,  45,  52,  61],
-                           [ 25,  26,  28,  30,  35,  42]])[:,::-1] / 1000.
-
-
-    lf = TableDist(alpha, size, crit_lf)
-    print(lf.prob(0.166, 20), 'should be:', 0.15)
-    print('')
-    print(lf.crit2(0.15, 20), 'should be:', 0.166, 'interp2d bad')
-    print(lf.crit(0.15, 20), 'should be:', 0.166, 'two 1d')
-    print(lf.crit3(0.15, 20), 'should be:', 0.166, 'Rbf')
-    print('')
-    print(lf.crit2(0.17, 20), 'should be in:', (.159, .166), 'interp2d bad')
-    print(lf.crit(0.17, 20), 'should be in:', (.159, .166), 'two 1d')
-    print(lf.crit3(0.17, 20), 'should be in:', (.159, .166), 'Rbf')
-    print('')
-    print(lf.crit2(0.19, 20), 'should be in:', (.159, .166), 'interp2d bad')
-    print(lf.crit(0.19, 20), 'should be in:', (.159, .166), 'two 1d')
-    print(lf.crit3(0.19, 20), 'should be in:', (.159, .166), 'Rbf')
-    print('')
-    print(lf.crit2(0.199, 20), 'should be in:', (.159, .166), 'interp2d bad')
-    print(lf.crit(0.199, 20), 'should be in:', (.159, .166), 'two 1d')
-    print(lf.crit3(0.199, 20), 'should be in:', (.159, .166), 'Rbf')
-    #testing
-    print(np.max(np.abs(np.array([lf.prob(c, size[i]) for i in range(len(size)) for c in crit_lf[i]]).reshape(-1,lf.n_alpha) - lf.alpha)))
-    #1.6653345369377348e-16
-    print(np.max(np.abs(np.array([lf.crit(c, size[i]) for i in range(len(size)) for c in lf.alpha]).reshape(-1,lf.n_alpha) - crit_lf)))
-    #6.9388939039072284e-18)
-    print(np.max(np.abs(np.array([lf.crit3(c, size[i]) for i in range(len(size)) for c in lf.alpha]).reshape(-1,lf.n_alpha) - crit_lf)))
-    #4.0615705243496336e-12)
-    print((np.array([lf.crit3(c, size[i]) for i in range(len(size)) for c in lf.alpha[:-1]*1.1]).reshape(-1,lf.n_alpha-1) < crit_lf[:,:-1]).all())
-    print((np.array([lf.crit3(c, size[i]) for i in range(len(size)) for c in lf.alpha[:-1]*1.1]).reshape(-1,lf.n_alpha-1) > crit_lf[:,1:]).all())
-    print((np.array([lf.prob(c*0.9, size[i]) for i in range(len(size)) for c in crit_lf[i,:-1]]).reshape(-1,lf.n_alpha-1) > lf.alpha[:-1]).all())
-    print((np.array([lf.prob(c*1.1, size[i]) for i in range(len(size)) for c in crit_lf[i,1:]]).reshape(-1,lf.n_alpha-1) < lf.alpha[1:]).all())
-    #start at size_idx=2 because of non-monotonicity of lf_crit
-    print((np.array([lf.prob(c, size[i]*0.9) for i in range(2,len(size)) for c in crit_lf[i,:-1]]).reshape(-1,lf.n_alpha-1) > lf.alpha[:-1]).all())


Test patch:
diff --git a/statsmodels/stats/tests/results/lilliefors_critical_value_simulation.py b/statsmodels/stats/tests/results/lilliefors_critical_value_simulation.py
new file mode 100644
--- /dev/null
+++ b/statsmodels/stats/tests/results/lilliefors_critical_value_simulation.py
@@ -0,0 +1,148 @@
+"""
+Simulate critical values for finite sample distribution
+and estimate asymptotic expansion parameters for the lilliefors tests
+"""
+import datetime as dt
+import gzip
+import io
+import logging
+import pickle
+from collections import defaultdict
+
+import numpy as np
+import pandas as pd
+from scipy import stats
+from yapf.yapflib.yapf_api import FormatCode
+
+import statsmodels.api as sm
+
+NUM_SIM = 10000000
+MAX_MEMORY = 2 ** 28
+SAMPLE_SIZES = [3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19,
+                20, 25, 30, 40, 50, 100, 200, 400, 800, 1600]
+MIN_SAMPLE_SIZE = {'normal': 4, 'exp': 3}
+MAX_SIZE = max(SAMPLE_SIZES)
+MAX_SIM_SIZE = MAX_MEMORY // (MAX_SIZE * 8)
+PERCENTILES = [1, 5, 10, 25, 50, 75, 90, 92.5, 95, 97.5, 99, 99.5, 99.7, 99.9]
+seed = 113682199084250344115761738871133961874
+seed = np.array([(seed >> (32 * i)) % 2 ** 32 for i in range(4)],
+                dtype=np.uint32)
+
+
+def simulations(sim_type, save=False):
+    rs = np.random.RandomState(seed)
+    remaining = NUM_SIM
+    results = defaultdict(list)
+    start = dt.datetime.now()
+    while remaining > 0:
+        this_iter = min(remaining, MAX_SIM_SIZE)
+        remaining -= this_iter
+        if sim_type == 'normal':
+            dist = rs.standard_normal
+        else:
+            dist = rs.standard_exponential
+        rvs = dist((MAX_SIZE, this_iter))
+        sample_sizes = [ss for ss in SAMPLE_SIZES if
+                        ss >= MIN_SAMPLE_SIZE[sim_type]]
+        for ss in sample_sizes:
+            sample = rvs[:ss]
+            mu = sample.mean(0)
+            if sim_type == 'normal':
+                std = sample.std(0, ddof=1)
+                z = (sample - mu) / std
+                cdf_fn = stats.norm.cdf
+            else:
+                z = sample / mu
+                cdf_fn = stats.expon.cdf
+            z = np.sort(z, axis=0)
+            nobs = ss
+            cdf = cdf_fn(z)
+            plus = np.arange(1.0, nobs + 1) / nobs
+            d_plus = (plus[:, None] - cdf).max(0)
+            minus = np.arange(0.0, nobs) / nobs
+            d_minus = (cdf - minus[:, None]).max(0)
+            d = np.max(np.abs(np.c_[d_plus, d_minus]), 1)
+            results[ss].append(d)
+        logging.log(logging.INFO,
+                    'Completed {0}, remaining {1}'.format(NUM_SIM - remaining,
+                                                          remaining))
+        elapsed = dt.datetime.now() - start
+        rem = elapsed.total_seconds() / (NUM_SIM - remaining) * remaining
+        logging.log(logging.INFO,
+                    '({0}) Time remaining {1:0.1f}s'.format(sim_type, rem))
+
+    for key in results:
+        results[key] = np.concatenate(results[key])
+
+    if save:
+        file_name = 'lilliefors-sim-{0}-results.pkl.gz'.format(sim_type)
+        with gzip.open(file_name, 'wb', 5) as pkl:
+            pickle.dump(results, pkl)
+
+    crit_vals = {}
+    for key in results:
+        crit_vals[key] = np.percentile(results[key], PERCENTILES)
+
+    start = 20
+    num = len([k for k in crit_vals if k >= start])
+    all_x = np.zeros((num * len(PERCENTILES), len(PERCENTILES) + 2))
+    all_y = np.zeros(num * len(PERCENTILES))
+    loc = 0
+    for i, perc in enumerate(PERCENTILES):
+        y = pd.DataFrame(results).quantile(perc / 100.)
+        y = y.loc[start:]
+        all_y[loc:loc + len(y)] = np.log(y)
+        x = y.index.values.astype(np.float)
+        all_x[loc:loc + len(y), -2:] = np.c_[np.log(x), np.log(x) ** 2]
+        all_x[loc:loc + len(y), i:(i + 1)] = 1
+        loc += len(y)
+    w = np.ones_like(all_y).reshape(len(PERCENTILES), -1)
+    w[6:, -5:] = 3
+    w = w.ravel()
+    res = sm.WLS(all_y, all_x, weights=w).fit()
+    params = []
+    for i in range(len(PERCENTILES)):
+        params.append(np.r_[res.params[i], res.params[-2:]])
+    params = np.array(params)
+
+    df = pd.DataFrame(params).T
+    df.columns = PERCENTILES
+    asymp_crit_vals = {}
+    for col in df:
+        asymp_crit_vals[col] = df[col].values
+
+    code = '{0}_crit_vals = '.format(sim_type)
+    code += str(crit_vals).strip() + '\n\n'
+    code += '\n# Coefficients are model '
+    code += 'log(cv) = b[0] + b[1] log(n) + b[2] log(n)**2\n'
+    code += '{0}_asymp_crit_vals = '.format(sim_type)
+    code += str(asymp_crit_vals) + '\n\n'
+    return code
+
+
+if __name__ == '__main__':
+    logging.getLogger().setLevel(logging.INFO)
+    header = '"""\nThis file is automatically generated by ' \
+             'littlefors_critical_values.py.\nDo not directly modify' \
+             'this file.\n\nValue based on 10,000,000 simulations."""\n\n'
+    header += 'from numpy import array\n\n'
+    header += 'PERCENTILES = ' + str(PERCENTILES).strip() + '\n\n'
+    header += 'SAMPLE_SIZES = ' + str(SAMPLE_SIZES).strip() + '\n\n'
+    normal = simulations('normal', True)
+    exp = simulations('exp', True)
+    footer = """
+# Critical Value
+critical_values = {'normal': normal_crit_vals,
+                   'exp': exp_crit_vals}
+asymp_critical_values = {'normal': normal_asymp_crit_vals,
+                         'exp': exp_asymp_crit_vals}
+
+"""
+    cv_filename = '../../_lilliefors_critical_values.py'
+    with io.open(cv_filename, 'w', newline='\n') as cv:
+        cv.write(FormatCode(header)[0])
+        cv.write(FormatCode(normal)[0])
+        cv.write('\n\n')
+        cv.write(FormatCode(exp)[0])
+        cv.write('\n\n')
+        cv.write(FormatCode(footer)[0])
diff --git a/statsmodels/stats/tests/test_diagnostic.py b/statsmodels/stats/tests/test_diagnostic.py
--- a/statsmodels/stats/tests/test_diagnostic.py
+++ b/statsmodels/stats/tests/test_diagnostic.py
@@ -596,7 +596,7 @@ def test_normality(self):
         #> lt = lillie.test(residuals(fm)[1:20])
         #> mkhtest(lt, "lilliefors", "-")
         lilliefors3 = dict(statistic=0.1333956004203103,
-                          pvalue=0.20, parameters=(), distr='-')
+                           pvalue=0.455683, parameters=(), distr='-')
 
         lf1 = smsdia.lilliefors(res.resid)
         lf2 = smsdia.lilliefors(res.resid**2)
diff --git a/statsmodels/stats/tests/test_lilliefors.py b/statsmodels/stats/tests/test_lilliefors.py
--- a/statsmodels/stats/tests/test_lilliefors.py
+++ b/statsmodels/stats/tests/test_lilliefors.py
@@ -1,9 +1,10 @@
-
 import numpy as np
+import pytest
 from numpy.testing import assert_almost_equal
 from scipy import stats
 
-from statsmodels.stats._lilliefors import lilliefors
+from statsmodels.stats._lilliefors import lilliefors, get_lilliefors_table, \
+    kstest_fit, ksstat
 
 
 class TestLilliefors(object):
@@ -16,7 +17,7 @@ def test_normal(self):
         # require(nortest)
         # lillie.test(x_n)
 
-        d_ks_norm, p_norm = lilliefors(x_n, dist='norm')
+        d_ks_norm, p_norm = lilliefors(x_n, dist='norm', pvalmethod='approx')
         # shift normal distribution > 0 to exactly mirror R `KScorrect` test
         # R `KScorrect` requires all values tested for exponential
         # distribution to be > 0
@@ -24,13 +25,23 @@ def test_normal(self):
         # require(KScorrect)
         # LcKS(x_n+abs(min(x_n))+0.001, 'pexp')
 
-        d_ks_exp, p_exp = lilliefors(x_n+np.abs(x_n.min()) + 0.001, dist='exp')
+        d_ks_exp, p_exp = lilliefors(x_n + np.abs(x_n.min()) + 0.001,
+                                     dist='exp', pvalmethod='approx')
         # assert normal
         assert_almost_equal(d_ks_norm, 0.025957, decimal=3)
-        assert_almost_equal(p_norm, 0.2000, decimal=3)
+        assert_almost_equal(p_norm, 0.64175, decimal=3)
         # assert exp
         assert_almost_equal(d_ks_exp, 0.3436007, decimal=3)
-        assert_almost_equal(p_exp, 0.01, decimal=3)
+        assert_almost_equal(p_exp, 0.001, decimal=3)
+
+    def test_normal_table(self):
+        np.random.seed(3975)
+        x_n = stats.norm.rvs(size=500)
+
+        d_ks_norm, p_norm = lilliefors(x_n, dist='norm', pvalmethod='table')
+        # assert normal
+        assert_almost_equal(d_ks_norm, 0.025957, decimal=3)
+        assert_almost_equal(p_norm, 0.64175, decimal=3)
 
     def test_expon(self):
         np.random.seed(3975)
@@ -38,22 +49,54 @@ def test_expon(self):
         # R function call:
         # require(nortest)
         # lillie.test(x_n)
-        d_ks_norm, p_norm = lilliefors(x_e, dist='norm')
+        d_ks_norm, p_norm = lilliefors(x_e, dist='norm', pvalmethod='approx')
         # R function call:
         # require(KScorrect)
         # LcKS(x_e, 'pexp')
-        d_ks_exp, p_exp = lilliefors(x_e, dist='exp')
+        d_ks_exp, p_exp = lilliefors(x_e, dist='exp', pvalmethod='approx')
         # assert normal
         assert_almost_equal(d_ks_norm, 0.15581, decimal=3)
         assert_almost_equal(p_norm, 2.2e-16, decimal=3)
         # assert exp
         assert_almost_equal(d_ks_exp, 0.02763748, decimal=3)
-        assert_almost_equal(p_exp, 0.200, decimal=3)
+        assert_almost_equal(p_exp, 0.72540, decimal=3)
 
     def test_pval_bounds(self):
-        x = np.arange(1, 10)
-        d_ks_n, p_n = lilliefors(x, dist='norm')
-        d_ks_e, p_e = lilliefors(x, dist='exp')
+        x = stats.norm.ppf((np.arange(10.) + 0.5) / 10)
+        d_ks_n, p_n = lilliefors(x, dist='norm', pvalmethod='approx')
+        x = stats.expon.ppf((np.arange(10.) + 0.5) / 10)
+        d_ks_e, p_e = lilliefors(x, dist='exp', pvalmethod='approx')
+
+        assert_almost_equal(p_n, 0.99, decimal=7)
+        assert_almost_equal(p_e, 0.99, decimal=7)
+
+    def test_min_nobs(self):
+        x = np.arange(3.)
+        with pytest.raises(ValueError):
+            lilliefors(x, dist='norm', pvalmethod='approx')
+        x = np.arange(2.)
+        with pytest.raises(ValueError):
+            lilliefors(x, dist='exp', pvalmethod='approx')
+
+    @pytest.mark.smoke
+    def test_large_sample(self, reset_randomstate):
+        x = np.random.randn(10000)
+        lilliefors(x, pvalmethod='approx')
+
+
+def test_get_lilliefors_errors(reset_randomstate):
+    with pytest.raises(ValueError):
+        get_lilliefors_table(dist='unknown')
+    with pytest.raises(ValueError):
+        kstest_fit(np.random.standard_normal(100), dist='unknown',
+                   pvalmethod='table')
+
 
-        assert_almost_equal(p_n, 0.200, decimal=7)
-        assert_almost_equal(p_e, 0.200, decimal=7)
+def test_ksstat(reset_randomstate):
+    x = np.random.uniform(0, 1, 100)
+    two_sided = ksstat(x, 'uniform', alternative='two_sided')
+    greater = ksstat(x, 'uniform', alternative='greater')
+    lower = ksstat(x, stats.uniform, alternative='lower')
+    print(two_sided, greater, lower)
+    assert lower <= two_sided
+    assert greater <= two_sided
diff --git a/statsmodels/stats/tests/test_tabledist.py b/statsmodels/stats/tests/test_tabledist.py
new file mode 100644
--- /dev/null
+++ b/statsmodels/stats/tests/test_tabledist.py
@@ -0,0 +1,101 @@
+"""
+Test data from Lilliefors test for normality
+An Analytic Approximation to the Distribution of Lilliefors's
+Test Statistic for Normality
+Author(s): Gerard E. Dallal and Leland WilkinsonSource: The American
+Statistician, Vol. 40, No. 4 (Nov., 1986), pp. 294-296
+"""
+import numpy as np
+from numpy.testing import assert_almost_equal, assert_allclose
+
+from statsmodels.stats.tabledist import TableDist
+
+
+def test_tabledist():
+    # for this test alpha is sf probability, i.e. right tail probability
+    alpha = np.array([0.2, 0.15, 0.1, 0.05, 0.01, 0.001])[::-1]
+    size = np.array([4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15,
+                     16, 17, 18, 19, 20, 25, 30, 40, 100, 400, 900], float)
+
+    # critical values, rows are by sample size, columns are by alpha
+    crit_lf = np.array([[303, 321, 346, 376, 413, 433],
+                        [289, 303, 319, 343, 397, 439],
+                        [269, 281, 297, 323, 371, 424],
+                        [252, 264, 280, 304, 351, 402],
+                        [239, 250, 265, 288, 333, 384],
+                        [227, 238, 252, 274, 317, 365],
+                        [217, 228, 241, 262, 304, 352],
+                        [208, 218, 231, 251, 291, 338],
+                        [200, 210, 222, 242, 281, 325],
+                        [193, 202, 215, 234, 271, 314],
+                        [187, 196, 208, 226, 262, 305],
+                        [181, 190, 201, 219, 254, 296],
+                        [176, 184, 195, 213, 247, 287],
+                        [171, 179, 190, 207, 240, 279],
+                        [167, 175, 185, 202, 234, 273],
+                        [163, 170, 181, 197, 228, 266],
+                        [159, 166, 176, 192, 223, 260],
+                        [143, 150, 159, 173, 201, 236],
+                        [131, 138, 146, 159, 185, 217],
+                        [115, 120, 128, 139, 162, 189],
+                        [74, 77, 82, 89, 104, 122],
+                        [37, 39, 41, 45, 52, 61],
+                        [25, 26, 28, 30, 35, 42]])[:, ::-1] / 1000.
+
+    lf = TableDist(alpha, size, crit_lf)
+
+    assert_almost_equal(lf.prob(0.166, 20), 0.15)
+    assert_almost_equal(lf.crit(0.15, 20), 0.166)
+    assert_almost_equal(lf.crit3(0.15, 20), 0.166)
+
+    assert .159 <= lf.crit(0.17, 20) <= 166
+    assert .159 <= lf.crit3(0.17, 20) <= .166
+
+    assert .159 <= lf.crit(0.19, 20) <= .166
+    assert .159 <= lf.crit3(0.19, 20) <= .166
+
+    assert .159 <= lf.crit(0.199, 20) <= .166
+    assert .159 <= lf.crit3(0.199, 20) <= .166
+
+    # testing
+    vals = [lf.prob(c, size[i]) for i in range(len(size)) for c in crit_lf[i]]
+    vals = np.array(vals).reshape(-1, lf.n_alpha)
+    delta = np.abs(vals) - lf.alpha
+    assert_allclose(delta, np.zeros_like(delta))
+
+    # 1.6653345369377348e-16
+    vals = [lf.crit(c, size[i]) for i in range(len(size)) for c in lf.alpha]
+    vals = np.array(vals).reshape(-1, lf.n_alpha)
+    delta = np.abs(vals - crit_lf)
+    assert_allclose(delta, np.zeros_like(delta))
+
+    # 6.9388939039072284e-18)
+    print(np.max(np.abs(np.array(
+        [lf.crit3(c, size[i]) for i in range(len(size)) for c in
+         lf.alpha]).reshape(-1, lf.n_alpha) - crit_lf)))
+    # 4.0615705243496336e-12)
+    vals = [lf.crit3(c, size[i]) for i in range(len(size))
+            for c in lf.alpha[:-1] * 1.1]
+    vals = np.array(vals).reshape(-1, lf.n_alpha - 1)
+    assert (vals < crit_lf[:, :-1]).all()
+
+    vals = [lf.crit3(c, size[i]) for i in range(len(size)) for c in
+            lf.alpha[:-1] * 1.1]
+    vals = np.array(vals).reshape(-1, lf.n_alpha - 1)
+    assert (vals > crit_lf[:, 1:]).all()
+
+    vals = [lf.prob(c * 0.9, size[i]) for i in range(len(size))
+            for c in crit_lf[i, :-1]]
+    vals = np.array(vals).reshape(-1, lf.n_alpha - 1)
+    assert (vals > lf.alpha[:-1]).all()
+
+    vals = [lf.prob(c * 1.1, size[i]) for i in range(len(size)) for c in
+            crit_lf[i, 1:]]
+    vals = np.array(vals).reshape(-1, lf.n_alpha - 1)
+    assert (vals < lf.alpha[1:]).all()
+
+    # start at size_idx=2 because of non-monotonicity of lf_crit
+    vals = [lf.prob(c, size[i] * 0.9) for i in range(2, len(size))
+            for c in crit_lf[i, :-1]]
+    vals = np.array(vals).reshape(-1, lf.n_alpha - 1)
+    assert (vals > lf.alpha[:-1]).all()