<jupyter_start><jupyter_text>Tutorial: How to Create Workflows with RFlowThis tutorial shows how the RFlow module can help to develop scientific-computational experiments.Consider the hypothetical routine of computing the frequency of words from a text file. It is a simplification of a typical stage in machine learning, natural language processing, or even bioinformatics. Although simple, it summarizes two practical problems addressed by researchers:* How to reproduce and share experiments.* How to do experiments based on interchangeable parts.In this example, the first step taken by many developers would to start with the main logic for counting:<jupyter_code>def compute_word_freq(input_filepath):
    """Compute word frequency from a input text file.
    
    Args:
        input_filepath (str): Input text file path.
           
    Returns:
        Dict[str: int]: Each key is a word, and its 
         values are their frequency.
    """

    word_freq = {}

    # Count the words
    with open(input_filepath, 'r') as stream:
        for line in stream:
            for word in line.split():
                word = word.lower()
                word_freq[word] = word_freq.get(word, 0) + 1

    return word_freq<jupyter_output><empty_output><jupyter_text>The above function returns a dictionary with the count of every word. It's run as follows on the `big-text.txt` text file:<jupyter_code>word_freq = compute_word_freq('big-text.txt')<jupyter_output><empty_output><jupyter_text>An essential part of any experimentation is visualizing stage results. A researcher could write the following code to show the `K` most frequent words:<jupyter_code>def print_kmost(word_freq, K=5):
    """Prints the K most frequent words.
    
    Args:
        word_freq (Dict[str: int]): Each key is a word, 
         and its values are its counting.
        K (int): Specify how many words to show.
    """

    ordered_words = sorted(word_freq.items(),
                           key=lambda item: item[1],
                           reverse=True)
    for i, (word, count) in enumerate(ordered_words):
        if i == K:
            break
        print('{}-nth word: "{}" happens {} times'.format(
              i, word, count))<jupyter_output><empty_output><jupyter_text>Then the complete code of this small experiment becomes:<jupyter_code>%%time
word_freq = compute_word_freq('big-text.txt')
print_kmost(word_freq, K=10)<jupyter_output>0-nth word: "the" happens 78172 times
1-nth word: "of" happens 39450 times
2-nth word: "and" happens 37009 times
3-nth word: "to" happens 28295 times
4-nth word: "in" happens 21393 times
5-nth word: "a" happens 20610 times
6-nth word: "that" happens 11483 times
7-nth word: "he" happens 11481 times
8-nth word: "was" happens 11159 times
9-nth word: "his" happens 9961 times
CPU times: user 558 ms, sys: 20.3 ms, total: 578 ms
Wall time: 566 ms<jupyter_text>Actual experiments, as typically found in Deep Learning or Big-data, may take hours or days.In order not to perform processing unnecessarily, a common strategy is to leave the experiment reentrant. That is, saving intermediate results in files, and load them in future executions.Continuing with this idea, routines to save and load data can be like this:<jupyter_code>def save_freq(word_freq, output_filepath):    
    with open(output_filepath, 'w') as stream:
        for word, cont in word_freq.items():
            stream.write('{} {}\n'.format(word, cont))
            
def load_freq(input_filepath):
    with open(input_filepath, 'r') as stream:
        word_freq = {}
        for line in stream:
            word, cont = line.split()
            word_freq[word] = int(cont)
    return word_freq<jupyter_output><empty_output><jupyter_text>The naive way of joining the steps is to check for the existence of the generated files manually:<jupyter_code>import os

def exp1():
    if not os.path.exists("big-text-freq.txt"):
        word_freq = compute_word_freq("big-text.txt")
        save_freq(word_freq, "big-text-freq.txt")
    else:
        word_freq = load_freq("big-text-freq.txt")

    print_kmost(word_freq, 10)
# For demonstration pruposes, assure that 
# the frequency file doesn't exists.
import os

if os.path.exists('big-text-freq.txt'):
    os.remove('big-text-freq.txt')
%%time
exp1()<jupyter_output>0-nth word: "the" happens 78172 times
1-nth word: "of" happens 39450 times
2-nth word: "and" happens 37009 times
3-nth word: "to" happens 28295 times
4-nth word: "in" happens 21393 times
5-nth word: "a" happens 20610 times
6-nth word: "that" happens 11483 times
7-nth word: "he" happens 11481 times
8-nth word: "was" happens 11159 times
9-nth word: "his" happens 9961 times
CPU times: user 648 ms, sys: 16.3 ms, total: 664 ms
Wall time: 649 ms<jupyter_text>Now rerunning the experiment, the processing time will undergo a drastic reduction:<jupyter_code>%%time
exp1()<jupyter_output>0-nth word: "the" happens 78172 times
1-nth word: "of" happens 39450 times
2-nth word: "and" happens 37009 times
3-nth word: "to" happens 28295 times
4-nth word: "in" happens 21393 times
5-nth word: "a" happens 20610 times
6-nth word: "that" happens 11483 times
7-nth word: "he" happens 11481 times
8-nth word: "was" happens 11159 times
9-nth word: "his" happens 9961 times
CPU times: user 94 ms, sys: 67 µs, total: 94.1 ms
Wall time: 91.1 ms<jupyter_text>Note that reading the results of a file was at least 2 times faster than re-computing again.However, actual experiments contain multiple stages, and it would not be productive to repeat the reentrant logic code every time. More importantly, if parameters changes, then the affected parts or dependencies need to be rerun. For example, `compute_word_freq` stage depends on the data of 'big.txt'. Dependency means that if the file "big.txt" is modified, then the count must be (re)done even if its intermediary exists.Tools like Make, Luigi, Weka, and our RFlow, encapsulate this check by modeling experiment as a direct acyclic graph (DAG). With experiments being viewed as graphs, the computations and data become the nodes, and their dependencies become the edges.RFlow also represents experiments as DAG. We'll start a graph with RFlow; then we'll define a node for computing word frequencies, and add it to the DAG. One way to get a DAG object is to call `rflow.get_graph`:<jupyter_code>import rflow
exp1 = rflow.get_graph('exp1')<jupyter_output><empty_output><jupyter_text>This will create a DAG named `'exp1'`. On RFlow, any DAG can be retrieved from the `get_graph` function.To define nodes, users should inherit from the class `rflow.Interface`. The derived classes must implement a `evaluate()` function, and a optional `load()`. The first one is where the main logic should put, and save its intermediary results. The second one, `load()`, should keep the node reentrant, by loading the previously computed value. The node type `WordFrequency` will carry previous word frequency functionality of computing and load:<jupyter_code>class WordFrequency(rflow.Interface):
    """Compute word frequencies from a input text file.
    """

    def evaluate(self, resource, input_resource):
        """Args:

            resource (skrkit.workflow.FSResource): Reentrancy checkpoint. 

            input_resource (skrkit.workflow.FSResource): Resource pointing to 
                the input text file path.
             
        Returns:
            Dict[str: int]: Each key is a word, and values are its
             counting.
        """
        
        word_freq = compute_word_freq(input_resource.filepath)        
        save_freq(word_freq, resource.filepath)
        return word_freq

    def load(self, resource):
        """Loads the data from previous `evaluate()` call.

        Args:
            resource (skrkit.workflow.FSResource): Reentrancy checkpoint. 

        Returns:
            Dict[str: int]: Each key is a word, and values are its
             counting.
        """

        return load_freq(resource.filepath)<jupyter_output><empty_output><jupyter_text>The above code needs some clarications.    As early stated, RFlow can track changes of nodes inputs. To track changes that occur inside external entities such files or database entry, RFlow provides classes derived from `rflow.Resource` which encapsulates existence testing and content representation. The `evaluate()` function, calls the main logic and save the results. Node's `evaluate()` function may take any number of arguments. In the above case, it accepts two arguments: `resource` and `input_resource`. The `resource` name is a special one for RFlow. Arguments named as this are expected to be a `Resource` type. It tells to RFlow that the node is reentrant, and its value must refer to an intermediary checkpoint destination. The code refers to a `filepath` attribute, meaning that `WordFrequency` expects a file system resource - a `rflow.FSResource` instance.The second argument is also expected to be a `rflow.FSResource`. So RFlow will detect changes on the file content, and rerun the `evaluate()` if they occur.The `load()` function is called whatever RFlow judges that no alteration occurred to the node's inputs. So it can be loaded.Until now, only the node definition were shown. To put the class into use, it must be instantiated and added to a DAG:<jupyter_code>exp1.word_freq = WordFrequency(rflow.FSResource('big-text-freq.txt'))<jupyter_output><empty_output><jupyter_text>A node is added by instancing and assigning it to a DAG attribute. For those not familiar with Python, new attributes can be added to an object on the fly. Every instanced node have a unique name given by its attribute name, as `word_freq` in the previous line. This name can be referenced to invoke it from command-line.The constructor argument, `rflow.FSResource('big-text-freq.txt')`, is the `resource` argument value. The `resource` parameter is so special that it be can passed directly on the constructor. Following its node definition, it's a file system resource.Other arguments are assigned to nodes by using its `.args` attribute. By reflection, RFlow fills `.args` with attributes named after each  `evaluate()` arguments. For example, to set `input_resource` pointing to the 'big-text.txt' file system resource:<jupyter_code>exp1.word_freq.args.input_resource = rflow.FSResource('big-text.txt')<jupyter_output><empty_output><jupyter_text>To execute any node, invoke its `call()` method instead of `evaluate()` or `load()`. This function will verify the reentracy conditions, and use the appropiate method:<jupyter_code>word_freq = exp1.word_freq.call()<jupyter_output>[32mLOAD exp1:word_freq
[0m^[32mDONE exp1:word_freq
[0m<jupyter_text>The `call()` method returns the same as `evaluate()` (or `load()`). Let's keep building the rest of the experiment, now by defining a node to show words.<jupyter_code>class PrintKMost(rflow.Interface):
    """Prints the K most frequent words.    
    """

    def evaluate(self, word_freq, K):   
        """Args:
        word_freq (Dict[str: int]): Each key is a word, 
         and its values are their frequency.
         
        K (int): Specify how many words to show.
        """ 

        ordered_words = sorted(word_freq.items(),
                               key=lambda item: item[1],
                               reverse=True)
        for i, (word, count) in enumerate(ordered_words):
            if i == K:
                break
            print('{}-nth word: "{}" happens {} times'.format(
                  i, word, count))<jupyter_output><empty_output><jupyter_text>The `PrintKMost` node is a simpler one. As it doesn't produce any intermediary results, so no do need to have a `resource` argument. Differently from the `WordFrequency`, it accepts common python object like the `word_freq` dict or the `K` integer.  Adding to the graph:<jupyter_code>exp1.kmost_freq = PrintKMost()
with exp1.kmost_freq as args:    
    args.word_freq = exp1.word_freq
    args.K = 10<jupyter_output><empty_output><jupyter_text>As a shortcut to `args` attribute, users may use the `with` statement to not repeat the `graph.vertice.args.` pattern. When setting `args.word_freq = exp1.word_freq` argument, we create a edge from the `exp1.word_freq` node to `exp1.kmost_freq`.  The return value of `exp1.word_freq` will be send to the `word_freq` argument of `kmost_freq`. The `K` argument was set to a constant value.Like for `word_freq`, the `kmost_freq` is executed with `call()`. Besides checking reentrancy, the call method resolves dependency call, executing only the need nodes.<jupyter_code>exp1.kmost_freq.call()<jupyter_output>[32mUPDATE  exp1:kmost_freq
[0m.[33mRUN  exp1:kmost_freq
[0m0-nth word: "the" happens 78172 times
1-nth word: "of" happens 39450 times
2-nth word: "and" happens 37009 times
3-nth word: "to" happens 28295 times
4-nth word: "in" happens 21393 times
5-nth word: "a" happens 20610 times
6-nth word: "that" happens 11483 times
7-nth word: "he" happens 11481 times
8-nth word: "was" happens 11159 times
9-nth word: "his" happens 9961 times
.^[33mexp1:kmost_freq
[0m<jupyter_text>In that call, `exp1.word_freq` was evaluated. The next example will clean the middle node, and rerun:<jupyter_code>exp1.word_freq.clear()
exp1.kmost_freq.call()<jupyter_output>Removing resource big-text-freq.txt
.[33mUPDATE  exp1:kmost_freq
[0m..[34mUPDATE  exp1:word_freq
[0m...[35mRUN  exp1:word_freq
[0m...^[35mexp1:word_freq
[0m...[35mRUN  exp1:kmost_freq
[0m0-nth word: "the" happens 78172 times
1-nth word: "of" happens 39450 times
2-nth word: "and" happens 37009 times
3-nth word: "to" happens 28295 times
4-nth word: "in" happens 21393 times
5-nth word: "a" happens 20610 times
6-nth word: "that" happens 11483 times
7-nth word: "he" happens 11481 times
8-nth word: "was" happens 11159 times
9-nth word: "his" happens 9961 times
...^[35mexp1:kmost_freq
[0m<jupyter_text>Executing it again:<jupyter_code># RFlow caches previous values. 
# Use `redo=True` to force the call
# node execution
exp1.kmost_freq.call(redo=True)<jupyter_output>...[35mUPDATE  exp1:kmost_freq
[0m....[36mRUN  exp1:kmost_freq
[0m0-nth word: "the" happens 78172 times
1-nth word: "of" happens 39450 times
2-nth word: "and" happens 37009 times
3-nth word: "to" happens 28295 times
4-nth word: "in" happens 21393 times
5-nth word: "a" happens 20610 times
6-nth word: "that" happens 11483 times
7-nth word: "he" happens 11481 times
8-nth word: "was" happens 11159 times
9-nth word: "his" happens 9961 times
....^[36mexp1:kmost_freq
[0m<jupyter_text>The `.args` value restrictionsRFlow writes all arguments to a database, so every node instance has a signature of its previous call. If the signature matches with the stored one, the node is considered updated. However, constant values such `kmost_freq.args.K = 10` must have two restrictions:* Must be serializable with `pickle`;* Must have a `__equal__` implementation.Those two restrictions are needed to compare and write the signatures. Suppose that we're using the following `Uncomparable` class, that does not have a `__eq__` method:<jupyter_code>class Uncomparable:
    def __init__(self, a):
        self.a = a

    def __str__(self):
        return str(self.a)<jupyter_output><empty_output><jupyter_text>Uncomparable objects happens mostly with C/C++ wrappers or too big to compare objects. If we try to add to the following DAG:<jupyter_code>class Print(rflow.Interface):
    def evaluate(self, obj):
        print(obj)
        
ta = rflow.get_graph('test-args', overwrite=True)
ta.print_const = Print()
try:
    ta.print_const.args.obj = Uncomparable(4)
except rflow.WorkflowError as error:
    print("Error: {}".format(error))<jupyter_output>Error: Assigning a non equalable value that is not derived from BaseNode or Resource<jupyter_text>An error is raised because `Uncomparable` does not have a `__eq__` override. To solve this problem, create `rflow.Interface`s to instantiate uncomparable or not serializable objects. Then pass  RFlow-friendly arguments to keep the DAG is sensible to changes on those objects.<jupyter_code>class GetUncomparable(rflow.Interface):
    def evaluate(self, int_value):
        return Uncomparable(int_value)
    
ta.get_uncomp = GetUncomparable()
ta.get_uncomp.args.int_value = 4

ta.print_const.args.obj = ta.get_uncomp
ta.print_const.call()<jupyter_output>....[36mUPDATE  test-args:print_const
[0m.....[37mUPDATE  test-args:get_uncomp
[0m......[30mRUN  test-args:get_uncomp
[0m......^[30mtest-args:get_uncomp
[0m......[30mRUN  test-args:print_const
[0m4
......^[30mtest-args:print_const
[0m<jupyter_text>The later way advantage is that the construction arguments are tracked.  Viewing GraphsRFlow graphs can be view with Graphviz:<jupyter_code>import graphviz
import rflow.viz as viz

viz.dag2dot(exp1)<jupyter_output><empty_output><jupyter_text>Adding more nodesSuppose that a new thesis arrived about random texting counting. Researchers want to base their new experiments on the former base, so they know that are using the same procedures on both thesis.With RFlow, they could define a new graph vertex for generating random text and insert into a new graph.First, the next code will define the random text generator interface:<jupyter_code>import random

class RandomText(rflow.Interface):
    """Generates random text.
    """

    def evaluate(self, resource, num_of_words, selected_words):
        """Args:

            resource (rflow.FSResource): File resource to 
             write the random words.

            num_of_words (int): How many words to generate.

            selected_words (List[str]): Which words to use.
        """

        with open(resource.filepath, 'w') as file_stream:
            for i in range(num_of_words):
                random_word = random.sample(selected_words, 1)[0]
                file_stream.write(random_word)
                file_stream.write(' ')

    def load(self):
        """The `load()` method indicates to RFlow that node is reentrant. 
        As `evaluate()` didn't return anything, we can leave it empty.        
        """
        pass<jupyter_output><empty_output><jupyter_text>Next step will be creating a new graph named `exp2` to hold our new experiment.<jupyter_code>exp2 = rflow.get_graph('exp2', overwrite=True)<jupyter_output><empty_output><jupyter_text>The `RandomText` is instantiated with the name `random_text`. It will generate the resource `'random-text.txt'`:<jupyter_code>exp2.random_text = RandomText(rflow.FSResource('random-text.txt'))<jupyter_output><empty_output><jupyter_text>According to logic of the interface, it accepts `num_of_words` as how many words to generate, and `selected_words` as which words to use. Let generate 50 words with the classic "lorem ipsum" text. The `with` shortcut will be used to set the `exp2.random_text.args` attributes:<jupyter_code>with exp2.random_text as args:
    args.num_of_words = 50
    args.selected_words = [
            'lorem', 'ipsum', 'dolor', 'sit', 'amet',
            'consectetur', 'adipiscing', 'elit', 'vestibulum',
            'non', 'feugiat', 'felis']<jupyter_output><empty_output><jupyter_text>Let connect the `random_text` node to a `WordFrequency` instance.<jupyter_code>exp2.word_freq = WordFrequency(rflow.FSResource('random-text-freq.txt'))
exp2.word_freq.args.input_resource = exp2.random_text.resource<jupyter_output><empty_output><jupyter_text>In exp1, `word_freq` was direct assigned to `kmost_freq.args.word_freq`. In the above code, the assignment is between resource to another resource. Assigning a node's `.resource` is a special case which also generates dependency between nodes.Now reusing the `PrintKMost` viewing node:<jupyter_code>exp2.kmost_freq = PrintKMost()
with exp2.kmost_freq as args:    
    args.word_freq = exp2.word_freq
    args.K = 10<jupyter_output><empty_output><jupyter_text>The final DAG becomes:<jupyter_code>viz.dag2dot(exp2)<jupyter_output><empty_output><jupyter_text>Executing it two times to check whatever RFlow not execute `random_text` or `word_freq` without necessity.<jupyter_code>exp2.random_text.clear() # Remove previous run random text

exp2.kmost_freq.call()<jupyter_output>Removing resource random-text.txt
......[30mUPDATE  exp2:kmost_freq
[0m.......[32mUPDATE  exp2:word_freq
[0m........[33mUPDATE  exp2:random_text
[0m.........[34mRUN  exp2:random_text
[0m.........^[34mexp2:random_text
[0m.........[34mRUN  exp2:word_freq
[0m.........^[34mexp2:word_freq
[0m.........[34mRUN  exp2:kmost_freq
[0m0-nth word: "vestibulum" happens 7 times
1-nth word: "amet" happens 7 times
2-nth word: "ipsum" happens 6 times
3-nth word: "lorem" happens 5 times
4-nth word: "felis" happens 5 times
5-nth word: "feugiat" happens 4 times
6-nth word: "non" happens 4 times
7-nth word: "sit" happens 3 times
8-nth word: "elit" happens 3 times
9-nth word: "consectetur" happens 2 times
.........^[34mexp2:kmost_freq
[0m<jupyter_text>First time executed as expected, running all the DAG.<jupyter_code># Use the redo flag again, to force
# kmost_freq execution.
exp2.kmost_freq.call(redo=True)<jupyter_output>.........[34mUPDATE  exp2:kmost_freq
[0m..........[35mRUN  exp2:kmost_freq
[0m0-nth word: "vestibulum" happens 7 times
1-nth word: "amet" happens 7 times
2-nth word: "ipsum" happens 6 times
3-nth word: "lorem" happens 5 times
4-nth word: "felis" happens 5 times
5-nth word: "feugiat" happens 4 times
6-nth word: "non" happens 4 times
7-nth word: "sit" happens 3 times
8-nth word: "elit" happens 3 times
9-nth word: "consectetur" happens 2 times
..........^[35mexp2:kmost_freq
[0m<jupyter_text>As expected, the system didn't evaluate two times `random_text` or `word_freq`, just `kmost_freq`.For testing raw python values changes, the next code modifies the random generator parameter to make 100 words:<jupyter_code>exp2.random_text.args.num_of_words = 100
exp2.kmost_freq.call(redo=True)<jupyter_output>..........[35mUPDATE  exp2:kmost_freq
[0m...........[36mUPDATE  exp2:word_freq
[0m............[37mUPDATE  exp2:random_text
[0m.............[30mRUN  exp2:random_text
[0m.............^[30mexp2:random_text
[0m.............[30mRUN  exp2:word_freq
[0m.............^[30mexp2:word_freq
[0m.............[30mRUN  exp2:kmost_freq
[0m0-nth word: "dolor" happens 14 times
1-nth word: "sit" happens 11 times
2-nth word: "vestibulum" happens 10 times
3-nth word: "non" happens 10 times
4-nth word: "elit" happens 8 times
5-nth word: "lorem" happens 8 times
6-nth word: "consectetur" happens 8 times
7-nth word: "ipsum" happens 7 times
8-nth word: "adipiscing" happens 7 times
9-nth word: "feugiat" happens 6 times
.............^[30mexp2:kmost_freq
[0m<jupyter_text>When changing `num_of_words` to 100, RFlow detected this and recalled all dependent nodes.  Putting DAG inside workflow.py filesRFlow DAG can be used inside Jupyter notebooks, but it is primarily designed to run from files named `workflow.py`. This makes more comfortable sharing and reusing code. For the word counting example, create the file [workflow.py](workflow.py) in the same directory as the "big.txt" file.  Put all node definition code in it. The graph code will be the same, except that it should use the `rflow.graph()` decorator to provide an initialized graph object. The next snippet shows how the graph code should be placed on the file:```python@rflow.graph()def exp2(g):    g.random_text = RandomText(rflow.FSResource('random-text.txt'))    with g.random_text as args:        args.num_of_words = 50        args.selected_words = [            'lorem', 'ipsum', 'dolor', 'sit', 'amet',            'consectetur', 'adipiscing', 'elit', 'vestibulum',            'non', 'feugiat', 'felis']    g.word_freq = WordFrequency(rflow.FSResource('random-text-freq.txt'))    g.word_freq.args.input_resource = g.random_text.resource    g.kmost_freq = PrintKMost()    with g.kmost_freq as args:            args.word_freq = g.word_freq        args.K = 10```On the above code: * The graph's name is its function name - `exp2`.* The `g` variable is a already created graph by the decorator.Graphs then can be executed with `rflow` command. The first one will `clean` the `word_freq` node:<jupyter_code>!rflow exp2 clean word_freq<jupyter_output>Removing resource random-text-freq.txt<jupyter_text>The first argument to `rflow` is the target graph. The second one is the command. There are the following commands:<jupyter_code>!rflow exp2 -h<jupyter_output>usage: rflow [-h] {exp2} {run,touch,print-run,viz-dag,help,clean}

RFlow workflow runner

positional arguments:
  {exp2}
  {run,touch,print-run,viz-dag,help,clean}

optional arguments:
  -h, --help            show this help message and exit<jupyter_text>The `run` command executes a given node. Use the  `-h` flag to show the available nodes:<jupyter_code>!rflow exp2 run -h<jupyter_output>usage: rflow [-h] [--redo] node

Executes the workflow to a node.

positional arguments:
  node        random_text, word_freq, kmost_freq

optional arguments:
  -h, --help  show this help message and exit
  --redo, -r  Redo the last node, whatever even if it's updated (default:
              False)<jupyter_text>To run the `kmost_freq` from command-line:<jupyter_code>!rflow exp2 run kmost_freq<jupyter_output>[32mUPDATE  exp2:kmost_freq
[0m.[33mUPDATE  exp2:word_freq
[0m..[34mUPDATE  exp2:random_text
[0m...[35mRUN  exp2:random_text
[0m...^[35mexp2:random_text
[0m...[35mRUN  exp2:word_freq
[0m...^[35mexp2:word_freq
[0m...[35mRUN  exp2:kmost_freq
[0m0-nth word: "sit" happens 7 times
1-nth word: "adipiscing" happens 6 times
2-nth word: "non" happens 6 times
3-nth word: "ipsum" happens 6 times
4-nth word: "dolor" happens 5 times
5-nth word: "elit" happens 5 times
6-nth word: "feugiat" happens 4 times
7-nth word: "amet" happens 4 times
8-nth word: "lorem" happens 3 times
9-nth word: "vestibulum" happens 3 times
...^[35mexp2:kmost_freq
[0m<jupyter_text>Testing again should not evaluate `word_freq` again.<jupyter_code>!rflow exp2 run kmost_freq<jupyter_output>[32mUPDATE  exp2:kmost_freq
[0m.[33mLOAD exp2:word_freq
[0m.^[33mDONE exp2:word_freq
[0m.[33mRUN  exp2:kmost_freq
[0m0-nth word: "sit" happens 7 times
1-nth word: "adipiscing" happens 6 times
2-nth word: "non" happens 6 times
3-nth word: "ipsum" happens 6 times
4-nth word: "dolor" happens 5 times
5-nth word: "elit" happens 5 times
6-nth word: "feugiat" happens 4 times
7-nth word: "amet" happens 4 times
8-nth word: "lorem" happens 3 times
9-nth word: "vestibulum" happens 3 times
.^[33mexp2:kmost_freq
[0m<jupyter_text>Graphviz visualizations are available on command line too:```shell$rflow exp2 viz-dag```  Using sub-graphsSometimes researchers want to keep two parameterized branches of an experiment. In RFlow they can create sub-functions to generate on the fly.Suppose that we want to track the effects of counting 50 or 100 random words. Next code creates a function to generate DAGs with different word counts, through `word_count` parameter.<jupyter_code>def _sub_graph(s, word_count):
    s.random_text = RandomText(rflow.FSResource('{}-random-text.txt'.format(word_count)))
    with s.random_text as args:
        args.num_of_words = word_count
        args.selected_words = [
            'lorem', 'ipsum', 'dolor', 'sit', 'amet',
            'consectetur', 'adipiscing', 'elit', 'vestibulum',
            'non', 'feugiat', 'felis']

    s.word_freq = WordFrequency(rflow.FSResource('{}-random-text-freq.txt'.format(word_count)))
    s.word_freq.args.input_resource = s.random_text.resource

    s.kmost_freq = PrintKMost()
    with s.kmost_freq as args:    
        args.word_freq = s.word_freq
        args.K = 10<jupyter_output><empty_output><jupyter_text>**Note:** The resource file paths were set to reflect their corresponding parameters. There are two ways to use `_sub_graph`. The first is by creating a new graph:<jupyter_code>exp2_50 = rflow.get_graph('exp2_50', overwrite=True)
exp2_100 = rflow.get_graph('exp2_100', overwrite=True)

_sub_graph(exp2_50, 50)
_sub_graph(exp2_100, 100)
viz.dag2dot(exp2_50)
viz.dag2dot(exp2_100)<jupyter_output><empty_output><jupyter_text>Alternatively, they can just be put in the same graph, but with different vertex names. The `prefix()` method returns a temporally graph, where each node has it name prefixed with the prefix string.  The following example creates a graph to hold 50 and 100 graphs. Each one prefixed with, respectively, `n50` and `n100` strings.<jupyter_code>exp2_50_100 = rflow.get_graph('exp2_50_100', overwrite=True)

_sub_graph(exp2_50_100.prefix('n50'), 50)
_sub_graph(exp2_50_100.prefix('n100'), 50)
viz.dag2dot(exp2_50_100)<jupyter_output><empty_output>
{'fraction_non_alphanumeric': 0.10570493173585957, 'fraction_numerical': 0.030962663694622458, 'mean_word_length': 4.083746458923513, 'pattern_counts': {'":': 0, '<': 140, '<?xml version=': 0, '>': 140, 'https://': 0, 'lorem ipsum': 1, 'www.': 0, 'xml': 0}, 'pii_count': 1, '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': '39', 'dir': 'jupyter-structured-clean-dedup', 'id': '403349', 'license': 'MIT', 'n_tokens_mistral': 9880, 'n_tokens_neox': 9125, 'n_words': 3174, 'path': 'doc/tutorials/wordcounter/tutorial.ipynb', 'repo_name': 'otaviog/rflow', 'size': '28712'}