<jupyter_start><jupyter_text>Example ML Flow  Introduction<jupyter_code>We'll consider a typical (but simplified) machine learning flow.

*(The code for this example is available in the Bionic repo at
example/ml_workflow.py.  Run* ``pip install 'bionic[examples]'``
*to ensure you have the required dependencies.)*

.. literalinclude:: ../../example/ml_workflow.py
    :language: python
    :linenos:<jupyter_output><empty_output><jupyter_text>Configure the PYTHONPATH for this notebook.import _tutorial_setup Delete the Bionic cache directory, to make sure we're starting fresh.import shutilshutil.rmtree('./bndata', ignore_errors=True)<jupyter_code>Let's start by importing the flow into a notebook and visualizing it.<jupyter_output><empty_output><jupyter_text>from example.ml_workflow import flowflow.render_dag()<jupyter_code>This is still a toy example, but it demonstrates a classic supervised
machine learning flow for binary classification:

* Load data (``raw_frame``)
* Clean it (``features_frame``)
* Split into training and test sets (``train_frame`` and ``test_frame``)
* Fit a model on the training data (``model``)
* Evaluate the model on the test data (``predictions_frame``, ``precision_recall_frame``)
* Visualize the model's performance (``all_hyperparams_pr_plot``)

As in the previous example, we can access any of the entity values.
However, this time we'll enable logging so we can see every entity that Bionic computes.<jupyter_output><empty_output><jupyter_text>import logging; logging.basicConfig(level='INFO', format='%(message)s')flow.get.train_frame().head() flow.get.all_hyperparams_pr_plot()<jupyter_code>Note that in the logs, each entity is computed exactly once --
every value is cached both in memory and on disk to avoid redundant computation.<jupyter_output><empty_output><jupyter_text>Changing Inputs<jupyter_code>Now that we have our flow, we might want to experiment with
different parameter settings.
For example, we can try changing our regularization hyperparameter ``C``:<jupyter_output><empty_output><jupyter_text>flow.setting('hyperparams_dict', {'C': 10}).get.all_hyperparams_pr_plot()<jupyter_code>Bionic re-runs the flow with the changed parameter,
generating a new plot.

Since we've created a new copy of our flow,
Bionic doesn't try to reuse its in-memory cache.
However, we can see it that it only recomputed those
entities whose values changed -- the others were loaded from disk.

Similarly, we can try training our model on a subset of the available features:<jupyter_output><empty_output><jupyter_text>flow.setting('feature_inclusion_regex', 'mean.*').get.all_hyperparams_pr_plot()<jupyter_code>Naturally, reducing the number of features makes the model perform worse.

We can also apply this flow to a completely different dataset,
such as the UCI ML Wine dataset.
(This is actually a multiclass dataset,
but to match the original example we'll use binary labels.)<jupyter_output><empty_output><jupyter_text>from sklearn import datasetsimport pandas as pdwine_data = datasets.load_wine()wine_frame = pd.DataFrame(    data=wine_data['data'],    columns=wine_data['feature_names'])wine_frame['target'] = (wine_data['target'] == 0)wine_flow = flow.setting('raw_frame', wine_frame)wine_flow.get.all_hyperparams_pr_plot()<jupyter_code>## Multiplicity<jupyter_output><empty_output><jupyter_text>Now we have a flow for evaluating any given set of parameters.But usually we want to compare multiple variations at once,within a single flow.Bionic lets us do this by setting multiple values for a single entity:<jupyter_code>flow3 = flow.setting('hyperparams_dict', values=[{'C': 0.1}, {'C': 1}, {'C': 10}])
flow3.get.all_hyperparams_pr_plot()<jupyter_output><empty_output><jupyter_text>Here we can see the PR curves for each of three hyperparameter configurations.But how did this work?Let's start by visualizing the dependency graph of our new flow:<jupyter_code>flow3.render_dag()<jupyter_output><empty_output><jupyter_text>When we specify multiple values for an entity, Bionic creates multiple instances of that entity *and of all downstream entities*.If there are three instances of ``hyperparams_dict``,there must also be three instances of ``model`` and ``precision_recall_frame``.However, we can also see that there is only one instance of ``all_hyperparams_plot_pr``.This is because we used the ``@gather`` decorator when defining this entity:Bionic has aggregated all the variations of ``hyperparameters_dict`` and ``precision_recall_frame``into a single frame, so they can be plotted together.Now, because we have three different model instances,it no longer makes sense to request "the" model:<jupyter_code>try:
    flow3.get.model()
except Exception as e:
    caught_exception = e
caught_exception<jupyter_output><empty_output><jupyter_text>If we want to access an entity with multiple values,we need to tell Bionic what data structure to use toaggregate them:<jupyter_code>flow3.get.model('series')<jupyter_output><empty_output><jupyter_text>Extended Multiplicity<jupyter_code>We can apply the same approach to any number of entities:<jupyter_output><empty_output><jupyter_text>flow6 = flow3.setting('feature_inclusion_regex', values=['.*', 'mean.*'])flow6.render_dag()<jupyter_code>Here we can see that each entity's multiplicity is propagated through the entire graph.
For example, there are two ``train_frame`` nodes and two ``test_frame`` nodes,
because those depend on ``feature_inclusion_regex`` but not on ``hyperparams_dict``.
However, there are six ``model`` nodes, because there are six (two times three) combinations of
``train_frame`` and ``hyperparams_dict``.

We can also see two instances of ``all_hyperparams_pr_plot``,
corresponding to the two instances of ``feature_inclusion_regex``.
This illustrates another nuance of how we used ``@gather``:
we asked Bionic to include *every* variation of ``hyperparams_dict``,
*along with* the associated values of ``precision_recall_frame``.
If we had used

.. code-block:: python

    @gather(
        over=['hyperparams_dict', 'feature_inclusion_regex'],
        also='precision_recall_frame',
        into='gathered_frame')
    def all_variations_pr_plot(...):
        ...

then all the values would be gathered into a single frame,
and there would be one single plot node.<jupyter_output><empty_output><jupyter_text>Wrapping Up<jupyter_code>This tutorial has illustrated four topics:

1. How to use Bionic to assemble a more complex data science flow.
2. How Bionic uses caching to avoid redundant computation.
3. How to repurpose an existing flow to use new parameters or data.
4. How to use multiplicity to compare several variations within a single flow.

These concepts should be enough to get started building real flows.
The next section of the documentation will discuss the underlying concepts in Bionic.<jupyter_output><empty_output>
{'fraction_non_alphanumeric': 0.08913486746286048, 'fraction_numerical': 0.0029129041654529565, 'mean_word_length': 6.360128617363344, 'pattern_counts': {'":': 0, '<': 61, '<?xml version=': 0, '>': 61, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'chain_length': '15', 'dir': 'jupyter-structured-clean-dedup', 'id': '303974', 'license': 'Apache-2.0', 'n_tokens_mistral': 2035, 'n_tokens_neox': 1950, 'n_words': 735, 'path': 'docs/tutorials/ml_workflow.ipynb', 'repo_name': 'baxen/bionic', 'size': '6866'}