In [1]:
from __future__ import annotations
from survey_kit import logger
from survey_kit.statistics.adapters import (
statsmodels_adapter,
linearmodels_adapter,
pyfixest_adapter,
polars_ds_adapter,
mi_ses_from_statsmodels,
mi_ses_from_linearmodels,
mi_ses_from_pyfixest,
mi_ses_from_polars_ds,
)
from survey_kit.utilities.dataframe import summary
from survey_kit.statistics.replicates import Replicates
from sample_data import make_implicates, with_bootstrap_weights
In [2]:
logger.info("survey_kit.statistics.adapters has four pure-Python regression")
logger.info("adapters, one per package - no R/Stata/rpy2/pystata needed. Every")
logger.info("adapter (these four, plus the R/Stata ones) returns the same")
logger.info("normalized shape:")
logger.info(" (df_estimates, df_ses, df_vcov, df_tidy)")
logger.info("and each has a matching mi_ses_from_<package>(...) - same shape as")
logger.info("mi_ses_from_r_fixest/mi_ses_from_stata - that runs it across")
logger.info("implicates, combined via Rubin's rules, taking the adapter's own")
logger.info("arguments directly instead of an arguments={} dict.")
survey_kit.statistics.adapters has four pure-Python regression
adapters, one per package - no R/Stata/rpy2/pystata needed. Every
adapter (these four, plus the R/Stata ones) returns the same
normalized shape:
(df_estimates, df_ses, df_vcov, df_tidy)
and each has a matching mi_ses_from_<package>(...) - same shape as
mi_ses_from_r_fixest/mi_ses_from_stata - that runs it across
implicates, combined via Rubin's rules, taking the adapter's own
arguments directly instead of an arguments={} dict.
In [3]:
df_implicates = make_implicates()
df = df_implicates[0]
logger.info("\n\nSample data (y = 1 + 2*x1 - 1.5*x2 + noise) - see sample_data.py:")
summary(df)
Sample data (y = 1 + 2*x1 - 1.5*x2 + noise) - see sample_data.py:
┌──────────┬─────┬─────────────┬───────────┬──────────┬───────────┬──────────┐ │ Variable ┆ n ┆ n (missing) ┆ mean ┆ std ┆ min ┆ max │ ╞══════════╪═════╪═════════════╪═══════════╪══════════╪═══════════╪══════════╡ │ x1 ┆ 300 ┆ 0 ┆ -0.035817 ┆ 1.01952 ┆ -3.106337 ┆ 3.066037 │ │ x2 ┆ 300 ┆ 0 ┆ -0.009609 ┆ 0.976813 ┆ -3.899422 ┆ 2.118803 │ │ y ┆ 300 ┆ 0 ┆ 0.931985 ┆ 2.489877 ┆ -5.57149 ┆ 7.779213 │ └──────────┴─────┴─────────────┴───────────┴──────────┴───────────┴──────────┘
Out[3]:
naive plan: (run LazyFrame.explain(optimized=True) to see the optimized plan)
SELECT [col("Variable"), col("n"), col("n (missing)"), col("mean"), col("std"), col("min"), col("max")] UNION PLAN 0: SELECT [col("Variable"), col("n"), col("n (missing)"), col("mean"), col("std"), col("min"), col("max")] WITH_COLUMNS: ["x1".alias("Variable")] SELECT [col("n (missing)"), col("max"), col("std"), col("n"), col("mean"), col("min")] SELECT [col("___index___"), col("x1_rawn_missing").alias("n (missing)"), col("x1_max").alias("max"), col("x1_std").alias("std"), col("x1_rawn").alias("n"), col("x1_mean").alias("mean"), col("x1_min").alias("min")] SELECT [col("___index___"), col("x1_rawn_missing"), col("x1_max"), col("x1_std"), col("x1_rawn"), col("x1_mean"), col("x1_min")] DF ["___index___", "x1_rawn", "x1_mean", "x1_std", ...]; PROJECT */19 COLUMNS PLAN 1: SELECT [col("Variable"), col("n"), col("n (missing)"), col("mean"), col("std"), col("min"), col("max")] WITH_COLUMNS: ["x2".alias("Variable")] SELECT [col("n (missing)"), col("max"), col("std"), col("n"), col("mean"), col("min")] SELECT [col("___index___"), col("x2_rawn_missing").alias("n (missing)"), col("x2_max").alias("max"), col("x2_std").alias("std"), col("x2_rawn").alias("n"), col("x2_mean").alias("mean"), col("x2_min").alias("min")] SELECT [col("___index___"), col("x2_rawn_missing"), col("x2_max"), col("x2_std"), col("x2_rawn"), col("x2_mean"), col("x2_min")] DF ["___index___", "x1_rawn", "x1_mean", "x1_std", ...]; PROJECT */19 COLUMNS PLAN 2: SELECT [col("Variable"), col("n"), col("n (missing)"), col("mean"), col("std"), col("min"), col("max")] WITH_COLUMNS: ["y".alias("Variable")] SELECT [col("n (missing)"), col("max"), col("std"), col("n"), col("mean"), col("min")] SELECT [col("___index___"), col("y_rawn_missing").alias("n (missing)"), col("y_max").alias("max"), col("y_std").alias("std"), col("y_rawn").alias("n"), col("y_mean").alias("mean"), col("y_min").alias("min")] SELECT [col("___index___"), col("y_rawn_missing"), col("y_max"), col("y_std"), col("y_rawn"), col("y_mean"), col("y_min")] DF ["___index___", "x1_rawn", "x1_mean", "x1_std", ...]; PROJECT */19 COLUMNS END UNION
In [4]:
logger.info("\n\nstatsmodels - y/x column lists rather than a formula, HC3")
logger.info("(heteroskedasticity-robust) SEs by default:")
(df_estimates, df_ses, df_vcov, df_tidy) = statsmodels_adapter(df, y="y", x=["x1", "x2"])
logger.info("\n Single df")
logger.info(df_estimates)
mi_sm = mi_ses_from_statsmodels(df_implicates=df_implicates, y="y", x=["x1", "x2"])
logger.info("\n Multiple imputation")
mi_sm.print()
statsmodels - y/x column lists rather than a formula, HC3
(heteroskedasticity-robust) SEs by default:
Single df
shape: (3, 2) ┌──────────┬───────────┐ │ Variable ┆ estimate │ │ --- ┆ --- │ │ str ┆ f64 │ ╞══════════╪═══════════╡ │ const ┆ 0.990009 │ │ x1 ┆ 2.016691 │ │ x2 ┆ -1.478573 │ └──────────┴───────────┘
Implicate #1
Implicate #2
Implicate #3
Implicate #4
Implicate #5
Multiple imputation
┌──────────┬───────────┐ │ Variable ┆ estimate │ ╞══════════╪═══════════╡ │ const ┆ 0.983256 │ │ ┆ 0.028769 │ │ x1 ┆ 1.996435 │ │ ┆ 0.047977 │ │ x2 ┆ -1.488951 │ │ ┆ 0.030939 │ └──────────┴───────────┘
In [5]:
logger.info("\n\nlinearmodels - formula syntax, IV/panel-capable (plain OLS via")
logger.info("IV2SLS with no instruments, as here):")
(df_estimates, df_ses, df_vcov, df_tidy) = linearmodels_adapter(
df, formula="y ~ 1 + x1 + x2"
)
logger.info("\n Single df")
logger.info(df_estimates)
mi_lm = mi_ses_from_linearmodels(df_implicates=df_implicates, formula="y ~ 1 + x1 + x2")
logger.info("\n Multiple imputation")
mi_lm.print()
linearmodels - formula syntax, IV/panel-capable (plain OLS via
IV2SLS with no instruments, as here):
Single df
shape: (3, 2) ┌───────────┬───────────┐ │ Variable ┆ estimate │ │ --- ┆ --- │ │ str ┆ f64 │ ╞═══════════╪═══════════╡ │ Intercept ┆ 0.990009 │ │ x1 ┆ 2.016691 │ │ x2 ┆ -1.478573 │ └───────────┴───────────┘
Implicate #1
Implicate #2
Implicate #3
Implicate #4
Implicate #5
Multiple imputation
┌───────────┬───────────┐ │ Variable ┆ estimate │ ╞═══════════╪═══════════╡ │ Intercept ┆ 0.983256 │ │ ┆ 0.028573 │ │ x1 ┆ 1.996435 │ │ ┆ 0.047803 │ │ x2 ┆ -1.488951 │ │ ┆ 0.03055 │ └───────────┴───────────┘
In [6]:
logger.info("\n\npyfixest - fixest-syntax formula, fixed effects supported")
logger.info("directly (e.g. 'y ~ x1 + x2 | firm') - generally the best default")
logger.info("of the four unless you specifically need something it doesn't")
logger.info("cover (see its docstring):")
(df_estimates, df_ses, df_vcov, df_tidy) = pyfixest_adapter(df, formula="y ~ x1 + x2")
logger.info("\n Single df")
logger.info(df_estimates)
mi_pf = mi_ses_from_pyfixest.feols(df_implicates=df_implicates, fml="y ~ x1 + x2")
logger.info("\n Multiple imputation")
mi_pf.print()
pyfixest - fixest-syntax formula, fixed effects supported
directly (e.g. 'y ~ x1 + x2 | firm') - generally the best default
of the four unless you specifically need something it doesn't
cover (see its docstring):
Single df
shape: (3, 2) ┌───────────┬───────────┐ │ Variable ┆ estimate │ │ --- ┆ --- │ │ str ┆ f64 │ ╞═══════════╪═══════════╡ │ Intercept ┆ 0.990009 │ │ x1 ┆ 2.016691 │ │ x2 ┆ -1.478573 │ └───────────┴───────────┘
Implicate #1
Implicate #2
Implicate #3
Implicate #4
Implicate #5
Multiple imputation
┌───────────┬───────────┐ │ Variable ┆ estimate │ ╞═══════════╪═══════════╡ │ Intercept ┆ 0.983256 │ │ ┆ 0.028668 │ │ x1 ┆ 1.996435 │ │ ┆ 0.047854 │ │ x2 ┆ -1.488951 │ │ ┆ 0.030649 │ └───────────┴───────────┘
In [7]:
logger.info("\n\nReplicate-weight bootstrapping instead of pyfixest's own vcov: pass")
logger.info("replicates=, and pyfixest_adapter runs once per replicate weight column")
logger.info("(point estimates only, vcov forced to \"iid\" since it's discarded")
logger.info("anyway) - the spread of estimates across replicates IS the SE, computed")
logger.info("by survey_kit's own Replicates/StatCalculator machinery, the same")
logger.info("approach mi_ses_from_stata/mi_ses_from_r_fixest's replicates= use. Every")
logger.info("mi_ses_from_<package> here (except .feglm/.femlm, whose underlying")
logger.info("estimators have no weights= argument to substitute a replicate column")
logger.info("into) supports this the same way.")
N_REPLICATES = 20
mi_pf_boot = mi_ses_from_pyfixest.feols(
df_implicates=with_bootstrap_weights(df_implicates, n_replicates=N_REPLICATES),
fml="y ~ x1 + x2",
replicates=Replicates(weight_stub="replicate_", n_replicates=N_REPLICATES, bootstrap=True),
)
mi_pf_boot.print()
Replicate-weight bootstrapping instead of pyfixest's own vcov: pass
replicates=, and pyfixest_adapter runs once per replicate weight column
(point estimates only, vcov forced to "iid" since it's discarded
anyway) - the spread of estimates across replicates IS the SE, computed
by survey_kit's own Replicates/StatCalculator machinery, the same
approach mi_ses_from_stata/mi_ses_from_r_fixest's replicates= use. Every
mi_ses_from_<package> here (except .feglm/.femlm, whose underlying
estimators have no weights= argument to substitute a replicate column
into) supports this the same way.
Implicate #1
Running _point_estimate
0....5....10....15....20
Implicate #2
Running _point_estimate
0....5....10....15....20
Implicate #3
Running _point_estimate
0
....5....10....15....20
Implicate #4
Running _point_estimate
0....5....10....15....20
Implicate #5
Running _point_estimate
0
....5....10....15....20
┌───────────┬───────────┐ │ Variable ┆ estimate │ ╞═══════════╪═══════════╡ │ Intercept ┆ 0.983256 │ │ ┆ 0.041516 │ │ x1 ┆ 1.996435 │ │ ┆ 0.059038 │ │ x2 ┆ -1.488951 │ │ ┆ 0.049244 │ └───────────┴───────────┘
In [8]:
logger.info("\n\npolars_ds - stays entirely in polars/narwhals, no pandas")
logger.info("conversion at all; doesn't compute a covariance matrix, so df_vcov")
logger.info("is always None here:")
(df_estimates, df_ses, df_vcov, df_tidy) = polars_ds_adapter(df, y="y", x=["x1", "x2"])
logger.info("\n Single df")
logger.info(df_estimates)
mi_pds = mi_ses_from_polars_ds(df_implicates=df_implicates, y="y", x=["x1", "x2"])
logger.info("\n Multiple imputation")
mi_pds.print()
polars_ds - stays entirely in polars/narwhals, no pandas
conversion at all; doesn't compute a covariance matrix, so df_vcov
is always None here:
Single df
shape: (3, 2) ┌──────────┬───────────┐ │ Variable ┆ estimate │ │ --- ┆ --- │ │ str ┆ f64 │ ╞══════════╪═══════════╡ │ x1 ┆ 2.016691 │ │ x2 ┆ -1.478573 │ │ __bias__ ┆ 0.990009 │ └──────────┴───────────┘
Implicate #1
Implicate #2
Implicate #3
Implicate #4
Implicate #5
Multiple imputation
┌──────────┬───────────┐ │ Variable ┆ estimate │ ╞══════════╪═══════════╡ │ x1 ┆ 1.996435 │ │ ┆ 0.047977 │ │ x2 ┆ -1.488951 │ │ ┆ 0.030939 │ │ __bias__ ┆ 0.983256 │ │ ┆ 0.028769 │ └──────────┴───────────┘
In [9]:
logger.info("\n\nFor R or Stata instead, see basic_r.py/basic_stata.py (or")
logger.info("r_arbitrary_estimators.py/stata_arbitrary_estimators.py for the generic")
logger.info("escape hatch into any function either package provides).")
For R or Stata instead, see basic_r.py/basic_stata.py (or
r_arbitrary_estimators.py/stata_arbitrary_estimators.py for the generic
escape hatch into any function either package provides).