statspai.frontier¶
frontier ¶
Stochastic frontier analysis (SFA).
Cross-sectional estimators: :func:frontier (half-normal / exponential /
truncated-normal; supports heteroskedastic sigma_u & sigma_v plus
inefficiency determinants emean).
Panel estimators: :func:xtfrontier with model in
{'ti', 'tvd', 'bc95'} (Pitt-Lee 1981, Battese-Coelli 1992,
Battese-Coelli 1995).
Helpers: :func:te_summary.
FrontierResult ¶
Bases: EconometricResults
Result object returned by :func:frontier and :func:xtfrontier.
Extends :class:~statspai.core.results.EconometricResults with
efficiency-score access, LR tests, and bootstrap helpers.
Examples:
>>> import statspai as sp
>>> import numpy as np, pandas as pd
>>> rng = np.random.default_rng(1)
>>> log_k = rng.normal(0, 1, 150)
>>> log_l = rng.normal(0, 1, 150)
>>> u = rng.exponential(0.3, 150)
>>> v = rng.normal(0, 0.2, 150)
>>> log_y = 1.0 + 0.4 * log_k + 0.5 * log_l + v - u
>>> df = pd.DataFrame({"log_y": log_y, "log_k": log_k, "log_l": log_l})
>>> res = sp.frontier(df, y="log_y", x=["log_k", "log_l"])
>>> type(res).__name__
'FrontierResult'
>>> eff = res.efficiency()
>>> bool((eff > 0).all() and (eff <= 1.0 + 1e-8).all())
True
summary ¶
Formatted summary table (Stata-style SFA block).
Overrides :class:EconometricResults.summary to hide per-observation
diagnostic arrays and surface the SFA-specific scalars.
efficiency ¶
Return unit-level technical efficiency scores.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
method
|
('bc', 'jlms')
|
'bc' (default) : Battese-Coelli (1988) |
'bc'
|
inefficiency ¶
Return E[u|eps] (inefficiency), Jondrow et al. (1982).
predict ¶
Out-of-sample prediction.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
new_data
|
DataFrame
|
Must contain the frontier regressors and, if the model has
|
required |
what
|
{'frontier', 'expected_inefficiency', 'expected_efficiency',
|
|
'frontier'
|
Returns:
| Type | Description |
|---|---|
Series
|
Indexed by the (post-dropna) rows of |
marginal_effects ¶
marginal_effects(kind: str = 'inefficiency', source: str = 'emean', at: str = 'observation') -> DataFrame
Marginal effects of inefficiency-shifting covariates.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
kind
|
'inefficiency'
|
Currently only |
'inefficiency'
|
source
|
('emean', 'usigma')
|
|
'emean'
|
at
|
('observation', 'mean', 'ame')
|
|
'observation'
|
Formulas
emean (truncated-normal): d E[u_i] / d z_ij = delta_j * [1 - (mu/sigma) * phi/Phi - (phi/Phi)^2] usigma (half-normal): d E[u_i] / d w_ij = gamma_j * sigma_u_i * sqrt(2/pi) usigma (exponential): d E[u_i] / d w_ij = gamma_j * sigma_u_i usigma (truncated-normal): d E[u_i] / d w_ij = gamma_j * sigma_u_i * [phi/Phi + ratio * phi/Phi * (phi/Phi - (-ratio))] (chain rule through sigma_u_i = exp(gamma'[1, w_i])).
returns_to_scale ¶
Sum of input elasticities (RTS) with Wald test H0: RTS = 1 (CRS).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
inputs
|
list of str
|
Input-variable names (should be log-transformed inputs in a
Cobb-Douglas frontier). Defaults to |
None
|
alpha
|
float
|
|
0.05
|
Returns:
| Type | Description |
|---|---|
dict with keys: ``rts``, ``se``, ``statistic``, ``pvalue``,
|
|
``ci_lower``, ``ci_upper``, ``interpretation``.
|
|
lr_test_no_inefficiency ¶
One-sided LR test H0: sigma_u = 0 (mixed chi-bar squared).
efficiency_ci ¶
efficiency_ci(alpha: float = 0.05, B: int = 500, method: Optional[str] = None, seed: Optional[int] = 0) -> DataFrame
Parametric-bootstrap CI for unit-level efficiency scores.
Draws (u_b, v_b) ~ posterior predictive using the fitted
variance parameters, then recomputes the Jondrow posterior for
the resampled composed error. Returns a DataFrame indexed like
:meth:efficiency with columns ['point', 'lower', 'upper'].
MetafrontierResult
dataclass
¶
Bases: ResultProtocolMixin
Container for a metafrontier fit.
Examples:
>>> import numpy as np
>>> import pandas as pd
>>> import statspai as sp
>>> rng = np.random.default_rng(2008)
>>> rows = []
>>> for g in ("A", "B"):
... shift = 0.0 if g == "A" else 0.3 # group B has a higher frontier
... for _ in range(40):
... x1 = rng.normal(0, 1)
... u = abs(rng.normal(0, 0.3))
... v = rng.normal(0, 0.15)
... y = 1.0 + shift + 0.5 * x1 + v - u
... rows.append({"y": y, "x1": x1, "group": g})
>>> df = pd.DataFrame(rows)
>>> res = sp.metafrontier(df, y="y", x=["x1"], group="group")
>>> isinstance(res, sp.MetafrontierResult)
True
>>> sorted(res.beta_groups)
['A', 'B']
>>> bool((res.tgr >= 0).all() and (res.tgr <= 1).all())
True
MalmquistResult
dataclass
¶
Bases: ResultProtocolMixin
Container for Malmquist productivity index decomposition.
Examples:
>>> import numpy as np
>>> import pandas as pd
>>> import statspai as sp
>>> rng = np.random.default_rng(1702)
>>> rows = []
>>> for t in (1, 2):
... for i in range(30):
... x1 = rng.normal(0, 1)
... u = abs(rng.normal(0, 0.3))
... v = rng.normal(0, 0.15)
... y = (1.0 + 0.05 * t) + 0.5 * x1 + v - u
... rows.append({"id": i, "t": t, "y": y, "x1": x1})
>>> df = pd.DataFrame(rows)
>>> res = sp.malmquist(df, y="y", x=["x1"], id="id", time="t")
>>> isinstance(res, sp.MalmquistResult)
True
>>> list(res.index_table.columns)
['id', 't_from', 't_to', 'm_index', 'ec', 'tc']
index_table
instance-attribute
¶
Wide table: one row per (id, period pair) with columns
['m_index', 'ec', 'tc'] plus the original id / period columns.
period_frontiers
instance-attribute
¶
period_frontiers: Dict[Any, FrontierResult]
Frontier fit per period.
summary_by_period
instance-attribute
¶
Mean M / EC / TC per period transition.
frontier ¶
frontier(data: DataFrame, y: str, x: List[str], *, dist: str = 'half-normal', cost: bool = False, usigma: Optional[List[str]] = None, vsigma: Optional[List[str]] = None, emean: Optional[List[str]] = None, te_method: str = 'bc', vce: str = 'oim', cluster: Optional[str] = None, B: int = 400, seed: Optional[int] = None, maxiter: int = 2000, tol: float = 1e-10, alpha: float = 0.05, start: Optional[ndarray] = None) -> FrontierResult
Estimate a cross-sectional stochastic frontier model by ML.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
data
|
DataFrame
|
Cross-sectional data. Rows with missing values in any referenced column are dropped. |
required |
y
|
str
|
Dependent variable (output for production, cost for cost frontier). |
required |
x
|
list of str
|
Frontier regressors (a constant is added automatically). |
required |
dist
|
('half-normal', 'exponential', 'truncated-normal')
|
Distribution of the inefficiency term |
'half-normal'
|
cost
|
bool
|
If True, estimate a cost frontier (composed error |
False
|
usigma
|
list of str
|
Columns parameterizing |
None
|
vsigma
|
list of str
|
Columns parameterizing |
None
|
emean
|
list of str
|
Columns parameterizing |
None
|
te_method
|
('bc', 'jlms')
|
Default technical-efficiency formula accessed via |
'bc'
|
vce
|
('oim', 'opg', 'robust')
|
Variance-covariance estimator:
|
'oim'
|
cluster
|
str
|
Cluster variable for cluster-robust SE (Liang-Zeger 1986). When
specified, implies |
None
|
maxiter
|
int
|
|
2000
|
tol
|
float
|
|
1e-10
|
alpha
|
float
|
|
0.05
|
start
|
ndarray
|
User-supplied starting values for the full parameter vector. |
None
|
Returns:
| Type | Description |
|---|---|
class:`FrontierResult`
|
|
Examples:
>>> import statspai as sp
>>> import numpy as np, pandas as pd
>>> rng = np.random.default_rng(0)
>>> n = 200
>>> log_k = rng.normal(0, 1, n)
>>> log_l = rng.normal(0, 1, n)
>>> v = rng.normal(0, 0.2, n)
>>> u = np.abs(rng.normal(0, 0.3, n)) # one-sided inefficiency
>>> df = pd.DataFrame({
... 'log_y': 0.3 * log_k + 0.6 * log_l + v - u,
... 'log_k': log_k,
... 'log_l': log_l,
... })
>>> res = sp.frontier(df, y='log_y', x=['log_k', 'log_l'])
>>> te = res.efficiency() # firm-level technical efficiency
>>> bool(0.0 < te.mean() <= 1.0)
True
>>> lr = res.lr_test_no_inefficiency() # H0: no inefficiency (sigma_u = 0)
>>> sorted(lr)
['df', 'pvalue', 'statistic']
xtfrontier ¶
xtfrontier(data: DataFrame, y: str, x: List[str], id: str, time: Optional[str] = None, *, model: str = 'ti', dist: str = 'half-normal', cost: bool = False, emean: Optional[List[str]] = None, vce: str = 'oim', cluster: Optional[str] = None, bias_correct: bool = False, n_quad: int = 24, maxiter: int = 500, tol: float = 1e-08, alpha: float = 0.05) -> FrontierResult
Panel stochastic frontier estimator.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
data
|
DataFrame
|
|
required |
y
|
str
|
|
required |
x
|
list of str
|
|
required |
id
|
str
|
Panel unit identifier. |
required |
time
|
str
|
Time variable (required for |
None
|
model
|
('ti', 'tvd', 'bc95', 'tfe', 'tre')
|
|
'ti'
|
dist
|
('half-normal', 'truncated-normal')
|
For |
'half-normal'
|
cost
|
bool
|
|
False
|
emean
|
list of str
|
Required for |
None
|
vce
|
('oim', 'opg', 'robust', 'cluster')
|
Variance-covariance estimator. |
'oim'
|
cluster
|
str
|
Column name for cluster-robust SEs. Defaults to |
None
|
bias_correct
|
bool
|
TFE-only. If True, applies Dhaene-Jochmans (2015) split-panel
jackknife to reduce the O(1/T) incidental-parameters bias on
|
False
|
n_quad
|
int
|
TRE-only. Number of Gauss-Hermite nodes used to integrate out
|
24
|
maxiter
|
see :func:`frontier`.
|
|
500
|
tol
|
see :func:`frontier`.
|
|
500
|
alpha
|
see :func:`frontier`.
|
|
500
|
Returns:
| Type | Description |
|---|---|
class:`~statspai.frontier.FrontierResult`
|
|
Examples:
>>> import statspai as sp
>>> import numpy as np, pandas as pd
>>> rng = np.random.default_rng(1)
>>> rows = []
>>> for fid in range(30):
... u_i = rng.exponential(0.3) # time-invariant inefficiency
... for t in range(6):
... log_k = rng.normal(0, 1)
... log_l = rng.normal(0, 1)
... v = rng.normal(0, 0.2)
... log_y = 1.0 + 0.4 * log_k + 0.5 * log_l + v - u_i
... rows.append(dict(firm=fid, year=t, log_y=log_y,
... log_k=log_k, log_l=log_l))
>>> df = pd.DataFrame(rows)
>>> res = sp.xtfrontier(
... df, y="log_y", x=["log_k", "log_l"],
... id="firm", time="year", model="ti")
>>> type(res).__name__
'FrontierResult'
>>> eff = res.efficiency()
>>> bool((eff > 0).all() and (eff <= 1.0 + 1e-8).all())
True
Notes
σ_u and σ_v conventions, and Stata scale diagnostics.
sigma_u and sigma_v in model_info are the underlying
normal standard deviations of the half-normal inefficiency and the
symmetric noise term, respectively. This matches R's
frontier::sfa(... , truncNorm=FALSE, timeEffect=FALSE) to
rel < 1e-4 on the production-frontier DGP in
tests/r_parity/29_panel_sfa.
Stata's xtfrontier ..., ti reports e(sigma_u) and the
intercept on its xtfrontier scale, while e(sigma_v) and the
slope coefficients align closely on the parity DGP. For
cross-language checks, compare the frontier slopes and treat the
intercept / variance-component rows as backend-scale diagnostics.
te_summary ¶
Return a small descriptive DataFrame of TE scores (summary stats only).
Examples:
>>> import statspai as sp
>>> import numpy as np, pandas as pd
>>> rng = np.random.default_rng(0)
>>> n = 80
>>> log_k = rng.normal(0, 1, n)
>>> log_l = rng.normal(0, 1, n)
>>> u = rng.exponential(0.3, n) # inefficiency
>>> log_y = 0.4 * log_k + 0.5 * log_l + rng.normal(0, 0.1, n) - u
>>> df = pd.DataFrame({"log_y": log_y, "log_k": log_k, "log_l": log_l})
>>> res = sp.frontier(df, y="log_y", x=["log_k", "log_l"])
>>> s = sp.te_summary(res)
>>> list(s.columns)[:3]
['n', 'mean', 'std']
>>> s.index.tolist()
['efficiency']
te_rank ¶
te_rank(result: Any, method: Optional[str] = None, with_ci: bool = False, alpha: float = 0.05, B: int = 500, seed: Optional[int] = 0) -> DataFrame
Return efficiency scores sorted descending, with rank column.
If with_ci=True, calls :meth:FrontierResult.efficiency_ci for
parametric-bootstrap bounds. For very large samples prefer a small B.
Examples:
>>> import statspai as sp
>>> import numpy as np, pandas as pd
>>> rng = np.random.default_rng(0)
>>> n = 80
>>> log_k = rng.normal(0, 1, n)
>>> log_l = rng.normal(0, 1, n)
>>> u = rng.exponential(0.3, n) # inefficiency
>>> log_y = 0.4 * log_k + 0.5 * log_l + rng.normal(0, 0.1, n) - u
>>> df = pd.DataFrame({"log_y": log_y, "log_k": log_k, "log_l": log_l})
>>> res = sp.frontier(df, y="log_y", x=["log_k", "log_l"])
>>> ranked = sp.te_rank(res)
>>> list(ranked.columns)
['efficiency', 'rank']
>>> int(ranked["rank"].min())
1
zisf ¶
zisf(data: DataFrame, y: str, x: List[str], *, zprob: Optional[List[str]] = None, dist: str = 'half-normal', cost: bool = False, vce: str = 'oim', cluster: Optional[str] = None, maxiter: int = 500, tol: float = 1e-08, alpha: float = 0.05) -> FrontierResult
Zero-Inefficiency Stochastic Frontier (Kumbhakar-Parmeter-Tsionas 2013).
The population is a mixture of two regimes:
- Fully efficient (share
p_i):y_it = x_it' beta + v_it. - Inefficient (share
1 - p_i): standard composed-error frontiery = x'beta + v + sign * u.
The mixing probability p_i is parameterised via a logit link:
p_i = expit(z_i' theta) where z_i = [1, zprob_vars_i].
If zprob=None the probability is constant across observations.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
zprob
|
list of str
|
Covariates for the mixing probability; a constant is added. |
None
|
dist
|
'half-normal'
|
Distribution of |
'half-normal'
|
Returns:
| Type | Description |
|---|---|
class:`~statspai.frontier.FrontierResult`
|
|
Examples:
>>> import statspai as sp
>>> import numpy as np
>>> import pandas as pd
>>> rng = np.random.default_rng(0)
>>> n = 150
>>> log_k = rng.normal(size=n)
>>> log_l = rng.normal(size=n)
>>> v = rng.normal(scale=0.2, size=n)
>>> eff = rng.uniform(size=n) < 0.5 # half the firms are fully efficient
>>> u = np.where(eff, 0.0, np.abs(rng.normal(scale=0.3, size=n)))
>>> log_y = 1.0 + 0.6 * log_k + 0.3 * log_l + v - u
>>> df = pd.DataFrame({"log_y": log_y, "log_k": log_k, "log_l": log_l})
>>> res = sp.zisf(df, y="log_y", x=["log_k", "log_l"])
>>> bool(0.0 <= res.model_info["mean_p_efficient"] <= 1.0)
True
lcsf ¶
lcsf(data: DataFrame, y: str, x: List[str], *, z_class: Optional[List[str]] = None, dist: str = 'half-normal', cost: bool = False, vce: str = 'oim', cluster: Optional[str] = None, maxiter: int = 500, tol: float = 1e-08, alpha: float = 0.05) -> FrontierResult
Two-class Latent-Class SFA (Orea-Kumbhakar 2004; Greene 2005).
Each observation belongs latently to class 1 or class 2, each with
its own frontier coefficients beta_k and variance parameters
sigma_v_k, sigma_u_k. Class probability optionally depends
on z_class via logit.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
z_class
|
list of str
|
Covariates shifting the class-1 logit probability. |
None
|
Returns:
| Type | Description |
|---|---|
FrontierResult with extended ``params`` block and per-obs posterior
|
|
class probabilities in ``diagnostics['p_class1_posterior']``.
|
|
Examples:
>>> import statspai as sp
>>> import numpy as np
>>> import pandas as pd
>>> rng = np.random.default_rng(0)
>>> n = 150
>>> log_k = rng.normal(size=n)
>>> log_l = rng.normal(size=n)
>>> v = rng.normal(scale=0.2, size=n)
>>> cls = rng.uniform(size=n) < 0.5 # two technology classes
>>> u = np.where(cls, np.abs(rng.normal(scale=0.15, size=n)),
... np.abs(rng.normal(scale=0.6, size=n)))
>>> log_y = 1.0 + 0.6 * log_k + 0.3 * log_l + v - u
>>> df = pd.DataFrame({"log_y": log_y, "log_k": log_k, "log_l": log_l})
>>> res = sp.lcsf(df, y="log_y", x=["log_k", "log_l"])
>>> res.model_info["n_classes"]
2
translog_design ¶
translog_design(data: DataFrame, inputs: List[str], *, include_interactions: bool = True, include_squares: bool = True, interaction_prefix: str = '') -> DataFrame
Build a translog design matrix from Cobb-Douglas inputs.
Translog is log y = alpha + sum_k beta_k * log x_k
+ 0.5 sum_k sum_l gamma_{kl} * log x_k * log x_l.
This helper takes input columns (already in log form) and returns a
DataFrame with the original columns plus squares x_k^2 / 2 and
cross-products x_k * x_l that can be fed straight to
:func:frontier / :func:xtfrontier as additional regressors.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
data
|
DataFrame
|
|
required |
inputs
|
list of str
|
Columns containing |
required |
include_interactions
|
bool
|
If True, adds |
True
|
include_squares
|
bool
|
If True, adds |
True
|
interaction_prefix
|
str
|
Optional prefix for the generated columns (e.g., |
""
|
Returns:
| Type | Description |
|---|---|
DataFrame
|
Original data + appended translog terms. Two lists are stored
on
|
Examples:
>>> import statspai as sp
>>> import numpy as np, pandas as pd
>>> rng = np.random.default_rng(0)
>>> n = 60
>>> df = pd.DataFrame({
... "log_k": rng.normal(2.0, 0.5, n),
... "log_l": rng.normal(1.5, 0.5, n),
... })
>>> df["log_y"] = 0.3 * df["log_k"] + 0.6 * df["log_l"] + rng.normal(0, 0.2, n)
>>> df_tl = sp.translog_design(df, inputs=["log_k", "log_l"])
>>> sorted(df_tl.attrs["translog_added_terms"])
['log_k_sq', 'log_k_x_log_l', 'log_l_sq']
>>> # Option A — one-liner, pass the full translog regressor list:
>>> terms = df_tl.attrs["translog_terms"]
>>> res = sp.frontier(df_tl, y="log_y", x=terms)
>>> # Option B — extend an existing x list without double-counting:
>>> base = ["log_k", "log_l"]
>>> res2 = sp.frontier(df_tl, y="log_y",
... x=base + df_tl.attrs["translog_added_terms"])