Tutorial¶
This tutorial builds a study of the Wisconsin breast cancer data in stages. It
starts by applying a few techniques by hand and ends with a project that
compares alternatives, reads its settings from a file, and exports everything
needed to reproduce it. Every example on this page is run by the amos unit
tests, so you can copy and paste them.
The vocabulary¶
| Part | What it is | In code |
|---|---|---|
| Dataset | Your data and everything learned from it. | amos.Dataset |
| Technique | One method, such as "standard" scaling or a "logit" model. | Subclasses of amos.Operation |
| Worker | A stage of a project, such as "analyst". | A chrisjen worker |
| Step | A part of a worker with alternative techniques, such as "scale". | A step node in a worker |
| Project | The settings, the workflow built from them, and the result. | amos.Project |
amos is built on chrisjen, which
turns settings into workflows. amos adds the Dataset that flows through
them and the data science techniques that work on it.
1. Load your data¶
A Dataset holds your data in a pandas.DataFrame. Its label is the column
that models predict, and its seed makes every random process reproducible:
import sklearn.datasets
import amos
cancer = sklearn.datasets.load_breast_cancer(as_frame = True)
data = cancer.frame
dataset = amos.Dataset(data, label = "target", seed = 43)
print(dataset)
# Dataset(rows=569, columns=31, label='target', task='classify')
The task, "classify" or "regress", is inferred from the label: a label of
text, booleans, or a few whole numbers is classified, and other numbers are
regressed. You can also pass task. A dataset sorts its features into kinds,
which techniques use to choose the columns they change:
print(dataset.classes)
# [0, 1]
print(len(dataset.numerics), dataset.categoricals, dataset.booleans)
# 30 [] []
2. Apply techniques by hand¶
Every technique has an apply method that changes the dataset and returns
it. The techniques are grouped in modules by what they do:
from amos import describers, metrics, models, splitters, transformers
describers.Summarize().apply(dataset)
splitters.Stratified().apply(dataset)
transformers.Standard().apply(dataset)
models.Logit().apply(dataset)
metrics.RocAuc().apply(dataset)
print(len(dataset.train), len(dataset.test))
# 426 143
print(dataset.metrics["roc_auc"] > 0.95)
# True
summarize added a table of summary statistics, stratified split the rows
into training and test sets with the same share of each class, standard
rescaled the numeric features, logit fitted a logistic regression to the
training rows and predicted the test rows, and roc_auc scored the
predictions. The scaler learned the means and standard deviations from the
training rows only, so nothing about the test rows leaked into the model.
Each technique recorded what it did:
print([entry["technique"] for entry in dataset.history])
# ['summarize', 'stratified', 'standard', 'logit', 'roc_auc']
print(dataset.history[2]["tool"])
# sklearn.preprocessing.StandardScaler
print(dataset.history[1]["seed"], dataset.history[1]["test"])
# 43 143
3. Describe a project¶
Applying techniques by hand is fine for exploring. For a study, describe the
whole project in settings. A project needs a section whose name ends in
_project, and its {name}_workers setting lists the workers, in order. Each
worker has a section that lists its techniques. The "general" section holds
the label and the seed:
settings = {
"general": {"label": "target", "seed": 43},
"cancer_project": {"cancer_workers": "explorer, analyst, critic"},
"explorer": {"techniques": "summarize, label_balance"},
"analyst": {"techniques": "stratified, standard, logit"},
"critic": {"techniques": "roc_auc, f1, confusion"},
}
project = amos.Project.create(settings, item = data)
print(project.result.metrics["roc_auc"] == dataset.metrics["roc_auc"])
# True
print(list(project.result.tables))
# ['summarize', 'label_balance', 'confusion']
The project did the same work as the techniques applied by hand, and got the
same result, because both used the same seed. Project.create worked on a
copy of data, so the data you passed is never changed.
4. Compare alternatives¶
There is rarely one obvious way to prepare data or one obvious model. An
experiment tries several. Give the analyst steps, each with a list of
alternative techniques, and a criterion to compare them with:
settings["analyst"] = {
"design": "experiment",
"criterion": "roc_auc",
"steps": "split, scale, model",
"split_techniques": "stratified",
"scale_techniques": "standard, robust, none",
"model_techniques": "logit, random_forest",
}
project = amos.Project.create(settings, item = data)
analyst = {node.name: node for node in project.workflow}["analyst"]
print(len(analyst.results))
# 6
The experiment tried every combination of one technique from each step (1
splitter, 3 scalers, and 2 models make 6), each on its own copy of the data.
It kept the combination with the highest ROC AUC on the test rows and passed it
on to the critic. none is a technique that does nothing, so it tests whether
scaling helps at all.
Every combination is compared in a table, named for the worker. Each combination is labeled with its steps, each named "{technique}_{step}":
table = project.result.tables["analyst_comparison"]
print(list(table.columns))
# ['rank', 'score', 'roc_auc']
print(table.index[0] == analyst.winner)
# True
print(table.index[0].startswith("stratified_split > "))
# True
The combinations are always tried in the same order, and ties go to the
first, so the comparison is the same every time. A contest (from
chrisjen) does the same thing without the table.
5. Use a settings file¶
Settings can live in an ini, toml, json, yaml, or Python file. In an ini file,
numbers, True and False, and comma-separated lists are converted for you:
[general]
label = target
seed = 43
[cancer_project]
cancer_workers = explorer, analyst, critic
[explorer]
techniques = summarize, label_balance
[analyst]
design = experiment
criterion = roc_auc
steps = split, scale, model
split_techniques = stratified
scale_techniques = standard, robust, none
model_techniques = logit, random_forest
[critic]
techniques = scorecard, confusion
[random_forest_parameters]
n_estimators = 200
max_depth = 5
Pass the path to the file instead of a dict:
project = amos.Project.create("study.ini", item = data)
print(sorted(project.result.metrics)[:4])
# ['accuracy', 'balanced_accuracy', 'f1', 'log_loss']
The scorecard compared every branch of the experiment on every standard
metric for the task, and stored the final model's scores in metrics. It is
ready to publish:
scorecard = project.scorecard
print(len(scorecard.table), scorecard.heading)
# 6 6 branches ranked by roc_auc
print(scorecard.to_markdown().splitlines()[0])
# | rank | split | scale | model | roc_auc | accuracy | balanced_accuracy | precision | recall | f1 | log_loss |
Save it with to_csv, to_markdown, to_word (a Word document), or
to_image (such as a png file), or in all four formats with export.
6. Set parameters¶
A section named {technique}_parameters holds parameters for that technique,
as random_forest_parameters does above. They are passed to the tool that the
technique wraps (here, scikit-learn's RandomForestClassifier), and a tool
only receives the parameters it accepts. A section for a worker (such as
analyst_parameters) passes its parameters to every technique in the worker.
Any model can search for the best value of parameters given as lists. Add "search" ("grid", "random", or "optuna") and list the values:
settings["analyst"]["model_techniques"] = "random_forest"
settings["random_forest_parameters"] = {
"search": "grid",
"max_depth": [3, 6],
"n_estimators": [50, 100],
"cv": 3,
}
project = amos.Project.create(settings, item = data)
search = project.result.tables["random_forest_search"]
print(len(search), list(search.columns[-3:]))
# 4 ['mean_test_score', 'std_test_score', 'rank_test_score']
The search used 3-fold cross-validation on the training rows only and refitted
the best combination to all of them. For a random or Optuna search, a list
of two numbers is a range to draw from, and "n_iter" sets the number of
draws. An Optuna search (which needs the tuning extra) learns from each
draw which values to try next.
7. Evaluate and draw¶
The critic evaluates the model, and the artist draws it. Tables go in
tables and figures in figures:
settings["critic"] = {"techniques": "scorecard, confusion, permutation_importance"}
settings["artist"] = {"techniques": "roc_curve, importance_plot"}
settings["cancer_project"]["cancer_workers"] = "explorer, analyst, critic, artist"
project = amos.Project.create(settings, item = data)
print(project.result.tables["confusion"].to_numpy().sum())
# 143
figure = project.result.figures["roc_curve"]
print(type(figure).__name__)
# Figure
Figures are matplotlib figures. In a notebook, a figure is shown when it is
the last line of a cell. Save one with figure.savefig("roc.png").
8. Report and export¶
A project's report describes what it did:
lines = project.report.contents.splitlines()
print(lines[0])
# project: cancer
print(lines[1].startswith("id: cancer_"))
# True
print(lines[2])
# workflow: explorer > analyst > critic > artist
Each run has an id, which is the name of the project and the date and time
unless you pass id to Project.create. export saves the
report, the settings, the versions of Python and every package that did the
work, the history of every technique and its parameters, the metrics, the
predictions, and every table and figure:
folder = project.export()
print(sorted(path.name for path in (folder / "tables").iterdir()))
# ['analyst_comparison.csv', 'confusion.csv', 'label_balance.csv', 'permutation_importance.csv', 'random_forest_search.csv', 'scorecard.csv', 'summarize.csv']
Anyone with the settings file, the data, and "environment.json" can reproduce these results exactly.
Next steps¶
- The advanced user guide describes the settings and shows how to write your own techniques, and the technique catalog lists every technique.
- The recipes show complete studies: regression with statistical inference, imbalanced classes, categorical data, and more.