(C)ompute (O)perations in (D)ependency (O)rder
Project description
CODO: Compute Operations in Dependency Order
CODO is an easy-to-use library which abstracts the programming pattern where a set of operations (functions) with the same parameter lists must be computed over the same data, but where some of those operations might depend on other operations' outputs.
A simple example is computing precision, recall, accuracy, and f1 scores for classification tasks.
All of these can be computed with their own functions that each take two arrays holding the true and predicted class labels, but then we'd be computing TN, FN, FP, and FN multiple times over (and we'd even have to re-compute precision and recall to get the f1 score). Alternatively, you could compute all these metrics in the scope of a single function, but for more complex tasks, it's often desirable to isolate this kind of logic into separate functions that each have one job.
CODO to the rescue: this library allows you to easily define these operations to isolate their logic with a simple API, automatically figure out the right order to run them, run the "shared" logic only once, and (optionally) parallelize where possible.
How to use
-
Define the params that each
Operationwill take. Important: AllOperations you plan to compute in the sameCODOobject must accept the same argument list. We wrap those params with a dataclass to enable full type safety. In our example, we'll define the types for the "true" and "predicted" class labels.from dataclasses import dataclass @dataclass class MetricsParams: y_true: list[int] y_pred: list[int]
-
Define operations by subclassing
codo.Operation. The library defines this base class with two generic parameters that specify the input and output types for its_call()method, respectively (see code comments for details).Subclasses must override the
_call()method, and may set a list of itsdependenciesas a class variable.The
_call()method takes two parameters:acc: Acodo.Accumulator[ParamsT]object, whereParamsTis a generic variable that in our case represents ourMetricsParamsdataclass. Essentially this is a glorifieddictproviding full type safety that stores the result of operations computed earlier in the dependency tree. You can access those results by keying intoaccwith the operation's class.params: An instance of theMetricsParamsdataclass we defined above.
from codo import Operation from typing import override # When we subclass `Operation[MetricsParams, int]` below, the generics # are providing a shorthand for the following signature for the `_call()` method: # # def _call( # self, # acc: codo.Accumulator[MetricsParams], # params: MetricsParams # ) -> int: # ... # # The first generic param sets the type for `params`, # and the second sets the method's return type. class TruePositives(Operation[MetricsParams, int]): @override def _call(self, acc, params): return sum(t == 1 and p == 1 for t, p in zip(true, pred)) class FalsePositives(Operation[MetricsParams, int]): @override def _call(self, acc, params): return sum(t == 0 and p == 1 for t, p in zip(true, pred)) class Precision(Operation[MetricsParams, float]): dependencies = [TruePositives, FalsePositives] @override def _call(self, acc, params): tp = acc[TruePositives] fp = acc[FalsePositives] # ^ Both resolve to `int` types here thanks to # generics on TruePositives/FalsePositives! return tp / (tp + fp)
-
Create a
CODOobject with yourOperations, then call it like a function to get the outputs.You may need to define
Operations for intermediate steps where you don't actually care about the output—in this case, you can setsilent=Truefor that operation. This includes them in the dependency graph, but discards their output in the final returned dict unlesswith_silents=True.from codo import CODO codo = CODO([ Precision(), TruePositives(silent=True), FalsePositives(silent=True), ]) # ^ Order doesn't matter in this list, Operations # will be computed in a valid dependency order. codo( MetricsParams( y_true=[1, 0, 1, 1, 0, 1], y_pred=[1, 1, 1, 0, 0, 1] ), # with_silents=True ) # {<class '__main__.Precision'>: 0.75}
Full example
The code below computes precision, recall, accuracy, and f1 scores.
from typing import override
"""
Define parameter list for all Operations
"""
@dataclass
class MetricsParams:
y_true: list[int]
y_pred: list[int]
"""
Define your Operations
"""
class TruePositives(Operation[MetricsParams, int]):
@override
def __call__(self, acc, params):
return sum(t == p == 1 for t, p in zip(params.y_true, params.y_pred))
class TrueNegatives(Operation[MetricsParams, int]):
@override
def __call__(self, acc, params):
return sum(t == p == 0 for t, p in zip(params.y_true, params.y_pred))
class FalsePositives(Operation[MetricsParams, int]):
@override
def __call__(self, acc, params):
return sum(t == 0 and p == 1 for t, p in zip(params.y_true, params.y_pred))
class FalseNegatives(Operation[MetricsParams, int]):
@override
def __call__(self, acc, params):
return sum(t == 1 and p == 0 for t, p in zip(params.y_true, params.y_pred))
class Precision(Operation[MetricsParams, float]):
dependencies = [TruePositives, FalsePositives]
@override
def __call__(self, acc, params):
tp = acc[TruePositives] # Resolves to `int` type
fp = acc[FalsePositives]
return tp / (tp + fp)
class Recall(Operation[MetricsParams, float]):
dependencies = [TruePositives, FalseNegatives]
@override
def __call__(self, acc, params):
tp = acc[TruePositives]
fn = acc[FalseNegatives]
return tp / (tp + fn)
class Accuracy(Operation[MetricsParams, float]):
dependencies = [TruePositives, TrueNegatives, FalsePositives, FalseNegatives]
@override
def __call__(self, acc, params):
tp = acc[TruePositives]
tn = acc[TrueNegatives]
fp = acc[FalsePositives]
fn = acc[FalseNegatives]
return (tp + tn) / (tp + tn + fp + fn)
class F1Score(Operation[MetricsParams, float]):
dependencies = [Precision, Recall]
@override
def __call__(self, acc, params):
precision = acc[Precision]
recall = acc[Recall]
return 2 * (precision * recall) / (precision + recall)
# Wrap all Operations in a CODO object. Defining the generic param type
# in-line allows you to query into `result` below with type safety.
# Dependency order doesn't matter in this list.
codo = CODO[MetricsParams](
[
Precision(),
Recall(),
Accuracy(),
F1Score(),
TruePositives(silent=True),
TrueNegatives(silent=True),
FalsePositives(silent=True),
FalseNegatives(silent=True),
]
)
# Call your object like a function on arbitrary data
result = codo(
MetricsParams(y_true=[1, 0, 1, 1, 0, 1], y_pred=[1, 1, 1, 0, 0, 1]),
# with_silents=True,
)
# accuracy = result[Accuracy] # Resolves to `float` type
print(result)
'''
{
<class '__main__.Precision'>: 0.75,
<class '__main__.Recall'>: 0.75,
<class '__main__.Accuracy'>: 0.6666666666666666,
<class '__main__.F1Score'>: 0.75
}
'''
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distributions
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file codo_api-1.0.0-py3-none-any.whl.
File metadata
- Download URL: codo_api-1.0.0-py3-none-any.whl
- Upload date:
- Size: 6.4 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.1.0 CPython/3.12.8
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
eeb496f2043ca4fb94445d054bce404a234807ae8d87167bf63eb9fd390bd204
|
|
| MD5 |
65d85f1cd89ace8691e7e0a6ad5665b4
|
|
| BLAKE2b-256 |
e3ba2957060d445237d972adbf8b414411b84627fd1fca406dd7630f4aa10b57
|