Skip to content

Advanced User Guide

The tutorial shows how to build a study. This guide describes how amos works, every setting, and how to add your own techniques. The technique catalog lists every technique. Every example on this page is run by the amos unit tests.

How the pieces fit together

settings ──create──▶ Idea ──draft──▶ workflow ──apply──▶ Dataset ──▶ report, export
(file or dict)     (chrisjen      (graph of workers,   (your data and
                    settings)      steps, techniques)   all it taught)

amos is built on chrisjen, which reads settings and builds a workflow from them. Everything that chrisjen does works in amos, so its advanced guide applies here too. amos adds:

Class Description
Project A chrisjen.Project that makes your data into a Dataset, applies the workflow to a copy of it, and can export the results.
Dataset The item that flows through the workflow: the data, the label, the split, the model, and everything learned.
Operation Base class for every amos technique (a chrisjen.Technique that works on a Dataset).
Loader, Cleaner, Munger, Merger, Shaper, Describer, Splitter, Transformer, Sampler, Model, Validator, Metric, Evaluator, Inferer, Plot The genres of techniques. Transformer has the genres Imputer, Scaler, Encoder, Mixer, and Reducer, Metric has the genre GroupMetric (fairness metrics), and Inferer has a genre for each package of causal inference (Doubleml, Dowhy, Causalml, and Tigramite).
Experiment A design that compares every combination of techniques and keeps a table of how each did.
Findings The default report.

The library

Every technique is a class, stored in the chrisjen library (also available as amos.library) under its snake case name as soon as it is defined. That is how names in settings are found. The genres are nested layers of the library:

import dataclasses

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

import amos

print(sorted(amos.library["vertex"]["operation"]))
# ['cleaner', 'describer', 'evaluator', 'inferer', 'loader', 'merger', 'metric', 'model', 'munger', 'plot', 'sampler', 'shaper', 'splitter', 'transformer', 'validator']
print(amos.library.classify("smote"), amos.library.classify("one_hot"))
# sampler encoder
print(amos.library.all["random_forest"])
# <class 'amos.models.RandomForest'>

Names must be unique across the library, including the names of chrisjen classes such as "none", "flow", and "summary". The technique catalog lists every technique by genre.

Settings reference

A project's settings are sections of settings. amos reads two special sections. Every other section is read by chrisjen.

Section Setting Meaning
general label The column that models predict.
task "classify" or "regress", if it should not be inferred from the label.
seed Seed for every random process (passed as random_state).
groups Columns that identify groups of rows (see "Groups and fairness" below).
files root_folder Folder for the project's files. Defaults to the current folder.
input_folder Folder (in the root folder) where data files named by a relative path are looked for, and where loaders save downloads.
output_folder Folder (in the root folder) in which export makes a folder for each run.
export_results Whether to export the results after every run. Defaults to False.

The chrisjen settings, in brief:

Setting Meaning
A section named {name}_project The project. Its {name}_workers setting lists the workers, in order.
A section named for a worker The worker's settings, below.
steps or {name}_steps The steps of the worker (or of name).
techniques or {name}_techniques The techniques of the worker (or of the step name). In an experiment, the techniques of a step are alternatives.
design or {name}_design How the worker applies its nodes. Defaults to flow.
criterion or {name}_criterion What an experiment or contest compares: a metric (such as roc_auc) or another chrisjen.Criteria.
A section named {name}_parameters Parameters for the technique, step, or worker name.
A section named {technique}_{step}_parameters Parameters for one technique in one step.

The designs:

Design What it does
flow Applies its nodes one after another. The default.
experiment Tries every combination of one technique from each step, keeps the best, and adds a comparison table.
contest Like experiment, without the table.
survey Tries every combination and averages the results. The results must be numbers, so it is rarely used with a Dataset.
benchmark Repeats its nodes until its criterion is met.

The dataset

A Dataset has these attributes:

Attribute Meaning
data The data, a pandas.DataFrame. It stays one DataFrame after the data is split.
label The name of the column that models predict.
task "classify" or "regress".
seed Seed for every random process.
groups Columns that identify groups of rows. They stay in the data but are not features.
train, test Index labels of the training and test rows. test is None until the data is split.
synthetic Index labels of the training rows that a sampler made up (synthetic rows and extra copies of a row).
model The fitted model.
predictions Predictions for the test rows (or every row, if the data is not split).
probabilities Predicted probabilities of each class, for the same rows.
metrics Scores, by the name of each metric.
tables, figures Tables and figures, by the name of the technique that made them.
fitted Fitted tools (such as a scaler), by the name of the technique that fitted them.
history A record of each technique applied, with its tool and parameters.
branches A record of every branch of the most recent experiment (for the scorecard).

Its properties select parts of the data: x and y (the features and the label), x_train, y_train, x_test, and y_test, and the names of the features of each kind:

data = pd.DataFrame({
    "age": [31, 45, 52, 38],
    "court": ["state", "federal", "state", "state"],
    "appealed": [True, False, True, False],
    "decided": pd.to_datetime(["2020-01-02", "2021-03-04", "2022-05-06", "2023-07-08"]),
    "reversed": ["yes", "no", "no", "yes"],
})
dataset = amos.Dataset(data, label = "reversed")
print(dataset.numerics, dataset.categoricals, dataset.booleans, dataset.dates)
# ['age'] ['court'] ['appealed'] ['decided']
print(dataset.task, dataset.classes)
# classify ['no', 'yes']

Models cannot use dates or text, so encode them (dates with date_parts, and text with an encoder such as one_hot or tfidf) or remove them before the model step.

Dataset.create makes a dataset from a DataFrame, a Polars DataFrame or LazyFrame, a numpy array, a dict of columns, the path to a data file (csv, tsv, Excel, parquet, feather, json, Stata, SPSS, or pickle), or a scikit-learn dataset loaded with as_frame = True. A Project does this for you.

Loading data

A loader makes the dataset from a source named in the settings, so a project with a loader needs no item. Loaders are usually the first step of the wrangler. They work through the project's clerk (a nagata.FileManager): a file named by a relative path is looked for in the current folder and then in the clerk's input folder (the input_folder of the "files" section), and downloads are saved in the input folder.

Loader Source Other parameters
load_file The path of a data file.
download The URL of a file (http or https). It is saved in the input folder and only downloaded again if it is missing. file_name (what to save it as), refresh (to download it again), and timeout (in seconds)
openml The name (such as "credit-g") or id (such as 31) of an OpenML dataset, which scikit-learn saves in the input folder. Its default target is the label unless "label" is set. version, target_column, and the other parameters of sklearn.datasets.fetch_openml

Every loader takes a source, and the label, task, and groups of the data, which default to those in the "general" section. The dataset's history records the source. This project loads a file from its input folder:

court,appeals,reversed
state,2,yes
federal,1,no
state,2,yes
state,0,no
federal,3,yes
state,1,no
settings = {
    "general": {"label": "reversed", "seed": 43},
    "files": {"input_folder": "data"},
    "cases_project": {"cases_workers": "wrangler, explorer"},
    "wrangler": {
        "steps": "load, clean",
        "load_techniques": "load_file",
        "clean_techniques": "drop_duplicates",
    },
    "load_file_parameters": {"source": "cases.csv"},
    "explorer": {"techniques": "label_balance"},
}
project = amos.Project.create(settings, id = "loaded")
print(project.result)
# Dataset(rows=5, columns=3, label='reversed', task='classify')
print(project.result.history[0])
# {'technique': 'load_file', 'source': 'cases.csv', 'rows': 6, 'columns': 3}

load_file and download load a file in any format that the clerk knows (such as csv, tsv, Excel, parquet, feather, json, Stata, SAS, and SPSS), which they find from the file's extension. pandas opens a compressed file (such as "cases.csv.gz") itself. Set file_format (such as "csv") for a file whose name does not say what it is, and member to load one file from a zip archive, which is extracted to a folder named for the archive. Other parameters, such as sep, go to the pandas reader if it accepts them. Unlike the clerk's own defaults (which suit quick tests), loaders read every row and read text as UTF-8, as pandas does.

A file online is named the same way, and is downloaded the first time the project runs:

[wrangler]
steps = load, clean
load_techniques = download
clean_techniques = drop_duplicates

[download_parameters]
source = https://example.com/data/cases.csv.gz

To write a loader for another source, see "Writing your own techniques" below.

Munging data

Cleaners remove rows and columns. Mungers change what is in the columns, or make new columns from them, without adding or removing rows. Each one changes a whole column at once with the vectorized methods of pandas, so they are fast even with a lot of data. None of them learns from the data, so, like cleaners, they belong in the wrangler, before the data is split. The technique catalog describes each one:

Kind Mungers
Text strip_text, normalize_text, replace_text
Patterns in text flag_patterns, count_patterns, map_patterns, extract_pattern, extract_all, split_text
Values and types map_values, parse_numbers, parse_dates, parse_booleans, convert_types, auto_categorize
Combining columns coalesce, combine_flags, derive_columns

Many mungers search text for patterns, which are regular expressions. A pattern is found anywhere in the text, so "revers" is found in "Reversed and remanded", and "revers|vacat" finds either word. Characters with special meanings, such as "." and "(", need a backslash to stand for themselves ("F.3d"). Set "ignorecase" to ignore the difference between capital and lower-case letters. Missing text matches nothing.

A munger that searches text reads one column. flag_patterns and count_patterns make a column for each name in their "patterns", and the others make the column in their "name" (or, without one, replace the column they read). Parameters that map names or patterns to values, such as "patterns", need settings that have mappings: a toml, json, or yaml file, or a Python dict (an ini file has none). In toml, text in single quotes keeps its backslashes as they are:

[coding_project]
coding_workers = "wrangler"

[wrangler]
techniques = "split_text, flag_patterns, map_patterns, parse_numbers"

[split_text_parameters]
column = "caption"
pattern = '\s+v\.\s+'
names = ["party1", "party2"]

[flag_patterns_parameters]
column = "caption"

[flag_patterns_parameters.patterns]
government = 'United States|\bState of\b'

[map_patterns_parameters]
column = "disposition"
name = "outcome"
ignorecase = true

[map_patterns_parameters.patterns]
'revers|vacat' = "reversed"
affirm = "affirmed"

[parse_numbers_parameters]
columns = "damages"
opinions = pd.DataFrame({
    "caption": ["United States v. Smith", "Jones v. Acme Corp.", "Doe v. Roe"],
    "disposition": ["REVERSED and remanded", "Affirmed.", "Vacated."],
    "damages": ["$1,200", "n/a", "$350.50"],
})
coding = amos.Project.create("coding.toml", item = opinions)
coded = coding.result.data
print(coded["party1"].tolist(), coded["party2"].tolist())
# ['United States', 'Jones', 'Doe'] ['Smith', 'Acme Corp.', 'Roe']
print(coded["government"].tolist(), coded["outcome"].tolist())
# [True, False, False] ['reversed', 'affirmed', 'reversed']
print(coded["damages"].tolist())
# [1200.0, nan, 350.5]
print(coding.result.history[0])
# {'technique': 'split_text', 'changed': [], 'created': ['party1', 'party2']}

Each munger records the columns whose values or types it changed and the columns it made. Like any technique, a munger can also be applied by hand. A pattern with named groups makes a column of each group:

citations = amos.Dataset(
    pd.DataFrame({"cite": ["512 F.3d 1093", "98 F.4th 12", "unpublished"]}))
amos.mungers.ExtractPattern().apply(
    citations, column = "cite", pattern = r"(?P<volume>\d+) F\.\w+ (?P<page>\d+)")
amos.mungers.ParseNumbers().apply(citations, columns = ["volume", "page"])
print(citations.data["volume"].tolist(), citations.data["page"].tolist())
# [512.0, 98.0, nan] [1093.0, 12.0, nan]

Merging data

Research often needs data from sources that are not organized the same way: one table has a row for each case, and another has a row for each court, or for each court that each judge has served on. A merger matches each row of the data to a row of another table and adds that row's columns. The mergers differ in how they match rows (the technique catalog lists them):

Merger A row of the data gets Its parameters
merge_keys The columns of the row with the same keys (such as its court). on, other_on, ignorecase, and duplicates
merge_nearest The columns of the row with the nearest date or number (such as the last rating of its court before it was decided). column, other_column, direction, tolerance, and keys, if any
merge_ranges The columns of the row whose range has its date or number in it (such as the president in office in its year). column, start, end, duplicates, and keys, if any
merge_summary Summaries of every row with the same keys (such as the number of judges of its court and the share of them who are women). how, count, and the keys

A merger never adds, removes, or reorders rows. The data keeps the unit that it studies, its label, its groups, and its index, so a merger cannot quietly multiply or lose your observations. A row without a match gets missing values. So start from the table whose rows are what you study (which is the table with the label), and merge the others into it. If the other table describes something else (judges, when the rows of the data are cases), reshape the data first (see reshaping data).

Every merger takes these parameters:

Parameter Meaning
source The other table: the path of a data file, which is found and loaded as a loader would (a relative path is looked for in the current folder and then in the clerk's input folder). In Python, it can also be a DataFrame, a Dataset, or anything else that Dataset.create accepts.
reader Parameters for loading the file, such as file_format, member, or sep.
columns The columns of the other table to add. By default, they are every column whose name the data does not already have (other than the keys of the other table, which repeat those of the data).
prefix Text to put before the names of the added columns, such as "judge_".
indicator The name of a column to make that says whether each row was matched.

on names the key columns of the data, and other_on those of the other table, if they have other names there. A key must be the same kind of data (numbers, dates, or text) in both tables. Set ignorecase to match text keys without regard to capital letters or the spaces around them. A missing key matches nothing. A row can match only one row, so merge_keys and merge_ranges stop if a row matches several, unless duplicates says to use the "first" or "last" of them (merge_summary summarizes them instead).

The history records how many rows were matched, which is worth checking (and reporting) every time:

court,circuit,judgeships
First Circuit,1,6
Ninth Circuit,9,29
Second Circuit,2,13
rulings = pd.DataFrame({
    "case": ["a1", "a2", "a3", "a4"],
    "court": ["First Circuit", "ninth circuit", "First Circuit", "Tax Court"],
    "year": [2001, 2015, 2019, 2010],
    "reversed": [True, False, True, False],
})
merging = {
    "general": {"label": "reversed"},
    "files": {"input_folder": "data"},
    "rulings_project": {"rulings_workers": "wrangler"},
    "wrangler": {"techniques": "merge_keys"},
    "merge_keys_parameters": {
        "source": "courts.csv",
        "on": "court",
        "ignorecase": True,
        "indicator": "court_found",
    },
}
merged = amos.Project.create(merging, item = rulings).result
print(merged.data["circuit"].tolist(), merged.data["court_found"].tolist())
# [1.0, 9.0, 1.0, nan] [True, True, True, False]
print(merged.history[0])
# {'technique': 'merge_keys', 'source': 'courts.csv', 'rows': 4, 'matched': 3, 'created': ['circuit', 'judgeships', 'court_found']}

merge_ranges and merge_nearest match rows in time. A range has a start and an end (both are in the range), and a missing one means that the range has no limit on that side, as with a term that is still going on. merge_nearest looks "before" a row's date or number unless direction says "after" or "nearest", so that nothing is used that was not yet known, and no farther than tolerance, if it is set:

terms = pd.DataFrame({
    "president": ["Clinton", "Bush", "Obama", "Trump"],
    "began": [1993, 2001, 2009, 2017],
    "ended": [2000, 2008, 2016, 2020],
})
amos.mergers.MergeRanges().apply(
    merged, source = terms, column = "year", start = "began", end = "ended",
    columns = "president")
print(merged.data["president"].tolist())
# ['Bush', 'Obama', 'Trump', 'Obama']

merge_summary is for a table with several rows for each row of the data. It summarizes them as group_rows does: by default, with the mean of each column of numbers and booleans (the mean of a boolean is the share that are true). count names a column for the number of rows:

name,court,began,ended,party,woman
Lynch,First Circuit,1995,,-1,True
Selya,First Circuit,1986,2021,1,False
Boudin,First Circuit,1992,2021,1,False
Barron,First Circuit,2014,,-1,False
Kozinski,Ninth Circuit,1985,2017,1,False
Reinhardt,Ninth Circuit,1980,2018,-1,False
amos.mergers.MergeSummary().apply(
    merged, source = "data/judges.csv", on = "court", ignorecase = True,
    count = "judges", prefix = "bench_")
print(merged.data["bench_judges"].tolist(), merged.data["bench_woman"].tolist())
# [4.0, 2.0, 4.0, nan] [0.25, 0.0, 0.25, nan]

Reshaping data

Shapers change what a row is: they rearrange the same data so that each row stands for something else. Each has an opposite (the technique catalog lists them):

Shaper What it does Its parameters
wide_to_long Stacks columns into rows: a row for each state, with a column for each year, becomes a row for each state in each year. columns (with name and value), or stubs (with name and separator)
long_to_wide Spreads the rows of each group into columns, which is the opposite. names, values or stubs, ids, and separator
lists_to_rows Makes a row for each item of a list: a row for each case becomes a row for each judge on each case. column, separator, name, and position
group_rows Makes one row for each group of rows, with summaries of its columns, which is the opposite. by, how, and count

The rows of reshaped data are not the rows that it began with, so:

  • The rows get new index labels (0, 1, 2, and so on). When rows are repeated, an index with a name is kept as a column of that name.
  • A shaper must come before the data is split, so shapers belong in the wrangler.
  • The label and groups must still be columns afterward. If they are not, set the shaper's "label" and "groups" (and "task") parameters to those of the reshaped data, as for a loader.

wide_to_long stacks the columns that it is given into a value column, with a name column that says which column each value came from. With stubs, it stacks sets of columns that begin with the same names, as panel data often has: each stub becomes a column, and name has the rest of each name. long_to_wide does the opposite, with one column of values (whose new columns are named for the names alone) or one or more stubs:

states = pd.DataFrame({
    "state": ["Kansas", "Missouri"],
    "income_2019": [51, 48],
    "income_2020": [52, 49],
    "tax_2019": [0.05, 0.04],
    "tax_2020": [0.05, 0.05],
})
long = amos.shapers.WideToLong().apply(
    states, stubs = ["income", "tax"], name = "year")
print(long.data.to_dict("list"))
# {'state': ['Kansas', 'Kansas', 'Missouri', 'Missouri'], 'year': [2019, 2020, 2019, 2020], 'income': [51, 52, 48, 49], 'tax': [0.05, 0.05, 0.04, 0.05]}
amos.shapers.LongToWide().apply(long, names = "year", stubs = ["income", "tax"])
print(list(long.data.columns))
# ['state', 'income_2019', 'income_2020', 'tax_2019', 'tax_2020']

group_rows summarizes the columns of each group with the summaries in its "how": "mean", "median", "sum", "min", "max", "std", "count", "nunique", "first", "last", "any", "all", or "list". Without one, a column that is the same throughout each group keeps its value (so a case's year stays its year), the mean is taken of other numbers and booleans, and the first value is taken of anything else. A dict gives columns their own summaries.

Together, shapers and mergers join tables whose rows are different things. Opinions name the judges of each case in one cell, and the table of judges above has a row for each court that each judge has served on. To study the panels, this project makes a row for each judge on each case, adds what is known about the judge of that name who was serving on the court that year, and groups the rows into cases again:

[general]
label = "reversed"

[files]
input_folder = "data"

[panels_project]
panels_workers = "wrangler"

[wrangler]
techniques = "lists_to_rows, merge_ranges, group_rows"

[lists_to_rows_parameters]
column = "panel"
separator = ";"
name = "judge"

[merge_ranges_parameters]
source = "judges.csv"
column = "year"
start = "began"
end = "ended"
on = ["judge", "court"]
other_on = ["name", "court"]
ignorecase = true
columns = ["party", "woman"]
prefix = "panel_"
indicator = "found"

[group_rows_parameters]
by = "case"
count = "judges"

[group_rows_parameters.how]
judge = "list"
found = "sum"
argued = pd.DataFrame({
    "case": ["a1", "a2", "a3"],
    "court": ["First Circuit", "Ninth Circuit", "First Circuit"],
    "year": [2001, 2015, 2019],
    "panel": ["LYNCH; SELYA; BOUDIN", "KOZINSKI; REINHARDT", "LYNCH; BARRON; SOUTER"],
    "reversed": [True, False, True],
})
panels = amos.Project.create("panels.toml", item = argued).result
print(panels.data["judges"].tolist(), panels.data["found"].tolist())
# [3, 2, 3] [3, 2, 2]
print(panels.data["panel_party"].round(2).tolist(), panels.data["panel_woman"].tolist())
# [0.33, 0.0, -1.0] [0.3333333333333333, 0.0, 0.5]
print(panels.data["reversed"].tolist(), panels.label, panels.task)
# [True, False, True] reversed classify
print(panels.history[1])
# {'technique': 'merge_ranges', 'source': 'judges.csv', 'rows': 8, 'matched': 7, 'created': ['panel_party', 'panel_woman', 'found']}

Justice Souter sat with the First Circuit after he retired, so no row of the table of judges matches him, and the means of the third panel leave him out. The "found" column says so, which is why an indicator is worth keeping.

How techniques work

Parameters

A technique receives its parameters from the settings and from the workers that contain it, as in chrisjen: a technique's own parameters are overridden by those of its step, then by those of its workers, and then by any passed to Project.apply. A technique that wraps a tool passes the tool only the parameters it accepts, so a parameter set for a whole worker does not break the techniques that do not use it.

If the dataset has a seed and a tool accepts a random_state that was not set, the seed is used. Every random process in a project is reproducible.

Columns

Each genre of transformer changes the features of some kinds by default: imputers change the numeric features with missing values (mode_impute changes features of every kind), scalers and mixers change the numeric features, encoders change the categorical and text features, and reducers change the numeric and boolean features. Pass columns to choose others:

cancer = sklearn.datasets.load_breast_cancer(as_frame = True)
dataset = amos.Dataset.create(cancer, seed = 43)
amos.transformers.MinMax().apply(dataset, columns = ["mean radius"])
print(dataset.history[-1]["columns"])
# ['mean radius']
print(dataset.data["mean radius"].max())
# 1.0

The columns that a transformer makes take the place of the columns it changed, so the order of the other columns does not change. Transformers are fitted to the training rows (all rows, before the data is split) and then change every row. The fitted tool is stored in fitted:

print(type(dataset.fitted["min_max"]).__name__)
# MinMaxScaler

Labels and the positive class

Metrics, encoders, and models handle labels of any type. For a binary label, the positive class is the last of Dataset.classes (such as 1, True, or "yes"), and metrics such as precision and roc_auc are about that class. For more than two classes, precision, recall, and f1 are averaged over the classes ("macro") and roc_auc compares each class with the rest. Pass average, pos_label, or the other parameters of the scikit-learn metric to change this. Encoders that learn from the label (such as target) are given the position of each class when the classes are text.

Every model accepts these parameters:

Parameter Meaning
search "grid", "random", or "optuna". Without it, lists are passed to the model as they are.
cv Number of cross-validation folds. Defaults to 5.
n_iter Number of combinations to try in a random or Optuna search. Defaults to 10.
scoring The scikit-learn scorer to compare with, such as "roc_auc". Defaults to the model's own score.

With a search, every parameter given as a list is searched. In a random or Optuna search, a list of two numbers is a range (whole numbers are drawn from it, inclusive; other numbers are drawn from between them). An Optuna search chooses each combination to try from the results of the ones before, so it usually finds good values in fewer tries, and it searches a range that spans two or more orders of magnitude (such as a learning rate from 0.001 to 0.1) on a log scale. The search uses the training rows only, and the results of every combination are stored in tables as "{name}_search".

amos.splitters.Stratified().apply(dataset)
amos.models.RandomForest().apply(
    dataset, search = "random", n_estimators = [20, 60], max_depth = [2, 8],
    n_iter = 3, cv = 3)
search = dataset.tables["random_forest_search"]
print(len(search), search["param_n_estimators"].between(20, 60).all())
# 3 True
amos.models.RandomForest().apply(
    dataset, search = "optuna", n_estimators = [20, 60], max_depth = [2, 8],
    n_iter = 3, cv = 3)
print(list(dataset.tables["random_forest_search"].columns[:2]))
# ['param_max_depth', 'param_n_estimators']

Statistical inference

Machine learning models predict. Research usually also needs inference: which features matter, by how much, and how surely. ols and glm wrap statsmodels and store a table of coefficients with their standard errors, test statistics, p-values, and 95% confidence intervals:

diabetes = sklearn.datasets.load_diabetes(as_frame = True)
dataset = amos.Dataset.create(diabetes, seed = 43)
amos.models.OLS().apply(dataset)
table = dataset.tables["ols_coefficients"]
print(list(table.columns))
# ['coefficient', 'standard_error', 'statistic', 'p_value', 'ci_lower', 'ci_upper']
print(table.loc["bmi", "p_value"] < 0.001)
# True

glm is a logistic regression (without a penalty) for classification and a linear regression for regression. Set its "family" parameter for other generalized linear models, such as "poisson" for counts. Every model that wraps statsmodels stores the same table:

Model statsmodels Use it for
ols OLS Least squares regression.
wls WLS Least squares with a weight for each row (the "weights" column).
glm GLM Generalized linear models of any "family".
quantile_regression QuantReg A "quantile" of the label (the median by default), which outliers sway less.
robust_regression RLM Regression that gives outliers less weight, by "norm" (Huber's by default).
mixedlm MixedLM Regression with a random intercept for each of "groups" (such as each judge).
gee GEE A glm for rows correlated within "groups", with "covariance" "independence" or "exchangeable".
poisson, negative_binomial, generalized_poisson, zero_inflated_poisson Poisson, NegativeBinomial, GeneralizedPoisson, ZeroInflatedPoisson Counts: whole numbers of 0 or more, which may vary more than a Poisson's or have extra zeros.
logit_sm, probit Logit, Probit Two classes. Unlike logit (scikit-learn's logistic regression), logit_sm has no penalty.
binomial_bayes_mixedglm BinomialBayesMixedGLM Two classes, with a random intercept for each of "groups", fitted by variational Bayes. Its table has the mean and standard deviation of each coefficient's posterior and its 95% credible interval, and no p-values.
mnlogit MNLogit Any number of classes. Each class after the first has its own coefficients, named "{feature} ({class})".
ordinal_regression OrderedModel Ordered classes, as an ordered logit (or probit, with "distribution"). Its classes are ordered as the categories of an ordered categorical label, and otherwise sorted.

mixedlm, gee, and binomial_bayes_mixedglm use the dataset's first group unless "groups" names a column, and the groups and weights are not features. The predictions of mixedlm and binomial_bayes_mixedglm use the fixed effects only, so they suit groups that the model has not seen. A feature that is a combination of others (collinear) has no coefficient of its own. ols, wls, glm, quantile_regression, and robust_regression share the effect among such features, and the other models leave them out: the model's dropped_ lists them, and their rows in the table of coefficients are empty. amos classifies whole numbers with few values, so set "task" to "regress" in the "general" section for a label of counts. The rest of statsmodels' models either do not predict a label from features (such as MANOVA; PCA and Factor are below), or condition the effects of groups away so that they cannot predict new rows (such as ConditionalLogit; use fixest for fixed effects).

Two of the critic's evaluators describe the features that the model learned from (the real training rows), rather than the model, with statsmodels' PCA and Factor. pca reports the eigenvalue of each principal component and the share of the variance it explains (and stores the loadings as "pca_loadings"), which shows how many dimensions the features really have. Unlike the pca_reduce reducer, it does not change the data. factor_analysis reports the loading of each feature on a few hidden factors (by default, one for each eigenvalue of the correlations above 1, rotated by varimax), with each feature's communality and uniqueness:

features = amos.Dataset.create(cancer, seed = 43)
amos.splitters.Stratified().apply(features)
amos.evaluators.PCA().apply(features)
print(round(features.tables["pca"].loc["component_6", "cumulative"], 2))
# 0.89
amos.evaluators.FactorAnalysis().apply(features)
print(list(features.tables["factor_analysis"].columns[-3:]))
# ['factor_6', 'communality', 'uniqueness']

fixest wraps pyfixest for regressions with fixed effects (an intercept for each level of a column, such as each judge or year) and standard errors clustered by a column. The columns named by its "fixed_effects" and "cluster" parameters are usually the dataset's groups, and they are not used as features. It is a least squares regression, or a logit model for classification; set "family" to "poisson" for counts or "probit":

clinics = diabetes.frame.copy()
clinics["clinic"] = [f"c{i % 10}" for i in range(len(clinics))]
dataset = amos.Dataset(clinics, label = "target", seed = 43, groups = ["clinic"])
amos.models.Fixest().apply(dataset, fixed_effects = "clinic", cluster = "clinic")
print(list(dataset.tables["fixest_coefficients"].index[:3]))
# ['age', 'sex', 'bmi']

Groups and fairness

Research about people often needs to know whether a model treats groups differently. Name the columns that identify groups (such as race, court, or judge) as the dataset's groups, or in the "groups" setting of the "general" section. Groups stay in the data but are not features, so transformers and models do not use them. Then:

  • The fairness metrics (demographic_parity, equalized_odds, and equal_opportunity, with a "_ratio" version of each) compare the model's predictions across the dataset's first group, or the column named by their "group" parameter. They wrap fairlearn and need a label with two classes.
  • fairness makes a table of the count, selection rate, accuracy, and true and false positive rates of each group, with the largest difference and smallest ratio between groups.
  • The scorecard adds demographic_parity and equalized_odds for every branch.
  • fixest can use groups as fixed effects and clusters, and group_split keeps each row of the dataset's first group in the same set.
data = cancer.frame.copy()
data["clinic"] = np.where(np.arange(len(data)) % 3 == 0, "north", "south")
dataset = amos.Dataset(data, label = "target", seed = 43, groups = ["clinic"])
print("clinic" in dataset.features)
# False
amos.splitters.Stratified().apply(dataset)
amos.transformers.Standard().apply(dataset)
amos.models.Logit().apply(dataset)
amos.evaluators.Fairness().apply(dataset)
print(list(dataset.tables["fairness"].index))
# ['north', 'south', 'difference', 'ratio']
amos.metrics.EqualizedOdds().apply(dataset)
print(0 <= dataset.metrics["equalized_odds"] <= 1)
# True

Rows that a sampler copies keep their groups. Synthetic rows (made by smote, for example) have no groups.

Uncertainty

conformal turns any model's predictions into intervals (for regression) or sets of classes (for classification) that contain the true value for at least a chosen share of rows, with MAPIE's cross-conformal methods. The share of test rows covered and the width of the intervals (or the size of the sets) are stored in metrics:

dataset = amos.Dataset.create(diabetes, seed = 43)
amos.splitters.TrainTest().apply(dataset)
amos.models.Linear().apply(dataset)
amos.evaluators.Conformal().apply(dataset, confidence = 0.9)
print(list(dataset.tables["conformal"].columns))
# ['actual', 'prediction', 'lower', 'upper', 'covered']
print(dataset.metrics["coverage"] > 0.8)
# True

Causal inference

A model predicts the label. An inferer measures how much a treatment (such as a program or a ruling) changes the label, after accounting for the other features, or, for a time series, which variables cause which others and when. The inferers in amos.inferers belong to the critic, since they judge what the data can say about causes, and they come from four packages. Each package's inferers are a genre of Inferer:

Genre Inferers Python
doubleml partially_linear, partially_linear_iv, partially_logistic, partially_linear_panel, interactive_regression, interactive_iv, potential_outcomes, quantile_effects, difference_in_differences, regression_discontinuity, sample_selection every version
dowhy regression_adjustment, glm_adjustment, doubly_robust, propensity_matching, propensity_stratification, propensity_weighting, distance_matching, instrumental_variable 3.13 and later
causalml s_learner, t_learner, x_learner, dr_learner, tmle 3.11 and 3.12
tigramite pcmci, pcmci_plus, lpcmci, time_series_effect every version

causalml has no version for Python 3.13 and later, and on Python 3.11 and 3.12 the versions of DoWhy that work with pandas 3 cannot be installed with causalml. So the causal extra installs DoubleML (with rdrobust, for regression_discontinuity) and tigramite everywhere, causalml on Python 3.11 and 3.12, and DoWhy on Python 3.13 and later. (On Python 3.11 and 3.12, pip install dowhy installs DoWhy if causalml is not installed.) The technique catalog describes each one. Which to use depends on the design of the study:

Design Inferers
A treatment that changes the label by the same amount for every row partially_linear, regression_adjustment
A treatment with two values, whose effect may differ from row to row interactive_regression, doubly_robust, propensity_matching, propensity_stratification, propensity_weighting, distance_matching, s_learner, t_learner, x_learner, dr_learner, tmle
A label with two classes partially_logistic, glm_adjustment (and the treatments of two values above, whose effects are changes in the chance of the second class)
Causes of the treatment that are not among the features, and an "instrument" partially_linear_iv, interactive_iv, instrumental_variable, quantile_effects (with "local_quantile")
A treatment given to the rows at or above a cutoff of a score ("running") regression_discontinuity
A treatment with several levels potential_outcomes, and causalml's learners (with "control")
Effects on the spread of the label, not only its mean quantile_effects
Units (or groups) seen before and after a treatment that some of them got, perhaps in different periods difference_in_differences
Units seen over several periods (a panel) partially_linear_panel
A label seen only for some rows sample_selection
A time series pcmci, pcmci_plus, lpcmci, time_series_effect

An inferer uses every row, so it needs no split. It stores a table under its name with a row for each effect (of each treatment, group, quantile, or level of the treatment): its estimate ("coefficient"), standard error, test statistic, p-value, and 95% confidence interval, which coefficient_plot can draw. The parameters of most inferers are:

Parameter Meaning
treatment The column with the treatment. Required, except by pcmci, pcmci_plus, and lpcmci.
outcome_model, treatment_model amos models of the label and of the treatment (and of any instrument), for DoubleML and causalml. By default, random_forest.
n_folds The number of folds for cross-fitting the models (5 by default).
instrument, time, unit, selected, running Columns with other roles in the design, for the inferers that need them.
simulations, refuters DoWhy's bootstrap samples (100 by default) and refuters (placebo_treatment_refuter, random_common_cause, and data_subset_refuter), whose results are stored as "{name}_refutations".
control causalml's value of the treatment that the others are compared to. Each row's effect is stored as "{name}_effects".
max_lag, alpha, test tigramite's longest lag (5 by default), level of significance (0.05 by default), and test of conditional independence ("parcorr", "robust_parcorr", or "cmiknn").
rng = np.random.default_rng(1)
age = rng.normal(40, 10, 500)
program = (rng.random(500) < 1 / (1 + np.exp(-(age - 40) / 5))).astype(int)
outcome = 2 * program + 0.1 * age + rng.normal(0, 1, 500)
treated = pd.DataFrame({"age": age, "program": program, "outcome": outcome})
dataset = amos.Dataset(treated, label = "outcome", seed = 43)
amos.inferers.PartiallyLinear().apply(
    dataset, treatment = "program", outcome_model = "linear",
    treatment_model = "logit")
effect = dataset.tables["partially_linear"].loc["program"]
print(effect["ci_lower"] < 2 < effect["ci_upper"])
# True
amos.inferers.QuantileEffects().apply(
    dataset, treatment = "program", outcome_model = "logit",
    treatment_model = "logit")
print(list(dataset.tables["quantile_effects"].index))
# [0.25, 0.5, 0.75]

For a time series, the rows must be in order. tigramite's methods find which variables cause which others, and at what lag. Here, filings raise hearings a week later, which raise the backlog two weeks after that:

rng = np.random.default_rng(2)
weeks = np.zeros((300, 3))
for week in range(2, 300):
    weeks[week, 0] = 0.7 * weeks[week - 1, 0] + rng.normal()
    weeks[week, 1] = (
        0.5 * weeks[week - 1, 1] + 0.6 * weeks[week - 1, 0] + rng.normal())
    weeks[week, 2] = (
        0.4 * weeks[week - 1, 2] + 0.8 * weeks[week - 2, 1] + rng.normal())
docket = pd.DataFrame(weeks, columns = ["filings", "hearings", "backlog"])
series = amos.Dataset(docket, label = "backlog", seed = 43)
amos.inferers.PCMCI().apply(series, max_lag = 3, alpha = 0.01)
print(series.tables["pcmci"][["cause", "effect", "lag"]].values.tolist())
# [['filings', 'filings', 1], ['filings', 'hearings', 1], ['hearings', 'hearings', 1], ['hearings', 'backlog', 2], ['backlog', 'backlog', 1]]
amos.inferers.TimeSeriesEffect().apply(
    series, treatment = "filings", lag = 3, max_lag = 3, alpha = 0.01)
effect = series.tables["time_series_effect"].iloc[0]
print(effect.name, effect["ci_lower"] < 0.6 * 0.8 < effect["ci_upper"])
# filings (lag 3) True

The estimates are causal only if the assumptions of their designs hold: most need every feature that affects both the treatment and the label to be among the features, and an instrument must affect the label only through the treatment. DoWhy's refuters test some of these assumptions.

Survival analysis

For the time until an event (such as rearrest or the end of a case), the label is the time, and a column says whether the event happened (1) or the row stopped being observed first (0, censored). kaplan_meier and survival_curves describe and draw the share of rows without the event over time, overall or by group (with a table of the number of rows at risk, if "at_risk" is set). cox is a Cox proportional hazards regression, with hazard ratios in its coefficients table, and concordance scores its predictions. They wrap statsmodels:

rng = np.random.default_rng(2)
prior = rng.poisson(2, 400).astype(float)
times = pd.DataFrame({
    "prior": prior,
    "arrested": (rng.random(400) < 0.7).astype(int),
    "days": rng.exponential(400 / (1 + prior)).round() + 1,
})
dataset = amos.Dataset(times, label = "days", seed = 43, groups = ["arrested"])
amos.describers.KaplanMeier().apply(dataset, event = "arrested")
amos.splitters.TrainTest().apply(dataset)
amos.models.Cox().apply(dataset, event = "arrested")
amos.metrics.Concordance().apply(dataset)
print(dataset.tables["cox_coefficients"].loc["prior", "hazard_ratio"] > 1)
# True
print(dataset.metrics["concordance"] > 0.5)
# True

Figures

The artist's plots draw matplotlib figures, which are stored in the dataset's figures and saved by Project.export. Every plot takes a "width" and "height" (in inches), a "title", and the style parameters below. They fall into a few families:

Family Plots
The data histograms, box_plots, count_plots, pair_plot, correlation_heatmap, missing_heatmap, label_plot
Classifiers roc_curve, precision_recall_curve, det_curve, calibration_curve, confusion_heatmap
Regressions actual_vs_predicted, residual_plot, qq_plot, influence_plot, coefficient_plot
Models importance_plot, partial_dependence, learning_curve, validation_curve, search_plot, prediction_intervals, tree_plot, shape_functions
SHAP values shap_bar, shap_beeswarm, shap_violin, shap_heatmap, shap_decision, shap_embedding, shap_waterfall, shap_force, shap_scatter, shap_partial_dependence, shap_group_difference
Groups and time fairness_plot, survival_curves

Plots of the data are split or colored by the label's classes (or by the dataset's first group, or by the column "by"). Plots of a table, such as coefficient_plot (of the coefficients of ols, glm, fixest, or cox, or of an inferer), search_plot, fairness_plot, and prediction_intervals, draw the table that another technique made, and the last two make it if it is missing. learning_curve and validation_curve fit copies of a scikit-learn model again, in cross-validation on the training rows.

The SHAP plots draw the SHAP values that shap_importance found (or find them) for the class "category", with shap's own plotting functions. shap draws with matplotlib.pyplot, so amos lends it each figure while it draws, without opening a window, and seeds the random numbers that shap uses to jitter dots, so that a figure is the same every time.

dataset = amos.Dataset.create(diabetes, seed = 43)
amos.splitters.TrainTest().apply(dataset)
amos.models.OLS().apply(dataset)
for plot in (amos.plots.CoefficientPlot(), amos.plots.QqPlot()):
    plot.apply(dataset)
amos.plots.InfluencePlot().apply(dataset, title = "Influence of each row")
print(sorted(dataset.figures))
# ['coefficient_plot', 'influence_plot', 'qq_plot']

Styles

Every figure has the same style, so that a paper's figures match. By default, it is the "science" style of SciencePlots with its "nature" style, which gives figures the size and fonts of a figure in Nature: one column (3.3 inches) wide, with 7-point text. The colors are SciencePlots' "bright" cycle (Paul Tol's), which people with color blindness can tell apart. Each plot takes:

Parameter Meaning
style Names of matplotlib or SciencePlots styles (such as "science", "nature", "ieee", "ggplot", or "default"), applied in order, or "xkcd", for figures that look drawn by hand. "none" uses the current settings of matplotlib.
colors A color cycle, such as "bright", "vibrant", "muted", or "high-contrast", applied after the style (so "xkcd" keeps it), or "none" for the colors of the style.
latex Whether to set the text with LaTeX, which must be installed. By default, matplotlib sets the text and math itself, whatever the style says.

Set them for every plot in the "{worker}_parameters" section of the artist, or for one plot in its own section. The defaults are options._PLOT_STYLE and options._PLOT_COLORS:

[artist_parameters]
style = xkcd

Unless a plot is given a "width" or "height", its usual size is scaled to the width of the style, and Project.export saves figures at 300 dots per inch (options._FIGURE_DPI), so that they are sharp in print. "xkcd" uses the "xkcd Script", "Comic Neue", or "Comic Sans MS" font if one is installed, and the usual font if not.

amos.plots.QqPlot().apply(dataset)
print(dataset.figures["qq_plot"].get_size_inches())
# [3.3   2.475]
amos.plots.QqPlot().apply(dataset, style = "xkcd")
print(dataset.figures["qq_plot"].get_size_inches())
# [6.4 4.8]

Plots drawn by other packages follow the style as far as those packages allow. shap draws with matplotlib, so the SHAP plots take the style's fonts, lines, and ticks. shap_bar and shap_waterfall also take the first two colors of the style's cycle (in "bright", blue for features that lower the prediction and red for those that raise it), but the other SHAP plots keep shap's own red and blue scale. shap also fixes the sizes of its text, so the SHAP plots (and statsmodels' influence_plot, which does too) keep their usual sizes (6.4 by 4.8 inches for most), so that their text fits.

Writing your own techniques

Subclass the genre that fits and write the method it requires. The class is in the library under its snake case name as soon as it is defined, so it can be named in settings right away.

Genre Write Returns
Loader load(self, source, **kwargs) The data: a DataFrame or anything that Dataset.create accepts. self.read(path, **kwargs) loads a file with the clerk.
Cleaner clean(self, data, **kwargs) The cleaned DataFrame.
Munger munge(self, data, **kwargs) The DataFrame with changed or new columns, and the same rows.
Merger match(self, data, other, **kwargs) For each row of the data, the position of the row of the other table that matches it, or -1 for none. The merger adds the columns of those rows. prepare(self, other, **kwargs) can change the other table first.
Shaper shape(self, data, **kwargs) The reshaped DataFrame.
Describer describe(self, item, **kwargs) A table, stored in tables.
Splitter divide(self, item, test_size, **kwargs) The training and test index labels.
Evaluator evaluate(self, item, **kwargs) A table, stored in tables.
Plot draw(self, item, figure, **kwargs) Nothing: it draws on figure.
Transformer, Sampler, Model, Validator, Metric Set contents to a tool.
Operation implement(self, item, **kwargs) The changed Dataset.

Keyword parameters of these methods are filled from the settings. For example, a cleaner that limits extreme values (winsorizes):

@dataclasses.dataclass
class Winsorize(amos.Cleaner):
    """Limits each numeric column to its 1st and 99th percentiles."""

    def clean(self, data, lower = 0.01, upper = 0.99, **kwargs):
        numbers = data.select_dtypes("number")
        limits = numbers.quantile([lower, upper])
        data[numbers.columns] = numbers.clip(
            limits.iloc[0], limits.iloc[1], axis = 1)
        return data


dataset = amos.Dataset.create(cancer, seed = 43)
Winsorize().apply(dataset, upper = 0.95)
print(dataset.history[-1])
# {'technique': 'winsorize', 'rows': [569, 569], 'columns': [31, 31]}

That cleaner looks at every row, so it belongs before the split. A technique that learns from the data should be a Transformer, so that it learns only from the training rows. Most transformers, samplers, models, and metrics need no code at all: set contents to the import path of a scikit-learn-style tool. Its parameters (and columns, for a transformer) come from the settings:

@dataclasses.dataclass
class Spline(amos.Mixer):
    contents: str = "sklearn.preprocessing.SplineTransformer"


@dataclasses.dataclass
class BayesRidge(amos.Model):
    contents: str = "sklearn.linear_model.BayesianRidge"


@dataclasses.dataclass
class MedianError(amos.Metric):
    contents: str = "sklearn.metrics.median_absolute_error"
    greater_is_better = False
    tasks = ("regress",)


settings = {
    "general": {"label": "target", "seed": 43},
    "diabetes_project": {"techniques": "train_test, spline, bayes_ridge, median_error"},
    "spline_parameters": {"columns": ["bmi"], "n_knots": 4},
}
project = amos.Project.create(settings, item = diabetes)
print(type(project.result.model).__name__, "median_error" in project.result.metrics)
# BayesianRidge True
print(project.result.features[2:5])
# ['bmi_sp_0', 'bmi_sp_1', 'bmi_sp_2']

A model whose tool differs by task sets tools (a dict mapping "classify" and "regress" to tools) instead of contents. A metric sets greater_is_better, tasks, and uses_probabilities as class attributes. Because every metric is also a chrisjen.Criteria, a new metric can be the criterion of an experiment.

Experiments

An experiment is a chrisjen contest that also records how every combination did. You can build one in code, as any chrisjen worker. Its populate method takes the nodes in order. A list of nodes is a set of alternatives:

dataset = amos.Dataset.create(cancer, seed = 43)
amos.splitters.Stratified().apply(dataset)
experiment = amos.Experiment(name = "models", criteria = amos.metrics.F1())
experiment.populate([
    [amos.transformers.Standard(), amos.transformers.Robust()],
    [amos.models.Baseline(), amos.models.Logit()],
])
result = experiment.apply(dataset)
print(experiment.winner.endswith("logit"))
# True
print(result.tables["models_comparison"].shape)
# (4, 3)

scores has the score of each combination, results the dataset that each made, and winner the label of the best. The criterion of an experiment can be a metric for which lower is better (such as log_loss). It is negated to rank the combinations, but the table shows its real value. The winning dataset also keeps a Branch for every combination in its branches: the technique used at each step, its score, and its predictions, which is what a scorecard compares.

Cross-validation

A validator measures how well the model does on rows that it did not learn from, without using the test rows. It is the last step of the analyst, after the model. It divides the training rows into folds with one of scikit-learn's cross-validation splitters (k_fold, stratified_k_fold, group_k_fold, time_series_split, and the others in the technique catalog), fits a new copy of the model to the rest of the training rows for each fold, and scores the copy on the fold. The scores of each fold are a table, stored under the validator's name, and their means are stored in metrics as "cv_{metric}". A scorecard and an experiment's comparison table show them beside the scores on the test rows:

[analyst]
design = experiment
criterion = roc_auc
steps = split, scale, sample, model, validate
split_techniques = stratified
scale_techniques = standard
sample_techniques = none, smote
model_techniques = logit, random_forest
validate_techniques = stratified_k_fold

A validator takes the parameters of its splitter (such as n_splits, the number of folds, and n_repeats for repeated_k_fold), and:

Parameter Meaning
metrics The metrics to score each fold with. Defaults to the metrics of a scorecard for the task.
groups For group_k_fold and the other validators of groups, the column that identifies each row's group. Defaults to the dataset's first group.
order A column (such as a date) to order the rows by before they are divided, for time_series_split.
shuffle Whether to shuffle the rows (with the dataset's seed) before dividing them, for a splitter that can. Defaults to True.
dataset = amos.Dataset.create(cancer, seed = 43)
amos.splitters.Stratified().apply(dataset)
amos.transformers.Standard().apply(dataset)
amos.samplers.Smote().apply(dataset)
amos.models.Logit().apply(dataset)
amos.validators.StratifiedKFold().apply(dataset, metrics = ["roc_auc", "f1"])
print(dataset.tables["stratified_k_fold"].columns.tolist())
# ['train', 'validation', 'roc_auc', 'f1']
print(sorted(name for name in dataset.metrics if name.startswith("cv_")))
# ['cv_f1', 'cv_roc_auc']

Rows that a sampler made up (the dataset's synthetic rows) are never scored, since they are not real observations. Instead, each sampler is applied again to the rows that each copy of the model learns from, as it was to all of the training rows. Transformers are not fitted again, so a validator scores the model given the features that the transformers made from all of the training rows, and a model whose parameters were searched for keeps the values found with all of the training rows.

leave_one_row_out predicts each row with a copy of the model fitted to every other row. A fold of one row cannot be scored alone, so its predictions are scored together, and its table has the prediction of each row.

Scorecards

The scorecard is the critic's summary of an analysis. It has one row for each branch of the most recent experiment (each combination of techniques), ranked by the experiment's criterion:

Column Meaning
rank 1 for the best branch.
One for each step The technique used at that step. Steps are named as in the settings (such as "scale"), or by their genre (such as "scaler") in an experiment built in code.
One for each metric The branch's score, computed from its own predictions on the test rows: the criterion first, then the other standard metrics for the task, then any other metrics the branches computed.

A dataset that did not come from an experiment gets one row for its final model, with the techniques that made it. As a technique in the critic, scorecard stores its table in the dataset's tables and the final model's scores in its metrics. Its "metrics" parameter chooses the metrics, and the scorecard_digits and scorecard_title settings (in the critic's section) set the digits shown and the title.

In Python, use Project.scorecard (which returns the critic's scorecard, or makes one) or Scorecard.create with a dataset or a project:

scorecard = amos.evaluators.Scorecard.create(result)
print(scorecard.heading)
# 4 branches ranked by f1
print(list(scorecard.table.columns[:4]))
# ['rank', 'scaler', 'model', 'f1']
print(scorecard.to_markdown().splitlines()[1])
# | ---: | --- | --- | ---: | ---: | ---: | ---: | ---: | ---: | ---: |

A scorecard can be saved in several formats:

Method Saves Needs
to_csv(path) A csv file, with scores at full precision. It also returns the text.
to_markdown(path) A Markdown table, with numbers aligned to the right. It also returns the text.
to_latex(path) A LaTeX table (with the booktabs package). It also returns the text.
to_html(path) An HTML table. It also returns the HTML. great_tables (amos[tables])
to_word(path) A Word document with a heading and a table. python-docx (amos[word])
to_image(path) An image of the table, in the format of the file's extension (such as png, svg, or pdf). to_figure returns the matplotlib figure instead. matplotlib (amos[plots])
export(folder, formats) Several formats, named "scorecard" (csv, md, docx, and png by default; also tex, html, svg, and pdf).

Scores in the Markdown table, Word document, and image are rounded to digits (3 by default). The title of the Word document and image is title, or a description of the branches if there is no title.

Reports and exports

Findings, the default report, describes the data, the split, the workflow, the winner of each experiment or contest, the model, the metrics, and the tables and figures. To use another report, pass a chrisjen.Report as report to Project.create.

Project.export saves these files with the project's clerk (a nagata.FileManager), in a folder named for the run in the output folder:

File Contents
report.txt The text of the report.
settings.json The settings of the project.
environment.json The versions of Python and of the packages used.
history.json Each technique applied, with its tool and parameters.
metrics.csv The model's metrics.
predictions.csv The labels, predictions, and probabilities of the predicted rows.
tables/{name}.csv Each table.
figures/{name}.png Each figure.
scorecard.csv, .md, .docx, .png The scorecard in each format. The Word document and image are saved if python-docx and matplotlib are installed.
data.csv The data after the workflow, if export(data = True).

Errors you may see

Error Cause
ValueError: there is no data The project has no item and no loader. Pass the data as item, or add a loader (such as load_file) to the wrangler.
ValueError: '...' has nothing to load A loader has no "source". Set it in the {loader}_parameters section.
ValueError: the pattern '...' is not valid: ... A munger's pattern is not a valid regular expression. A character with a special meaning (such as "(") needs a backslash to stand for itself.
TypeError: '...' uses text, but '...' is not text A munger that searches text was given a column of numbers, dates, or booleans.
ValueError: '...' has nothing to merge A merger has no "source". Set it in the {merger}_parameters section.
ValueError: ... rows of the other table have the same [...] as another of its rows A row of the data can match only one row of the other table. Set "duplicates" to "first" or "last" to use one of them, or summarize them with merge_summary.
TypeError: '...' cannot match '...', which is numbers in the data, with '...', which is text in the other table A key is not the same kind of data in both tables. Change the column of the data with a munger (such as parse_numbers, parse_dates, or convert_types).
ValueError: '...' would add columns named [...], which the data already has A merger was told to add a column whose name is taken. Set its "prefix".
ValueError: '...' changes the rows of the data, so it must come before the data is split A shaper came after a splitter. Reshape the data in the wrangler.
KeyError: the reshaped data has no columns [...], which are the label or groups A shaper left the data without its label or a group. Set the shaper's "label" or "groups" to those of the reshaped data.
ValueError: the dataset has no label A technique needs a label. Set "label" in the "general" section or pass it to Dataset.
ValueError: the data has not been split A technique asked for the test rows before a splitter was applied.
ValueError: '...' cannot use the dates or text in [...] A model was given text or dates. Encode them (with an encoder such as one_hot) or remove them (with drop_columns) first.
ValueError: '...' can classify, but the label is set to regress The model does only one task. Choose another model or set "task".
ValueError: '...' needs predictions: apply a model first A metric, evaluator, or plot came before the model.
ValueError: '...' needs predicted probabilities, which the model does not make The metric (such as roc_auc) needs probabilities.
ImportError: ... install it with "pip install amos[...]" The technique wraps an optional package that is not installed.
ValueError: '...' needs a group A fairness metric or fairness was used without groups. Set "groups" in the "general" section or pass "group".
KeyError: the fixed_effects column ... is not in the data A column named by a model's parameter (such as "fixed_effects", "cluster", or "event") is missing.
ValueError: '...' needs the name of a "treatment" column An inferer was used without a "treatment" parameter.
ImportError: causalml has no version for Python 3.13 and later ... causalml's inferers (such as x_learner) need Python 3.11 or 3.12. Use another package's inferer, such as DoubleML's interactive_regression.
ImportError: ... The causal extra ... installs DoWhy on Python 3.13 and later ... DoWhy's inferers (such as propensity_matching) need DoWhy, which the causal extra installs on Python 3.13 and later.
KeyError: no class named ... is in the library A name in the settings is not a technique. Check the spelling against the technique catalog.
ValueError: '...' needs the name of a "groups" column A splitter or validator of groups (such as group_k_fold) was used without groups. Set "groups" in the "general" section or pass "groups".
ValueError: a copy of the model could not predict ... A validator's copy of the model could not predict the rows of a fold. A model with fixed effects cannot predict a group it did not learn from, so use a validator that does not keep groups apart (such as k_fold).
ValueError: ... needs criteria to compare results An experiment or contest has no "criterion" setting.