Skip to content

Recipes

Short, complete studies for common jobs. They are run by the amos unit tests. See the tutorial for the basics, the advanced user guide for every setting, and the technique catalog for every technique.

Classify more than two classes

The wine data has three classes. Metrics such as f1 are averaged over the classes, and the confusion matrix shows which classes are confused:

import numpy as np
import pandas as pd
import sklearn.datasets

import amos

wine = sklearn.datasets.load_wine(as_frame = True)
settings = {
    "general": {"seed": 43},
    "wine_project": {"wine_workers": "analyst, critic"},
    "analyst": {
        "design": "experiment",
        "criterion": "f1",
        "steps": "split, scale, model",
        "split_techniques": "stratified",
        "scale_techniques": "standard",
        "model_techniques": "logit, knn, naive_bayes",
    },
    "critic": {"techniques": "classification_report, confusion"},
}
project = amos.Project.create(settings, item = wine)
print(project.result.classes)
# [0, 1, 2]
print(project.result.tables["confusion"].shape)
# (3, 3)
print(project.result.probabilities.shape)
# (45, 3)

Regression with statistical inference

A paper usually reports the coefficients of a regression, with their standard errors and p-values. The ols model wraps statsmodels and stores them in a table. Here, an experiment also checks whether a random forest predicts the held-out rows better than the regression:

diabetes = sklearn.datasets.load_diabetes(as_frame = True)
settings = {
    "general": {"seed": 43},
    "diabetes_project": {"diabetes_workers": "analyst, critic"},
    "analyst": {
        "design": "experiment",
        "criterion": "rmse",
        "steps": "split, model",
        "split_techniques": "train_test",
        "model_techniques": "ols, random_forest, baseline",
    },
    "critic": {"techniques": "scorecard"},
}
project = amos.Project.create(settings, item = diabetes)
comparison = project.result.tables["analyst_comparison"]
print(comparison.index[-1])
# train_test_split > baseline_model
print((comparison["rmse"] > 0).all())
# True

rmse is better when it is lower, so it is negated to rank the combinations, but the table shows its real value. To report the regression itself, fit it to every row (without a split):

dataset = amos.Dataset.create(diabetes)
amos.models.OLS().apply(dataset)
coefficients = dataset.tables["ols_coefficients"]
significant = coefficients[coefficients["p_value"] < 0.05].index
print(list(significant))
# ['const', 'sex', 'bmi', 'bp', 's5']

Imbalanced classes

When one class is rare, accuracy is misleading (always predicting the common class is accurate). Compare ways of balancing the training rows with a metric that weighs both classes, and let the critic's scorecard compare them on every metric:

features, label = sklearn.datasets.make_classification(
    n_samples = 1000, n_features = 8, weights = [0.95], random_state = 0)
data = pd.DataFrame(features, columns = [f"x{i}" for i in range(8)])
data["rare"] = label
settings = {
    "general": {"label": "rare", "seed": 43},
    "rare_project": {"rare_workers": "explorer, analyst, critic"},
    "explorer": {"techniques": "label_balance"},
    "analyst": {
        "design": "experiment",
        "criterion": "balanced_accuracy",
        "steps": "split, sample, model",
        "split_techniques": "stratified",
        "sample_techniques": "none, smote, random_under",
        "model_techniques": "logit",
    },
    "critic": {"techniques": "scorecard"},
}
project = amos.Project.create(settings, item = data)
print(project.result.tables["label_balance"]["count"].tolist())
# [944, 56]
scorecard = project.scorecard
print(sorted(scorecard.table["sample"]))
# ['none', 'random_under', 'smote']
print(list(scorecard.table.columns[4:7]))
# ['balanced_accuracy', 'accuracy', 'precision']

Only the training rows are resampled. The test rows keep their real balance, so the scores show how each model would do on new data.

Categorical data

Text and categories must become numbers before a model can use them. This recipe makes a small set of court cases, cleans it, and compares three encoders. The wrangler trims the text, removes the date, and makes columns with few values categorical:

rng = np.random.default_rng(0)
cases = pd.DataFrame({
    "court": rng.choice([" district ", "Appeals", "district", "supreme"], 400),
    "claim": rng.choice(["contract", "tort", "property", "civil rights"], 400),
    "judges": rng.integers(1, 4, 400),
    "filed": pd.date_range("2015-01-01", periods = 400, freq = "W"),
    "damages": rng.lognormal(10, 1, 400),
})
cases["won"] = (
    (cases["claim"] == "tort") ^ (rng.random(400) < 0.3)).map(
        {True: "plaintiff", False: "defendant"})
settings = {
    "general": {"label": "won", "seed": 43},
    "cases_project": {"cases_workers": "wrangler, analyst"},
    "wrangler": {"techniques": "strip_text, drop_columns, auto_categorize"},
    "strip_text_parameters": {"lowercase": True},
    "drop_columns_parameters": {"columns": ["filed"]},
    "auto_categorize_parameters": {"columns": ["court", "claim"]},
    "analyst": {
        "design": "experiment",
        "criterion": "roc_auc",
        "steps": "split, encode, scale, model",
        "split_techniques": "stratified",
        "encode_techniques": "one_hot, target, count",
        "scale_techniques": "standard",
        "model_techniques": "logit",
    },
}
project = amos.Project.create(settings, item = cases)
result = project.result
print(result.categoricals, len(result.tables["analyst_comparison"]))
# [] 3

Any technique can also be applied by hand, to check what it does:

cleaned = amos.mungers.StripText().apply(cases.copy(), lowercase = True)
print(sorted(cleaned.data["court"].unique()))
# ['appeals', 'district', 'supreme']

The label is text, and the positive class is the last in sorted order ("plaintiff"), so roc_auc measures how well each model finds the cases that plaintiffs won.

Does feature selection matter?

A robustness check: compare keeping every feature with two ways of reducing them, for two models. With none in a step, the table shows whether the step helps at all:

cancer = sklearn.datasets.load_breast_cancer(as_frame = True)
settings = {
    "general": {"seed": 43},
    "cancer_project": {
        "design": "experiment",
        "criterion": "roc_auc",
        "steps": "split, scale, reduce, model",
        "split_techniques": "stratified",
        "scale_techniques": "standard",
        "reduce_techniques": "none, k_best, pca_reduce",
        "model_techniques": "logit, random_forest",
    },
    "k_best_parameters": {"k": 5},
    "random_forest_parameters": {"n_estimators": 50},
}
project = amos.Project.create(settings, item = cancer)
comparison = project.result.tables["cancer_comparison"]
print(len(comparison))
# 6
print(all(comparison["roc_auc"] > 0.9))
# True

The project section is itself a worker, so a small study can put its steps there instead of in an "analyst" section.

Explain a model

SHAP values show how much each feature moves each prediction. shap_importance stores the mean absolute value for each feature, and the artist draws the values with shap's own plots: shap_beeswarm shows each row's SHAP values for the most important features, and shap_waterfall shows how the features move one row's prediction:

settings = {
    "general": {"seed": 43},
    "cancer_project": {
        "techniques": "stratified, gradient_boosting, shap_importance, shap_beeswarm, shap_waterfall",
    },
    "shap_importance_parameters": {"rows": 50},
}
project = amos.Project.create(settings, item = cancer)
importance = project.result.tables["shap_importance"]
print(importance.index.name, len(importance))
# feature 30
print(sorted(project.result.figures))
# ['shap_beeswarm', 'shap_waterfall']

The other SHAP plots draw the same values in other ways: shap_bar, shap_violin, and shap_heatmap for every feature; shap_decision and shap_embedding for every row; shap_force for one row (like the waterfall); shap_scatter and shap_partial_dependence for one feature; and shap_group_difference for the difference between two groups. They draw the SHAP values that shap_importance found, or find them if it was not applied. Set "row" (an index label) in shap_waterfall_parameters to explain a particular row, and "category" to explain a class other than the last. importance_plot draws any table of importances as plain bars. permutation_importance works with any model, and feature_importance reports a model's own importances (or the size of its coefficients).

Check fairness across groups

Name the columns that identify groups in the "groups" setting of the "general" section. They are kept out of the features, and the scorecard adds fairness metrics for every branch when the label has two classes. The fairness table compares the final model across the groups:

data = cancer.frame.copy()
data["clinic"] = np.where(np.arange(len(data)) % 3 == 0, "north", "south")
settings = {
    "general": {"label": "target", "seed": 43, "groups": "clinic"},
    "cancer_project": {"cancer_workers": "analyst, critic"},
    "analyst": {
        "design": "experiment",
        "criterion": "roc_auc",
        "steps": "split, scale, model",
        "split_techniques": "stratified",
        "scale_techniques": "standard",
        "model_techniques": "logit, random_forest",
    },
    "critic": {"techniques": "scorecard, fairness"},
}
project = amos.Project.create(settings, item = data)
print(list(project.scorecard.table.columns[-2:]))
# ['demographic_parity', 'equalized_odds']
print(list(project.result.tables["fairness"].index))
# ['north', 'south', 'difference', 'ratio']

An artist with fairness_plot draws the table, and shap_group_difference shows which features explain the difference between the groups.

Fixed effects and clustered standard errors

Panel data, such as cases decided by many judges, often calls for fixed effects and standard errors clustered by group. fixest wraps pyfixest:

rng = np.random.default_rng(7)
judges = rng.integers(0, 20, 1000)
severity = rng.normal(0, 1, 1000)
represented = rng.integers(0, 2, 1000)
sentence = (
    12 + 3 * severity - 2 * represented + judges / 4 + rng.normal(0, 2, 1000))
cases = pd.DataFrame({
    "severity": severity,
    "represented": represented,
    "judge": [f"judge {j}" for j in judges],
    "sentence": sentence,
})
settings = {
    "general": {"label": "sentence", "seed": 43, "groups": "judge"},
    "sentencing_project": {"techniques": "train_test, fixest"},
    "fixest_parameters": {"fixed_effects": "judge", "cluster": "judge"},
}
project = amos.Project.create(settings, item = cases)
coefficients = project.result.tables["fixest_coefficients"]
print(coefficients["coefficient"].round().to_dict())
# {'severity': 3.0, 'represented': -2.0}

The table also has the standard errors, p-values, and confidence intervals. For a label with two classes, fixest fits a logit model.