Model-fitting utilities for feature-wise OLS and MixedLM summaries on AnnData objects.
This module provides two levels of API:
- low-level
*_wide(...)functions that fit models from a combined observation-plus-featureDataFrame; - higher-level
*_adata(...)wrappers that build that table fromAnnData, optionally filter observations first, and handle saving oradata.unsstorage.
The current primary APIs are:
fit_smf_ols_models_and_summarize_widefit_smf_ols_models_and_summarize_adatafit_smf_mixedlm_models_and_summarize_widefit_smf_mixedlm_models_and_summarize_adata
The old_fit_smf_ols_models_and_summarize_adata and old_fit_smf_mixedlm_models_and_summarize_adata helpers are legacy compatibility wrappers. They do not expose the newer filter or model-spec sidecar features and should not be treated as the preferred interface.
Use fit_smf_ols_models_and_summarize_adata(...) to fit one OLS model per feature.
def fit_smf_ols_models_and_summarize_adata(
adata,
layer=None,
use_raw=False,
feature_columns=None,
predictors=None,
model_name='OLS_predictors',
add_adata_var_column_key_list=None,
save_table=False,
save_model_spec_yaml: bool = False,
save_path=None,
save_result_to_adata_uns_as_dict=False,
include_fdr=True,
# --- new filter args ---
dataset_cfg=None,
filter_obs_boolean_column=None,
filter_obs_column_key=None,
filter_obs_column_values_list=None,
filter_obs_copy=True,
# when filtered internally, optionally also write results into the original adata.uns
save_results_to_original_adata_uns: bool = False,
# whether to return the filtered adata (work_adata) in addition to results
return_filtered_adata: bool = False,
):import adata_science_tools as adtl
ols_results = adtl.fit_smf_ols_models_and_summarize_adata(
adata,
layer="pgml",
predictors=["NHS_Case", "Age", "Gender"],
model_name="ols_unit",
save_table=True,
save_model_spec_yaml=True,
save_path="results/ols_results.csv",
save_result_to_adata_uns_as_dict=True,
include_fdr=False,
)Important behavior:
predictorsis normalized through a YAML-friendly list validator.- Numeric-like predictor columns are coerced to numeric dtype before fitting so continuous covariates do not become dummy-coded categories.
- Per-feature OLS p-value columns use the
P>|t|naming pattern. - If
include_fdr=True, per-term FDR columns are added with the_FDRsuffix.
Use fit_smf_mixedlm_models_and_summarize_adata(...) to fit one mixed-effects model per feature.
def fit_smf_mixedlm_models_and_summarize_adata(
adata,
layer=None,
use_raw=False,
feature_columns=None,
predictors=None,
group=None,
model_name='mixedlm_predictors',
reml=True,
add_adata_var_column_key_list=None,
save_table=False,
save_model_spec_yaml: bool = False,
save_path=None,
save_result_to_adata_uns_as_dict=False,
include_fdr=True,
# --- new filter args ---
dataset_cfg=None,
filter_obs_boolean_column=None,
filter_obs_column_key=None,
filter_obs_column_values_list=None,
filter_obs_copy=True,
# when filtered internally, optionally also write results into the original adata.uns
save_results_to_original_adata_uns: bool = False,
# whether to return the filtered adata (work_adata) in addition to results
return_filtered_adata: bool = False,
):mixedlm_results = adtl.fit_smf_mixedlm_models_and_summarize_adata(
adata,
layer="pgml",
predictors=["NHS_Case", "Age", "Gender"],
group="Batch",
model_name="mixedlm_unit",
reml=False,
save_table=True,
save_model_spec_yaml=True,
save_path="results/mixedlm_results.csv",
save_result_to_adata_uns_as_dict=True,
include_fdr=False,
)Important behavior:
groupis required.- MixedLM also expects a non-empty
predictorslist. - Per-term MixedLM p-value columns use the
P>|z|naming pattern. - The summary includes grouping-specific fields such as
n_groups,Method, random-effect variances, and, when available, per-group random effects.
Both *_adata(...) wrappers can create a filtered working AnnData through:
dataset_cfgfilter_obs_boolean_columnfilter_obs_column_keyfilter_obs_column_values_list
Example:
filtered_results = adtl.fit_smf_ols_models_and_summarize_adata(
adata,
dataset_cfg={
"filter_obs_boolean_column": "use_for_expectation",
},
layer="pgml",
predictors=["Age"],
model_name="ols_filtered",
return_filtered_adata=True,
)Wrapper behavior:
- if any filter inputs are provided, the wrapper creates
work_adatawithCFG_filter_adata_by_obs(...); - the fit is run against
work_adata, not the originaladata; - when
return_filtered_adata=Trueand a filtered copy was created, the wrapper returns(results, work_adata)instead of onlyresults.
The wide functions operate on a single obs_X_df table that contains:
- one column per feature, and
- one column per predictor, plus
groupfor MixedLM.
def fit_smf_ols_models_and_summarize_wide(
obs_X_df,
feature_columns=None,
predictors=None,
model_name='OLS',
include_fdr=True,
):def fit_smf_mixedlm_models_and_summarize_wide(
obs_X_df,
feature_columns=None,
predictors=None,
group=None,
model_name='mixedlm',
reml=True,
include_fdr=True,
):Use them when the combined table already exists or when you want to bypass AnnData handling:
fit_smf_ols_models_and_summarize_wide(obs_X_df, feature_columns, predictors, ...)fit_smf_mixedlm_models_and_summarize_wide(obs_X_df, feature_columns, predictors, group=..., ...)
The *_adata(...) wrappers build this table internally with make_df_obs_adataX(...), so most users should start with the AnnData APIs unless they already have the combined matrix.
Both OLS and MixedLM result tables are indexed by feature name and include var_names as the first column.
Column names are prefixed with model_name, so changing model_name changes the entire result schema namespace.
Representative OLS column patterns include:
<model_name>_Formula<model_name>_Converged<model_name>_Warnings<model_name>_nobs<model_name>_R-squared<model_name>_Coef_<term><model_name>_StdErr_<term><model_name>_tStat_<term><model_name>_P>|t|_<term><model_name>_P>|t|_<term>_FDR
Representative MixedLM column patterns include:
<model_name>_Formula<model_name>_Converged<model_name>_Warnings<model_name>_nobs<model_name>_n_groups<model_name>_Method<model_name>_Var_RE_<term><model_name>_Var_Residual<model_name>_Coef_<term><model_name>_StdErr_<term><model_name>_tStat_<term><model_name>_P>|z|_<term><model_name>_P>|z|_<term>_FDR
The exact set of columns depends on:
- the fitted design terms,
- the chosen
model_name, - whether warnings or random effects are present,
- whether
include_fdr=True.
The current *_adata(...) wrappers can write a sibling .model_spec.yaml file when:
save_table=Truesave_model_spec_yaml=Truesave_pathis provided
If save_model_spec_yaml=True is requested without save_table=True and save_path, the wrappers raise a ValueError.
Example:
ols_results = adtl.fit_smf_ols_models_and_summarize_adata(
adata,
layer="pgml",
predictors=["NHS_Case", "Age", "Gender"],
model_name="ols_unit",
save_table=True,
save_model_spec_yaml=True,
save_path="results/ols_results.csv",
)This writes:
results/ols_results.csvresults/ols_results.model_spec.yaml
The YAML captures the fitting contract for downstream consumers, including:
fit_methodmodel_namepredictorslayeruse_rawformula_rhscoefficient_termscoefficient_columns
For MixedLM sidecars, the YAML also includes:
groupreml
This is the metadata used by the expectation-correction workflow documented in _expectation_based_covar_correction.md, which can consume OLS summary outputs and derived coefficient tables.
When save_result_to_adata_uns_as_dict=True, the wrappers store results under model-type-specific namespaces.
OLS storage:
work_adata.uns["ols_model_results"][f"OLS_model_results_{model_name}"]work_adata.uns["ols_model_specs"][f"OLS_model_results_{model_name}"]whensave_model_spec_yaml=True
MixedLM storage:
work_adata.uns["mixedlm_model_results"][f"mixedlm_model_results_{model_name}"]work_adata.uns["mixedlm_model_specs"][f"mixedlm_model_results_{model_name}"]whensave_model_spec_yaml=True
If filtering created a new work_adata and save_results_to_original_adata_uns=True, the same results and model specs are also written into the original adata.uns.
The current implementation locks in these behaviors.
- Wrapper inputs such as
predictorsandadd_adata_var_column_key_listare normalized with_ensure_list(...). - YAML-style lists are expected.
- Passing a single string instead of a list raises a
TypeError. - Numeric-like predictors are coerced to numeric dtype before fitting.
- OLS treats missing and infinite values as
NaNand fits only complete-case rows for each feature. - If a feature has no complete-case rows after that cleanup, the OLS wide function does not raise.
- Instead, it records a summary row with
Converged=Falseand a warning message explaining why the fit was skipped.
- MixedLM raises early when required fit conditions are not met.
- Missing feature, predictor, or group columns raise a
ValueError. - Zero complete-case rows for a feature raise a
ValueError. - Fewer than two non-empty groups after filtering also raises a
ValueError. - MixedLM tries to collect random effects, but if covariance inversion fails it records that failure in the warnings field and continues building the summary row.
Both *_adata(...) wrappers can merge selected adata.var columns into the result table through add_adata_var_column_key_list.
Example:
ols_results = adtl.fit_smf_ols_models_and_summarize_adata(
adata,
layer="pgml",
predictors=["Age"],
model_name="ols_with_var",
add_adata_var_column_key_list=["gene_name", "feature_class"],
)Missing adata.var keys are skipped with a warning or print message, depending on the code path.
- Use the
*_adata(...)wrappers for most workflows. - Use the
*_wide(...)functions when you already have anobs_X_dftable. - Use
save_model_spec_yaml=Trueonly when you also want a saved CSV artifact. - Use OLS outputs as the upstream summary source for expectation-model workflows described in
_expectation_based_covar_correction.md.