Recipes¶
Short, complete examples for common jobs. Except where noted, they are run by
the chrisjen unit tests. See the tutorial for the basics and
the advanced user guide for details.
Pick the best of several methods¶
A forecast can be made in different ways. A contest runs each way on the same
data and keeps the one with the best score. The item here is a dict, and each
technique adds a "prediction" to it. The criterion returns the negative error,
so that the smallest error has the highest score.
import dataclasses
import statistics
import chrisjen
@dataclasses.dataclass
class LastValue(chrisjen.Technique):
def implement(self, item, **kwargs):
return {**item, "prediction": item["values"][-1]}
@dataclasses.dataclass
class Average(chrisjen.Technique):
def implement(self, item, **kwargs):
return {**item, "prediction": statistics.fmean(item["values"])}
@dataclasses.dataclass
class RecentAverage(chrisjen.Technique):
def implement(self, item, window = 3, **kwargs):
recent = item["values"][-window:]
return {**item, "prediction": statistics.fmean(recent)}
@dataclasses.dataclass
class ForecastError(chrisjen.Criteria):
def score(self, item):
return -abs(item["prediction"] - item["truth"])
settings = {
"forecast_project": {
"design": "contest",
"criterion": "forecast_error",
"techniques": "last_value, average, recent_average",
},
}
data = {"values": [10, 12, 11, 13, 14, 15], "truth": 16}
project = chrisjen.Project.create(settings, item = data)
print(project.workflow.winner, project.result["prediction"])
# last_value 15
Try several values of a parameter¶
Parameters belong to names, so to compare parameter values, give each value a name with a small subclass:
@dataclasses.dataclass
class Window2(RecentAverage):
parameters: dict = dataclasses.field(
default_factory = lambda: {"window": 2})
@dataclasses.dataclass
class Window5(RecentAverage):
parameters: dict = dataclasses.field(
default_factory = lambda: {"window": 5})
settings["forecast_project"]["techniques"] = "window2, window5"
project = chrisjen.Project.create(settings, item = data)
scores = {
name: project.workflow.criteria.score(result)
for name, result in project.workflow.results.items()}
print(project.workflow.winner, scores)
# window2 {'window2': -1.5, 'window5': -3.0}
A {name}_parameters section in the settings is added to these defaults (and
replaces any with the same name).
Average several approaches¶
survey runs every alternative and averages the results, instead of choosing
one:
@dataclasses.dataclass
class EstimateLow(chrisjen.Technique):
def implement(self, item, **kwargs):
return sum(item) * 0.9
@dataclasses.dataclass
class EstimateHigh(chrisjen.Technique):
def implement(self, item, **kwargs):
return sum(item) * 1.1
settings = {
"estimate_project": {
"design": "survey",
"techniques": "estimate_low, estimate_high",
},
}
print(round(chrisjen.Project.create(settings, item = [10, 20, 30]).result, 6))
# 60.0
Repeat until the result is good enough¶
benchmark repeats its sequence until its criteria's test passes:
@dataclasses.dataclass
class HalveStep(chrisjen.Technique):
def implement(self, item, **kwargs):
return item / 2
@dataclasses.dataclass
class SmallEnough(chrisjen.Criteria):
def score(self, item):
return item < 0.01
settings = {
"shrink_project": {
"design": "benchmark",
"criterion": "small_enough",
"techniques": "halve_step",
},
}
project = chrisjen.Project.create(settings, item = 1.0)
print(project.result, project.workflow.iterations)
# 0.0078125 7
Run one project on many inputs¶
A project's workflow is built once. apply can be called again with new items:
@dataclasses.dataclass
class DropNegatives(chrisjen.Technique):
def implement(self, item, **kwargs):
return [x for x in item if x >= 0]
@dataclasses.dataclass
class Scale(chrisjen.Technique):
def implement(self, item, factor = 2, **kwargs):
return [x * factor for x in item]
settings = {"clean_project": {"techniques": "drop_negatives, scale"}}
project = chrisjen.Project.create(settings, automatic = False)
for batch in ([1, -1], [2, 3], [-5]):
print(project.apply(batch))
# [2]
# [4, 6]
# []
Log every node¶
To add behavior to a design, subclass it. This one records the name of every
node before applying it. The new design is available as logged in settings:
@dataclasses.dataclass
class Logged(chrisjen.Flow):
log: list = dataclasses.field(default_factory = list)
def implement(self, item, **kwargs):
self.log.clear()
for path in self.walk():
for node in path:
self.log.append(node.name)
item = node.apply(item, **kwargs)
return item
settings = {
"logging_project": {
"design": "logged",
"techniques": "drop_negatives, scale",
},
}
project = chrisjen.Project.create(settings, item = [1, -1])
print(project.result, project.workflow.log)
# [2] ['drop_negatives', 'scale']
Use a data frame¶
The item can be any object. This example uses pandas (which you need to
install yourself) and saves the result with the project's clerk. It is not run
by the unit tests because pandas is an optional dependency.
# skip
import dataclasses
import pathlib
import tempfile
import pandas as pd
import chrisjen
@dataclasses.dataclass
class FillMissing(chrisjen.Technique):
def implement(self, item, value = 0, **kwargs):
return item.fillna(value)
@dataclasses.dataclass
class Standardize(chrisjen.Technique):
def implement(self, item, **kwargs):
return (item - item.mean()) / item.std()
@dataclasses.dataclass
class SummarizeColumns(chrisjen.Technique):
def implement(self, item, **kwargs):
return item.describe().loc[["mean", "std"]]
settings = {
"analysis_project": {"analysis_workers": "prepare, report"},
"prepare": {"techniques": "fill_missing, standardize"},
"report": {"techniques": "summarize_columns"},
"fill_missing_parameters": {"value": 0},
}
data = pd.DataFrame({"a": [1.0, None, 3.0], "b": [4.0, 5.0, 6.0]})
project = chrisjen.Project.create(
settings, item = data, clerk = pathlib.Path(tempfile.mkdtemp()))
print(project.result.round(2))
# a b
# mean 0.0 0.0
# std 1.0 1.0
project.clerk.save(project.result, file_name = "summary.csv")
Because the file name ends in ".csv", the clerk uses its CSV format.
Keep techniques in their own module¶
Techniques, designs, and criteria are added to the library when the module that
defines them is imported. Put them in a module, import it before you create a
project, and refer to them by name in a settings file. Nothing else needs to be
passed to Project.create.
# skip
# techniques.py
import dataclasses
import chrisjen
@dataclasses.dataclass
class Clean(chrisjen.Technique):
def implement(self, item, **kwargs):
...
# main.py
import chrisjen
import techniques # noqa: F401 (adds the techniques to the library)
project = chrisjen.Project.create("settings.ini", item = my_data)